Repository: typescript-eslint/tslint-to-eslint-config (ts)

Issue:
Allow tsconfig files and --typescript to inform --comment
### 🚀 Feature Request

Splitting out followup work from #246: it'd be easier for users to run `--comment` if it could use tsconfig file inclusions to fill in the list of files to convert.

#### Existing Behavior

Right now you have to manually pass globs to `--comment`

#### Change Proposal

If `--comment` is provided as `true` (no globs are passed to it) and a `tsconfig.json` is available, it should use TypeScript logic to figure out the file inclusion list from there.

Pull request:
Defaulted --comment globs to --typescript settings
## PR Checklist

-   [x] Addresses an existing issue: fixes #426
-   [x] That issue was marked as [`status: accepting prs`](https://github.com/typescript-eslint/tslint-to-eslint-config/labels/status%3A%20accepting%20prs)

## Overview

If `--comments` is provided as `true` from having no globs passed to it, we'll now default to a configuration provided by `--typescript` if available. That necessitates passing in a found TypeScript configuration to `convertComments`.

Uses `minimatch` (already a transitive runtime dependency) to emulate TypeScript's `exclude`, `files`, and `include`.

Patch:
diff --git a/README.md b/README.md
index 577d0fb..64d5ee4 100644
--- a/README.md
+++ b/README.md
@@ -54,14 +54,22 @@ Each of these flags is optional:
 #### `comments`
 
 ```shell
-npx tslint-to-eslint-config --comments 'src/**/*.ts'
+npx tslint-to-eslint-config --comments
 ```
 
 _Default: none_
 
-File glob path(s) to convert [TSLint rule flags](https://palantir.github.io/tslint/usage/rule-flags) to [ESLint inline comments](https://eslint.org/docs/user-guide/configuring#disabling-rules-with-inline-comments) in.
+Indicates to convert from [TSLint rule flags](https://palantir.github.io/tslint/usage/rule-flags) to [ESLint inline comments](https://eslint.org/docs/user-guide/configuring#disabling-rules-with-inline-comments).
 Comments such as `// tslint:disable: tslint-rule-name` will be converted to equivalents like `// eslint-disable eslint-rule-name`.
 
+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:
+
+```shell
+npx tslint-to-eslint-config --comments 'src/**/*.ts'
+```
+
 #### `config`
 
 ```shell
diff --git a/package.json b/package.json
index 5923d15..e8c7da0 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
         "commander": "6.1.0",
         "eslint-config-prettier": "6.11.0",
         "glob": "7.1.6",
+        "minimatch": "^3.0.4",
         "strip-json-comments": "3.1.1",
         "tslint": "6.1.3",
         "typescript": "4.0.2"
@@ -27,6 +28,7 @@
         "@types/eslint-config-prettier": "6.11.0",
         "@types/glob": "7.1.3",
         "@types/jest": "26.0.13",
+        "@types/minimatch": "^3.0.3",
         "@types/node": "12.12.21",
         "@typescript-eslint/eslint-plugin": "4.1.0",
         "@typescript-eslint/parser": "4.1.0",
diff --git a/src/cli/runCli.ts b/src/cli/runCli.ts
index e4fb670..f89f7b2 100644
--- a/src/cli/runCli.ts
+++ b/src/cli/runCli.ts
@@ -19,10 +19,7 @@ export const runCli = async (
     const command = new Command()
         .storeOptionsAsProperties(false)
         .usage("[options] <file ...> --language [language]")
-        .option(
-            "--comments [files]",
-            "convert tslint:disable rule flags in globbed files (experimental)",
-        )
+        .option("--comments [files]", "convert tslint:disable rule flags in files (experimental)")
         .option("--config [config]", "eslint configuration file to output to")
         .option("--editor [editor]", "editor configuration file to convert")
         .option("--eslint [eslint]", "eslint configuration file to convert using")
diff --git a/src/comments/convertComments.ts b/src/comments/convertComments.ts
index 2332c0e..4bf8cce 100644
--- a/src/comments/convertComments.ts
+++ b/src/comments/convertComments.ts
@@ -1,5 +1,8 @@
+import minimatch from "minimatch";
+
 import { GlobAsync } from "../adapters/globAsync";
 import { SansDependencies } from "../binding";
+import { TypeScriptConfiguration } from "../input/findTypeScriptConfiguration";
 import { ResultStatus, ResultWithDataStatus } from "../types";
 import { separateErrors, uniqueFromSources, isError } from "../utils";
 import { convertFileComments } from "./convertFileComments";
@@ -9,29 +12,48 @@ export type ConvertCommentsDependencies = {
     globAsync: GlobAsync;
 };
 
-const noGlobsResult: ResultWithDataStatus<string[]> = {
-    errors: [new Error("--comments requires file path globs to be passed.")],
-    status: ResultStatus.Failed,
-};
-
 export const convertComments = async (
     dependencies: ConvertCommentsDependencies,
     filePathGlobs: true | string | string[] | undefined,
+    typescriptConfiguration?: TypeScriptConfiguration,
 ): Promise<ResultWithDataStatus<string[] | undefined>> => {
+    let fromTypeScriptConfiguration: TypeScriptConfiguration | undefined;
+
+    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,
+            };
+        }
+
+        filePathGlobs = [
+            ...(typescriptConfiguration.files ?? []),
+            ...(typescriptConfiguration.include ?? []),
+        ];
+        fromTypeScriptConfiguration = typescriptConfiguration;
+    }
+
     if (filePathGlobs === undefined) {
         return {
             data: undefined,
             status: ResultStatus.Succeeded,
         };
     }
-
-    if (filePathGlobs === true) {
-        return noGlobsResult;
-    }
-
     const uniqueFilePathGlobs = uniqueFromSources(filePathGlobs);
     if (uniqueFilePathGlobs.join("") === "") {
-        return noGlobsResult;
+        return {
+            errors: [
+                new Error(
+                    "--comments found no files. Consider passing no globs to it, to default to all TypeScript files.",
+                ),
+            ],
+            status: ResultStatus.Failed,
+        };
     }
 
     const [fileGlobErrors, globbedFilePaths] = separateErrors(
@@ -45,7 +67,13 @@ export const convertComments = async (
     }
 
     const ruleConversionCache = new Map<string, string | undefined>();
-    const uniqueGlobbedFilePaths = uniqueFromSources(...globbedFilePaths);
+    const uniqueGlobbedFilePaths = uniqueFromSources(...globbedFilePaths).filter(
+        (filePathGlob) =>
+            !fromTypeScriptConfiguration?.exclude?.some((exclude) =>
+                minimatch(filePathGlob, exclude),
+            ),
+    );
+
     const fileFailures = (
         await Promise.all(
             uniqueGlobbedFilePaths.map(async (filePath) =>
diff --git a/src/conversion/convertLintConfig.ts b/src/conversion/convertLintConfig.ts
index 185ad27..435caf7 100644
--- a/src/conversion/convertLintConfig.ts
+++ b/src/conversion/convertLintConfig.ts
@@ -61,7 +61,10 @@ export const convertLintConfig = async (
     }
 
     // 5. Files to transform comments in have source text rewritten using the same rule conversion logic
-    const commentsResult = await dependencies.convertComments(settings.comments);
+    const commentsResult = await dependencies.convertComments(
+        settings.comments,
+        originalConfigurations.data.typescript,
+    );
 
     // 6. A summary of the results is printed to the user's console
     await dependencies.reportConversionResults(settings.config, summarizedConfiguration);
diff --git a/src/input/findTypeScriptConfiguration.ts b/src/input/findTypeScriptConfiguration.ts
index afc46e3..b5acc24 100644
--- a/src/input/findTypeScriptConfiguration.ts
+++ b/src/input/findTypeScriptConfiguration.ts
@@ -8,6 +8,9 @@ export type TypeScriptConfiguration = {
         lib?: string[];
         target?: string;
     };
+    exclude?: string[];
+    files?: string[];
+    include?: string[];
 };
 
 const defaultTypeScriptConfiguration = {
@@ -29,6 +32,7 @@ export const findTypeScriptConfiguration = async (
     return rawConfiguration instanceof Error
         ? rawConfiguration
         : {
+              ...rawConfiguration,
               compilerOptions: {
                   ...defaultTypeScriptConfiguration.compilerOptions,
                   ...rawConfiguration.compilerOptions,


Test patch:
diff --git a/src/comments/convertComments.test.ts b/src/comments/convertComments.test.ts
index 48d6b0f..ed17c7e 100644
--- a/src/comments/convertComments.test.ts
+++ b/src/comments/convertComments.test.ts
@@ -5,7 +5,7 @@ const createStubDependencies = (
     overrides: Partial<ConvertCommentsDependencies> = {},
 ): ConvertCommentsDependencies => ({
     convertFileComments: jest.fn(),
-    globAsync: jest.fn().mockResolvedValue(["src/index.ts"]),
+    globAsync: jest.fn().mockResolvedValue(["src/a.ts", "src/b.ts"]),
     ...overrides,
 });
 
@@ -24,7 +24,7 @@ describe("convertComments", () => {
         });
     });
 
-    it("returns an error when --comments is given as a boolean value", async () => {
+    it("returns an error when --comments is given as a boolean value without a TypeScript configuration", async () => {
         // Arrange
         const dependencies = createStubDependencies();
 
@@ -38,6 +38,57 @@ describe("convertComments", () => {
         });
     });
 
+    it("includes TypeScript files when --comments is given as a boolean value with a TypeScript files configuration", async () => {
+        // Arrange
+        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,
+        });
+    });
+
+    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"],
+        });
+
+        // Assert
+        expect(result).toEqual({
+            data: ["src/a.ts", "src/b.ts"],
+            status: ResultStatus.Succeeded,
+        });
+    });
+
+    it("excludes TypeScript exclusions when --comments is given as a boolean value with a TypeScript excludes configuration", 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,
+        });
+    });
+
     it("returns an error when there are no file path globs", async () => {
         // Arrange
         const dependencies = createStubDependencies();
@@ -95,7 +146,7 @@ describe("convertComments", () => {
 
         // Assert
         expect(result).toEqual({
-            data: ["src/index.ts"],
+            data: ["src/a.ts", "src/b.ts"],
             status: ResultStatus.Succeeded,
         });
     });