From de1ec8f45d0b117dec157d89b7ef46aef0f363a7 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 11 Sep 2020 15:11:12 -0400 Subject: [PATCH 1/2] Allowed passing a tsconfig.json to --comments --- src/cli/main.ts | 29 ++++--- src/comments/collectCommentFileNames.test.ts | 81 ++++++++++++++++++ src/comments/collectCommentFileNames.ts | 50 ++++++++++++ src/comments/convertComments.test.ts | 86 ++++++++------------ src/comments/convertComments.ts | 69 ++++++++-------- 5 files changed, 219 insertions(+), 96 deletions(-) create mode 100644 src/comments/collectCommentFileNames.test.ts create mode 100644 src/comments/collectCommentFileNames.ts diff --git a/src/cli/main.ts b/src/cli/main.ts index c25f916ee..168d5fd28 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -6,6 +6,10 @@ import { globAsync } from "../adapters/globAsync"; import { nativeImporter } from "../adapters/nativeImporter"; import { processLogger } from "../adapters/processLogger"; import { bind } from "../binding"; +import { + collectCommentFileNames, + CollectCommentFileNamesDependencies, +} from "../comments/collectCommentFileNames"; import { convertComments, ConvertCommentsDependencies } from "../comments/convertComments"; import { ConvertFileCommentsDependencies, @@ -72,16 +76,6 @@ import { mergers } from "../rules/mergers"; import { rulesConverters } from "../rules/rulesConverters"; import { runCli, RunCliDependencies } from "./runCli"; -const convertFileCommentsDependencies: ConvertFileCommentsDependencies = { - converters: rulesConverters, - fileSystem: fsFileSystem, -}; - -const convertCommentsDependencies: ConvertCommentsDependencies = { - convertFileComments: bind(convertFileComments, convertFileCommentsDependencies), - globAsync, -}; - const convertRulesDependencies: ConvertRulesDependencies = { converters: rulesConverters, mergers, @@ -117,6 +111,21 @@ const findOriginalConfigurationsDependencies: FindOriginalConfigurationsDependen mergeLintConfigurations, }; +const convertFileCommentsDependencies: ConvertFileCommentsDependencies = { + converters: rulesConverters, + fileSystem: fsFileSystem, +}; + +const collectCommentFileNamesDependencies: CollectCommentFileNamesDependencies = { + findTypeScriptConfiguration: bind(findTypeScriptConfiguration, findConfigurationDependencies), +}; + +const convertCommentsDependencies: ConvertCommentsDependencies = { + convertFileComments: bind(convertFileComments, convertFileCommentsDependencies), + collectCommentFileNames: bind(collectCommentFileNames, collectCommentFileNamesDependencies), + globAsync, +}; + const reportCommentResultsDependencies: ReportCommentResultsDependencies = { logger: processLogger, }; diff --git a/src/comments/collectCommentFileNames.test.ts b/src/comments/collectCommentFileNames.test.ts new file mode 100644 index 000000000..46db1b3c2 --- /dev/null +++ b/src/comments/collectCommentFileNames.test.ts @@ -0,0 +1,81 @@ +import { collectCommentFileNames } from "./collectCommentFileNames"; + +describe("collectCommentFileNames", () => { + it("returns an error result when filePathGlobs is true and typescriptConfiguration is undefined", async () => { + const findTypeScriptConfiguration = jest.fn(); + + const result = await collectCommentFileNames({ findTypeScriptConfiguration }, true); + + expect(result).toEqual(expect.any(Error)); + }); + + it("returns the typescript configuration when filePathGlobs is true and typescriptConfiguration exists", async () => { + const findTypeScriptConfiguration = jest.fn(); + const typescriptConfiguration = { + include: ["a.ts"], + }; + + const result = await collectCommentFileNames( + { findTypeScriptConfiguration }, + true, + typescriptConfiguration, + ); + + expect(result).toEqual(typescriptConfiguration); + }); + + it("returns the input file paths when filePathGlobs is an array", async () => { + const findTypeScriptConfiguration = jest.fn(); + const filePathGlobs = ["a.ts"]; + + const result = await collectCommentFileNames( + { findTypeScriptConfiguration }, + filePathGlobs, + ); + + expect(result).toEqual({ + include: filePathGlobs, + }); + }); + + it("returns the input file path when filePathGlobs is a source file path string", async () => { + const findTypeScriptConfiguration = jest.fn(); + const filePathGlobs = "a.ts"; + + const result = await collectCommentFileNames( + { findTypeScriptConfiguration }, + filePathGlobs, + ); + + expect(result).toEqual({ + include: [filePathGlobs], + }); + }); + + it("returns the failure when filePathGlobs is a config file path string and reading it fails", async () => { + const error = new Error("Failure!"); + const findTypeScriptConfiguration = jest.fn().mockResolvedValue(error); + + const result = await collectCommentFileNames( + { findTypeScriptConfiguration }, + "tsconfig.json", + ); + + expect(result).toEqual(error); + }); + + it("returns the typescript configuration from disk when filePathGlobs is a config path string and reading it succeeds", async () => { + const findTypeScriptConfiguration = jest.fn().mockResolvedValue({ + include: ["a.ts"], + }); + + const result = await collectCommentFileNames( + { findTypeScriptConfiguration }, + "tsconfig.json", + ); + + expect(result).toEqual({ + include: ["a.ts"], + }); + }); +}); diff --git a/src/comments/collectCommentFileNames.ts b/src/comments/collectCommentFileNames.ts new file mode 100644 index 000000000..e8f8c337a --- /dev/null +++ b/src/comments/collectCommentFileNames.ts @@ -0,0 +1,50 @@ +import { SansDependencies } from "../binding"; +import { + findTypeScriptConfiguration, + TypeScriptConfiguration, +} from "../input/findTypeScriptConfiguration"; +import { uniqueFromSources } from "../utils"; + +export type CollectCommentFileNamesDependencies = { + findTypeScriptConfiguration: SansDependencies; +}; + +export type CommentFileNames = { + exclude?: string[]; + include: string[]; +}; + +export const collectCommentFileNames = async ( + dependencies: CollectCommentFileNamesDependencies, + filePathGlobs: true | string | string[], + typescriptConfiguration?: TypeScriptConfiguration, +): Promise => { + if (filePathGlobs === true) { + if (!typescriptConfiguration) { + return new Error( + "--comments indicated to convert files listed in a tsconfig.json, but one was not found on disk or specified by with --typescript.", + ); + } + + return { + exclude: typescriptConfiguration.exclude, + include: uniqueFromSources( + typescriptConfiguration.files, + typescriptConfiguration.include, + ), + }; + } + + if (typeof filePathGlobs === "string" && filePathGlobs.endsWith(".json")) { + const findResult = await dependencies.findTypeScriptConfiguration(filePathGlobs); + if (findResult instanceof Error) { + return findResult; + } + + return await collectCommentFileNames(dependencies, true, findResult); + } + + return { + include: uniqueFromSources(filePathGlobs), + }; +}; diff --git a/src/comments/convertComments.test.ts b/src/comments/convertComments.test.ts index ed17c7eb6..a3bfea47a 100644 --- a/src/comments/convertComments.test.ts +++ b/src/comments/convertComments.test.ts @@ -4,13 +4,16 @@ import { convertComments, ConvertCommentsDependencies } from "./convertComments" const createStubDependencies = ( overrides: Partial = {}, ): ConvertCommentsDependencies => ({ + collectCommentFileNames: async () => ({ + include: ["a.ts"], + }), convertFileComments: jest.fn(), - globAsync: jest.fn().mockResolvedValue(["src/a.ts", "src/b.ts"]), + globAsync: jest.fn().mockResolvedValue(["a.ts", "b.ts"]), ...overrides, }); describe("convertComments", () => { - it("returns an empty result when --comments is not provided", async () => { + it("returns an empty result when filePathGlobs is undefined", async () => { // Arrange const dependencies = createStubDependencies(); @@ -24,74 +27,48 @@ describe("convertComments", () => { }); }); - it("returns an error when --comments is given as a boolean value without a TypeScript configuration", async () => { + it("returns the failure result when collectCommentFileNames fails", async () => { // Arrange - const dependencies = createStubDependencies(); + const error = new Error("Failure!"); + const dependencies = createStubDependencies({ + collectCommentFileNames: async () => error, + }); // Act const result = await convertComments(dependencies, true); // Assert expect(result).toEqual({ - errors: expect.arrayContaining([expect.any(Error)]), + errors: [error], status: ResultStatus.Failed, }); }); - it("includes TypeScript files when --comments is given as a boolean value with a TypeScript files configuration", async () => { + it("returns the failure result when a file path glob fails", async () => { // Arrange + const globAsyncError = new Error(); const dependencies = createStubDependencies({ - globAsync: jest.fn().mockResolvedValue(["src/a.ts"]), - }); - - // Act - const result = await convertComments(dependencies, true, { - files: ["src/a.ts"], - }); - - // Assert - expect(result).toEqual({ - data: ["src/a.ts"], - status: ResultStatus.Succeeded, + globAsync: jest.fn().mockResolvedValueOnce(globAsyncError), }); - }); - - it("includes TypeScript inclusions when --comments is given as a boolean value with a TypeScript include configuration", async () => { - // Arrange - const dependencies = createStubDependencies(); // Act - const result = await convertComments(dependencies, true, { - include: ["src/*.ts"], - }); + const result = await convertComments(dependencies, ["*.ts"]); // Assert expect(result).toEqual({ - data: ["src/a.ts", "src/b.ts"], - status: ResultStatus.Succeeded, + errors: [globAsyncError], + status: ResultStatus.Failed, }); }); - it("excludes TypeScript exclusions when --comments is given as a boolean value with a TypeScript excludes configuration", async () => { + it("returns an error when there are no resultant file paths", async () => { // Arrange - const dependencies = createStubDependencies(); - - // Act - const result = await convertComments(dependencies, true, { - exclude: ["src/b.ts"], - include: ["src/*.ts"], - }); - - // Assert - expect(result).toEqual({ - data: ["src/a.ts"], - status: ResultStatus.Succeeded, + const dependencies = createStubDependencies({ + collectCommentFileNames: async () => ({ + include: [], + }), + globAsync: jest.fn().mockResolvedValueOnce([]), }); - }); - - it("returns an error when there are no file path globs", async () => { - // Arrange - const dependencies = createStubDependencies(); // Act const result = await convertComments(dependencies, []); @@ -103,26 +80,29 @@ describe("convertComments", () => { }); }); - it("returns the failure result when a file path glob fails", async () => { + it("returns an error when there all globbed file paths are excluded", async () => { // Arrange - const globAsyncError = new Error(); const dependencies = createStubDependencies({ - globAsync: jest.fn().mockResolvedValueOnce(globAsyncError), + collectCommentFileNames: async () => ({ + exclude: ["*.ts"], + include: ["a.ts"], + }), + globAsync: jest.fn().mockResolvedValueOnce(["a.ts"]), }); // Act - const result = await convertComments(dependencies, ["*.ts"]); + const result = await convertComments(dependencies, []); // Assert expect(result).toEqual({ - errors: [globAsyncError], + errors: expect.arrayContaining([expect.any(Error)]), status: ResultStatus.Failed, }); }); it("returns the failure result when a file conversion fails", async () => { // Arrange - const fileConversionError = new Error(); + const fileConversionError = new Error("Failure!"); const dependencies = createStubDependencies({ convertFileComments: jest.fn().mockResolvedValueOnce(fileConversionError), }); @@ -146,7 +126,7 @@ describe("convertComments", () => { // Assert expect(result).toEqual({ - data: ["src/a.ts", "src/b.ts"], + data: ["a.ts", "b.ts"], status: ResultStatus.Succeeded, }); }); diff --git a/src/comments/convertComments.ts b/src/comments/convertComments.ts index 4bf8ccef8..ada2e9bb0 100644 --- a/src/comments/convertComments.ts +++ b/src/comments/convertComments.ts @@ -5,11 +5,13 @@ import { SansDependencies } from "../binding"; import { TypeScriptConfiguration } from "../input/findTypeScriptConfiguration"; import { ResultStatus, ResultWithDataStatus } from "../types"; import { separateErrors, uniqueFromSources, isError } from "../utils"; +import { collectCommentFileNames } from "./collectCommentFileNames"; import { convertFileComments } from "./convertFileComments"; export type ConvertCommentsDependencies = { convertFileComments: SansDependencies; globAsync: GlobAsync; + collectCommentFileNames: SansDependencies; }; export const convertComments = async ( @@ -17,35 +19,38 @@ export const convertComments = async ( filePathGlobs: true | string | string[] | undefined, typescriptConfiguration?: TypeScriptConfiguration, ): Promise> => { - let fromTypeScriptConfiguration: TypeScriptConfiguration | undefined; + if (filePathGlobs === undefined) { + return { + data: undefined, + status: ResultStatus.Succeeded, + }; + } - if (filePathGlobs === true) { - if (!typescriptConfiguration) { - return { - errors: [ - new Error( - "--comments indicated to convert files listed in a tsconfig.json, but one was not found on disk or specified by with --typescript.", - ), - ], - status: ResultStatus.Failed, - }; - } + const commentFileNames = await dependencies.collectCommentFileNames( + filePathGlobs, + typescriptConfiguration, + ); - filePathGlobs = [ - ...(typescriptConfiguration.files ?? []), - ...(typescriptConfiguration.include ?? []), - ]; - fromTypeScriptConfiguration = typescriptConfiguration; + if (commentFileNames instanceof Error) { + return { + errors: [commentFileNames], + status: ResultStatus.Failed, + }; } - if (filePathGlobs === undefined) { + const { exclude, include } = commentFileNames; + const [fileGlobErrors, globbedFilePaths] = separateErrors( + await Promise.all(include.map(dependencies.globAsync)), + ); + + if (fileGlobErrors.length !== 0) { return { - data: undefined, - status: ResultStatus.Succeeded, + errors: fileGlobErrors, + status: ResultStatus.Failed, }; } - const uniqueFilePathGlobs = uniqueFromSources(filePathGlobs); - if (uniqueFilePathGlobs.join("") === "") { + + if (globbedFilePaths.join("") === "") { return { errors: [ new Error( @@ -56,24 +61,22 @@ export const convertComments = async ( }; } - const [fileGlobErrors, globbedFilePaths] = separateErrors( - await Promise.all(uniqueFilePathGlobs.map(dependencies.globAsync)), + const uniqueGlobbedFilePaths = uniqueFromSources(...globbedFilePaths).filter( + (filePathGlob) => !exclude?.some((exclusion) => minimatch(filePathGlob, exclusion)), ); - if (fileGlobErrors.length !== 0) { + + if (uniqueGlobbedFilePaths.join("") === "") { return { - errors: fileGlobErrors, + errors: [ + new Error( + `All files passed to --comments were excluded. Consider removing 'exclude' from your TypeScript configuration.`, + ), + ], status: ResultStatus.Failed, }; } const ruleConversionCache = new Map(); - const uniqueGlobbedFilePaths = uniqueFromSources(...globbedFilePaths).filter( - (filePathGlob) => - !fromTypeScriptConfiguration?.exclude?.some((exclude) => - minimatch(filePathGlob, exclude), - ), - ); - const fileFailures = ( await Promise.all( uniqueGlobbedFilePaths.map(async (filePath) => From 48ba9b91032657bf045a3e5b63acca96851ece6a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 14 Sep 2020 23:01:18 -0400 Subject: [PATCH 2/2] Added mention in README.md --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 64d5ee482..8d27092dc 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ We **strongly** advise reading [docs/FAQs.md](./docs/FAQs.md) before planning yo Each of these flags is optional: -- **[`comments`](#comments)**: File glob path(s) to convert TSLint rule flags to ESLint within. +- **[`comments`](#comments)**: TypeScript configuration or file glob path(s) to convert TSLint rule flags to ESLint within. - **[`config`](#config)**: Path to print the generated ESLint configuration file to. - **[`editor`](#editor)**: Path to an editor configuration file to convert linter settings within. - **[`eslint`](#eslint)**: Path to an ESLint configuration file to read settings from. @@ -64,7 +64,13 @@ Comments such as `// tslint:disable: tslint-rule-name` will be converted to equi If passed without arguments, respects the `excludes`, `files`, and `includes` in your TypeScript configuration. -Alternately, you can specify which files to convert comments in as globs: +If passed a single file path ending with `.json`, that is treated as a TypeScript configuration file describing with files to convert. + +```shell +npx tslint-to-eslint-config --comments tsconfig.json +``` + +If passed any other arguments, those are treated as glob paths for file paths to convert: ```shell npx tslint-to-eslint-config --comments 'src/**/*.ts'