From 77317dbd2c6e204772f63c0f6df6d2b5d30f1e35 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 20 Oct 2019 13:55:25 -0400 Subject: [PATCH 1/4] WIP: Factor in 'raw' configurations on top of 'reported' ones --- package-lock.json | 3 +- package.json | 1 + .../{importer.ts => nativeImporter.ts} | 2 +- src/cli/main.ts | 15 ++- src/conversion/convertConfig.test.ts | 7 +- src/conversion/convertConfig.ts | 4 +- src/creation/eslint/createEnv.ts | 4 +- .../formatting/formatters/formatJsonOutput.ts | 8 +- .../simplification/retrieveExtendsValues.ts | 36 +++--- src/creation/writeConversionResults.test.ts | 87 ++++++++++--- src/creation/writeConversionResults.ts | 13 +- src/input/findESLintConfiguration.test.ts | 117 ++++++++++++++++-- src/input/findESLintConfiguration.ts | 67 +++++++--- src/input/findOriginalConfigurations.test.ts | 67 ++++++---- src/input/findOriginalConfigurations.ts | 13 +- src/input/findPackagesConfiguration.ts | 9 +- src/input/findRawConfiguration.ts | 21 ++++ ...t.ts => findReportedConfiguration.test.ts} | 12 +- ...ration.ts => findReportedConfiguration.ts} | 4 +- src/input/findTSLintConfiguration.ts | 51 +++++--- src/input/findTslintConfiguration.test.ts | 70 +++++++++-- src/input/findTypeScriptConfiguration.ts | 9 +- src/input/importer.ts | 50 ++++++++ src/input/mergeLintConfigurations.test.ts | 81 +++++++----- src/input/mergeLintConfigurations.ts | 20 +-- 25 files changed, 589 insertions(+), 182 deletions(-) rename src/adapters/{importer.ts => nativeImporter.ts} (70%) create mode 100644 src/input/findRawConfiguration.ts rename src/input/{findConfiguration.test.ts => findReportedConfiguration.test.ts} (75%) rename src/input/{findConfiguration.ts => findReportedConfiguration.ts} (88%) create mode 100644 src/input/importer.ts diff --git a/package-lock.json b/package-lock.json index 4632e3d0d..f5695cf4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6781,8 +6781,7 @@ "strip-json-comments": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", - "dev": true + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==" }, "supports-color": { "version": "5.5.0", diff --git a/package.json b/package.json index 658825674..419df3b10 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "dependencies": { "chalk": "2.4.2", "commander": "3.0.2", + "strip-json-comments": "^3.0.1", "tslint": "5.20.0", "typescript": "3.6.3" }, diff --git a/src/adapters/importer.ts b/src/adapters/nativeImporter.ts similarity index 70% rename from src/adapters/importer.ts rename to src/adapters/nativeImporter.ts index 7aa40a2f6..c0975b7ba 100644 --- a/src/adapters/importer.ts +++ b/src/adapters/nativeImporter.ts @@ -1,4 +1,4 @@ -export type Importer = (moduleName: string) => unknown | Error; +export type NativeImporter = (moduleName: string) => unknown | Error; export const nativeImporter = async (moduleName: string) => { try { diff --git a/src/cli/main.ts b/src/cli/main.ts index c6a3535f1..3460bdf28 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -1,9 +1,9 @@ import { EOL } from "os"; -import { nativeImporter } from "../adapters/importer"; -import { processLogger } from "../adapters/processLogger"; import { childProcessExec } from "../adapters/childProcessExec"; import { fsFileSystem } from "../adapters/fsFileSystem"; +import { nativeImporter } from "../adapters/nativeImporter"; +import { processLogger } from "../adapters/processLogger"; import { bind } from "../binding"; import { ConvertConfigDependencies, convertConfig } from "../conversion/convertConfig"; import { removeExtendsDuplicatedRules } from "../creation/simplification/removeExtendsDuplicatedRules"; @@ -27,6 +27,7 @@ import { findPackagesConfiguration } from "../input/findPackagesConfiguration"; import { findESLintConfiguration } from "../input/findESLintConfiguration"; import { findTSLintConfiguration } from "../input/findTSLintConfiguration"; import { findTypeScriptConfiguration } from "../input/findTypeScriptConfiguration"; +import { importer, ImporterDependencies } from "../input/importer"; import { mergeLintConfigurations } from "../input/mergeLintConfigurations"; import { reportConversionResults, @@ -42,8 +43,16 @@ const convertRulesDependencies = { mergers, }; +const nativeImporterDependencies: ImporterDependencies = { + fileSystem: fsFileSystem, + nativeImporter: nativeImporter, +}; + +const boundImporter = bind(importer, nativeImporterDependencies); + const findConfigurationDependencies = { exec: childProcessExec, + importer: boundImporter, }; const findOriginalConfigurationsDependencies: FindOriginalConfigurationsDependencies = { @@ -59,7 +68,7 @@ const reportConversionResultsDependencies: ReportConversionResultsDependencies = }; const retrieveExtendsValuesDependencies: RetrieveExtendsValuesDependencies = { - importer: nativeImporter, + importer: boundImporter, }; const simplifyPackageRulesDependencies: SimplifyPackageRulesDependencies = { diff --git a/src/conversion/convertConfig.test.ts b/src/conversion/convertConfig.test.ts index aadc25758..8a456bb12 100644 --- a/src/conversion/convertConfig.test.ts +++ b/src/conversion/convertConfig.test.ts @@ -21,8 +21,11 @@ const createStubDependencies = ( const createStubOriginalConfigurationsData = () => ({ tslint: { - rules: [], - rulesDirectory: [], + full: { + rules: [], + rulesDirectory: [], + }, + raw: {}, }, }); diff --git a/src/conversion/convertConfig.ts b/src/conversion/convertConfig.ts index a6308d039..6757e49d9 100644 --- a/src/conversion/convertConfig.ts +++ b/src/conversion/convertConfig.ts @@ -30,14 +30,14 @@ export const convertConfig = async ( // 2. TSLint rules are converted into their ESLint configurations const ruleConversionResults = dependencies.convertRules( - originalConfigurations.data.tslint.rules, + originalConfigurations.data.tslint.full.rules, ); // 3. ESLint configurations are simplified based on extended ESLint presets const simplifiedConfiguration = { ...ruleConversionResults, ...(await dependencies.simplifyPackageRules( - originalConfigurations.data.eslint, + originalConfigurations.data.eslint && originalConfigurations.data.eslint.full, ruleConversionResults, )), }; diff --git a/src/creation/eslint/createEnv.ts b/src/creation/eslint/createEnv.ts index 4022ad4c8..b878a4eb0 100644 --- a/src/creation/eslint/createEnv.ts +++ b/src/creation/eslint/createEnv.ts @@ -1,9 +1,9 @@ -import { OriginalConfigurations } from "../../input/findOriginalConfigurations"; +import { AllOriginalConfigurations } from "../../input/findOriginalConfigurations"; export const createEnv = ({ packages, typescript, -}: Pick) => { +}: Pick) => { const browser = typescript === undefined || typescript.compilerOptions.lib === undefined || diff --git a/src/creation/formatting/formatters/formatJsonOutput.ts b/src/creation/formatting/formatters/formatJsonOutput.ts index 6aceb00ee..31e76e66d 100644 --- a/src/creation/formatting/formatters/formatJsonOutput.ts +++ b/src/creation/formatting/formatters/formatJsonOutput.ts @@ -1,4 +1,8 @@ import { EOL } from "os"; -export const formatJsonOutput = (configuration: unknown) => - `${JSON.stringify(configuration, undefined, 4)}${EOL}`; +export const formatJsonOutput = (configuration: any) => { + const keys = Object.keys(configuration).sort(); + const sortedConfiguration = Object.fromEntries(keys.map(key => [key, configuration[key]])); + + return `${JSON.stringify(sortedConfiguration, undefined, 4)}${EOL}`; +}; diff --git a/src/creation/simplification/retrieveExtendsValues.ts b/src/creation/simplification/retrieveExtendsValues.ts index aedb5334e..6138d23ff 100644 --- a/src/creation/simplification/retrieveExtendsValues.ts +++ b/src/creation/simplification/retrieveExtendsValues.ts @@ -1,10 +1,11 @@ -import { Importer } from "../../adapters/importer"; +import { SansDependencies } from "../../binding"; import { ConfigurationError } from "../../errors/configurationError"; import { ESLintConfiguration } from "../../input/findESLintConfiguration"; +import { importer } from "../../input/importer"; import { resolveExtensionNames } from "./resolveExtensionNames"; export type RetrieveExtendsValuesDependencies = { - importer: Importer; + importer: SansDependencies; }; export type RetrievedExtensionValues = { @@ -12,20 +13,6 @@ export type RetrievedExtensionValues = { 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[], @@ -34,11 +21,26 @@ export const retrieveExtendsValues = async ( const configurationErrors: ConfigurationError[] = []; const extensionNames = resolveExtensionNames(rawExtensionNames); + const builtInExtensionGetters = new Map Promise>([ + [ + "eslint:all", + async () => + (await dependencies.importer("eslint/conf/eslint-all")) as ESLintConfiguration, + ], + [ + "eslint:recommended", + async () => + (await dependencies.importer( + "eslint/conf/eslint-recommended", + )) as ESLintConfiguration, + ], + ]); + await Promise.all( extensionNames.map(async extensionName => { const getBuiltInExtension = builtInExtensionGetters.get(extensionName); if (getBuiltInExtension !== undefined) { - importedExtensions.push(await getBuiltInExtension(dependencies.importer)); + importedExtensions.push(await getBuiltInExtension()); return; } diff --git a/src/creation/writeConversionResults.test.ts b/src/creation/writeConversionResults.test.ts index 5af5c928c..b64e1c6a6 100644 --- a/src/creation/writeConversionResults.test.ts +++ b/src/creation/writeConversionResults.test.ts @@ -1,12 +1,15 @@ import { createEmptyConversionResults } from "../conversion/conversionResults.stubs"; import { writeConversionResults } from "./writeConversionResults"; -import { OriginalConfigurations } from "../input/findOriginalConfigurations"; +import { AllOriginalConfigurations } from "../input/findOriginalConfigurations"; import { formatJsonOutput } from "./formatting/formatters/formatJsonOutput"; -const createStubOriginalConfigurations = (overrides: Partial = {}) => ({ +const createStubOriginalConfigurations = (overrides: Partial = {}) => ({ tslint: { - rulesDirectory: [], - rules: {}, + full: { + rulesDirectory: [], + rules: {}, + }, + raw: {}, }, ...overrides, }); @@ -14,9 +17,7 @@ const createStubOriginalConfigurations = (overrides: Partial { it("excludes the tslint plugin when there are no missing rules", async () => { // Arrange - const conversionResults = createEmptyConversionResults({ - converted: new Map(), - }); + const conversionResults = createEmptyConversionResults(); const fileSystem = { writeFile: jest.fn().mockReturnValue(Promise.resolve()) }; // Act @@ -50,7 +51,6 @@ describe("writeConversionResults", () => { it("includes typescript-eslint plugin settings when there are missing rules", async () => { // Arrange const conversionResults = createEmptyConversionResults({ - converted: new Map(), missing: [ { ruleArguments: [], @@ -100,16 +100,17 @@ describe("writeConversionResults", () => { it("includes the original eslint configuration when it exists", async () => { // Arrange - const conversionResults = createEmptyConversionResults({ - converted: new Map(), - }); + const conversionResults = createEmptyConversionResults(); const eslint = { - env: {}, - extends: [], - globals: { - Promise: true, + full: { + env: {}, + extends: [], + globals: { + Promise: true, + }, + rules: {}, }, - rules: {}, + raw: {}, }; const originalConfigurations = createStubOriginalConfigurations({ eslint, @@ -128,19 +129,71 @@ describe("writeConversionResults", () => { expect(fileSystem.writeFile).toHaveBeenLastCalledWith( ".eslintrc.json", formatJsonOutput({ - ...eslint, env: { browser: true, es6: true, node: true, }, + extends: [], + rules: {}, parser: "@typescript-eslint/parser", parserOptions: { project: "tsconfig.json", sourceType: "module", }, plugins: ["@typescript-eslint"], + }), + ); + }); + + it("includes raw globals when they exist", async () => { + // Arrange + const conversionResults = createEmptyConversionResults(); + const eslint = { + full: { + env: {}, + extends: [], rules: {}, + }, + raw: { + globals: { + Promise: true, + }, + }, + }; + 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({ + env: { + browser: true, + es6: true, + node: true, + }, + extends: [], + rules: {}, + globals: { + Promise: true, + }, + parser: "@typescript-eslint/parser", + parserOptions: { + project: "tsconfig.json", + sourceType: "module", + }, + plugins: ["@typescript-eslint"], }), ); }); diff --git a/src/creation/writeConversionResults.ts b/src/creation/writeConversionResults.ts index 49be3ebd7..6fc0ebd99 100644 --- a/src/creation/writeConversionResults.ts +++ b/src/creation/writeConversionResults.ts @@ -1,7 +1,7 @@ import { FileSystem } from "../adapters/fileSystem"; import { RuleConversionResults } from "../rules/convertRules"; import { formatConvertedRules } from "./formatConvertedRules"; -import { OriginalConfigurations } from "../input/findOriginalConfigurations"; +import { AllOriginalConfigurations } from "../input/findOriginalConfigurations"; import { createEnv } from "./eslint/createEnv"; import { formatOutput } from "./formatting/formatOutput"; @@ -13,24 +13,29 @@ export const writeConversionResults = async ( dependencies: WriteConversionResultsDependencies, outputPath: string, ruleConversionResults: RuleConversionResults, - originalConfigurations: OriginalConfigurations, + originalConfigurations: AllOriginalConfigurations, ) => { const plugins = ["@typescript-eslint"]; + const { eslint, tslint } = originalConfigurations; if (ruleConversionResults.missing.length !== 0) { plugins.push("@typescript-eslint/tslint"); } const output = { - ...(originalConfigurations.eslint && originalConfigurations.eslint), + ...(eslint && eslint.full), env: createEnv(originalConfigurations), + ...(eslint && { globals: eslint.raw.globals }), parser: "@typescript-eslint/parser", parserOptions: { project: "tsconfig.json", sourceType: "module", }, plugins, - rules: formatConvertedRules(ruleConversionResults, originalConfigurations.tslint), + rules: { + ...(eslint && eslint.full.rules), + ...formatConvertedRules(ruleConversionResults, tslint.full), + }, }; return await dependencies.fileSystem.writeFile(outputPath, formatOutput(outputPath, output)); diff --git a/src/input/findESLintConfiguration.test.ts b/src/input/findESLintConfiguration.test.ts index d19dba98c..01529dc8f 100644 --- a/src/input/findESLintConfiguration.test.ts +++ b/src/input/findESLintConfiguration.test.ts @@ -1,7 +1,16 @@ -import { findESLintConfiguration } from "./findESLintConfiguration"; +import { + findESLintConfiguration, + FindESLintConfigurationDependencies, +} from "./findESLintConfiguration"; import { createStubExec, createStubThrowingExec } from "../adapters/exec.stubs"; import { TSLintToESLintSettings } from "../types"; +const createStubDependencies = (overrides: Partial = {}) => ({ + exec: createStubExec({ stdout: "{}" }), + importer: async () => ({}), + ...overrides, +}); + const createStubRawSettings = (overrides: Partial = {}) => ({ config: "./eslintrc.js", eslint: undefined, @@ -9,10 +18,32 @@ const createStubRawSettings = (overrides: Partial = {}) }); describe("findESLintConfiguration", () => { - it("returns an error when one occurs", async () => { + it("returns an error when exec returns one", async () => { // Arrange const message = "error"; - const dependencies = { exec: createStubThrowingExec({ stderr: message }) }; + const dependencies = createStubDependencies({ + exec: createStubThrowingExec({ stderr: message }), + }); + + // Act + const result = await findESLintConfiguration(dependencies, createStubRawSettings()); + + // Assert + expect(result).toEqual( + expect.objectContaining({ + message, + }), + ); + }); + + it("returns an error when importer returns one", async () => { + // Arrange + const message = "error"; + const dependencies = createStubDependencies({ + importer: async () => { + throw new Error(message); + }, + }); // Act const result = await findESLintConfiguration(dependencies, createStubRawSettings()); @@ -27,7 +58,7 @@ describe("findESLintConfiguration", () => { it("defaults the configuration file when one isn't provided", async () => { // Arrange - const dependencies = { exec: createStubExec() }; + const dependencies = createStubDependencies({ exec: createStubExec() }); // Act await findESLintConfiguration(dependencies, createStubRawSettings()); @@ -38,7 +69,7 @@ describe("findESLintConfiguration", () => { it("includes a configuration file in the ESLint command when one is provided", async () => { // Arrange - const dependencies = { exec: createStubExec() }; + const dependencies = createStubDependencies({ exec: createStubExec() }); const config = createStubRawSettings({ eslint: "./custom/eslintrc.js", }); @@ -54,7 +85,72 @@ describe("findESLintConfiguration", () => { it("applies ESLint defaults when none are provided", async () => { // Arrange - const dependencies = { exec: createStubExec({ stdout: "{}" }) }; + const dependencies = createStubDependencies({ exec: createStubExec({ stdout: "{}" }) }); + const config = createStubRawSettings({ + eslint: "./custom/eslintrc.js", + }); + + // Act + const result = await findESLintConfiguration(dependencies, config); + + // Assert + expect(result).toEqual({ + full: { + env: {}, + extends: [], + rules: {}, + }, + raw: { + extends: [], + }, + }); + }); + + it("doesn't apply raw extends on top of reported when they don't exist", async () => { + // Arrange + const reportedExtends = ["reported"]; + const dependencies = createStubDependencies({ + exec: createStubExec({ + stdout: JSON.stringify({ + extends: reportedExtends, + }), + }), + importer: async () => ({}), + }); + const config = createStubRawSettings({ + eslint: "./custom/eslintrc.js", + }); + + // Act + const result = await findESLintConfiguration(dependencies, config); + + // Assert + expect(result).toEqual({ + full: { + env: {}, + extends: ["reported"], + rules: {}, + }, + raw: { + extends: [], + }, + }); + }); + + it("applies raw extends on top of reported when they exist", async () => { + // Arrange + const raw = { + extends: ["raw", "duplicated"], + }; + const reportedExtends = ["reported", "duplicated"]; + const dependencies = createStubDependencies({ + exec: createStubExec({ + stdout: JSON.stringify({ + extends: reportedExtends, + }), + }), + importer: async () => raw, + }); const config = createStubRawSettings({ eslint: "./custom/eslintrc.js", }); @@ -64,9 +160,12 @@ describe("findESLintConfiguration", () => { // Assert expect(result).toEqual({ - env: {}, - extends: [], - rules: {}, + full: { + env: {}, + extends: ["raw", "duplicated", "reported"], + rules: {}, + }, + raw, }); }); }); diff --git a/src/input/findESLintConfiguration.ts b/src/input/findESLintConfiguration.ts index 7a4817cc2..1d23181e3 100644 --- a/src/input/findESLintConfiguration.ts +++ b/src/input/findESLintConfiguration.ts @@ -1,12 +1,16 @@ +import { Exec } from "../adapters/exec"; +import { SansDependencies } from "../binding"; import { ESLintRuleSeverity } from "../rules/types"; import { TSLintToESLintSettings } from "../types"; -import { findConfiguration, FindConfigurationDependencies } from "./findConfiguration"; +import { findRawConfiguration } from "./findRawConfiguration"; +import { findReportedConfiguration } from "./findReportedConfiguration"; +import { OriginalConfigurations } from "./findOriginalConfigurations"; +import { importer } from "./importer"; export type ESLintConfiguration = { - env: { - [i: string]: boolean; - }; + env: Record; extends: string | string[]; + globals?: Record; rules: ESLintConfigurationRules; }; @@ -27,20 +31,47 @@ const defaultESLintConfiguration = { rules: {}, }; +export type FindESLintConfigurationDependencies = { + exec: Exec; + importer: SansDependencies; +}; + export const findESLintConfiguration = async ( - dependencies: FindConfigurationDependencies, + dependencies: FindESLintConfigurationDependencies, rawSettings: Pick, -): Promise => { - const rawConfiguration = await findConfiguration( - dependencies.exec, - "eslint --print-config", - rawSettings.eslint || rawSettings.config, - ); - - return rawConfiguration instanceof Error - ? rawConfiguration - : { - ...defaultESLintConfiguration, - ...rawConfiguration, - }; +): Promise | Error> => { + const filePath = rawSettings.eslint || rawSettings.config; + const [rawConfiguration, reportedConfiguration] = await Promise.all([ + findRawConfiguration>(dependencies.importer, filePath, { + extends: [], + }), + findReportedConfiguration>( + dependencies.exec, + "eslint --print-config", + filePath, + ), + ]); + + if (rawConfiguration instanceof Error) { + return rawConfiguration; + } + + if (reportedConfiguration instanceof Error) { + return reportedConfiguration; + } + + return { + full: { + ...defaultESLintConfiguration, + ...reportedConfiguration, + extends: Array.from( + new Set( + [[rawConfiguration.extends || []], [reportedConfiguration.extends || []]].flat( + Infinity, + ), + ), + ), + }, + raw: rawConfiguration, + }; }; diff --git a/src/input/findOriginalConfigurations.test.ts b/src/input/findOriginalConfigurations.test.ts index 98a793960..7c476711a 100644 --- a/src/input/findOriginalConfigurations.test.ts +++ b/src/input/findOriginalConfigurations.test.ts @@ -1,6 +1,7 @@ import { findOriginalConfigurations, FindOriginalConfigurationsDependencies, + OriginalConfigurations, } from "./findOriginalConfigurations"; import { ResultStatus, TSLintToESLintSettings } from "../types"; import { TSLintConfiguration } from "./findTSLintConfiguration"; @@ -11,27 +12,37 @@ const createRawSettings = (overrides: Partial = {}) => ( ...overrides, }); -const createDependencies = (overrides: Partial = {}) => ({ +const createStubDependencies = ( + overrides: Partial = {}, +) => ({ findESLintConfiguration: async () => ({ - env: {}, - extends: [], - rules: {}, + full: { + env: {}, + extends: [], + rules: {}, + }, + raw: {}, }), findPackagesConfiguration: async () => ({ dependencies: {}, devDependencies: {}, }), findTSLintConfiguration: async () => ({ - rulesDirectory: [], - rules: {}, + full: { + rulesDirectory: [], + rules: {}, + }, + raw: {}, }), findTypeScriptConfiguration: async () => ({ compilerOptions: { target: "es3", }, }), - mergeLintConfigurations: (_: ESLintConfiguration | Error, tslint: TSLintConfiguration) => - tslint, + mergeLintConfigurations: ( + _: OriginalConfigurations | Error, + tslint: OriginalConfigurations, + ) => tslint, ...overrides, }); @@ -39,7 +50,7 @@ describe("findOriginalConfigurations", () => { it("returns errors when the tslint finder returns an error", async () => { // Arrange const complaint = "Complaint from TSLint"; - const dependencies = createDependencies({ + const dependencies = createStubDependencies({ findTSLintConfiguration: async () => new Error(complaint), }); @@ -55,7 +66,7 @@ describe("findOriginalConfigurations", () => { it("returns only tslint results when the other finders return errors", async () => { // Arrange - const dependencies = createDependencies({ + const dependencies = createStubDependencies({ findESLintConfiguration: async () => new Error("one"), findPackagesConfiguration: async () => new Error("two"), findTypeScriptConfiguration: async () => new Error("three"), @@ -68,8 +79,11 @@ describe("findOriginalConfigurations", () => { expect(result).toEqual({ data: { tslint: { - rulesDirectory: [], - rules: {}, + full: { + rulesDirectory: [], + rules: {}, + }, + raw: {}, }, }, status: ResultStatus.Succeeded, @@ -79,7 +93,7 @@ describe("findOriginalConfigurations", () => { it("returns an error when an optional configuration returns an error and the user asked for it", async () => { // Arrange const eslint = new Error("one"); - const dependencies = createDependencies({ + const dependencies = createStubDependencies({ findESLintConfiguration: async () => eslint, }); @@ -100,7 +114,7 @@ describe("findOriginalConfigurations", () => { it("returns successful results when an optional configuration returns an error and the user didn't ask for it", async () => { // Arrange - const dependencies = createDependencies({ + const dependencies = createStubDependencies({ findESLintConfiguration: async () => new Error("one"), }); @@ -115,8 +129,11 @@ describe("findOriginalConfigurations", () => { devDependencies: {}, }, tslint: { - rulesDirectory: [], - rules: {}, + full: { + rulesDirectory: [], + rules: {}, + }, + raw: {}, }, typescript: { compilerOptions: { @@ -130,7 +147,7 @@ describe("findOriginalConfigurations", () => { it("returns successful results when all finders succeed", async () => { // Arrange - const dependencies = createDependencies(); + const dependencies = createStubDependencies(); // Act const result = await findOriginalConfigurations(dependencies, createRawSettings()); @@ -139,17 +156,23 @@ describe("findOriginalConfigurations", () => { expect(result).toEqual({ data: { eslint: { - env: {}, - extends: [], - rules: {}, + full: { + env: {}, + extends: [], + rules: {}, + }, + raw: {}, }, packages: { dependencies: {}, devDependencies: {}, }, tslint: { - rulesDirectory: [], - rules: {}, + full: { + rulesDirectory: [], + rules: {}, + }, + raw: {}, }, typescript: { compilerOptions: { diff --git a/src/input/findOriginalConfigurations.ts b/src/input/findOriginalConfigurations.ts index 754b6bd09..2f3484efd 100644 --- a/src/input/findOriginalConfigurations.ts +++ b/src/input/findOriginalConfigurations.ts @@ -17,17 +17,22 @@ export type FindOriginalConfigurationsDependencies = { mergeLintConfigurations: typeof mergeLintConfigurations; }; -export type OriginalConfigurations = { - eslint?: ESLintConfiguration; +export type OriginalConfigurations = { + full: Configuration; + raw: Partial; +}; + +export type AllOriginalConfigurations = { + eslint?: OriginalConfigurations; packages?: PackagesConfiguration; - tslint: TSLintConfiguration; + tslint: OriginalConfigurations; typescript?: TypeScriptConfiguration; }; export const findOriginalConfigurations = async ( dependencies: FindOriginalConfigurationsDependencies, rawSettings: TSLintToESLintSettings, -): Promise> => { +): Promise> => { // Simultaneously search for all required configuration types const [eslint, packages, tslint, typescript] = await Promise.all([ dependencies.findESLintConfiguration(rawSettings), diff --git a/src/input/findPackagesConfiguration.ts b/src/input/findPackagesConfiguration.ts index 54ab88758..e27fc4d29 100644 --- a/src/input/findPackagesConfiguration.ts +++ b/src/input/findPackagesConfiguration.ts @@ -1,4 +1,7 @@ -import { findConfiguration, FindConfigurationDependencies } from "./findConfiguration"; +import { + findReportedConfiguration, + FindReportedConfigurationDependencies, +} from "./findReportedConfiguration"; export type PackagesConfiguration = { dependencies: { @@ -15,10 +18,10 @@ const defaultPackagesConfiguration = { }; export const findPackagesConfiguration = async ( - dependencies: FindConfigurationDependencies, + dependencies: FindReportedConfigurationDependencies, config: string | undefined, ): Promise => { - const rawConfiguration = await findConfiguration( + const rawConfiguration = await findReportedConfiguration( dependencies.exec, "cat", config || "./package.json", diff --git a/src/input/findRawConfiguration.ts b/src/input/findRawConfiguration.ts new file mode 100644 index 000000000..4faf5f4a5 --- /dev/null +++ b/src/input/findRawConfiguration.ts @@ -0,0 +1,21 @@ +import { SansDependencies } from "../binding"; +import { importer } from "./importer"; + +export const findRawConfiguration = async ( + fileImporter: SansDependencies, + filePath: string, + defaults: Partial, +): Promise => { + let results: Configuration; + + try { + results = (await fileImporter(filePath)) as Configuration; + } catch (error) { + return error; + } + + return { + ...defaults, + ...results, + }; +}; diff --git a/src/input/findConfiguration.test.ts b/src/input/findReportedConfiguration.test.ts similarity index 75% rename from src/input/findConfiguration.test.ts rename to src/input/findReportedConfiguration.test.ts index d2a9f214d..55a2390a4 100644 --- a/src/input/findConfiguration.test.ts +++ b/src/input/findReportedConfiguration.test.ts @@ -1,14 +1,14 @@ import { createStubExec, createStubThrowingExec } from "../adapters/exec.stubs"; -import { findConfiguration } from "./findConfiguration"; +import { findReportedConfiguration } from "./findReportedConfiguration"; -describe("findConfiguration", () => { +describe("findReportedConfiguration", () => { it("returns stderr as an error when the command fails with a zero exit code", async () => { // Arrange const stderr = "error"; const exec = createStubExec({ stderr }); // Act - const result = await findConfiguration(exec, "command", "sample.json"); + const result = await findReportedConfiguration(exec, "command", "sample.json"); // Assert expect(result).toEqual(new Error(stderr)); @@ -20,7 +20,7 @@ describe("findConfiguration", () => { const exec = createStubThrowingExec({ stderr }); // Act - const result = await findConfiguration(exec, "command", "sample.json"); + const result = await findReportedConfiguration(exec, "command", "sample.json"); // Assert expect(result).toEqual(new Error(stderr)); @@ -32,7 +32,7 @@ describe("findConfiguration", () => { const exec = createStubExec({ stdout }); // Act - const result = await findConfiguration(exec, "command", "sample.json"); + const result = await findReportedConfiguration(exec, "command", "sample.json"); // Assert expect(result).toEqual( @@ -49,7 +49,7 @@ describe("findConfiguration", () => { const exec = createStubExec({ stdout }); // Act - const result = await findConfiguration(exec, "command", "sample.json"); + const result = await findReportedConfiguration(exec, "command", "sample.json"); // Assert expect(result).toEqual({ diff --git a/src/input/findConfiguration.ts b/src/input/findReportedConfiguration.ts similarity index 88% rename from src/input/findConfiguration.ts rename to src/input/findReportedConfiguration.ts index d91acb948..977d143b5 100644 --- a/src/input/findConfiguration.ts +++ b/src/input/findReportedConfiguration.ts @@ -4,11 +4,11 @@ export type DeepPartial = { [P in keyof T]: T[P] extends {} ? DeepPartial : T[P]; }; -export type FindConfigurationDependencies = { +export type FindReportedConfigurationDependencies = { exec: Exec; }; -export const findConfiguration = async ( +export const findReportedConfiguration = async ( exec: Exec, command: string, config: string, diff --git a/src/input/findTSLintConfiguration.ts b/src/input/findTSLintConfiguration.ts index 97962ff0e..d14bfdc45 100644 --- a/src/input/findTSLintConfiguration.ts +++ b/src/input/findTSLintConfiguration.ts @@ -1,44 +1,67 @@ +import { findRawConfiguration } from "./findRawConfiguration"; +import { findReportedConfiguration } from "./findReportedConfiguration"; import { Exec } from "../adapters/exec"; -import { findConfiguration } from "./findConfiguration"; +import { OriginalConfigurations } from "./findOriginalConfigurations"; +import { SansDependencies } from "../binding"; +import { importer } from "./importer"; export type TSLintConfiguration = { + extends?: string[]; rulesDirectory: string[]; rules: TSLintConfigurationRules; }; -export type TSLintConfigurationRules = { - [i: string]: any; -}; +export type TSLintConfigurationRules = Record; const defaultTSLintConfiguration = { + extends: [], rulesDirectory: [], rules: {}, }; export type FindTSLintConfigurationDependencies = { exec: Exec; + importer: SansDependencies; }; export const findTSLintConfiguration = async ( dependencies: FindTSLintConfigurationDependencies, config: string | undefined, -): Promise => { - const rawConfiguration = await findConfiguration( - dependencies.exec, - "tslint --print-config", - config || "./tslint.json", - ); +): Promise | Error> => { + const filePath = config || "./tslint.json"; + const [rawConfiguration, reportedConfiguration] = await Promise.all([ + findRawConfiguration>(dependencies.importer, filePath, { + extends: [], + }), + findReportedConfiguration( + dependencies.exec, + "tslint --print-config", + config || "./tslint.json", + ), + ]); - if (rawConfiguration instanceof Error) { - if (rawConfiguration.message.includes("unknown option `--print-config")) { + if (reportedConfiguration instanceof Error) { + if (reportedConfiguration.message.includes("unknown option `--print-config")) { return new Error("TSLint v5.18 required. Please update your version."); } + return reportedConfiguration; + } + + if (rawConfiguration instanceof Error) { return rawConfiguration; } return { - ...defaultTSLintConfiguration, - ...rawConfiguration, + full: { + ...defaultTSLintConfiguration, + ...rawConfiguration, + extends: Array.from( + new Set( + [[rawConfiguration.extends], [reportedConfiguration.extends]].flat(Infinity), + ), + ), + }, + raw: rawConfiguration, }; }; diff --git a/src/input/findTslintConfiguration.test.ts b/src/input/findTslintConfiguration.test.ts index a429a8fbf..557c71153 100644 --- a/src/input/findTslintConfiguration.test.ts +++ b/src/input/findTslintConfiguration.test.ts @@ -1,11 +1,22 @@ -import { findTSLintConfiguration } from "./findTSLintConfiguration"; +import { + findTSLintConfiguration, + FindTSLintConfigurationDependencies, +} from "./findTSLintConfiguration"; import { createStubExec, createStubThrowingExec } from "../adapters/exec.stubs"; +const createStubDependencies = (overrides: Partial = {}) => ({ + exec: createStubExec({ stdout: "{}" }), + importer: async () => ({}), + ...overrides, +}); + describe("findTSLintConfiguration", () => { - it("returns an error when one occurs", async () => { + it("returns an error when exec returns one", async () => { // Arrange const stderr = "error"; - const dependencies = { exec: createStubThrowingExec({ stderr }) }; + const dependencies = createStubDependencies({ + exec: createStubThrowingExec({ stderr }), + }); // Act const result = await findTSLintConfiguration(dependencies, undefined); @@ -18,10 +29,32 @@ describe("findTSLintConfiguration", () => { ); }); + it("returns an error when importer returns one", async () => { + // Arrange + const message = "error"; + const dependencies = createStubDependencies({ + importer: async () => { + throw new Error(message); + }, + }); + + // Act + const result = await findTSLintConfiguration(dependencies, undefined); + + // Assert + expect(result).toEqual( + expect.objectContaining({ + message, + }), + ); + }); + it("replaces an error with a v5.18 request when the --print-config option is unsupported", async () => { // Arrange const stderr = "unknown option `--print-config"; - const dependencies = { exec: createStubThrowingExec({ stderr }) }; + const dependencies = createStubDependencies({ + exec: createStubThrowingExec({ stderr }), + }); // Act const result = await findTSLintConfiguration(dependencies, undefined); @@ -36,7 +69,9 @@ describe("findTSLintConfiguration", () => { it("defaults the configuration file when one isn't provided", async () => { // Arrange - const dependencies = { exec: createStubExec() }; + const dependencies = createStubDependencies({ + exec: createStubExec(), + }); // Act await findTSLintConfiguration(dependencies, undefined); @@ -47,7 +82,9 @@ describe("findTSLintConfiguration", () => { it("includes a configuration file in the TSLint command when one is provided", async () => { // Arrange - const dependencies = { exec: createStubExec() }; + const dependencies = createStubDependencies({ + exec: createStubExec(), + }); const config = "./custom/tslint.json"; // Act @@ -61,7 +98,18 @@ describe("findTSLintConfiguration", () => { it("applies TSLint defaults when none are provided", async () => { // Arrange - const dependencies = { exec: createStubExec({ stdout: "{}" }) }; + const raw = { + extends: ["raw", "duplicated"], + }; + const reportedExtends = ["reported", "duplicated"]; + const dependencies = createStubDependencies({ + exec: createStubExec({ + stdout: JSON.stringify({ + extends: reportedExtends, + }), + }), + importer: async () => raw, + }); const config = "./custom/tslint.json"; // Act @@ -69,8 +117,12 @@ describe("findTSLintConfiguration", () => { // Assert expect(result).toEqual({ - rulesDirectory: [], - rules: {}, + full: { + extends: ["raw", "duplicated", "reported"], + rulesDirectory: [], + rules: {}, + }, + raw, }); }); }); diff --git a/src/input/findTypeScriptConfiguration.ts b/src/input/findTypeScriptConfiguration.ts index 27badf3a0..2284e1bea 100644 --- a/src/input/findTypeScriptConfiguration.ts +++ b/src/input/findTypeScriptConfiguration.ts @@ -1,4 +1,7 @@ -import { findConfiguration, FindConfigurationDependencies } from "./findConfiguration"; +import { + findReportedConfiguration, + FindReportedConfigurationDependencies, +} from "./findReportedConfiguration"; export type TypeScriptConfiguration = { compilerOptions: { @@ -14,10 +17,10 @@ const defaultTypeScriptConfiguration = { }; export const findTypeScriptConfiguration = async ( - dependencies: FindConfigurationDependencies, + dependencies: FindReportedConfigurationDependencies, config: string | undefined, ): Promise => { - const rawConfiguration = await findConfiguration( + const rawConfiguration = await findReportedConfiguration( dependencies.exec, "tsc --showConfig -p", config || "./tsconfig.json", diff --git a/src/input/importer.ts b/src/input/importer.ts new file mode 100644 index 000000000..b8c31b8fe --- /dev/null +++ b/src/input/importer.ts @@ -0,0 +1,50 @@ +import * as path from "path"; +import * as stripJsonComments from "strip-json-comments"; + +import { FileSystem } from "../adapters/fileSystem"; +import { NativeImporter } from "../adapters/nativeImporter"; + +export type ImporterDependencies = { + fileSystem: Pick; + nativeImporter: NativeImporter; +}; + +export const importer = async (dependencies: ImporterDependencies, moduleName: string) => { + const pathAttempts = [path.join(process.cwd(), moduleName), moduleName]; + + const importFile = async (filePath: string) => { + if (!filePath.endsWith(".json")) { + return await dependencies.nativeImporter(filePath); + } + + if (!(await dependencies.fileSystem.fileExists(filePath))) { + return undefined; + } + + const rawJsonContents = await dependencies.fileSystem.readFile(filePath); + if (rawJsonContents instanceof Error) { + return rawJsonContents; + } + + try { + return JSON.parse(stripJsonComments(rawJsonContents)); + } catch (error) { + return error; + } + }; + + for (const pathAttempt of pathAttempts) { + try { + const result = await importFile(pathAttempt); + if (result) { + return result; + } + } catch {} + } + + return new Error( + `Could not find '${moduleName}' after trying: ${pathAttempts + .map(attempt => `'${attempt}'`) + .join(", ")}`, + ); +}; diff --git a/src/input/mergeLintConfigurations.test.ts b/src/input/mergeLintConfigurations.test.ts index 56297d8cc..b6b1bf01a 100644 --- a/src/input/mergeLintConfigurations.test.ts +++ b/src/input/mergeLintConfigurations.test.ts @@ -1,12 +1,17 @@ import { mergeLintConfigurations } from "./mergeLintConfigurations"; import { ESLintConfiguration } from "./findESLintConfiguration"; +import { OriginalConfigurations } from "./findOriginalConfigurations"; +import { TSLintConfiguration } from "./findTSLintConfiguration"; -const stubTSLintConfiguration = { - rulesDirectory: [], - rules: { - disabled: true, - enabled: true, +const stubTSLintConfiguration: OriginalConfigurations = { + full: { + rulesDirectory: [], + rules: { + disabled: true, + enabled: true, + }, }, + raw: {}, }; describe("mergeLintConfigurations", () => { @@ -23,10 +28,13 @@ describe("mergeLintConfigurations", () => { it("returns the tslint configuration when the eslint configuration doesn't have tslint rules", () => { // Arrange - const eslint: ESLintConfiguration = { - env: {}, - extends: [], - rules: {}, + const eslint: OriginalConfigurations = { + full: { + env: {}, + extends: [], + rules: {}, + }, + raw: {}, }; // Act @@ -38,17 +46,20 @@ describe("mergeLintConfigurations", () => { it("returns the tslint configuration when the eslint configuration's tslint rules are disabled", () => { // Arrange - const eslint: ESLintConfiguration = { - env: {}, - extends: [], - rules: { - "@typescript-eslint/tslint/config": [ - "off", - { - extra: true, - }, - ], + const eslint: OriginalConfigurations = { + full: { + env: {}, + extends: [], + rules: { + "@typescript-eslint/tslint/config": [ + "off", + { + extra: true, + }, + ], + }, }, + raw: {}, }; // Act @@ -63,17 +74,20 @@ describe("mergeLintConfigurations", () => { const extraRules = { extra: true, }; - const eslint: ESLintConfiguration = { - env: {}, - extends: [], - rules: { - "@typescript-eslint/tslint/config": [ - "error", - { - rules: extraRules, - }, - ], + const eslint: OriginalConfigurations = { + full: { + env: {}, + extends: [], + rules: { + "@typescript-eslint/tslint/config": [ + "error", + { + rules: extraRules, + }, + ], + }, }, + raw: {}, }; // Act @@ -82,9 +96,12 @@ describe("mergeLintConfigurations", () => { // Assert expect(result).toEqual({ ...stubTSLintConfiguration, - rules: { - ...stubTSLintConfiguration.rules, - ...extraRules, + full: { + ...stubTSLintConfiguration.full, + rules: { + ...stubTSLintConfiguration.full.rules, + ...extraRules, + }, }, }); }); diff --git a/src/input/mergeLintConfigurations.ts b/src/input/mergeLintConfigurations.ts index 662be3536..de2bb80fd 100644 --- a/src/input/mergeLintConfigurations.ts +++ b/src/input/mergeLintConfigurations.ts @@ -1,24 +1,28 @@ -import { TSLintConfiguration } from "./findTSLintConfiguration"; import { ESLintConfiguration } from "./findESLintConfiguration"; +import { OriginalConfigurations } from "./findOriginalConfigurations"; +import { TSLintConfiguration } from "./findTSLintConfiguration"; export const mergeLintConfigurations = ( - eslint: ESLintConfiguration | Error, - tslint: TSLintConfiguration, -) => { + eslint: OriginalConfigurations | Error, + tslint: OriginalConfigurations, +): OriginalConfigurations => { if (eslint instanceof Error) { return tslint; } - const mappedConfig = eslint.rules["@typescript-eslint/tslint/config"]; + const mappedConfig = eslint.full.rules["@typescript-eslint/tslint/config"]; if (!(mappedConfig instanceof Array) || mappedConfig[0] === "off") { return tslint; } return { ...tslint, - rules: { - ...tslint.rules, - ...mappedConfig[1].rules, + full: { + ...tslint.full, + rules: { + ...tslint.full.rules, + ...mappedConfig[1].rules, + }, }, }; }; From eadca31019035111c122a04bb43fe2b85b1d9dad Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 20 Oct 2019 16:02:53 -0400 Subject: [PATCH 2/4] Added test coverage and finished sorting output keys --- src/cli/main.ts | 1 + src/conversion/convertConfig.ts | 2 +- .../formatting/formatters/formatJsOutput.ts | 7 +- .../formatting/formatters/formatJsonOutput.ts | 7 +- .../formatting/formatters/withKeysSorted.ts | 7 + .../simplifyPackageRules.test.ts | 33 ++-- .../simplification/simplifyPackageRules.ts | 7 +- src/input/findESLintConfiguration.ts | 14 +- src/input/importer.test.ts | 145 ++++++++++++++++++ src/input/importer.ts | 5 +- .../converters/tests/member-access.test.ts | 4 +- tsconfig.json | 1 + 12 files changed, 187 insertions(+), 46 deletions(-) create mode 100644 src/creation/formatting/formatters/withKeysSorted.ts create mode 100644 src/input/importer.test.ts diff --git a/src/cli/main.ts b/src/cli/main.ts index 3460bdf28..efd20eb6c 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -45,6 +45,7 @@ const convertRulesDependencies = { const nativeImporterDependencies: ImporterDependencies = { fileSystem: fsFileSystem, + getCwd: () => process.cwd(), nativeImporter: nativeImporter, }; diff --git a/src/conversion/convertConfig.ts b/src/conversion/convertConfig.ts index 6757e49d9..17195d7f5 100644 --- a/src/conversion/convertConfig.ts +++ b/src/conversion/convertConfig.ts @@ -37,7 +37,7 @@ export const convertConfig = async ( const simplifiedConfiguration = { ...ruleConversionResults, ...(await dependencies.simplifyPackageRules( - originalConfigurations.data.eslint && originalConfigurations.data.eslint.full, + originalConfigurations.data.eslint, ruleConversionResults, )), }; diff --git a/src/creation/formatting/formatters/formatJsOutput.ts b/src/creation/formatting/formatters/formatJsOutput.ts index d5e020879..23bc78f29 100644 --- a/src/creation/formatting/formatters/formatJsOutput.ts +++ b/src/creation/formatting/formatters/formatJsOutput.ts @@ -1,4 +1,7 @@ import { EOL } from "os"; -export const formatJsOutput = (configuration: unknown) => - `module.exports = ${JSON.stringify(configuration, undefined, 4)};${EOL}`; +import { withKeysSorted } from "./withKeysSorted"; + +export const formatJsOutput = (configuration: any) => { + return `module.exports = ${JSON.stringify(withKeysSorted(configuration), undefined, 4)};${EOL}`; +}; diff --git a/src/creation/formatting/formatters/formatJsonOutput.ts b/src/creation/formatting/formatters/formatJsonOutput.ts index 31e76e66d..16298f1d2 100644 --- a/src/creation/formatting/formatters/formatJsonOutput.ts +++ b/src/creation/formatting/formatters/formatJsonOutput.ts @@ -1,8 +1,7 @@ import { EOL } from "os"; -export const formatJsonOutput = (configuration: any) => { - const keys = Object.keys(configuration).sort(); - const sortedConfiguration = Object.fromEntries(keys.map(key => [key, configuration[key]])); +import { withKeysSorted } from "./withKeysSorted"; - return `${JSON.stringify(sortedConfiguration, undefined, 4)}${EOL}`; +export const formatJsonOutput = (configuration: any) => { + return `${JSON.stringify(withKeysSorted(configuration), undefined, 4)}${EOL}`; }; diff --git a/src/creation/formatting/formatters/withKeysSorted.ts b/src/creation/formatting/formatters/withKeysSorted.ts new file mode 100644 index 000000000..b0fad6bba --- /dev/null +++ b/src/creation/formatting/formatters/withKeysSorted.ts @@ -0,0 +1,7 @@ +export const withKeysSorted = (input: any) => { + return Object.fromEntries( + Object.keys(input) + .sort((a, b) => a.localeCompare(b)) + .map(key => [key, input[key]]), + ); +}; diff --git a/src/creation/simplification/simplifyPackageRules.test.ts b/src/creation/simplification/simplifyPackageRules.test.ts index b95b73075..e603dbcbb 100644 --- a/src/creation/simplification/simplifyPackageRules.test.ts +++ b/src/creation/simplification/simplifyPackageRules.test.ts @@ -8,6 +8,14 @@ const createStubDependencies = () => ({ retrieveExtendsValues: jest.fn(), }); +const createStubESLintConfiguration = (fullExtends: string | string[]) => ({ + full: { + env: {}, + extends: fullExtends, + rules: {}, + }, +}); + describe("simplifyPackageRules", () => { it("returns the conversion results directly when there is no loaded eslint configuration", async () => { // Arrange @@ -26,29 +34,10 @@ describe("simplifyPackageRules", () => { 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 eslint = createStubESLintConfiguration([]); const ruleConversionResults = createEmptyConversionResults(); // Act @@ -82,9 +71,7 @@ describe("simplifyPackageRules", () => { importedExtensions: [], }), }; - const eslint = { - extends: ["extension-name"], - }; + const eslint = createStubESLintConfiguration(["extension-name"]); const ruleConversionResults = createEmptyConversionResults(); // Act diff --git a/src/creation/simplification/simplifyPackageRules.ts b/src/creation/simplification/simplifyPackageRules.ts index 550fff9f6..b813ddda8 100644 --- a/src/creation/simplification/simplifyPackageRules.ts +++ b/src/creation/simplification/simplifyPackageRules.ts @@ -3,6 +3,7 @@ import { RuleConversionResults } from "../../rules/convertRules"; import { removeExtendsDuplicatedRules } from "./removeExtendsDuplicatedRules"; import { retrieveExtendsValues } from "./retrieveExtendsValues"; import { ESLintConfiguration } from "../../input/findESLintConfiguration"; +import { OriginalConfigurations } from "../../input/findOriginalConfigurations"; export type SimplifyPackageRulesDependencies = { removeExtendsDuplicatedRules: typeof removeExtendsDuplicatedRules; @@ -13,15 +14,15 @@ export type SimplifiedRuleConversionResults = Pick | undefined, + eslint: Pick, "full"> | undefined, ruleConversionResults: SimplifiedRuleConversionResults, ): Promise => { - if (eslint === undefined || eslint.extends === undefined || eslint.extends.length === 0) { + if (eslint === undefined || eslint.full.extends.length === 0) { return ruleConversionResults; } const { configurationErrors, importedExtensions } = await dependencies.retrieveExtendsValues( - eslint.extends, + eslint.full.extends, ); const converted = dependencies.removeExtendsDuplicatedRules( diff --git a/src/input/findESLintConfiguration.ts b/src/input/findESLintConfiguration.ts index 1d23181e3..3704bb65f 100644 --- a/src/input/findESLintConfiguration.ts +++ b/src/input/findESLintConfiguration.ts @@ -42,7 +42,7 @@ export const findESLintConfiguration = async ( ): Promise | Error> => { const filePath = rawSettings.eslint || rawSettings.config; const [rawConfiguration, reportedConfiguration] = await Promise.all([ - findRawConfiguration>(dependencies.importer, filePath, { + findRawConfiguration(dependencies.importer, filePath, { extends: [], }), findReportedConfiguration>( @@ -60,17 +60,15 @@ export const findESLintConfiguration = async ( return reportedConfiguration; } + const extensions = [rawConfiguration.extends, [reportedConfiguration.extends || []]].flat( + Infinity, + ); + return { full: { ...defaultESLintConfiguration, ...reportedConfiguration, - extends: Array.from( - new Set( - [[rawConfiguration.extends || []], [reportedConfiguration.extends || []]].flat( - Infinity, - ), - ), - ), + extends: Array.from(new Set(extensions)), }, raw: rawConfiguration, }; diff --git a/src/input/importer.test.ts b/src/input/importer.test.ts new file mode 100644 index 000000000..861bc37ba --- /dev/null +++ b/src/input/importer.test.ts @@ -0,0 +1,145 @@ +import * as path from "path"; + +import { importer } from "./importer"; + +const stubCwd = "/path/to/cwd"; + +type StubImporterSettings = { + files?: Record; + modules?: Record; +}; + +const createStubDependencies = ({ files = {}, modules = {} }: StubImporterSettings = {}) => { + const fileSystem = { + fileExists: async (filePath: string) => filePath in files, + readFile: async (filePath: string) => files[filePath], + }; + + const getCwd = () => stubCwd; + const nativeImporter = async (filePath: string) => modules[filePath]; + + return { fileSystem, getCwd, nativeImporter }; +}; + +describe("importer", () => { + it("natively imports a non-JSON module when its relative path exists", async () => { + // Arrange + const moduleName = "./relative.js"; + const contents = { key: "value" }; + const dependencies = createStubDependencies({ + modules: { + [moduleName]: contents, + }, + }); + + // Act + const imported = await importer(dependencies, moduleName); + + // Assert + expect(imported).toEqual(contents); + }); + + it("natively imports a non-JSON module when its cwd-joined path exists", async () => { + // Arrange + const moduleName = "./relative.js"; + const contents = { key: "value" }; + const dependencies = createStubDependencies({ + modules: { + [path.join(stubCwd, moduleName)]: contents, + }, + }); + + // Act + const imported = await importer(dependencies, moduleName); + + // Assert + expect(imported).toEqual(contents); + }); + + it("reads a JSONC module when its relative path exists", async () => { + // Arrange + const moduleName = "./relative.json"; + const contents = { key: "value" }; + const dependencies = createStubDependencies({ + files: { + [moduleName]: JSON.stringify(contents), + }, + }); + + // Act + const imported = await importer(dependencies, moduleName); + + // Assert + expect(imported).toEqual(contents); + }); + + it("reads a JSONC module when its cwd-joined path exists", async () => { + // Arrange + const moduleName = "./relative.json"; + const contents = { key: "value" }; + const dependencies = createStubDependencies({ + files: { + [path.join(stubCwd, moduleName)]: JSON.stringify(contents), + }, + }); + + // Act + const imported = await importer(dependencies, moduleName); + + // Assert + expect(imported).toEqual(contents); + }); + + it("returns an error when reading a JSONC module gives an error", async () => { + // Arrange + const moduleName = "./relative.json"; + const error = new Error("NOPE, INVALID"); + const dependencies = createStubDependencies({ + files: { + [moduleName]: error, + }, + }); + + // Act + const imported = await importer(dependencies, moduleName); + + // Assert + expect(imported).toEqual(error); + }); + + it("returns an error when parsing a JSONC module gives an error", async () => { + // Arrange + const moduleName = "./relative.json"; + const dependencies = createStubDependencies({ + files: { + [moduleName]: "NOPE, INVALID", + }, + }); + + // Act + const imported = await importer(dependencies, moduleName); + + // Assert + expect(imported).toEqual( + expect.objectContaining({ + message: "Unexpected token N in JSON at position 0", + }), + ); + }); + + it("returns an error when neither module locations exist for a path", async () => { + // Arrange + const moduleName = "./relative.json"; + const dependencies = createStubDependencies(); + + // Act + const imported = await importer(dependencies, moduleName); + + // Assert + expect(imported).toEqual( + expect.objectContaining({ + message: expect.stringContaining(`Could not find '${moduleName}' after trying:`), + }), + ); + }); +}); diff --git a/src/input/importer.ts b/src/input/importer.ts index b8c31b8fe..03955ffba 100644 --- a/src/input/importer.ts +++ b/src/input/importer.ts @@ -1,16 +1,17 @@ import * as path from "path"; -import * as stripJsonComments from "strip-json-comments"; +import stripJsonComments from "strip-json-comments"; import { FileSystem } from "../adapters/fileSystem"; import { NativeImporter } from "../adapters/nativeImporter"; export type ImporterDependencies = { fileSystem: Pick; + getCwd: () => string; nativeImporter: NativeImporter; }; export const importer = async (dependencies: ImporterDependencies, moduleName: string) => { - const pathAttempts = [path.join(process.cwd(), moduleName), moduleName]; + const pathAttempts = [path.join(dependencies.getCwd(), moduleName), moduleName]; const importFile = async (filePath: string) => { if (!filePath.endsWith(".json")) { diff --git a/src/rules/converters/tests/member-access.test.ts b/src/rules/converters/tests/member-access.test.ts index b803dff23..826db2707 100644 --- a/src/rules/converters/tests/member-access.test.ts +++ b/src/rules/converters/tests/member-access.test.ts @@ -10,9 +10,7 @@ describe(convertMemberAccess, () => { rules: [ { ruleName: "@typescript-eslint/explicit-member-accessibility", - ruleArguments: [ - { overrides: { constructors: "off" } }, - ], + ruleArguments: [{ overrides: { constructors: "off" } }], }, ], }); diff --git a/tsconfig.json b/tsconfig.json index ad0c7da74..d836d023d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "alwaysStrict": true, "declaration": true, + "esModuleInterop": true, "incremental": true, "lib": ["esnext"], "module": "commonjs", From 02d10e1fd73e0f06c9c863f471173eaffab6eeb0 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 20 Oct 2019 16:04:20 -0400 Subject: [PATCH 3/4] Added a bit of JSDoc documentation --- src/input/findOriginalConfigurations.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/input/findOriginalConfigurations.ts b/src/input/findOriginalConfigurations.ts index 2f3484efd..fcfe10140 100644 --- a/src/input/findOriginalConfigurations.ts +++ b/src/input/findOriginalConfigurations.ts @@ -17,8 +17,18 @@ export type FindOriginalConfigurationsDependencies = { mergeLintConfigurations: typeof mergeLintConfigurations; }; +/** + * Both found configurations for a particular linter. + */ export type OriginalConfigurations = { + /** + * Settings reported by the linter's native --print-config equivalent. + */ full: Configuration; + + /** + * Raw import results from `import`ing the configuration file. + */ raw: Partial; }; From e49e5fd7308555652600f5440db5cbe97b5d55c5 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 20 Oct 2019 16:07:32 -0400 Subject: [PATCH 4/4] Undid annoying src/rules/converters/no-implicit-dependencies.ts Prettier mismatch