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

Issue:
TSLint plugin needs to account for TSLint rule directories
Right now, `ruleDirectories` in `tslint.json` are completely ignored by this tool. 😮

Pull request:
Factored rule directories from original TSLint configs into output
Fixes #6.

Patch:
diff --git a/src/cli/main.ts b/src/cli/main.ts
index 94e023f..fec86a3 100644
--- a/src/cli/main.ts
+++ b/src/cli/main.ts
@@ -5,7 +5,7 @@ import { promisify } from "util";
 
 import { convertConfig } from "../convertConfig";
 import { createNewConfiguration } from "../creation/createNewConfiguration";
-import { findTslintRules } from "../input/findTslintRules";
+import { findTslintConfiguration } from "../input/findTslintConfiguration";
 import { TSLintToESLintSettings } from "../types";
 import { runCli } from "./runCli";
 
@@ -18,22 +18,24 @@ const logger = {
     stdout: process.stdout,
 };
 
-const ruleFinder = (config: string) => findTslintRules(config, promisify(exec));
-
 const fileExists = (filePath: string) => Promise.resolve(fs.existsSync(filePath));
 
 const runtime = {
     convertConfig: (settings: TSLintToESLintSettings) =>
         convertConfig({
-            createNewConfiguration: configConversionResults =>
-                createNewConfiguration(configConversionResults, fs.promises.writeFile),
+            createNewConfiguration: (conversionResults, originalConfiguration) =>
+                createNewConfiguration(
+                    conversionResults,
+                    originalConfiguration,
+                    fs.promises.writeFile,
+                ),
             fileExists,
+            findTslintConfiguration: (config: string) =>
+                findTslintConfiguration(config, promisify(exec)),
             logger,
-            ruleFinder,
             settings,
         }),
     logger,
-    ruleFinder,
 };
 
 export const main = async (argv: string[]) => {
diff --git a/src/convertConfig.ts b/src/convertConfig.ts
index 15e9b30..a4556a7 100644
--- a/src/convertConfig.ts
+++ b/src/convertConfig.ts
@@ -1,29 +1,32 @@
-import { FoundTSLintRules } from "./input/findTslintRules";
+import { TSLintConfiguration } from "./input/findTslintConfiguration";
 import { ProcessLogger } from "./logger";
 import { reportConversionResults } from "./reportConversionResults";
 import { convertRules, ConfigConversionResults } from "./rules/convertRules";
 import { converters } from "./rules/converters";
 import { TSLintToESLintSettings, TSLintToESLintResult, ResultStatus } from "./types";
 
-export type CreateNewConfiguration = (conversionResults: ConfigConversionResults) => Promise<void>;
+export type CreateNewConfiguration = (
+    conversionResults: ConfigConversionResults,
+    originalConfiguration: TSLintConfiguration,
+) => Promise<void>;
 
 export type FileExists = (filePath: string) => Promise<boolean>;
 
-export type RuleFinder = (config: string) => Promise<FoundTSLintRules | Error>;
+export type findTslintConfiguration = (config: string) => Promise<TSLintConfiguration | Error>;
 
 export type ConvertConfigRequest = {
     createNewConfiguration: CreateNewConfiguration;
     fileExists: FileExists;
+    findTslintConfiguration: findTslintConfiguration;
     logger: ProcessLogger;
-    ruleFinder: RuleFinder;
     settings: TSLintToESLintSettings;
 };
 
 export const convertConfig = async ({
     createNewConfiguration,
     fileExists,
+    findTslintConfiguration,
     logger,
-    ruleFinder,
     settings,
 }: ConvertConfigRequest): Promise<TSLintToESLintResult> => {
     const { config = "./tslint.json" } = settings;
@@ -34,23 +37,23 @@ export const convertConfig = async ({
         };
     }
 
-    const originalRules = await ruleFinder(config);
-    if (originalRules instanceof Error) {
+    const originalConfiguration = await findTslintConfiguration(config);
+    if (originalConfiguration instanceof Error) {
         return {
-            error: originalRules,
+            error: originalConfiguration,
             status: ResultStatus.Failed,
         };
     }
 
     const convertedRules = convertRules(
-        Object.entries(originalRules.rules).map(([ruleName, value]) => ({
+        Object.entries(originalConfiguration.rules).map(([ruleName, value]) => ({
             ruleName,
             ...value,
         })),
         converters,
     );
 
-    await createNewConfiguration(convertedRules);
+    await createNewConfiguration(convertedRules, originalConfiguration);
     reportConversionResults(convertedRules, logger);
 
     return {
diff --git a/src/creation/createNewConfiguration.ts b/src/creation/createNewConfiguration.ts
index c02cfa2..e8b3d7f 100644
--- a/src/creation/createNewConfiguration.ts
+++ b/src/creation/createNewConfiguration.ts
@@ -1,3 +1,4 @@
+import { TSLintConfiguration } from "../input/findTslintConfiguration";
 import { ConfigConversionResults } from "../rules/convertRules";
 import { formatConvertedRules } from "./formatConvertedRules";
 
@@ -5,6 +6,7 @@ export type WriteFile = (filePath: string, contents: string) => Promise<void>;
 
 export const createNewConfiguration = async (
     conversionResults: ConfigConversionResults,
+    originalConfiguration: TSLintConfiguration,
     writeFile: WriteFile,
 ) => {
     const output = {
@@ -15,7 +17,7 @@ export const createNewConfiguration = async (
         ...(conversionResults.missing.length && {
             plugins: ["@typescript-eslint/tslint"],
         }),
-        rules: formatConvertedRules(conversionResults),
+        rules: formatConvertedRules(conversionResults, originalConfiguration),
     };
 
     await writeFile(".eslintrc.json", JSON.stringify(output, undefined, 4));
diff --git a/src/creation/formatConvertedRules.ts b/src/creation/formatConvertedRules.ts
index 5224d14..8d875e8 100644
--- a/src/creation/formatConvertedRules.ts
+++ b/src/creation/formatConvertedRules.ts
@@ -1,8 +1,12 @@
 import { ConfigConversionResults } from "../rules/convertRules";
 import { ESLintRuleOptions } from "../rules/types";
 import { formatMissingRules } from "./formatMissingRules";
+import { TSLintConfiguration } from "../input/findTslintConfiguration";
 
-export const formatConvertedRules = (conversionResults: ConfigConversionResults) => {
+export const formatConvertedRules = (
+    conversionResults: ConfigConversionResults,
+    originalConfiguration: TSLintConfiguration,
+) => {
     const output: { [i: string]: string | any[] } = {};
     const sortedRuleEntries = Array.from(conversionResults.converted).sort(
         ([ruleNameA], [ruleNameB]) => ruleNameA.localeCompare(ruleNameB),
@@ -13,7 +17,10 @@ export const formatConvertedRules = (conversionResults: ConfigConversionResults)
     }
 
     if (conversionResults.missing.length !== 0) {
-        output["@typescript-eslint/tslint/config"] = formatMissingRules(conversionResults.missing);
+        output["@typescript-eslint/tslint/config"] = formatMissingRules(
+            conversionResults.missing,
+            originalConfiguration.ruleDirectories,
+        );
     }
 
     return output;
diff --git a/src/creation/formatMissingRules.ts b/src/creation/formatMissingRules.ts
index 0d75e7b..1bc924c 100644
--- a/src/creation/formatMissingRules.ts
+++ b/src/creation/formatMissingRules.ts
@@ -1,6 +1,6 @@
 import { TSLintRuleOptions } from "../rules/types";
 
-export const formatMissingRules = (missing: TSLintRuleOptions[]) => {
+export const formatMissingRules = (missing: TSLintRuleOptions[], ruleDirectories: string[]) => {
     const rules: { [i: string]: unknown } = {};
 
     for (const rule of missing.sort((a, b) => a.ruleName.localeCompare(b.ruleName))) {
@@ -12,8 +12,8 @@ export const formatMissingRules = (missing: TSLintRuleOptions[]) => {
     return [
         "error",
         {
+            ...(ruleDirectories.length !== 0 && { ruleDirectories }),
             rules,
-            // TODO: rulesDirectory?
         },
     ];
 };
diff --git a/src/input/findTslintRules.ts b/src/input/findTslintConfiguration.ts
similarity index 73%
rename from src/input/findTslintRules.ts
rename to src/input/findTslintConfiguration.ts
index 57f462a..77756c5 100644
--- a/src/input/findTslintRules.ts
+++ b/src/input/findTslintConfiguration.ts
@@ -1,4 +1,5 @@
-export type FoundTSLintRules = {
+export type TSLintConfiguration = {
+    ruleDirectories: string[];
     rules: {
         [i: string]: any;
     };
@@ -6,10 +7,10 @@ export type FoundTSLintRules = {
 
 export type ChildProcessExec = (command: string) => Promise<{ stderr: string; stdout: string }>;
 
-export const findTslintRules = async (
+export const findTslintConfiguration = async (
     config: string,
     childProcessExec: ChildProcessExec,
-): Promise<FoundTSLintRules | Error> => {
+): Promise<TSLintConfiguration | Error> => {
     const command = buildCommand(config);
     const { stderr, stdout } = await childProcessExec(command);
 
@@ -18,7 +19,7 @@ export const findTslintRules = async (
     }
 
     try {
-        return JSON.parse(stdout) as FoundTSLintRules;
+        return JSON.parse(stdout) as TSLintConfiguration;
     } catch (error) {
         return new Error(`Error parsing TSLint configuration: ${error}`);
     }


Test patch:
diff --git a/src/convertConfig.test.ts b/src/convertConfig.test.ts
index a089bd5..066f069 100644
--- a/src/convertConfig.test.ts
+++ b/src/convertConfig.test.ts
@@ -5,7 +5,7 @@ import { ResultStatus } from "./types";
 const defaultRequest = {
     settings: {},
     logger: createStubLogger(),
-    ruleFinder: jest.fn().mockReturnValue(Promise.resolve(new Error())),
+    findTslintConfiguration: jest.fn().mockReturnValue(Promise.resolve(new Error())),
     fileExists: jest.fn().mockReturnValue(Promise.resolve(true)),
     createNewConfiguration: jest.fn().mockReturnValue(Promise.resolve()),
 };
@@ -41,7 +41,7 @@ describe("convertConfig", () => {
         await convertConfig(request);
 
         // Assert
-        expect(request.ruleFinder).toHaveBeenLastCalledWith(request.settings.config);
+        expect(request.findTslintConfiguration).toHaveBeenLastCalledWith(request.settings.config);
     });
 
     it("searches for ./tslint.json by default when no settings.config is provided", async () => {
@@ -55,15 +55,15 @@ describe("convertConfig", () => {
         await convertConfig(request);
 
         // Assert
-        expect(request.ruleFinder).toHaveBeenLastCalledWith("./tslint.json");
+        expect(request.findTslintConfiguration).toHaveBeenLastCalledWith("./tslint.json");
     });
 
-    it("returns a failure result when ruleFinder returns an error", async () => {
+    it("returns a failure result when findTslintConfiguration returns an error", async () => {
         // Arrange
         const error = new Error("oh no");
         const request = {
             ...defaultRequest,
-            ruleFinder: jest.fn().mockReturnValue(Promise.resolve(error)),
+            findTslintConfiguration: jest.fn().mockReturnValue(Promise.resolve(error)),
         };
 
         // Act
@@ -76,11 +76,11 @@ describe("convertConfig", () => {
         });
     });
 
-    it("creates a new configuration when ruleFinder returns rules", async () => {
+    it("creates a new configuration when findTslintConfiguration returns rules", async () => {
         // Arrange
         const request = {
             ...defaultRequest,
-            ruleFinder: jest.fn().mockReturnValue({
+            findTslintConfiguration: jest.fn().mockReturnValue({
                 rules: {
                     "sample-rule": {
                         ruleArguments: ["one", "two"],
diff --git a/src/creation/createNewConfiguration.test.ts b/src/creation/createNewConfiguration.test.ts
index 1813313..eea955e 100644
--- a/src/creation/createNewConfiguration.test.ts
+++ b/src/creation/createNewConfiguration.test.ts
@@ -2,6 +2,11 @@ import { emptyConversionResults } from "../stubs";
 import { createNewConfiguration } from "./createNewConfiguration";
 import { ConfigConversionResults } from "../rules/convertRules";
 
+const originalConfiguration = {
+    ruleDirectories: [],
+    rules: {},
+};
+
 describe("createNewConfiguration", () => {
     it("excludes the tslint plugin when there are no missing rules", async () => {
         // Arrange
@@ -12,7 +17,7 @@ describe("createNewConfiguration", () => {
         const writeFile = jest.fn().mockReturnValue(Promise.resolve());
 
         // Act
-        await createNewConfiguration(conversionResults, writeFile);
+        await createNewConfiguration(conversionResults, originalConfiguration, writeFile);
 
         // Assert
         expect(writeFile).toHaveBeenLastCalledWith(
@@ -47,7 +52,7 @@ describe("createNewConfiguration", () => {
         const writeFile = jest.fn().mockReturnValue(Promise.resolve());
 
         // Act
-        await createNewConfiguration(conversionResults, writeFile);
+        await createNewConfiguration(conversionResults, originalConfiguration, writeFile);
 
         // Assert
         expect(writeFile).toHaveBeenLastCalledWith(
diff --git a/src/creation/formatConvertedRules.test.ts b/src/creation/formatConvertedRules.test.ts
index da2916b..0175463 100644
--- a/src/creation/formatConvertedRules.test.ts
+++ b/src/creation/formatConvertedRules.test.ts
@@ -2,6 +2,11 @@ import { ConfigConversionResults } from "../rules/convertRules";
 import { emptyConversionResults } from "../stubs";
 import { formatConvertedRules } from "./formatConvertedRules";
 
+const originalConfiguration = {
+    ruleDirectories: [],
+    rules: {},
+};
+
 describe("formatConvertedRules", () => {
     it("prints rules sorted by name when there are multiple rules", () => {
         // Arrange
@@ -26,7 +31,7 @@ describe("formatConvertedRules", () => {
         };
 
         // Act
-        const output = formatConvertedRules(results);
+        const output = formatConvertedRules(results, originalConfiguration);
 
         // Assert
         expect(output).toEqual({
@@ -51,7 +56,7 @@ describe("formatConvertedRules", () => {
         };
 
         // Act
-        const output = formatConvertedRules(results);
+        const output = formatConvertedRules(results, originalConfiguration);
 
         // Assert
         expect(output).toEqual({
@@ -76,7 +81,7 @@ describe("formatConvertedRules", () => {
         };
 
         // Act
-        const output = formatConvertedRules(results);
+        const output = formatConvertedRules(results, originalConfiguration);
 
         // Assert
         expect(output).toEqual({
@@ -101,7 +106,7 @@ describe("formatConvertedRules", () => {
         };
 
         // Act
-        const output = formatConvertedRules(results);
+        const output = formatConvertedRules(results, originalConfiguration);
 
         // Assert
         expect(output).toEqual({
@@ -133,7 +138,7 @@ describe("formatConvertedRules", () => {
         };
 
         // Act
-        const output = formatConvertedRules(results);
+        const output = formatConvertedRules(results, originalConfiguration);
 
         // Assert
         expect(output).toEqual({
diff --git a/src/creation/formatMissingRules.test.ts b/src/creation/formatMissingRules.test.ts
index b8805ee..2baf620 100644
--- a/src/creation/formatMissingRules.test.ts
+++ b/src/creation/formatMissingRules.test.ts
@@ -13,7 +13,7 @@ describe("formatMissingRules", () => {
         ];
 
         // Act
-        const output = formatMissingRules(missing);
+        const output = formatMissingRules(missing, []);
 
         // Assert
         expect(output).toEqual([
@@ -37,7 +37,7 @@ describe("formatMissingRules", () => {
         ];
 
         // Act
-        const output = formatMissingRules(missing);
+        const output = formatMissingRules(missing, []);
 
         // Assert
         expect(output).toEqual([
@@ -66,7 +66,7 @@ describe("formatMissingRules", () => {
         ];
 
         // Act
-        const output = formatMissingRules(missing);
+        const output = formatMissingRules(missing, []);
 
         // Assert
         expect(output).toEqual([
@@ -96,7 +96,7 @@ describe("formatMissingRules", () => {
         ];
 
         // Act
-        const output = formatMissingRules(missing);
+        const output = formatMissingRules(missing, []);
 
         // Assert
         expect(output).toEqual([
@@ -108,4 +108,30 @@ describe("formatMissingRules", () => {
             },
         ]);
     });
+
+    it("includes rule directories when there are rule directories", () => {
+        // Arrange
+        const ruleDirectories = ["./path/to/rules"];
+        const missing: TSLintRuleOptions[] = [
+            {
+                ruleArguments: [],
+                ruleName: "tslint-rule-a",
+                ruleSeverity: "warning",
+            },
+        ];
+
+        // Act
+        const output = formatMissingRules(missing, ruleDirectories);
+
+        // Assert
+        expect(output).toEqual([
+            "error",
+            {
+                ruleDirectories,
+                rules: {
+                    "tslint-rule-a": true,
+                },
+            },
+        ]);
+    });
 });
diff --git a/src/input/findTslintRules.test.ts b/src/input/findTslintConfiguration.test.ts
similarity index 75%
rename from src/input/findTslintRules.test.ts
rename to src/input/findTslintConfiguration.test.ts
index b2620a9..21a14e8 100644
--- a/src/input/findTslintRules.test.ts
+++ b/src/input/findTslintConfiguration.test.ts
@@ -1,13 +1,13 @@
-import { findTslintRules } from "./findTslintRules";
+import { findTslintConfiguration } from "./findTslintConfiguration";
 
-describe("findTslintRules", () => {
+describe("findTslintConfiguration", () => {
     it("returns stderr as an error when the command fails", async () => {
         // Arrange
         const [stderr, stdout] = ["error", ""];
         const childProcessExec = () => Promise.resolve({ stderr, stdout });
 
         // Act
-        const result = await findTslintRules("tslint.json", childProcessExec);
+        const result = await findTslintConfiguration("tslint.json", childProcessExec);
 
         // Assert
         expect(result).toEqual(new Error(stderr));
@@ -19,7 +19,7 @@ describe("findTslintRules", () => {
         const childProcessExec = () => Promise.resolve({ stderr, stdout });
 
         // Act
-        const result = await findTslintRules("tslint.json", childProcessExec);
+        const result = await findTslintConfiguration("tslint.json", childProcessExec);
 
         // Assert
         expect(result).toEqual(
@@ -36,7 +36,7 @@ describe("findTslintRules", () => {
         const childProcessExec = () => Promise.resolve({ stderr, stdout });
 
         // Act
-        const result = await findTslintRules("tslint.json", childProcessExec);
+        const result = await findTslintConfiguration("tslint.json", childProcessExec);
 
         // Assert
         expect(result).toEqual(rules);