diff --git a/.eslintrc.js b/.eslintrc.js index 03f9d2312..d51ea9b33 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,18 +17,19 @@ module.exports = { "@typescript-eslint/no-use-before-define": 0, "@typescript-eslint/prefer-interface": 0, "default-case": 0, + "guard-for-in": 0, "import/no-extraneous-dependencies": [ "error", { devDependencies: ["**/*.stubs.ts", "**/*.test.*"] }, ], "import/first": 0, - "import/newline-after-import": 0, "import/no-unresolved": 0, "import/prefer-default-export": 0, "no-console": 0, "no-continue": 0, "no-empty-function": 0, "no-restricted-syntax": 0, + "no-param-reassign": 0, "no-shadow": 0, "no-undef": 0, "no-useless-constructor": 0, diff --git a/README.md b/README.md index 10a3007b2..e41a56350 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ npx tslint-to-eslint-config --eslint ./path/to/eslintrc.js _Default: `--config`'s value_ Path to an ESLint configuration file to read settings from. -This isn't yet used for anything, but will eventually inform settings for the generated ESLint configuration file. +The generated ESLint configuration file will include any settings `import`ed from this file. #### `package` diff --git a/src/adapters/importer.ts b/src/adapters/importer.ts new file mode 100644 index 000000000..7aa40a2f6 --- /dev/null +++ b/src/adapters/importer.ts @@ -0,0 +1,9 @@ +export type Importer = (moduleName: string) => unknown | Error; + +export const nativeImporter = async (moduleName: string) => { + try { + return await import(moduleName); + } catch (error) { + return error; + } +}; diff --git a/src/cli/main.ts b/src/cli/main.ts index 15ae45bd7..cbfd1f197 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -1,11 +1,20 @@ import { EOL } from "os"; -import { bind } from "../binding"; -import { runCli, RunCliDependencies } from "./runCli"; +import { nativeImporter } from "../adapters/importer"; import { processLogger } from "../adapters/processLogger"; import { childProcessExec } from "../adapters/childProcessExec"; import { fsFileSystem } from "../adapters/fsFileSystem"; +import { bind } from "../binding"; import { ConvertConfigDependencies, convertConfig } from "../conversion/convertConfig"; +import { removeExtendsDuplicatedRules } from "../creation/simplification/removeExtendsDuplicatedRules"; +import { + RetrieveExtendsValuesDependencies, + retrieveExtendsValues, +} from "../creation/simplification/retrieveExtendsValues"; +import { + simplifyPackageRules, + SimplifyPackageRulesDependencies, +} from "../creation/simplification/simplifyPackageRules"; import { writeConversionResults, WriteConversionResultsDependencies, @@ -14,6 +23,7 @@ import { findOriginalConfigurations, FindOriginalConfigurationsDependencies, } from "../input/findOriginalConfigurations"; +import { findPackagesConfiguration } from "../input/findPackagesConfiguration"; import { findESLintConfiguration } from "../input/findESLintConfiguration"; import { findTSLintConfiguration } from "../input/findTSLintConfiguration"; import { findTypeScriptConfiguration } from "../input/findTypeScriptConfiguration"; @@ -24,7 +34,7 @@ import { import { converters } from "../rules/converters"; import { convertRules } from "../rules/convertRules"; import { mergers } from "../rules/mergers"; -import { findPackagesConfiguration } from "../input/findPackagesConfiguration"; +import { runCli, RunCliDependencies } from "./runCli"; const convertRulesDependencies = { converters, @@ -46,6 +56,15 @@ const reportConversionResultsDependencies: ReportConversionResultsDependencies = logger: processLogger, }; +const retrieveExtendsValuesDependencies: RetrieveExtendsValuesDependencies = { + importer: nativeImporter, +}; + +const simplifyPackageRulesDependencies: SimplifyPackageRulesDependencies = { + removeExtendsDuplicatedRules, + retrieveExtendsValues: bind(retrieveExtendsValues, retrieveExtendsValuesDependencies), +}; + const writeConversionResultsDependencies: WriteConversionResultsDependencies = { fileSystem: fsFileSystem, }; @@ -57,6 +76,7 @@ const convertConfigDependencies: ConvertConfigDependencies = { findOriginalConfigurationsDependencies, ), reportConversionResults: bind(reportConversionResults, reportConversionResultsDependencies), + simplifyPackageRules: bind(simplifyPackageRules, simplifyPackageRulesDependencies), writeConversionResults: bind(writeConversionResults, writeConversionResultsDependencies), }; diff --git a/src/conversion/conversionResults.stubs.ts b/src/conversion/conversionResults.stubs.ts index 0acb2d7f6..57249adb2 100644 --- a/src/conversion/conversionResults.stubs.ts +++ b/src/conversion/conversionResults.stubs.ts @@ -1,11 +1,11 @@ import { RuleConversionResults } from "../rules/convertRules"; export const createEmptyConversionResults = ( - overrides: Partial, + overrides: Partial = {}, ): RuleConversionResults => ({ converted: new Map(), failed: [], missing: [], - packages: new Set(), + plugins: new Set(), ...overrides, }); diff --git a/src/conversion/convertConfig.test.ts b/src/conversion/convertConfig.test.ts index fb21682db..ddb9735d7 100644 --- a/src/conversion/convertConfig.test.ts +++ b/src/conversion/convertConfig.test.ts @@ -4,9 +4,10 @@ import { OriginalConfigurations } from "../input/findOriginalConfigurations"; const createStubDependencies = ( overrides: Pick, -) => ({ +): ConvertConfigDependencies => ({ convertRules: jest.fn(), reportConversionResults: jest.fn(), + simplifyPackageRules: async (_configurations, data) => data, writeConversionResults: jest.fn().mockReturnValue(Promise.resolve()), ...overrides, }); diff --git a/src/conversion/convertConfig.ts b/src/conversion/convertConfig.ts index 6701fd21d..81e8fbb9b 100644 --- a/src/conversion/convertConfig.ts +++ b/src/conversion/convertConfig.ts @@ -1,4 +1,5 @@ import { SansDependencies } from "../binding"; +import { simplifyPackageRules } from "../creation/simplification/simplifyPackageRules"; import { writeConversionResults } from "../creation/writeConversionResults"; import { findOriginalConfigurations } from "../input/findOriginalConfigurations"; import { reportConversionResults } from "../reporting/reportConversionResults"; @@ -9,6 +10,7 @@ export type ConvertConfigDependencies = { convertRules: SansDependencies; findOriginalConfigurations: SansDependencies; reportConversionResults: SansDependencies; + simplifyPackageRules: SansDependencies; writeConversionResults: SansDependencies; }; @@ -24,13 +26,20 @@ export const convertConfig = async ( const ruleConversionResults = dependencies.convertRules( originalConfigurations.data.tslint.rules, ); + const mergedConfiguration = { + ...ruleConversionResults, + ...(await dependencies.simplifyPackageRules( + originalConfigurations.data.eslint, + ruleConversionResults, + )), + }; await dependencies.writeConversionResults( settings.config, - ruleConversionResults, + mergedConfiguration, originalConfigurations.data, ); - dependencies.reportConversionResults(ruleConversionResults); + dependencies.reportConversionResults(mergedConfiguration); return { status: ResultStatus.Succeeded, diff --git a/src/creation/simplification/removeExtendsDuplicatedRules.test.ts b/src/creation/simplification/removeExtendsDuplicatedRules.test.ts new file mode 100644 index 000000000..5c64be1d4 --- /dev/null +++ b/src/creation/simplification/removeExtendsDuplicatedRules.test.ts @@ -0,0 +1,165 @@ +import { + ESLintConfiguration, + ESLintConfigurationRuleValue, +} from "../../input/findESLintConfiguration"; +import { ESLintRuleOptions } from "../../rules/types"; +import { removeExtendsDuplicatedRules } from "./removeExtendsDuplicatedRules"; + +const prepareTestRule = ( + ruleOptions: Partial, + extensionConfiguration: ESLintConfigurationRuleValue = 2, +) => { + const ruleName = "rule-a"; + const allRules = new Map([ + [ + ruleName, + { + ruleArguments: [], + ruleName, + ruleSeverity: "off", + ...ruleOptions, + }, + ], + ]); + const extensions: Partial[] = [ + { + rules: { + [ruleName]: extensionConfiguration, + }, + }, + ]; + + return { ruleName, allRules, extensions }; +}; + +describe("removeExtendsDuplicatedRules", () => { + it("keeps a rule when there are no rules in the extension", () => { + // Arrange + const { allRules } = prepareTestRule( + { + ruleName: "mismatched", + }, + 2, + ); + + // Act + const differentRules = removeExtendsDuplicatedRules(allRules, [{}]); + + // Assert + expect(differentRules.size).toBe(1); + }); + + it("keeps a rule when it doesn't match any existing rule", () => { + // Arrange + const { allRules, extensions } = prepareTestRule( + { + ruleName: "mismatched", + }, + 2, + ); + + // Act + const differentRules = removeExtendsDuplicatedRules(allRules, extensions); + + // Assert + expect(differentRules.size).toBe(1); + }); + + it("removes a rule when it matches an existing rule as numbers", () => { + // Arrange + const { allRules, extensions } = prepareTestRule( + { + ruleSeverity: "warn", + }, + 1, + ); + + // Act + const differentRules = removeExtendsDuplicatedRules(allRules, extensions); + + // Assert + expect(differentRules.size).toBe(0); + }); + + it("keeps a rule when it conflicts with an existing rule as numbers", () => { + // Arrange + const { allRules, extensions } = prepareTestRule( + { + ruleSeverity: "warn", + }, + 2, + ); + + // Act + const differentRules = removeExtendsDuplicatedRules(allRules, extensions); + + // Assert + expect(differentRules.size).toBe(1); + }); + + it("removes a rule when it matches an existing rule as strings", () => { + // Arrange + const { allRules, extensions } = prepareTestRule( + { + ruleSeverity: "warn", + }, + "warn", + ); + + // Act + const differentRules = removeExtendsDuplicatedRules(allRules, extensions); + + // Assert + expect(differentRules.size).toBe(0); + }); + + it("keeps a rule when it conflicts with an existing rule as strings", () => { + // Arrange + const { allRules, extensions } = prepareTestRule( + { + ruleSeverity: "warn", + }, + "error", + ); + + // Act + const differentRules = removeExtendsDuplicatedRules(allRules, extensions); + + // Assert + expect(differentRules.size).toBe(1); + }); + + it("removes a rule when it matches an existing rule as objects", () => { + // Arrange + const { allRules, extensions } = prepareTestRule( + { + ruleArguments: ["some-argument"], + ruleSeverity: "warn", + }, + ["warn", "some-argument"], + ); + + // Act + const differentRules = removeExtendsDuplicatedRules(allRules, extensions); + + // Assert + expect(differentRules.size).toBe(0); + }); + + it("keeps a rule when it conflicts with an existing rule as objects", () => { + // Arrange + const { allRules, extensions } = prepareTestRule( + { + ruleArguments: ["some-argument-one"], + ruleSeverity: "warn", + }, + ["warn", "some-argument-modified"], + ); + + // Act + const differentRules = removeExtendsDuplicatedRules(allRules, extensions); + + // Assert + expect(differentRules.size).toBe(1); + }); +}); diff --git a/src/creation/simplification/removeExtendsDuplicatedRules.ts b/src/creation/simplification/removeExtendsDuplicatedRules.ts new file mode 100644 index 000000000..2e1744699 --- /dev/null +++ b/src/creation/simplification/removeExtendsDuplicatedRules.ts @@ -0,0 +1,87 @@ +import { isDeepStrictEqual } from "util"; + +import { + ESLintConfiguration, + ESLintConfigurationRuleValue, +} from "../../input/findESLintConfiguration"; +import { convertRawESLintRuleSeverity } from "../../rules/convertRuleSeverity"; +import { ESLintRuleOptions } from "../../rules/types"; + +export const removeExtendsDuplicatedRules = ( + allRules: Map, + extensions: Partial[], +): Map => { + const differentRules = new Map(); + const mergedExtensionRules = mergeExtensions(extensions); + + for (const [ruleName, value] of allRules) { + if (!ruleValuesAreTheSame(value, mergedExtensionRules.get(ruleName))) { + differentRules.set(ruleName, value); + } + } + + return differentRules; +}; + +const mergeExtensions = (extensions: Partial[]) => { + const mergedRules = new Map(); + + for (const extension of extensions) { + if (extension.rules === undefined) { + continue; + } + + for (const ruleName in extension.rules) { + mergedRules.set(ruleName, formatRuleArguments(ruleName, extension.rules[ruleName])); + } + } + + return mergedRules; +}; + +const formatRuleArguments = ( + ruleName: string, + originalValue: ESLintConfigurationRuleValue, +): ESLintRuleOptions => { + if (typeof originalValue === "number") { + return { + ruleArguments: [], + ruleName, + ruleSeverity: convertRawESLintRuleSeverity(originalValue), + }; + } + + if (typeof originalValue === "string") { + return { + ruleArguments: [], + ruleName, + ruleSeverity: originalValue, + }; + } + + return { + ruleArguments: originalValue.slice(1), + ruleName, + ruleSeverity: convertRawESLintRuleSeverity(originalValue[0]), + }; +}; + +const ruleValuesAreTheSame = ( + configurationValue: ESLintRuleOptions, + extensionValue: ESLintRuleOptions | undefined, +) => { + return ( + extensionValue !== undefined && + configurationValue.ruleSeverity === extensionValue.ruleSeverity && + isDeepStrictEqual( + { + ruleArguments: [], + ...configurationValue.ruleArguments, + }, + { + ruleArguments: [], + ...extensionValue.ruleArguments, + }, + ) + ); +}; diff --git a/src/creation/simplification/resolveExtensionNames.test.ts b/src/creation/simplification/resolveExtensionNames.test.ts new file mode 100644 index 000000000..5fa8322ca --- /dev/null +++ b/src/creation/simplification/resolveExtensionNames.test.ts @@ -0,0 +1,68 @@ +import { resolveExtensionNames } from "./resolveExtensionNames"; + +describe("resolveExtensionNames", () => { + it("returns a single plugin name as an array when given a single string", () => { + // Arrange + const rawExtensionName = "eslint-plugin-linting"; + + // Act + const extensionNames = resolveExtensionNames(rawExtensionName); + + // Assert + expect(extensionNames).toEqual([rawExtensionName]); + }); + + it("returns multiple plugin names as an array when given an array", () => { + // Arrange + const rawExtensionNames = ["eslint-plugin-one", "eslint-plugin-two"]; + + // Act + const extensionNames = resolveExtensionNames(rawExtensionNames); + + // Assert + expect(extensionNames).toEqual(rawExtensionNames); + }); + + it("prepends eslint-plugin- when a plugin doesn't start with it", () => { + // Arrange + const rawExtensionName = "custom"; + + // Act + const extensionNames = resolveExtensionNames(rawExtensionName); + + // Assert + expect(extensionNames).toEqual(["eslint-plugin-custom"]); + }); + + it("doesn't prepend eslint-plugin- when a plugin starts with .", () => { + // Arrange + const rawExtensionName = "../my-config.js"; + + // Act + const extensionNames = resolveExtensionNames(rawExtensionName); + + // Assert + expect(extensionNames).toEqual(["../my-config.js"]); + }); + + it("doesn't prepend eslint-plugin- when a plugin starts with t-plugin-", () => { + // Arrange + const rawExtensionName = "eslint-plugin-value"; + + // Act + const extensionNames = resolveExtensionNames(rawExtensionName); + + // Assert + expect(extensionNames).toEqual(["eslint-plugin-value"]); + }); + it("doesn't prepend eslint-plugin- when a plugin starts with eslint:", () => { + // Arrange + const rawExtensionName = "eslint:recommended"; + + // Act + const extensionNames = resolveExtensionNames(rawExtensionName); + + // Assert + expect(extensionNames).toEqual(["eslint:recommended"]); + }); +}); diff --git a/src/creation/simplification/resolveExtensionNames.ts b/src/creation/simplification/resolveExtensionNames.ts new file mode 100644 index 000000000..7b1b1b645 --- /dev/null +++ b/src/creation/simplification/resolveExtensionNames.ts @@ -0,0 +1,13 @@ +export const resolveExtensionNames = (rawExtensionNames: string | string[]) => { + if (typeof rawExtensionNames === "string") { + rawExtensionNames = [rawExtensionNames]; + } + + return rawExtensionNames.map(rawExtensionName => + rawExtensionName.startsWith(".") || + rawExtensionName.startsWith("eslint-plugin-") || + rawExtensionName.startsWith("eslint:") + ? rawExtensionName + : `eslint-plugin-${rawExtensionName}`, + ); +}; diff --git a/src/creation/simplification/retrieveExtendsValues.test.ts b/src/creation/simplification/retrieveExtendsValues.test.ts new file mode 100644 index 000000000..aec6c80e7 --- /dev/null +++ b/src/creation/simplification/retrieveExtendsValues.test.ts @@ -0,0 +1,87 @@ +import { retrieveExtendsValues } from "./retrieveExtendsValues"; + +describe("retrieveExtendsValues", () => { + it("retrieves eslint-all when an extension is named eslint:all", async () => { + // Arrange + const eslintAll = { rules: {} }; + const importer = async (extensionName: string) => + extensionName === "eslint/conf/eslint-all" + ? eslintAll + : new Error(`Unknown extension name: '${extensionName}`); + + // Act + const { importedExtensions } = await retrieveExtendsValues({ importer }, "eslint:all"); + + // Assert + expect(importedExtensions).toEqual([eslintAll]); + }); + + it("retrieves eslint-recommended when an extension is named eslint:recommended", async () => { + // Arrange + const eslintRecommended = { rules: {} }; + const importer = async (extensionName: string) => + extensionName === "eslint/conf/eslint-recommended" + ? eslintRecommended + : new Error(`Unknown extension name: '${extensionName}`); + + // Act + const { importedExtensions } = await retrieveExtendsValues( + { importer }, + "eslint:recommended", + ); + + // Assert + expect(importedExtensions).toEqual([eslintRecommended]); + }); + + it("reports a failure when an extension fails to import", async () => { + // Arrange + const error = new Error("Oh no"); + const importer = async () => error; + + // Act + const { configurationErrors } = await retrieveExtendsValues({ importer }, "extension-name"); + + // Assert + expect(configurationErrors).toEqual([expect.objectContaining({ error })]); + }); + + it("retrieves an extension when an import succeeds", async () => { + // Arrange + const extension = { rules: {} }; + const importer = async () => extension; + + // Act + const { importedExtensions } = await retrieveExtendsValues({ importer }, "extension-name"); + + // Assert + expect(importedExtensions).toEqual([extension]); + }); + + it("retrieves multiple extensions when multiple are provided", async () => { + // Arrange + const extensions = { + "eslint-plugin-one": { + rules: { + "rule-one": {}, + }, + }, + "eslint-plugin-two": { + rules: { + "rule-two": {}, + }, + }, + } as const; + const importer = async (extensionName: string) => + extensions[extensionName as keyof typeof extensions]; + + // Act + const { importedExtensions } = await retrieveExtendsValues( + { importer }, + Object.keys(extensions), + ); + + // Assert + expect(importedExtensions).toEqual(Object.values(extensions)); + }); +}); diff --git a/src/creation/simplification/retrieveExtendsValues.ts b/src/creation/simplification/retrieveExtendsValues.ts new file mode 100644 index 000000000..aedb5334e --- /dev/null +++ b/src/creation/simplification/retrieveExtendsValues.ts @@ -0,0 +1,61 @@ +import { Importer } from "../../adapters/importer"; +import { ConfigurationError } from "../../errors/configurationError"; +import { ESLintConfiguration } from "../../input/findESLintConfiguration"; +import { resolveExtensionNames } from "./resolveExtensionNames"; + +export type RetrieveExtendsValuesDependencies = { + importer: Importer; +}; + +export type RetrievedExtensionValues = { + configurationErrors: ConfigurationError[]; + importedExtensions: Partial[]; +}; + +const builtInExtensionGetters = new Map< + string, + (importer: Importer) => Promise +>([ + [ + "eslint:all", + async importer => (await importer("eslint/conf/eslint-all")) as ESLintConfiguration, + ], + [ + "eslint:recommended", + async importer => (await importer("eslint/conf/eslint-recommended")) as ESLintConfiguration, + ], +]); + +export const retrieveExtendsValues = async ( + dependencies: RetrieveExtendsValuesDependencies, + rawExtensionNames: string | string[], +): Promise => { + const importedExtensions: Partial[] = []; + const configurationErrors: ConfigurationError[] = []; + const extensionNames = resolveExtensionNames(rawExtensionNames); + + await Promise.all( + extensionNames.map(async extensionName => { + const getBuiltInExtension = builtInExtensionGetters.get(extensionName); + if (getBuiltInExtension !== undefined) { + importedExtensions.push(await getBuiltInExtension(dependencies.importer)); + return; + } + + const imported = await dependencies.importer(extensionName); + + if (imported instanceof Error) { + configurationErrors.push( + new ConfigurationError( + imported, + `Could not resolve ESLint extension '${extensionName}'.`, + ), + ); + } else { + importedExtensions.push(imported as Partial); + } + }), + ); + + return { configurationErrors, importedExtensions }; +}; diff --git a/src/creation/simplification/simplifyPackageRules.test.ts b/src/creation/simplification/simplifyPackageRules.test.ts new file mode 100644 index 000000000..b95b73075 --- /dev/null +++ b/src/creation/simplification/simplifyPackageRules.test.ts @@ -0,0 +1,103 @@ +import { ConfigurationError } from "../../errors/configurationError"; +import { ESLintRuleOptions } from "../../rules/types"; +import { createEmptyConversionResults } from "../../conversion/conversionResults.stubs"; +import { simplifyPackageRules } from "./simplifyPackageRules"; + +const createStubDependencies = () => ({ + removeExtendsDuplicatedRules: jest.fn(), + retrieveExtendsValues: jest.fn(), +}); + +describe("simplifyPackageRules", () => { + it("returns the conversion results directly when there is no loaded eslint configuration", async () => { + // Arrange + const dependencies = createStubDependencies(); + const eslint = undefined; + const ruleConversionResults = createEmptyConversionResults(); + + // Act + const simplifiedResults = await simplifyPackageRules( + dependencies, + eslint, + ruleConversionResults, + ); + + // Assert + expect(simplifiedResults).toBe(ruleConversionResults); + }); + + it("returns the conversion results directly when the eslint configuration doesn't extend", async () => { + // Arrange + const dependencies = createStubDependencies(); + const eslint = {}; + const ruleConversionResults = createEmptyConversionResults(); + + // Act + const simplifiedResults = await simplifyPackageRules( + dependencies, + eslint, + ruleConversionResults, + ); + + // Assert + expect(simplifiedResults).toBe(ruleConversionResults); + }); + + it("returns the conversion results directly when the eslint configuration has an empty extends", async () => { + // Arrange + const dependencies = createStubDependencies(); + const eslint = { + extends: [], + }; + const ruleConversionResults = createEmptyConversionResults(); + + // Act + const simplifiedResults = await simplifyPackageRules( + dependencies, + eslint, + ruleConversionResults, + ); + + // Assert + expect(simplifiedResults).toBe(ruleConversionResults); + }); + + it("includes deduplicated rules and extension failures when the eslint configuration extends", async () => { + // Arrange + const configurationErrors = [new ConfigurationError(new Error("oh no"), "darn")]; + const deduplicatedRules = new Map([ + [ + "rule-name", + { + ruleArguments: [], + ruleName: "rule-name", + ruleSeverity: "warn", + }, + ], + ]); + const dependencies = { + removeExtendsDuplicatedRules: () => deduplicatedRules, + retrieveExtendsValues: async () => ({ + configurationErrors, + importedExtensions: [], + }), + }; + const eslint = { + extends: ["extension-name"], + }; + const ruleConversionResults = createEmptyConversionResults(); + + // Act + const simplifiedResults = await simplifyPackageRules( + dependencies, + eslint, + ruleConversionResults, + ); + + // Assert + expect(simplifiedResults).toEqual({ + converted: deduplicatedRules, + failed: configurationErrors, + }); + }); +}); diff --git a/src/creation/simplification/simplifyPackageRules.ts b/src/creation/simplification/simplifyPackageRules.ts new file mode 100644 index 000000000..550fff9f6 --- /dev/null +++ b/src/creation/simplification/simplifyPackageRules.ts @@ -0,0 +1,36 @@ +import { SansDependencies } from "../../binding"; +import { RuleConversionResults } from "../../rules/convertRules"; +import { removeExtendsDuplicatedRules } from "./removeExtendsDuplicatedRules"; +import { retrieveExtendsValues } from "./retrieveExtendsValues"; +import { ESLintConfiguration } from "../../input/findESLintConfiguration"; + +export type SimplifyPackageRulesDependencies = { + removeExtendsDuplicatedRules: typeof removeExtendsDuplicatedRules; + retrieveExtendsValues: SansDependencies; +}; + +export type SimplifiedRuleConversionResults = Pick; + +export const simplifyPackageRules = async ( + dependencies: SimplifyPackageRulesDependencies, + eslint: Partial | undefined, + ruleConversionResults: SimplifiedRuleConversionResults, +): Promise => { + if (eslint === undefined || eslint.extends === undefined || eslint.extends.length === 0) { + return ruleConversionResults; + } + + const { configurationErrors, importedExtensions } = await dependencies.retrieveExtendsValues( + eslint.extends, + ); + + const converted = dependencies.removeExtendsDuplicatedRules( + ruleConversionResults.converted, + importedExtensions, + ); + + return { + converted, + failed: [...ruleConversionResults.failed, ...configurationErrors], + }; +}; diff --git a/src/creation/writeConversionResults.test.ts b/src/creation/writeConversionResults.test.ts index 2da4e7999..5af5c928c 100644 --- a/src/creation/writeConversionResults.test.ts +++ b/src/creation/writeConversionResults.test.ts @@ -1,13 +1,15 @@ import { createEmptyConversionResults } from "../conversion/conversionResults.stubs"; import { writeConversionResults } from "./writeConversionResults"; +import { OriginalConfigurations } from "../input/findOriginalConfigurations"; import { formatJsonOutput } from "./formatting/formatters/formatJsonOutput"; -const originalConfigurations = { +const createStubOriginalConfigurations = (overrides: Partial = {}) => ({ tslint: { rulesDirectory: [], rules: {}, }, -}; + ...overrides, +}); describe("writeConversionResults", () => { it("excludes the tslint plugin when there are no missing rules", async () => { @@ -22,7 +24,7 @@ describe("writeConversionResults", () => { { fileSystem }, ".eslintrc.json", conversionResults, - originalConfigurations, + createStubOriginalConfigurations(), ); // Assert @@ -64,7 +66,7 @@ describe("writeConversionResults", () => { { fileSystem }, ".eslintrc.json", conversionResults, - originalConfigurations, + createStubOriginalConfigurations(), ); // Assert @@ -95,4 +97,51 @@ describe("writeConversionResults", () => { }), ); }); + + it("includes the original eslint configuration when it exists", async () => { + // Arrange + const conversionResults = createEmptyConversionResults({ + converted: new Map(), + }); + const eslint = { + env: {}, + extends: [], + globals: { + Promise: true, + }, + rules: {}, + }; + const originalConfigurations = createStubOriginalConfigurations({ + eslint, + }); + const fileSystem = { writeFile: jest.fn().mockReturnValue(Promise.resolve()) }; + + // Act + await writeConversionResults( + { fileSystem }, + ".eslintrc.json", + conversionResults, + originalConfigurations, + ); + + // Assert + expect(fileSystem.writeFile).toHaveBeenLastCalledWith( + ".eslintrc.json", + formatJsonOutput({ + ...eslint, + env: { + browser: true, + es6: true, + node: true, + }, + parser: "@typescript-eslint/parser", + parserOptions: { + project: "tsconfig.json", + sourceType: "module", + }, + plugins: ["@typescript-eslint"], + rules: {}, + }), + ); + }); }); diff --git a/src/creation/writeConversionResults.ts b/src/creation/writeConversionResults.ts index 07f6802d7..4aeb04361 100644 --- a/src/creation/writeConversionResults.ts +++ b/src/creation/writeConversionResults.ts @@ -20,7 +20,9 @@ export const writeConversionResults = async ( if (ruleConversionResults.missing.length !== 0) { plugins.push("@typescript-eslint/tslint"); } + const output = { + ...(originalConfigurations.eslint && originalConfigurations.eslint), env: createEnv(originalConfigurations), parser: "@typescript-eslint/parser", parserOptions: { diff --git a/src/errors/configurationError.ts b/src/errors/configurationError.ts new file mode 100644 index 000000000..5b7662725 --- /dev/null +++ b/src/errors/configurationError.ts @@ -0,0 +1,3 @@ +export class ConfigurationError { + public constructor(public readonly error: Error, public readonly complaint: string) {} +} diff --git a/src/rules/conversionError.ts b/src/errors/conversionError.ts similarity index 75% rename from src/rules/conversionError.ts rename to src/errors/conversionError.ts index 367db4a00..9a06ee374 100644 --- a/src/rules/conversionError.ts +++ b/src/errors/conversionError.ts @@ -1,4 +1,4 @@ -import { TSLintRuleOptions } from "./types"; +import { TSLintRuleOptions } from "../rules/types"; export class ConversionError { public constructor( diff --git a/src/input/findESLintConfiguration.test.ts b/src/input/findESLintConfiguration.test.ts index 08118a901..d19dba98c 100644 --- a/src/input/findESLintConfiguration.test.ts +++ b/src/input/findESLintConfiguration.test.ts @@ -65,6 +65,7 @@ describe("findESLintConfiguration", () => { // Assert expect(result).toEqual({ env: {}, + extends: [], rules: {}, }); }); diff --git a/src/input/findESLintConfiguration.ts b/src/input/findESLintConfiguration.ts index 4171dda42..7a4817cc2 100644 --- a/src/input/findESLintConfiguration.ts +++ b/src/input/findESLintConfiguration.ts @@ -1,3 +1,4 @@ +import { ESLintRuleSeverity } from "../rules/types"; import { TSLintToESLintSettings } from "../types"; import { findConfiguration, FindConfigurationDependencies } from "./findConfiguration"; @@ -5,13 +6,24 @@ export type ESLintConfiguration = { env: { [i: string]: boolean; }; - rules: { - [i: string]: number | [string, any]; - }; + extends: string | string[]; + rules: ESLintConfigurationRules; +}; + +export type ESLintConfigurationRules = { + [i: string]: ESLintConfigurationRuleValue; }; +export type ESLintConfigurationRuleValue = + | 0 + | 1 + | 2 + | ESLintRuleSeverity + | [ESLintRuleSeverity, any]; + const defaultESLintConfiguration = { env: {}, + extends: [], rules: {}, }; diff --git a/src/input/findOriginalConfigurations.test.ts b/src/input/findOriginalConfigurations.test.ts index c1777a887..cd476f2ef 100644 --- a/src/input/findOriginalConfigurations.test.ts +++ b/src/input/findOriginalConfigurations.test.ts @@ -14,6 +14,7 @@ const createRawSettings = () => ({ const createDependencies = (overrides: Partial = {}) => ({ findESLintConfiguration: async () => ({ env: {}, + extends: [], rules: {}, }), findPackagesConfiguration: async () => ({ @@ -85,6 +86,7 @@ describe("findOriginalConfigurations", () => { data: { eslint: { env: {}, + extends: [], rules: {}, }, packages: { diff --git a/src/reporting/reportConversionResults.test.ts b/src/reporting/reportConversionResults.test.ts index ddc3e6477..bbd335d2d 100644 --- a/src/reporting/reportConversionResults.test.ts +++ b/src/reporting/reportConversionResults.test.ts @@ -1,8 +1,9 @@ -import { ConversionError } from "../rules/conversionError"; import { ESLintRuleOptions } from "../rules/types"; import { reportConversionResults } from "./reportConversionResults"; import { createStubLogger, expectEqualWrites } from "../adapters/logger.stubs"; import { createEmptyConversionResults } from "../conversion/conversionResults.stubs"; +import { ConversionError } from "../errors/conversionError"; +import { ConfigurationError } from "../errors/configurationError"; describe("reportConversionResults", () => { it("logs a successful conversion when there is one converted rule", () => { @@ -64,6 +65,25 @@ describe("reportConversionResults", () => { ); }); + it("logs a failed configuration when there is one failed configuration error", () => { + // Arrange + const conversionResults = createEmptyConversionResults({ + failed: [new ConfigurationError(new Error("and a one"), "some complaint")], + }); + + const logger = createStubLogger(); + + // Act + reportConversionResults({ logger }, conversionResults); + + // Assert + expectEqualWrites( + logger.stderr.write, + "💀 1 error thrown. 💀", + `Check ${logger.debugFileName} for details.`, + ); + }); + it("logs a failed conversion when there is one failed conversion", () => { // Arrange const conversionResults = createEmptyConversionResults({ @@ -84,7 +104,7 @@ describe("reportConversionResults", () => { // Assert expectEqualWrites( logger.stderr.write, - "💀 1 rule threw an error; using eslint-plugin-tslint instead. 💀", + "💀 1 error thrown. 💀", `Check ${logger.debugFileName} for details.`, ); }); @@ -114,7 +134,7 @@ describe("reportConversionResults", () => { // Assert expectEqualWrites( logger.stderr.write, - "💀 2 rules threw errors; using eslint-plugin-tslint instead. 💀", + "💀 2 errors thrown. 💀", `Check ${logger.debugFileName} for details.`, ); }); @@ -181,10 +201,10 @@ describe("reportConversionResults", () => { ); }); - it("logs a missing package when there is a missing package", () => { + it("logs a missing plugin when there is a missing plugin", () => { // Arrange const conversionResults = createEmptyConversionResults({ - packages: new Set(["package-one"]), + plugins: new Set(["plugin-one"]), }); const logger = createStubLogger(); @@ -196,14 +216,14 @@ describe("reportConversionResults", () => { expectEqualWrites( logger.stdout.write, "⚡ 1 package is required for new ESLint rules. ⚡", - "\tpackage-one", + "\tplugin-one", ); }); - it("logs missing packages when there are missing packages", () => { + it("logs missing plugins when there are missing plugins", () => { // Arrange const conversionResults = createEmptyConversionResults({ - packages: new Set(["package-one", "package-two"]), + plugins: new Set(["plugin-one", "plugin-two"]), }); const logger = createStubLogger(); @@ -215,8 +235,8 @@ describe("reportConversionResults", () => { expectEqualWrites( logger.stdout.write, "⚡ 2 packages are required for new ESLint rules. ⚡", - "\tpackage-one", - "\tpackage-two", + "\tplugin-one", + "\tplugin-two", ); }); }); diff --git a/src/reporting/reportConversionResults.ts b/src/reporting/reportConversionResults.ts index dd32b4217..4ff8dce5d 100644 --- a/src/reporting/reportConversionResults.ts +++ b/src/reporting/reportConversionResults.ts @@ -2,7 +2,8 @@ import chalk from "chalk"; import { EOL } from "os"; import { Logger } from "../adapters/logger"; -import { ConversionError } from "../rules/conversionError"; +import { ConfigurationError } from "../errors/configurationError"; +import { ConversionError } from "../errors/conversionError"; import { RuleConversionResults } from "../rules/convertRules"; import { TSLintRuleOptions, ESLintRuleOptions } from "../rules/types"; @@ -26,8 +27,8 @@ export const reportConversionResults = ( logMissingRules(ruleConversionResults.missing, dependencies.logger); } - if (ruleConversionResults.packages.size !== 0) { - logMissingPackages(ruleConversionResults.packages, dependencies.logger); + if (ruleConversionResults.plugins.size !== 0) { + logMissingPlugins(ruleConversionResults.plugins, dependencies.logger); } }; @@ -41,19 +42,18 @@ const logSuccessfulConversions = (converted: Map, log logger.stdout.write(chalk.greenBright(` ✨${EOL}`)); }; -const logFailedConversions = (failed: ConversionError[], logger: Logger) => { +const logFailedConversions = (failed: (ConfigurationError | ConversionError)[], logger: Logger) => { logger.stderr.write(`${chalk.redBright(`💀 ${failed.length}`)}`); - logger.stderr.write( - chalk.red(` rule${failed.length === 1 ? " threw an error" : "s threw errors"}`), - ); - logger.stderr.write(chalk.red("; using eslint-plugin-tslint instead.")); + logger.stderr.write(chalk.red(` error${failed.length === 1 ? "" : "s"}`)); + logger.stderr.write(chalk.red(" thrown.")); logger.stderr.write(chalk.redBright(` 💀${EOL}`)); logger.info.write( failed - .map( - failed => - `${failed.tslintRule.ruleName} threw an error during conversion: ${failed.error.stack}.${EOL}`, + .map(failed => + failed instanceof ConfigurationError + ? `${failed.complaint}: ${failed.error.stack}${EOL}` + : `${failed.tslintRule.ruleName} threw an error during conversion: ${failed.error.stack}${EOL}`, ) .join(""), ); @@ -80,16 +80,16 @@ const logMissingRules = (missing: TSLintRuleOptions[], logger: Logger) => { ); }; -const logMissingPackages = (packages: Set, logger: Logger) => { - logger.stdout.write(chalk.cyanBright(`⚡ ${packages.size}`)); +const logMissingPlugins = (plugins: Set, logger: Logger) => { + logger.stdout.write(chalk.cyanBright(`⚡ ${plugins.size}`)); logger.stdout.write(chalk.cyan(" package")); - logger.stdout.write(chalk.cyan(packages.size === 1 ? " is" : "s are")); + logger.stdout.write(chalk.cyan(plugins.size === 1 ? " is" : "s are")); logger.stdout.write(chalk.cyan(` required for new ESLint rules.`)); logger.stdout.write(chalk.cyanBright(` ⚡${EOL}`)); logger.stdout.write( - Array.from(packages) - .map(packageName => `\t${chalk.cyanBright(packageName)}${EOL}`) + Array.from(plugins) + .map(pluginName => `\t${chalk.cyanBright(pluginName)}${EOL}`) .join(""), ); }; diff --git a/src/rules/convertRule.test.ts b/src/rules/convertRule.test.ts index 3448d6efe..74776ae84 100644 --- a/src/rules/convertRule.test.ts +++ b/src/rules/convertRule.test.ts @@ -1,6 +1,6 @@ +import { ConversionError } from "../errors/conversionError"; import { convertRule } from "./convertRule"; import { RuleConverter } from "./converter"; -import { ConversionError } from "./conversionError"; import { TSLintRuleOptions } from "./types"; describe("convertRule", () => { diff --git a/src/rules/convertRule.ts b/src/rules/convertRule.ts index a9a4622c5..80a6ded7c 100644 --- a/src/rules/convertRule.ts +++ b/src/rules/convertRule.ts @@ -1,6 +1,6 @@ -import { TSLintRuleOptions } from "./types"; -import { ConversionError } from "./conversionError"; +import { ConversionError } from "../errors/conversionError"; import { RuleConverter } from "./converter"; +import { TSLintRuleOptions } from "./types"; export const convertRule = ( tslintRule: TSLintRuleOptions, diff --git a/src/rules/convertRuleSeverity.test.ts b/src/rules/convertRuleSeverity.test.ts index a200969dc..c72b200d2 100644 --- a/src/rules/convertRuleSeverity.test.ts +++ b/src/rules/convertRuleSeverity.test.ts @@ -1,4 +1,4 @@ -import { convertRuleSeverity } from "./convertRuleSeverity"; +import { convertTSLintRuleSeverity, convertRawESLintRuleSeverity } from "./convertRuleSeverity"; describe("convertRuleSeverity", () => { it("returns error when the severity is error", () => { @@ -6,7 +6,7 @@ describe("convertRuleSeverity", () => { const tslintSeverity = "error"; // Act - const eslintSeverity = convertRuleSeverity(tslintSeverity); + const eslintSeverity = convertTSLintRuleSeverity(tslintSeverity); // Assert expect(eslintSeverity).toEqual("error"); @@ -17,7 +17,7 @@ describe("convertRuleSeverity", () => { const tslintSeverity = "off"; // Act - const eslintSeverity = convertRuleSeverity(tslintSeverity); + const eslintSeverity = convertTSLintRuleSeverity(tslintSeverity); // Assert expect(eslintSeverity).toEqual("off"); @@ -28,9 +28,55 @@ describe("convertRuleSeverity", () => { const tslintSeverity = "warning"; // Act - const eslintSeverity = convertRuleSeverity(tslintSeverity); + const eslintSeverity = convertTSLintRuleSeverity(tslintSeverity); // Assert expect(eslintSeverity).toEqual("warn"); }); }); + +describe("convertRawESLintRuleSeverity", () => { + it("returns off when the severity is 0", () => { + // Arrange + const rawSeverity = 0; + + // Act + const converted = convertRawESLintRuleSeverity(rawSeverity); + + // Assert + expect(converted).toEqual("off"); + }); + + it("returns off when the severity is 2", () => { + // Arrange + const rawSeverity = 1; + + // Act + const converted = convertRawESLintRuleSeverity(rawSeverity); + + // Assert + expect(converted).toEqual("warn"); + }); + + it("returns off when the severity is 3", () => { + // Arrange + const rawSeverity = 2; + + // Act + const converted = convertRawESLintRuleSeverity(rawSeverity); + + // Assert + expect(converted).toEqual("error"); + }); + + it("returns the original severity when it's a string", () => { + // Arrange + const rawSeverity = "warn"; + + // Act + const converted = convertRawESLintRuleSeverity(rawSeverity); + + // Assert + expect(converted).toEqual("warn"); + }); +}); diff --git a/src/rules/convertRuleSeverity.ts b/src/rules/convertRuleSeverity.ts index a33b957f5..861a1eab4 100644 --- a/src/rules/convertRuleSeverity.ts +++ b/src/rules/convertRuleSeverity.ts @@ -3,6 +3,26 @@ import { ESLintRuleSeverity, TSLintRuleSeverity } from "./types"; /** * Converts a TSLint rule severity string to the ESLint equivalent. */ -export const convertRuleSeverity = (tslintSeverity: TSLintRuleSeverity): ESLintRuleSeverity => { +export const convertTSLintRuleSeverity = ( + tslintSeverity: TSLintRuleSeverity, +): ESLintRuleSeverity => { return tslintSeverity === "warning" ? "warn" : tslintSeverity; }; + +export const convertRawESLintRuleSeverity = ( + rawSeverity: 0 | 1 | 2 | ESLintRuleSeverity, +): ESLintRuleSeverity => { + switch (rawSeverity) { + case 0: + return "off"; + + case 1: + return "warn"; + + case 2: + return "error"; + + default: + return rawSeverity; + } +}; diff --git a/src/rules/convertRules.test.ts b/src/rules/convertRules.test.ts index 5b3c3518d..bb1108904 100644 --- a/src/rules/convertRules.test.ts +++ b/src/rules/convertRules.test.ts @@ -1,6 +1,6 @@ +import { ConversionError } from "../errors/conversionError"; import { convertRules } from "./convertRules"; import { TSLintRuleOptions } from "./types"; -import { ConversionError } from "./conversionError"; describe("convertRules", () => { it("doesn't marks a disabled rule as missing when its converter returns undefined", () => { @@ -182,7 +182,7 @@ describe("convertRules", () => { ); }); - it("marks a new package when a conversion has a new package", () => { + it("marks a new plugin when a conversion has a new plugin", () => { // Arrange const tslintRule: TSLintRuleOptions = { ruleArguments: [], @@ -190,19 +190,19 @@ describe("convertRules", () => { ruleSeverity: "error", }; const conversionResult = { - packages: ["extra-package"], + plugins: ["extra-plugin"], rules: [], }; const converters = new Map([[tslintRule.ruleName, () => conversionResult]]); const mergers = new Map(); // Act - const { packages } = convertRules( + const { plugins } = convertRules( { converters, mergers }, { [tslintRule.ruleName]: tslintRule }, ); // Assert - expect(packages).toEqual(new Set(["extra-package"])); + expect(plugins).toEqual(new Set(["extra-plugin"])); }); }); diff --git a/src/rules/convertRules.ts b/src/rules/convertRules.ts index d5bf1763a..31fef3f25 100644 --- a/src/rules/convertRules.ts +++ b/src/rules/convertRules.ts @@ -1,8 +1,9 @@ +import { ConfigurationError } from "../errors/configurationError"; +import { ConversionError } from "../errors/conversionError"; import { TSLintConfigurationRules } from "../input/findTSLintConfiguration"; -import { ConversionError } from "./conversionError"; import { RuleConverter } from "./converter"; import { convertRule } from "./convertRule"; -import { convertRuleSeverity } from "./convertRuleSeverity"; +import { convertTSLintRuleSeverity } from "./convertRuleSeverity"; import { formatRawTslintRule } from "./formatRawTslintRule"; import { RuleMerger } from "./merger"; import { TSLintRuleOptions, ESLintRuleOptions } from "./types"; @@ -14,9 +15,9 @@ export type ConvertRulesDependencies = { export type RuleConversionResults = { converted: Map; - failed: ConversionError[]; + failed: (ConfigurationError | ConversionError)[]; missing: TSLintRuleOptions[]; - packages: Set; + plugins: Set; }; export const convertRules = ( @@ -26,7 +27,7 @@ export const convertRules = ( const converted = new Map(); const failed: ConversionError[] = []; const missing: TSLintRuleOptions[] = []; - const packages = new Set(); + const plugins = new Set(); for (const [ruleName, value] of Object.entries(rawTslintRules)) { const tslintRule = formatRawTslintRule(ruleName, value); @@ -49,7 +50,7 @@ export const convertRules = ( const existingConversion = converted.get(changes.ruleName); const newConversion = { ...changes, - ruleSeverity: convertRuleSeverity(tslintRule.ruleSeverity), + ruleSeverity: convertTSLintRuleSeverity(tslintRule.ruleSeverity), }; if (existingConversion === undefined) { @@ -78,12 +79,12 @@ export const convertRules = ( } } - if (conversion.packages !== undefined) { - for (const newPackage of conversion.packages) { - packages.add(newPackage); + if (conversion.plugins !== undefined) { + for (const newPlugin of conversion.plugins) { + plugins.add(newPlugin); } } } - return { converted, failed, missing, packages }; + return { converted, failed, missing, plugins }; }; diff --git a/src/rules/converter.ts b/src/rules/converter.ts index 1a275b7b7..77d63fb2a 100644 --- a/src/rules/converter.ts +++ b/src/rules/converter.ts @@ -1,4 +1,4 @@ -import { ConversionError } from "./conversionError"; +import { ConversionError } from "../errors/conversionError"; import { TSLintRuleOptions } from "./types"; /** @@ -23,9 +23,9 @@ export type ConversionResult = { notices?: string[]; /** - * Any packages that should now be installed if not already. + * Any plugins that should now be installed if not already. */ - packages?: string[]; + plugins?: string[]; /** * At least one equivalent ESLint rule and options.