diff --git a/package-lock.json b/package-lock.json index 4ff70cd10..dbcd1f05f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2970,6 +2970,12 @@ "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", "dev": true }, + "@types/lodash": { + "version": "4.14.162", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.162.tgz", + "integrity": "sha512-alvcho1kRUnnD1Gcl4J+hK0eencvzq9rmzvFPRmP5rPHx9VVsJj6bKLTATPVf9ktgv4ujzh7T+XWKp+jhuODig==", + "dev": true + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -3840,6 +3846,11 @@ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", "dev": true }, + "coffeescript": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.12.7.tgz", + "integrity": "sha512-pLXHFxQMPklVoEekowk8b3erNynC+DVJzChxS/LCBBgR6/8AJkHivkm//zbowcfc7BTCAjryuhx6gPqPRfsFoA==" + }, "collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -3976,6 +3987,14 @@ } } }, + "cson-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/cson-parser/-/cson-parser-4.0.5.tgz", + "integrity": "sha512-XgloWiJcHy3TeuonPJseMfZOuwDpfczIQ12xw+DS2D8TCFxbj61gVlu8Cfs6lOwucQlrJFCvBTiovBqYMwPfQw==", + "requires": { + "coffeescript": "^1.10.0" + } + }, "cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -7239,10 +7258,9 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash.sortby": { "version": "4.7.0", diff --git a/package.json b/package.json index b3f0be1b9..307d84aa5 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,10 @@ "dependencies": { "chalk": "4.1.0", "commander": "6.1.0", + "cson-parser": "^4.0.5", "eslint-config-prettier": "6.13.0", "glob": "7.1.6", + "lodash": "^4.17.20", "minimatch": "3.0.4", "strip-json-comments": "3.1.1", "tslint": "6.1.3", @@ -28,6 +30,7 @@ "@types/eslint-config-prettier": "6.11.0", "@types/glob": "7.1.3", "@types/jest": "26.0.14", + "@types/lodash": "^4.14.162", "@types/minimatch": "3.0.3", "@types/node": "12.12.21", "@typescript-eslint/eslint-plugin": "4.4.1", diff --git a/src/cli/main.ts b/src/cli/main.ts index 6064b82a9..5dd5ffe5b 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -14,16 +14,7 @@ import { ReportCommentResultsDependencies, reportCommentResults, } from "../converters/comments/reporting/reportCommentResults"; -import { - ConvertEditorConfigDependencies, - convertEditorConfig, -} from "../converters/editorConfigs/convertEditorConfig"; -import { - ConvertEditorSettingsDependencies, - convertEditorSettings, -} from "../converters/editorConfigs/convertEditorSettings"; -import { editorSettingsConverters } from "../converters/editorConfigs/editorSettingsConverters"; -import { reportEditorSettingConversionResults } from "../converters/editorConfigs/reporting/reportEditorSettingConversionResults"; +import { convertEditorConfig } from "../converters/editorConfigs/convertEditorConfig"; import { ConvertLintConfigDependencies, convertLintConfig, @@ -44,6 +35,14 @@ import { ConvertFileCommentsDependencies, convertFileComments, } from "../converters/comments/convertFileComments"; +import { + convertEditorConfigs, + ConvertEditorConfigsDependencies, +} from "../converters/editorConfigs/convertEditorConfigs"; +import { convertAtomConfig } from "../converters/editorConfigs/converters/convertAtomConfig"; +import { convertVSCodeConfig } from "../converters/editorConfigs/converters/convertVSCodeConfig"; +import { reportEditorConfigConversionResults } from "../converters/editorConfigs/reporting/reportEditorConfigConversionResults"; +import { EditorConfigDescriptor } from "../converters/editorConfigs/types"; import { ConvertRulesDependencies, convertRules, @@ -67,17 +66,12 @@ import { } from "../converters/lintConfigs/reporting/packages/logMissingPackages"; import { runCli, RunCliDependencies } from "./runCli"; import { ruleMergers } from "../converters/lintConfigs/rules/ruleMergers"; -import { writeEditorConfigConversionResults } from "../converters/lintConfigs/writeEditorConfigConversionResults"; import { checkPrettierExtension } from "../converters/lintConfigs/summarization/prettier/checkPrettierExtension"; import { removeExtendsDuplicatedRules } from "../converters/lintConfigs/pruning/removeExtendsDuplicatedRules"; import { ExtractGlobPathsDependencies, extractGlobPaths, } from "../converters/comments/extractGlobPaths"; -import { - findEditorConfiguration, - FindEditorConfigurationDependencies, -} from "../input/findEditorConfiguration"; import { findESLintConfiguration } from "../input/findESLintConfiguration"; import { findOriginalConfigurations, @@ -103,10 +97,6 @@ const convertRulesDependencies: ConvertRulesDependencies = { ruleMergers, }; -const convertEditorSettingsDependencies: ConvertEditorSettingsDependencies = { - converters: editorSettingsConverters, -}; - const nativeImporterDependencies: ImporterDependencies = { fileSystem: fsFileSystem, getCwd: () => process.cwd(), @@ -120,11 +110,6 @@ const findConfigurationDependencies = { importer: boundImporter, }; -const findEditorConfigurationDependencies: FindEditorConfigurationDependencies = { - fileSystem: fsFileSystem, - importer: boundImporter, -}; - const findOriginalConfigurationsDependencies: FindOriginalConfigurationsDependencies = { findESLintConfiguration: bind(findESLintConfiguration, findConfigurationDependencies), findPackagesConfiguration: bind(findPackagesConfiguration, findConfigurationDependencies), @@ -175,20 +160,26 @@ const writeConversionResultsDependencies: WriteConversionResultsDependencies = { fileSystem: fsFileSystem, }; -const reportEditorSettingConversionResultsDependencies = { +const editorConfigDescriptors: EditorConfigDescriptor[] = [ + [".atom/config.cson", convertAtomConfig], + [".vscode/settings.json", convertVSCodeConfig], +]; + +const convertEditorConfigDependencies = { + editorConfigDescriptors, + fileSystem: fsFileSystem, +}; + +const reportEditorConfigConversionResultsDependencies = { logger: processLogger, }; -const convertEditorConfigDependencies: ConvertEditorConfigDependencies = { - findEditorConfiguration: bind(findEditorConfiguration, findEditorConfigurationDependencies), - convertEditorSettings: bind(convertEditorSettings, convertEditorSettingsDependencies), - reportEditorSettingConversionResults: bind( - reportEditorSettingConversionResults, - reportEditorSettingConversionResultsDependencies, - ), - writeEditorConfigConversionResults: bind( - writeEditorConfigConversionResults, - writeConversionResultsDependencies, +const convertEditorConfigsDependencies: ConvertEditorConfigsDependencies = { + convertEditorConfig: bind(convertEditorConfig, convertEditorConfigDependencies), + editorConfigDescriptors, + reportEditorConfigConversionResults: bind( + reportEditorConfigConversionResults, + reportEditorConfigConversionResultsDependencies, ), }; @@ -209,7 +200,7 @@ const convertLintConfigDependencies: ConvertLintConfigDependencies = { const runCliDependencies: RunCliDependencies = { converters: [ bind(convertLintConfig, convertLintConfigDependencies), - bind(convertEditorConfig, convertEditorConfigDependencies), + bind(convertEditorConfigs, convertEditorConfigsDependencies), bind(convertComments, convertCommentsDependencies), ], findOriginalConfigurations: bind( diff --git a/src/converters/editorConfigs/convertEditorConfig.test.ts b/src/converters/editorConfigs/convertEditorConfig.test.ts index 2b2e926b4..76f50d815 100644 --- a/src/converters/editorConfigs/convertEditorConfig.test.ts +++ b/src/converters/editorConfigs/convertEditorConfig.test.ts @@ -1,137 +1,69 @@ -import { ResultStatus, FailedResult } from "../../types"; -import { convertEditorConfig, ConvertEditorConfigDependencies } from "./convertEditorConfig"; -import { createEmptyEditorSettingConversionResults } from "./editorConversionResults.stubs"; -import { EditorSetting } from "./types"; +import { convertEditorConfig } from "./convertEditorConfig"; + +const stubPath = "./vscode/settings.json"; const stubSettings = { - config: "./eslintrc.js", - editor: "./my-editor/settings.json", + config: ".eslintrc.js", }; -const createStubDependencies = ( - overrides: Partial = {}, -): ConvertEditorConfigDependencies => ({ - convertEditorSettings: jest.fn(), - findEditorConfiguration: jest.fn().mockResolvedValue({}), - reportEditorSettingConversionResults: jest.fn(), - writeEditorConfigConversionResults: jest.fn().mockReturnValue(Promise.resolve()), - ...overrides, -}); - describe("convertEditorConfig", () => { - it("returns a success result when there is no original configuration", async () => { - // Arrange - const dependencies = createStubDependencies({ - findEditorConfiguration: async () => undefined, - }); - - // Act - const result = await convertEditorConfig(dependencies, stubSettings); - - // Assert - expect(result).toEqual({ - status: ResultStatus.Succeeded, - }); - }); - - it("returns the failure result when finding the original configurations fails", async () => { + it("returns an error when reading the file fails", async () => { // Arrange - const error = new Error(); - const findError: FailedResult = { - errors: [error], - status: ResultStatus.Failed, + const error = new Error("Oh no"); + const dependencies = { + fileSystem: { + readFile: jest.fn().mockResolvedValue(error), + writeFile: jest.fn(), + }, }; - const dependencies = createStubDependencies({ - findEditorConfiguration: async () => ({ - configPath: "", - result: error, - }), - }); - // Act - const result = await convertEditorConfig(dependencies, stubSettings); + const result = await convertEditorConfig(dependencies, jest.fn(), stubPath, stubSettings); // Assert - expect(result).toEqual(findError); + expect(result).toEqual(error); }); - it("returns the failure result when writing to the configuration file fails", async () => { + it("returns an error when writing to a file fails", async () => { // Arrange - const fileWriteError = new Error(); - const dependencies = createStubDependencies({ - writeEditorConfigConversionResults: jest.fn().mockResolvedValueOnce(fileWriteError), + const originalFileContents = "Hello"; + const error = new Error("Oh no!"); + const converter = (input: string) => ({ + contents: `${input} world!`, + missing: [], }); - - // Act - const result = await convertEditorConfig(dependencies, stubSettings); - - // Assert - expect(result).toEqual({ - errors: [fileWriteError], - status: ResultStatus.Failed, - }); - }); - - it("converts conversion results when finding the original configurations succeeds", async () => { - // Arrange - const originalConfig = { - "typescript.tsdk": "node_modules/typescript/lib", + const dependencies = { + fileSystem: { + readFile: jest.fn().mockResolvedValue(originalFileContents), + writeFile: jest.fn().mockResolvedValue(error), + }, }; - const dependencies = createStubDependencies({ - findEditorConfiguration: jest.fn().mockResolvedValue({ - result: originalConfig, - }), - }); - // Act - await convertEditorConfig(dependencies, stubSettings); + const result = await convertEditorConfig(dependencies, converter, stubPath, stubSettings); // Assert - expect(dependencies.convertEditorSettings).toHaveBeenCalledWith( - originalConfig, - stubSettings, - ); + expect(result).toEqual(error); }); - it("reports conversion results when settings are converted successfully", async () => { + it("returns the conversion data when writing to a file succeeds", async () => { // Arrange - const conversionResults = createEmptyEditorSettingConversionResults({ - converted: new Map([ - [ - "tslint-editor-setting-one", - { - editorSettingName: "tslint-editor-setting-one", - value: 42, - }, - ], - ]), - }); - - const dependencies = createStubDependencies({ - convertEditorSettings: jest.fn().mockReturnValue(conversionResults), + const originalFileContents = "Hello"; + const converter = (input: string) => ({ + contents: `${input} world!`, + missing: [], }); + const dependencies = { + fileSystem: { + readFile: jest.fn().mockResolvedValue(originalFileContents), + writeFile: jest.fn().mockResolvedValue(undefined), + }, + }; // Act - await convertEditorConfig(dependencies, stubSettings); - - // Assert - expect(dependencies.reportEditorSettingConversionResults).toHaveBeenCalledWith( - conversionResults, - ); - }); - - it("returns a successful result when finding the original configurations succeeds", async () => { - // Arrange - const dependencies = createStubDependencies(); - - // Act - const result = await convertEditorConfig(dependencies, stubSettings); + const result = await convertEditorConfig(dependencies, converter, stubPath, stubSettings); // Assert - expect(result).toEqual({ - status: ResultStatus.Succeeded, - }); + expect(result).toEqual(converter(originalFileContents)); }); }); diff --git a/src/converters/editorConfigs/convertEditorConfig.ts b/src/converters/editorConfigs/convertEditorConfig.ts index c490222bb..0654c55a4 100644 --- a/src/converters/editorConfigs/convertEditorConfig.ts +++ b/src/converters/editorConfigs/convertEditorConfig.ts @@ -1,66 +1,24 @@ -import { SansDependencies } from "../../binding"; -import { findEditorConfiguration } from "../../input/findEditorConfiguration"; -import { TSLintToESLintSettings, ResultWithStatus, ResultStatus } from "../../types"; -import { writeEditorConfigConversionResults } from "../lintConfigs/writeEditorConfigConversionResults"; -import { convertEditorSettings } from "./convertEditorSettings"; -import { reportEditorSettingConversionResults } from "./reporting/reportEditorSettingConversionResults"; +import { FileSystem } from "../../adapters/fileSystem"; +import { TSLintToESLintSettings } from "../../types"; +import { EditorConfigConverter } from "./types"; export type ConvertEditorConfigDependencies = { - convertEditorSettings: SansDependencies; - findEditorConfiguration: SansDependencies; - reportEditorSettingConversionResults: SansDependencies< - typeof reportEditorSettingConversionResults - >; - writeEditorConfigConversionResults: SansDependencies; + fileSystem: Pick; }; -/** - * Root-level driver to convert an editor configuration. - * @see `/docs/Architecture/Editors.md` for documentation. - */ export const convertEditorConfig = async ( dependencies: ConvertEditorConfigDependencies, + converter: EditorConfigConverter, + requestedPath: string, settings: TSLintToESLintSettings, -): Promise => { - // 1. An existing editor configuration is read from disk. - const configuration = await dependencies.findEditorConfiguration(settings.editor); - - // 2. If the existing configuration is not found or errored, nothing else needs to be done. - if (configuration === undefined) { - return { - status: ResultStatus.Succeeded, - }; - } - if (configuration.result instanceof Error) { - return { - errors: [configuration.result], - status: ResultStatus.Failed, - }; - } - - // 3. Configuration settings are converted to their ESLint equivalents. - const settingConversionResults = dependencies.convertEditorSettings( - configuration.result, - settings, - ); - - // 4. Those ESLint equivalents are written to the configuration file. - const fileWriteError = await dependencies.writeEditorConfigConversionResults( - configuration.configPath, - settingConversionResults, - configuration.result, - ); - if (fileWriteError !== undefined) { - return { - errors: [fileWriteError], - status: ResultStatus.Failed, - }; +) => { + const originalFileContents = await dependencies.fileSystem.readFile(requestedPath); + if (originalFileContents instanceof Error) { + return originalFileContents; } - // 5. Results from converting are reported to the user. - dependencies.reportEditorSettingConversionResults(settingConversionResults); + const conversion = converter(originalFileContents, settings); + const error = await dependencies.fileSystem.writeFile(requestedPath, conversion.contents); - return { - status: ResultStatus.Succeeded, - }; + return error ?? conversion; }; diff --git a/src/converters/editorConfigs/convertEditorConfigs.test.ts b/src/converters/editorConfigs/convertEditorConfigs.test.ts new file mode 100644 index 000000000..2ae8a1f78 --- /dev/null +++ b/src/converters/editorConfigs/convertEditorConfigs.test.ts @@ -0,0 +1,89 @@ +import { ResultStatus } from "../../types"; +import { convertEditorConfigs, ConvertEditorConfigsDependencies } from "./convertEditorConfigs"; + +const stubConfigPath = "stub.json"; + +const stubEditorConfigDescriptors = [[stubConfigPath, jest.fn()]] as const; + +const createStubDependencies = (overrides: Partial = {}) => ({ + convertEditorConfig: jest.fn(), + editorConfigDescriptors: stubEditorConfigDescriptors, + reportEditorConfigConversionResults: jest.fn(), + ...overrides, +}); + +const createSettings = (requestedPath?: string) => ({ + config: ".eslintrc.js", + editor: requestedPath, +}); + +describe("convertEditorConfigs", () => { + it("reports an error when an unknown editor config path is requested", async () => { + // Arrange + const unknownPath = "unknown/path.txt"; + const dependencies = createStubDependencies(); + const settings = createSettings(unknownPath); + + // Act + const result = await convertEditorConfigs(dependencies, settings); + + // Assert + const error = expect.objectContaining({ + message: `Unknown editor config path requested: 'unknown/path.txt'.`, + }); + expect(dependencies.reportEditorConfigConversionResults).toHaveBeenCalledWith({ + failed: new Map([[unknownPath, error]]), + successes: new Map(), + }); + expect(result).toEqual({ + errors: [error], + status: ResultStatus.Failed, + }); + }); + + it("reports an error when converting an editor config reports an error", async () => { + // Arrange + const error = new Error("Oh no!"); + const dependencies = createStubDependencies({ + convertEditorConfig: jest.fn().mockResolvedValue(error), + }); + const settings = createSettings(stubConfigPath); + + // Act + const result = await convertEditorConfigs(dependencies, settings); + + // Assert + expect(dependencies.reportEditorConfigConversionResults).toHaveBeenCalledWith({ + failed: new Map([[stubConfigPath, error]]), + successes: new Map(), + }); + expect(result).toEqual({ + errors: [error], + status: ResultStatus.Failed, + }); + }); + + it("reports a success when converting an editor config reports a success", async () => { + // Arrange + const success = { + contents: "Hello, world!", + missing: [], + }; + const dependencies = createStubDependencies({ + convertEditorConfig: jest.fn().mockResolvedValue(success), + }); + const settings = createSettings(stubConfigPath); + + // Act + const result = await convertEditorConfigs(dependencies, settings); + + // Assert + expect(dependencies.reportEditorConfigConversionResults).toHaveBeenCalledWith({ + failed: new Map(), + successes: new Map([[stubConfigPath, success]]), + }); + expect(result).toEqual({ + status: ResultStatus.Succeeded, + }); + }); +}); diff --git a/src/converters/editorConfigs/convertEditorConfigs.ts b/src/converters/editorConfigs/convertEditorConfigs.ts new file mode 100644 index 000000000..7a0f1dc7e --- /dev/null +++ b/src/converters/editorConfigs/convertEditorConfigs.ts @@ -0,0 +1,65 @@ +import { SansDependencies } from "../../binding"; +import { ResultStatus, ResultWithStatus, TSLintToESLintSettings } from "../../types"; +import { uniqueFromSources } from "../../utils"; +import { convertEditorConfig } from "./convertEditorConfig"; +import { reportEditorConfigConversionResults } from "./reporting/reportEditorConfigConversionResults"; +import { EditorConfigDescriptor, EditorConfigsConversionResults } from "./types"; + +export type ConvertEditorConfigsDependencies = { + convertEditorConfig: SansDependencies; + editorConfigDescriptors: readonly EditorConfigDescriptor[]; + reportEditorConfigConversionResults: SansDependencies< + typeof reportEditorConfigConversionResults + >; +}; +export const convertEditorConfigs = async ( + dependencies: ConvertEditorConfigsDependencies, + settings: TSLintToESLintSettings, +): Promise => { + const results: EditorConfigsConversionResults = { + failed: new Map(), + successes: new Map(), + }; + const requestedPaths = uniqueFromSources(settings.editor); + + await Promise.all( + requestedPaths.map(async (requestedPath) => { + const descriptor = dependencies.editorConfigDescriptors.find(([defaultPath]) => + defaultPathMatches(defaultPath, requestedPath), + ); + if (!descriptor) { + results.failed.set( + requestedPath, + new Error(`Unknown editor config path requested: '${requestedPath}'.`), + ); + return; + } + + const result = await dependencies.convertEditorConfig( + descriptor[1], + requestedPath, + settings, + ); + + if (result instanceof Error) { + results.failed.set(requestedPath, result); + } else { + results.successes.set(requestedPath, result); + } + }), + ); + + dependencies.reportEditorConfigConversionResults(results); + + return results.failed.size === 0 + ? { + status: ResultStatus.Succeeded, + } + : { + errors: Array.from(results.failed.values()), + status: ResultStatus.Failed, + }; +}; + +const defaultPathMatches = (defaultPath: string, requestedPath: string) => + requestedPath.replace(/\W+/g, "").endsWith(defaultPath.replace(/\W+/g, "")); diff --git a/src/converters/editorConfigs/convertEditorSetting.test.ts b/src/converters/editorConfigs/convertEditorSetting.test.ts deleted file mode 100644 index cdd42e0ba..000000000 --- a/src/converters/editorConfigs/convertEditorSetting.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { ConversionError } from "../../errors/conversionError"; -import { createStubTSLintToESLintSettings } from "../../settings.stubs"; -import { convertEditorSetting } from "./convertEditorSetting"; -import { EditorSettingConverter } from "./converter"; -import { EditorSetting } from "./types"; - -describe("convertEditorSetting", () => { - it("returns undefined when no converter exists for a setting", () => { - // Arrange - const converters = new Map(); - - // Act - const result = convertEditorSetting( - { - editorSettingName: "tslint-editor-setting", - value: "any value", - }, - converters, - createStubTSLintToESLintSettings(), - ); - - // Assert - expect(result).toEqual(undefined); - }); - - it("returns converter results when the converter does not throw an error", () => { - // Arrange - const converted = { - settings: [ - { - editorSettingName: "eslint-setting", - value: "new value", - }, - ], - }; - const converters = new Map([ - ["tslint-editor-setting", () => converted], - ]); - - // Act - const result = convertEditorSetting( - { - editorSettingName: "tslint-editor-setting", - value: "existing value", - }, - converters, - createStubTSLintToESLintSettings(), - ); - - // Assert - expect(result).toEqual(converted); - }); - - it("returns a conversion error when the converter throws an error", () => { - // Arrange - const error = new Error("oh no"); - const converters = new Map([ - [ - "tslint-editor-setting", - () => { - throw error; - }, - ], - ]); - const tsLintSetting: EditorSetting = { - editorSettingName: "tslint-editor-setting", - value: "existing value", - }; - - // Act - const result = convertEditorSetting( - tsLintSetting, - converters, - createStubTSLintToESLintSettings(), - ); - - // Assert - expect(result).toEqual(ConversionError.forSettingError(error, tsLintSetting)); - }); -}); diff --git a/src/converters/editorConfigs/convertEditorSetting.ts b/src/converters/editorConfigs/convertEditorSetting.ts deleted file mode 100644 index 66a4cd52f..000000000 --- a/src/converters/editorConfigs/convertEditorSetting.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ConversionError } from "../../errors/conversionError"; -import { TSLintToESLintSettings } from "../../types"; -import { EditorSettingConverter } from "./converter"; -import { EditorSetting } from "./types"; - -export const convertEditorSetting = ( - editorSetting: EditorSetting, - converters: Map, - settings: TSLintToESLintSettings, -) => { - const converter = converters.get(editorSetting.editorSettingName); - if (converter === undefined) { - return undefined; - } - - try { - return converter(editorSetting, settings); - } catch (error) { - return ConversionError.forSettingError(error, editorSetting); - } -}; diff --git a/src/converters/editorConfigs/convertEditorSettings.test.ts b/src/converters/editorConfigs/convertEditorSettings.test.ts deleted file mode 100644 index fd1a9bf0a..000000000 --- a/src/converters/editorConfigs/convertEditorSettings.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { ConversionError } from "../../errors/conversionError"; -import { createStubTSLintToESLintSettings } from "../../settings.stubs"; -import { convertEditorSettings } from "./convertEditorSettings"; -import { EditorSettingConversionResult, EditorSettingConverter } from "./converter"; -import { EditorSetting } from "./types"; - -describe("convertEditorSettings", () => { - it("skips entire conversion if none of the configurations is an editor setting", () => { - // Arrange - const { converters } = setupConversionEnvironment(); - - const editorConfiguration = { - notAnEditorSetting: "a", - }; - - // Act - const result = convertEditorSettings( - { converters }, - editorConfiguration, - createStubTSLintToESLintSettings(), - ); - - // Assert - expect(result).toEqual({ - converted: new Map(), - failed: [], - missing: [], - }); - }); - - it("skips a configuration if not an editor setting", () => { - // Arrange - const { editorSetting, converters } = setupConversionEnvironment({ - settings: [ - { - editorSettingName: "editor.eslint-setting-a", - value: "a", - }, - ], - }); - - const editorConfiguration = { - notAnEditorSetting: "a", - [editorSetting.editorSettingName]: editorSetting, - notAnEditorSettingEither: "b", - }; - - // Act - const result = convertEditorSettings( - { converters }, - editorConfiguration, - createStubTSLintToESLintSettings(), - ); - - // Assert - expect(result).toEqual({ - converted: new Map([ - [ - "editor.eslint-setting-a", - { - editorSettingName: "editor.eslint-setting-a", - value: "a", - }, - ], - ]), - failed: [], - missing: [], - }); - }); - - it("marks a setting as missing when its converter returns undefined", () => { - // Arrange - const { editorSetting, converters } = setupConversionEnvironment(); - - // Act - const result = convertEditorSettings( - { converters }, - { [editorSetting.editorSettingName]: editorSetting }, - createStubTSLintToESLintSettings(), - ); - - // Assert - expect(result).toEqual({ - converted: new Map(), - failed: [], - missing: [{ editorSettingName: editorSetting.editorSettingName }], - }); - }); - - it("marks a conversion as failed when returned a conversion error", () => { - // Arrange - const { editorSetting, converters } = setupConversionEnvironment(); - const conversionError = ConversionError.forSettingError(new Error(), editorSetting); - converters.set(editorSetting.editorSettingName, () => conversionError); - - // Act - const result = convertEditorSettings( - { converters }, - { [editorSetting.editorSettingName]: editorSetting }, - createStubTSLintToESLintSettings(), - ); - - // Assert - expect(result).toEqual({ - converted: new Map(), - failed: [conversionError], - missing: [], - }); - }); - - it("marks a converted setting name as converted when a conversion has settings", () => { - // Arrange - const { editorSetting, converters } = setupConversionEnvironment({ - settings: [ - { - editorSettingName: "eslint.configFile", - value: "a", - }, - ], - }); - - // Act - const result = convertEditorSettings( - { converters }, - { [editorSetting.editorSettingName]: editorSetting.value }, - createStubTSLintToESLintSettings(), - ); - - // Assert - expect(result).toEqual({ - converted: new Map([ - [ - "eslint.configFile", - { - editorSettingName: "eslint.configFile", - value: "a", - }, - ], - ]), - failed: [], - missing: [], - }); - }); -}); - -function setupConversionEnvironment(conversionResult?: EditorSettingConversionResult) { - const editorSetting = createSampleEditorSetting(); - const converters = createConverters(editorSetting, conversionResult); - - return { editorSetting, converters }; -} - -function createSampleEditorSetting(): EditorSetting { - return { - editorSettingName: "tslint.configFile", - value: "a", - }; -} - -function createConverters( - tslintSetting: EditorSetting, - conversionResult?: EditorSettingConversionResult, -): Map { - const converters = new Map(); - - if (conversionResult !== undefined) { - converters.set(tslintSetting.editorSettingName, () => conversionResult); - } - - return converters; -} diff --git a/src/converters/editorConfigs/convertEditorSettings.ts b/src/converters/editorConfigs/convertEditorSettings.ts deleted file mode 100644 index 9cf4cc25e..000000000 --- a/src/converters/editorConfigs/convertEditorSettings.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ConversionError } from "../../errors/conversionError"; -import { ErrorSummary } from "../../errors/errorSummary"; -import { TSLintToESLintSettings } from "../../types"; -import { convertEditorSetting } from "./convertEditorSetting"; -import { EditorSettingConverter } from "./converter"; -import { EditorSetting } from "./types"; - -const knownEditorSettings = new Set([ - "tslint.configFile", - "tslint.jsEnable", - "tslint.ignoreDefinitionFiles", - "tslint.exclude", - "tslint.alwaysShowRuleFailuresAsWarnings", - "tslint.suppressWhileTypeErrorsPresent", -]); - -export type ConvertEditorSettingsDependencies = { - converters: Map; -}; - -export type EditorSettingConversionResults = { - converted: Map; - failed: ErrorSummary[]; - missing: Pick[]; -}; - -// The entire editor configuration of any keys and values. -export type EditorConfiguration = Record; - -export const convertEditorSettings = ( - dependencies: ConvertEditorSettingsDependencies, - rawEditorConfiguration: EditorConfiguration, - settings: TSLintToESLintSettings, -): EditorSettingConversionResults => { - const converted = new Map(); - const failed: ConversionError[] = []; - const missing: Pick[] = []; - - for (const [configurationName, value] of Object.entries(rawEditorConfiguration)) { - // Configurations other than editor settings will be ignored. - // See: https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-typescript-tslint-plugin#configuration - if (!knownEditorSettings.has(configurationName)) { - continue; - } - - const editorSetting = { editorSettingName: configurationName, value }; - const conversion = convertEditorSetting(editorSetting, dependencies.converters, settings); - - if (conversion === undefined) { - const { editorSettingName } = editorSetting; - missing.push({ editorSettingName }); - continue; - } - - if (conversion instanceof ConversionError) { - failed.push(conversion); - continue; - } - - for (const changes of conversion.settings) { - converted.set(changes.editorSettingName, { ...changes }); - } - } - - return { converted, failed, missing }; -}; diff --git a/src/converters/editorConfigs/converter.ts b/src/converters/editorConfigs/converter.ts deleted file mode 100644 index 481c5ac61..000000000 --- a/src/converters/editorConfigs/converter.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ConversionError } from "../../errors/conversionError"; -import { TSLintToESLintSettings } from "../../types"; -import { EditorSetting } from "./types"; - -/** - * Attempts to convert a TSLint editor setting into the ESLint equivalents. - */ -export type EditorSettingConverter = ( - tslintEditorSetting: EditorSetting, - settings: TSLintToESLintSettings, -) => ConversionError | EditorSettingConversionResult | undefined; - -/** - * Successful result from converting a TSLint editor setting to its ESLint equivalents. - */ -export type EditorSettingConversionResult = { - /** - * At least one equivalent ESLint setting. - */ - settings: ConvertedEditorSettingChanges[]; -}; - -/** - * An ESLint editor setting equivalent to a previously enabled TSLint editor setting. - */ -export type ConvertedEditorSettingChanges = { - /** - * Any values for that ESLint editor setting. - */ - value: any; - - /** - * Equivalent ESLint editor setting name that should be enabled. - */ - editorSettingName: string; -}; diff --git a/src/converters/editorConfigs/converters/convertAtomConfig.test.ts b/src/converters/editorConfigs/converters/convertAtomConfig.test.ts new file mode 100644 index 000000000..7f83bf503 --- /dev/null +++ b/src/converters/editorConfigs/converters/convertAtomConfig.test.ts @@ -0,0 +1,71 @@ +import * as CsonParser from "cson-parser"; + +import { convertAtomConfig } from "./convertAtomConfig"; + +describe("convertAtomConfig", () => { + it("preserves the original config when no linter-tslint settings exist", () => { + // Arrange + const editorSettings = { unrelated: true }; + + // Act + const result = convertAtomConfig(CsonParser.stringify(editorSettings)); + + // Assert + expect(result).toEqual({ + contents: CsonParser.stringify(editorSettings, null, 4), + missing: [], + }); + }); + + it("includes useGlobalEslint when useLocalTslint exists", () => { + // Arrange + const editorSettings = { + "linter-tslint": { + useLocalTslint: true, + }, + unrelated: true, + }; + + // Act + const result = convertAtomConfig(CsonParser.stringify(editorSettings)); + + // Assert + expect(result).toEqual({ + contents: CsonParser.stringify( + { + "linter-tslint": { + useLocalTslint: true, + }, + unrelated: true, + "linter-eslint": { + global: { + useGlobalEslint: false, + }, + }, + }, + null, + 4, + ), + missing: [], + }); + }); + + it("includes missing notices when known missing settings are included", () => { + // Arrange + const editorSettings = { + "linter-tslint": { + enableSemanticRules: true, + rulesDirectory: true, + }, + }; + + // Act + const result = convertAtomConfig(CsonParser.stringify(editorSettings)); + + // Assert + expect(result).toEqual({ + contents: CsonParser.stringify(editorSettings, null, 4), + missing: ["enableSemanticRules", "rulesDirectory"], + }); + }); +}); diff --git a/src/converters/editorConfigs/converters/convertAtomConfig.ts b/src/converters/editorConfigs/converters/convertAtomConfig.ts new file mode 100644 index 000000000..b32ec4d97 --- /dev/null +++ b/src/converters/editorConfigs/converters/convertAtomConfig.ts @@ -0,0 +1,29 @@ +import * as CsonParser from "cson-parser"; +import { merge } from "lodash"; + +const knownMissingSettings = ["enableSemanticRules", "rulesDirectory"]; + +export const convertAtomConfig = (rawEditorSettings: string) => { + const editorSettings = CsonParser.parse(rawEditorSettings); + const linterSettings = editorSettings["linter-tslint"]; + const useLocalTslint = linterSettings?.useLocalTslint; + + const contents = CsonParser.stringify( + merge( + editorSettings, + useLocalTslint !== undefined && { + "linter-eslint": { + global: { + ...{ useGlobalEslint: !useLocalTslint }, + }, + }, + }, + ), + null, + 4, + ); + + const missing = knownMissingSettings.filter((setting) => linterSettings?.[setting]); + + return { contents, missing }; +}; diff --git a/src/converters/editorConfigs/converters/convertVSCodeConfig.test.ts b/src/converters/editorConfigs/converters/convertVSCodeConfig.test.ts new file mode 100644 index 000000000..1b3852e9d --- /dev/null +++ b/src/converters/editorConfigs/converters/convertVSCodeConfig.test.ts @@ -0,0 +1,120 @@ +import { convertVSCodeConfig } from "./convertVSCodeConfig"; + +const stubSettings = { + config: ".eslintrc.js", +}; + +describe("convertVSCodeConfig", () => { + it("preserves original settings when no TSLint settings exist", () => { + // Arrange + const editorSettings = { unrelated: true }; + + // Act + const result = convertVSCodeConfig(JSON.stringify(editorSettings), stubSettings); + + // Assert + expect(result).toEqual({ + contents: JSON.stringify(editorSettings, null, 4), + missing: [], + }); + }); + + it("includes eslint.autoFixOnSave when source.fixAll.tslint exists", () => { + // Arrange + const editorSettings = { + "editor.codeActionsOnSave": { + "source.fixAll.tslint": true, + }, + unrelated: true, + }; + + // Act + const result = convertVSCodeConfig(JSON.stringify(editorSettings), stubSettings); + + // Assert + expect(result).toEqual({ + contents: JSON.stringify( + { + "editor.codeActionsOnSave": { + "source.fixAll.tslint": true, + "eslint.autoFixOnSave": true, + }, + unrelated: true, + }, + null, + 4, + ), + missing: [], + }); + }); + + it("does not include configFile when tslint.configFile does not match the output config", () => { + // Arrange + const editorSettings = { + "tslint.configFile": "unrelated/path/tsconfig.json", + unrelated: true, + }; + + // Act + const result = convertVSCodeConfig(JSON.stringify(editorSettings), stubSettings); + + // Assert + expect(result).toEqual({ + contents: JSON.stringify(editorSettings, null, 4), + missing: [], + }); + }); + + it("includes configFile when tslint.configFile matches", () => { + // Arrange + const editorSettings = { + "tslint.configFile": "./tslint.json", + unrelated: true, + }; + + // Act + const result = convertVSCodeConfig(JSON.stringify(editorSettings), stubSettings); + + // Assert + expect(result).toEqual({ + contents: JSON.stringify( + { + "tslint.configFile": "./tslint.json", + unrelated: true, + "eslint.options": { + configFile: stubSettings.config, + }, + }, + null, + 4, + ), + missing: [], + }); + }); + + it("includes missing notices when known missing settings are included", () => { + // Arrange + const editorSettings = { + "tslint.alwaysShowRuleFailuresAsWarnings": true, + "tslint.exclude": true, + "tslint.ignoreDefinitionFiles": true, + "tslint.jsEnable": true, + "tslint.suppressWhileTypeErrorsPresent": true, + }; + + // Act + const result = convertVSCodeConfig(JSON.stringify(editorSettings), stubSettings); + + // Assert + expect(result).toEqual({ + contents: JSON.stringify(editorSettings, null, 4), + missing: [ + "tslint.alwaysShowRuleFailuresAsWarnings", + "tslint.exclude", + "tslint.ignoreDefinitionFiles", + "tslint.jsEnable", + "tslint.suppressWhileTypeErrorsPresent", + ], + }); + }); +}); diff --git a/src/converters/editorConfigs/converters/convertVSCodeConfig.ts b/src/converters/editorConfigs/converters/convertVSCodeConfig.ts new file mode 100644 index 000000000..f47694be8 --- /dev/null +++ b/src/converters/editorConfigs/converters/convertVSCodeConfig.ts @@ -0,0 +1,49 @@ +import { merge } from "lodash"; +import * as path from "path"; + +import { parseJson } from "../../../utils"; +import { EditorConfigConverter } from "../types"; + +const knownMissingSettings = [ + "tslint.alwaysShowRuleFailuresAsWarnings", + "tslint.exclude", + "tslint.ignoreDefinitionFiles", + "tslint.jsEnable", + "tslint.suppressWhileTypeErrorsPresent", +]; + +export const convertVSCodeConfig: EditorConfigConverter = (rawEditorSettings, settings) => { + const editorSettings = parseJson(rawEditorSettings); + const autoFixOnSave = editorSettings["editor.codeActionsOnSave"]?.["source.fixAll.tslint"]; + + // Only create a new config file path if the input and output configs roughly match + const eslintPathMatches = + editorSettings["tslint.configFile"] && + !path.relative( + path.dirname(editorSettings["tslint.configFile"]), + path.dirname(settings.config), + ); + + const contents = JSON.stringify( + merge( + {}, + editorSettings, + autoFixOnSave !== undefined && { + "editor.codeActionsOnSave": { + "eslint.autoFixOnSave": autoFixOnSave, + }, + }, + eslintPathMatches && { + "eslint.options": { + configFile: settings.config, + }, + }, + ), + null, + 4, + ); + + const missing = knownMissingSettings.filter((setting) => editorSettings[setting]); + + return { contents, missing }; +}; diff --git a/src/converters/editorConfigs/converters/editor-code-actions-on-save.ts b/src/converters/editorConfigs/converters/editor-code-actions-on-save.ts deleted file mode 100644 index f9e0b25a3..000000000 --- a/src/converters/editorConfigs/converters/editor-code-actions-on-save.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { EditorSettingConverter } from "../converter"; - -const SUB_SETTING_SOURCE_FIXALL = "source.fixAll.tslint"; - -export const convertEditorCodeActionsOnSave: EditorSettingConverter = ( - originalCodeActionsOnSave, -) => { - // Split properties to replace (into parent) and original ones. - const { - [SUB_SETTING_SOURCE_FIXALL]: originalSourceFixAllTsLint, - ...codeActionsOnSaveWithoutReplacedProperties - } = originalCodeActionsOnSave.value; - - return { - settings: [ - { - editorSettingName: "editor.codeActionsOnSave", - value: codeActionsOnSaveWithoutReplacedProperties, - }, - { - editorSettingName: "eslint.autoFixOnSave", - value: originalSourceFixAllTsLint, - }, - ], - }; -}; diff --git a/src/converters/editorConfigs/converters/tests/editor-code-actions-on-save.test.ts b/src/converters/editorConfigs/converters/tests/editor-code-actions-on-save.test.ts deleted file mode 100644 index 4e39f38a5..000000000 --- a/src/converters/editorConfigs/converters/tests/editor-code-actions-on-save.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { createStubTSLintToESLintSettings } from "../../../../settings.stubs"; -import { convertEditorCodeActionsOnSave } from "../editor-code-actions-on-save"; - -describe(convertEditorCodeActionsOnSave, () => { - test("conversion of 'source.fixAll.tslint' when value is true", () => { - const result = convertEditorCodeActionsOnSave( - { - editorSettingName: "editor.codeActionsOnSave", - value: { - "source.fixAll.tslint": true, - }, - }, - createStubTSLintToESLintSettings(), - ); - - expect(result).toEqual({ - settings: [ - { - editorSettingName: "editor.codeActionsOnSave", - value: {}, - }, - { - editorSettingName: "eslint.autoFixOnSave", - value: true, - }, - ], - }); - }); - - test("conversion of 'source.fixAll.tslint' when value is false", () => { - const result = convertEditorCodeActionsOnSave( - { - editorSettingName: "editor.codeActionsOnSave", - value: { - "source.fixAll.tslint": false, - }, - }, - createStubTSLintToESLintSettings(), - ); - - expect(result).toEqual({ - settings: [ - { - editorSettingName: "editor.codeActionsOnSave", - value: {}, - }, - { - editorSettingName: "eslint.autoFixOnSave", - value: false, - }, - ], - }); - }); - - test("conversion of 'source.fixAll.tslint' without touching any other 'editor.codeActionsOnSave'", () => { - const result = convertEditorCodeActionsOnSave( - { - editorSettingName: "editor.codeActionsOnSave", - value: { - "one-property": 42, - "source.fixAll.tslint": true, - "another-property": "foo", - }, - }, - createStubTSLintToESLintSettings(), - ); - - expect(result).toEqual({ - settings: [ - { - editorSettingName: "editor.codeActionsOnSave", - value: { - "one-property": 42, - "another-property": "foo", - }, - }, - { - editorSettingName: "eslint.autoFixOnSave", - value: true, - }, - ], - }); - }); -}); diff --git a/src/converters/editorConfigs/converters/tests/tslint-config-file.test.ts b/src/converters/editorConfigs/converters/tests/tslint-config-file.test.ts deleted file mode 100644 index 9bb3317ff..000000000 --- a/src/converters/editorConfigs/converters/tests/tslint-config-file.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { convertTSLintConfigFile } from "../tslint-config-file"; - -describe(convertTSLintConfigFile, () => { - test("conversion of 'tslint.configFile' when it roughly equals the ESLint file", () => { - const result = convertTSLintConfigFile( - { - editorSettingName: "tslint.configFile", - value: "./custom/tslint.json", - }, - { config: "./custom/eslintrc.js" }, - ); - - expect(result).toEqual({ - settings: [ - { - editorSettingName: "eslint.options", - value: { configFile: "./custom/eslintrc.js" }, - }, - ], - }); - }); - - test("conversion of 'tslint.configFile' when it does not roughly equal the ESLint file", () => { - const result = convertTSLintConfigFile( - { - editorSettingName: "tslint.configFile", - value: "./custom/tslint.json", - }, - { config: "./eslintrc.js" }, - ); - - expect(result).toEqual(undefined); - }); -}); diff --git a/src/converters/editorConfigs/converters/tslint-config-file.ts b/src/converters/editorConfigs/converters/tslint-config-file.ts deleted file mode 100644 index 53b8c3c65..000000000 --- a/src/converters/editorConfigs/converters/tslint-config-file.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as path from "path"; - -import { EditorSettingConverter } from "../converter"; - -export const convertTSLintConfigFile: EditorSettingConverter = ( - originalTSLintConfigFile, - settings, -) => { - // If the output ESLint config path doesn't roughly match the original TSLint path, skip this. - const tslintPath = originalTSLintConfigFile.value; - const eslintPath = settings.config; - if (path.relative(path.dirname(tslintPath), path.dirname(eslintPath))) { - return undefined; - } - - return { - settings: [ - { - editorSettingName: "eslint.options", - value: { configFile: settings.config }, - }, - ], - }; -}; diff --git a/src/converters/editorConfigs/editorConversionResults.stubs.ts b/src/converters/editorConfigs/editorConversionResults.stubs.ts deleted file mode 100644 index 83d755733..000000000 --- a/src/converters/editorConfigs/editorConversionResults.stubs.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { EditorSettingConversionResults } from "./convertEditorSettings"; - -export const createEmptyEditorSettingConversionResults = ( - overrides: Partial = {}, -): EditorSettingConversionResults => ({ - converted: new Map(), - failed: [], - missing: [], - ...overrides, -}); diff --git a/src/converters/editorConfigs/editorSettingsConverters.ts b/src/converters/editorConfigs/editorSettingsConverters.ts deleted file mode 100644 index c49181025..000000000 --- a/src/converters/editorConfigs/editorSettingsConverters.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { convertEditorCodeActionsOnSave } from "./converters/editor-code-actions-on-save"; -import { convertTSLintConfigFile } from "./converters/tslint-config-file"; - -/** - * Keys TSLint property names in editor settings to their ESLint editor settings converters. - */ -export const editorSettingsConverters = new Map([ - ["editor.codeActionsOnSave", convertEditorCodeActionsOnSave], - ["tslint.configFile", convertTSLintConfigFile], -]); diff --git a/src/converters/editorConfigs/reporting/reportEditorConfigConversionResults.test.ts b/src/converters/editorConfigs/reporting/reportEditorConfigConversionResults.test.ts new file mode 100644 index 000000000..9a88d0061 --- /dev/null +++ b/src/converters/editorConfigs/reporting/reportEditorConfigConversionResults.test.ts @@ -0,0 +1,120 @@ +import { EOL } from "os"; + +import { createStubLogger, expectEqualWrites } from "../../../adapters/logger.stubs"; +import { EditorConfigsConversionResults } from "../types"; +import { reportEditorConfigConversionResults } from "./reportEditorConfigConversionResults"; + +const createStubConversionResults = (overrides: Partial = {}) => ({ + failed: new Map(), + successes: new Map(), + ...overrides, +}); + +const stubSuccess = { + contents: "Hello, world!", + missing: [], +}; + +describe("reportEditorConfigConversionResults", () => { + it("logs a successful conversion when there is one converted editor setting", () => { + // Arrange + const conversionResults = createStubConversionResults({ + successes: new Map([["./one", stubSuccess]]), + }); + + const logger = createStubLogger(); + + // Act + reportEditorConfigConversionResults({ logger }, conversionResults); + + // Assert + expectEqualWrites( + logger.stdout.write, + `${EOL}✨ 1 editor file augmented with its ESLint equivalent. ✨${EOL}`, + ); + }); + + it("logs successful conversions when there are multiple converted settings", () => { + // Arrange + const conversionResults = createStubConversionResults({ + successes: new Map([ + ["./one", stubSuccess], + ["./two", stubSuccess], + ]), + }); + + const logger = createStubLogger(); + + // Act + reportEditorConfigConversionResults({ logger }, conversionResults); + + // Assert + expectEqualWrites( + logger.stdout.write, + `${EOL}✨ 2 editor files augmented with their ESLint equivalents. ✨${EOL}`, + ); + }); + + it("logs missing setting when there a successful conversion includes them", () => { + // Arrange + const missing = ["setting"]; + const conversionResults = createStubConversionResults({ + successes: new Map([["./one", { ...stubSuccess, missing }]]), + }); + + const logger = createStubLogger(); + + // Act + reportEditorConfigConversionResults({ logger }, conversionResults); + + // Assert + expectEqualWrites( + logger.stdout.write, + `${EOL}✨ 1 editor file augmented with its ESLint equivalent. ✨`, + ``, + `❓ 1 ./one editor setting is not known by tslint-to-eslint-config to have an ESLint equivalent. ❓`, + ` Check stub-output.log for details.`, + ); + }); + + it("logs a failed conversion when there is one failed conversion", () => { + // Arrange + const conversionResults = createStubConversionResults({ + failed: new Map([["./one", new Error("It broke.")]]), + }); + + const logger = createStubLogger(); + + // Act + reportEditorConfigConversionResults({ logger }, conversionResults); + + // Assert + expectEqualWrites( + logger.stderr.write, + `❌ 1 error thrown. ❌`, + ` Check ${logger.debugFileName} for details.`, + ); + }); + + it("logs failed conversions when there are multiple failed conversions", () => { + // Arrange + const conversionResults = createStubConversionResults({ + failed: new Map([ + ["./one", new Error("It broke.")], + ["./two", new Error("It really broke.")], + ]), + }); + + const logger = createStubLogger(); + + // Act + reportEditorConfigConversionResults({ logger }, conversionResults); + + // Assert + expectEqualWrites( + logger.stderr.write, + `❌ 2 errors thrown. ❌`, + ` Check ${logger.debugFileName} for details.`, + ); + }); +}); diff --git a/src/converters/editorConfigs/reporting/reportEditorConfigConversionResults.ts b/src/converters/editorConfigs/reporting/reportEditorConfigConversionResults.ts new file mode 100644 index 000000000..537ddb690 --- /dev/null +++ b/src/converters/editorConfigs/reporting/reportEditorConfigConversionResults.ts @@ -0,0 +1,43 @@ +import { Logger } from "../../../adapters/logger"; +import { + logSuccessfulConversions, + logFailedConversions, + logMissingConversionTarget, +} from "../../../reporting"; +import { EditorConfigsConversionResults } from "../types"; + +export type ReportEditorConfigConversionResultsDependencies = { + logger: Logger; +}; + +export const reportEditorConfigConversionResults = ( + dependencies: ReportEditorConfigConversionResultsDependencies, + results: EditorConfigsConversionResults, +) => { + if (results.successes.size !== 0) { + logSuccessfulConversions( + "editor file", + "augmented", + results.successes.size, + dependencies.logger, + ); + + for (const [filePath, success] of results.successes) { + if (success.missing.length) { + logMissingConversionTarget( + `${filePath} editor setting`, + (editorSetting) => editorSetting, + success.missing, + dependencies.logger, + ); + } + } + } + + if (results.failed.size !== 0) { + logFailedConversions( + Array.from(results.failed.values()).map((fail) => fail.message), + dependencies.logger, + ); + } +}; diff --git a/src/converters/editorConfigs/reporting/reportEditorSettingConversionResults.test.ts b/src/converters/editorConfigs/reporting/reportEditorSettingConversionResults.test.ts deleted file mode 100644 index d0c399fca..000000000 --- a/src/converters/editorConfigs/reporting/reportEditorSettingConversionResults.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { EOL } from "os"; - -import { createStubLogger, expectEqualWrites } from "../../../adapters/logger.stubs"; -import { createEmptyEditorSettingConversionResults } from "../editorConversionResults.stubs"; -import { EditorSetting } from "../types"; - -import { reportEditorSettingConversionResults } from "./reportEditorSettingConversionResults"; - -describe("reportEditorSettingConversionResults", () => { - it("logs a successful conversion when there is one converted editor setting", () => { - // Arrange - const conversionResults = createEmptyEditorSettingConversionResults({ - converted: new Map([ - [ - "tslint-editor-setting-one", - { - editorSettingName: "tslint-editor-setting-one", - value: 42, - }, - ], - ]), - }); - - const logger = createStubLogger(); - - // Act - reportEditorSettingConversionResults({ logger }, conversionResults); - - // Assert - expectEqualWrites( - logger.stdout.write, - `${EOL}✨ 1 editor setting replaced with its ESLint equivalent. ✨${EOL}`, - ); - }); - - it("logs successful conversions when there are multiple converted settings", () => { - // Arrange - const conversionResults = createEmptyEditorSettingConversionResults({ - converted: new Map([ - [ - "tslint-editor-setting-one", - { - editorSettingName: "tslint-editor-setting-one", - value: 42, - }, - ], - [ - "tslint-editor-setting-two", - { - editorSettingName: "tslint-editor-setting-two", - value: 4711, - }, - ], - ]), - }); - - const logger = createStubLogger(); - - // Act - reportEditorSettingConversionResults({ logger }, conversionResults); - - // Assert - expectEqualWrites( - logger.stdout.write, - `${EOL}✨ 2 editor settings replaced with their ESLint equivalents. ✨${EOL}`, - ); - }); - - it("logs a failed conversion when there is one failed conversion", () => { - // Arrange - const conversionResults = createEmptyEditorSettingConversionResults({ - failed: [{ getSummary: () => "It broke." }], - }); - - const logger = createStubLogger(); - - // Act - reportEditorSettingConversionResults({ logger }, conversionResults); - - // Assert - expectEqualWrites( - logger.stderr.write, - `❌ 1 error thrown. ❌`, - ` Check ${logger.debugFileName} for details.`, - ); - }); - - it("logs failed conversions when there are multiple failed conversions", () => { - // Arrange - const conversionResults = createEmptyEditorSettingConversionResults({ - failed: [{ getSummary: () => "It broke." }, { getSummary: () => "It really broke." }], - }); - - const logger = createStubLogger(); - - // Act - reportEditorSettingConversionResults({ logger }, conversionResults); - - // Assert - expectEqualWrites( - logger.stderr.write, - `❌ 2 errors thrown. ❌`, - ` Check ${logger.debugFileName} for details.`, - ); - }); - - it("logs a missing editor setting when there is a missing setting", () => { - // Arrange - const conversionResults = createEmptyEditorSettingConversionResults({ - missing: [ - { - editorSettingName: "tslint-editor-setting-one", - }, - ], - }); - - const logger = createStubLogger(); - - // Act - reportEditorSettingConversionResults({ logger }, conversionResults); - - // Assert - expectEqualWrites( - logger.stdout.write, - `❓ 1 editor setting is not known by tslint-to-eslint-config to have an ESLint equivalent. ❓`, - ` Check ${logger.debugFileName} for details.`, - ); - expectEqualWrites( - logger.info.write, - `1 editor setting is not known by tslint-to-eslint-config to have an ESLint equivalent:`, - ' * tslint-to-eslint-config does not know the ESLint equivalent for TSLint\'s "tslint-editor-setting-one".', - ); - }); - - it("logs missing settings when there are missing settings", () => { - // Arrange - const conversionResults = createEmptyEditorSettingConversionResults({ - missing: [ - { - editorSettingName: "tslint-editor-setting-one", - }, - { - editorSettingName: "tslint-editor-setting-two", - }, - ], - }); - - const logger = createStubLogger(); - - // Act - reportEditorSettingConversionResults({ logger }, conversionResults); - - // Assert - expectEqualWrites( - logger.stdout.write, - `❓ 2 editor settings are not known by tslint-to-eslint-config to have ESLint equivalents. ❓`, - ` Check ${logger.debugFileName} for details.`, - ); - expectEqualWrites( - logger.info.write, - `2 editor settings are not known by tslint-to-eslint-config to have ESLint equivalents:`, - ' * tslint-to-eslint-config does not know the ESLint equivalent for TSLint\'s "tslint-editor-setting-one".', - ' * tslint-to-eslint-config does not know the ESLint equivalent for TSLint\'s "tslint-editor-setting-two".', - ); - }); -}); diff --git a/src/converters/editorConfigs/reporting/reportEditorSettingConversionResults.ts b/src/converters/editorConfigs/reporting/reportEditorSettingConversionResults.ts deleted file mode 100644 index d7da61dcd..000000000 --- a/src/converters/editorConfigs/reporting/reportEditorSettingConversionResults.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Logger } from "../../../adapters/logger"; -import { - logSuccessfulConversions, - logFailedConversions, - logMissingConversionTarget, -} from "../../../reporting"; -import { EditorSettingConversionResults } from "../convertEditorSettings"; - -export type ReportEditorSettingConversionResultsDependencies = { - logger: Logger; -}; - -export const reportEditorSettingConversionResults = ( - dependencies: ReportEditorSettingConversionResultsDependencies, - editorSettingConversionResults: EditorSettingConversionResults, -) => { - if (editorSettingConversionResults.converted.size !== 0) { - logSuccessfulConversions( - "editor setting", - editorSettingConversionResults.converted, - dependencies.logger, - ); - } - - if (editorSettingConversionResults.failed.length !== 0) { - logFailedConversions(editorSettingConversionResults.failed, dependencies.logger); - } - - if (editorSettingConversionResults.missing.length !== 0) { - logMissingConversionTarget( - "editor setting", - (editorSetting) => editorSetting.editorSettingName, - editorSettingConversionResults.missing, - dependencies.logger, - ); - } -}; diff --git a/src/converters/editorConfigs/types.ts b/src/converters/editorConfigs/types.ts index 2c8ff4d54..a1e7b30ec 100644 --- a/src/converters/editorConfigs/types.ts +++ b/src/converters/editorConfigs/types.ts @@ -1,4 +1,18 @@ -export type EditorSetting = { - editorSettingName: string; - value: any; +import { TSLintToESLintSettings } from "../../types"; + +export type EditorConfigConverter = ( + rawEditorSettings: string, + settings: TSLintToESLintSettings, +) => EditorConfigConversionResults; + +export type EditorConfigConversionResults = { + contents: string; + missing: string[]; }; + +export type EditorConfigsConversionResults = { + failed: Map; + successes: Map; +}; + +export type EditorConfigDescriptor = readonly [string, EditorConfigConverter]; diff --git a/src/converters/lintConfigs/reporting/reportConfigConversionResults.ts b/src/converters/lintConfigs/reporting/reportConfigConversionResults.ts index d60613379..287a06e17 100644 --- a/src/converters/lintConfigs/reporting/reportConfigConversionResults.ts +++ b/src/converters/lintConfigs/reporting/reportConfigConversionResults.ts @@ -20,12 +20,20 @@ export const reportConfigConversionResults = async ( ruleConversionResults: SummarizedConfigResultsConfiguration, ) => { if (ruleConversionResults.converted.size !== 0) { - logSuccessfulConversions("rule", ruleConversionResults.converted, dependencies.logger); + logSuccessfulConversions( + "rule", + "replaced", + ruleConversionResults.converted.size, + dependencies.logger, + ); logNotices(ruleConversionResults.converted, dependencies.logger); } if (ruleConversionResults.failed.length !== 0) { - logFailedConversions(ruleConversionResults.failed, dependencies.logger); + logFailedConversions( + ruleConversionResults.failed.map((fail) => fail.getSummary()), + dependencies.logger, + ); } if (ruleConversionResults.missing.length !== 0) { diff --git a/src/converters/lintConfigs/writeEditorConfigConversionResults.test.ts b/src/converters/lintConfigs/writeEditorConfigConversionResults.test.ts deleted file mode 100644 index 7900e2e4c..000000000 --- a/src/converters/lintConfigs/writeEditorConfigConversionResults.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { createStubFileSystem } from "../../adapters/fileSystem.stub"; -import { EditorConfiguration } from "../../input/editorConfiguration"; -import { DeepPartial } from "../../input/findReportedConfiguration"; -import { EditorSettingConversionResults } from "../editorConfigs/convertEditorSettings"; -import { createEmptyEditorSettingConversionResults } from "../editorConfigs/editorConversionResults.stubs"; -import { EditorSetting } from "../editorConfigs/types"; -import { formatJsonOutput } from "./formatting/formatters/formatJsonOutput"; -import { - writeEditorConfigConversionResults, - WriteConversionResultsDependencies, -} from "./writeEditorConfigConversionResults"; - -const createStubDependencies = (overrides: Partial = {}) => ({ - fileSystem: createStubFileSystem(), - ...overrides, -}); - -describe("writeConversionResults", () => { - it("writes to correct output path with file system", async () => { - // Arrange - const dependencies = createStubDependencies(); - const outputPath = "/temp"; - const { originalConfig, conversionResults } = setupConversionEnvironment(); - - // Act - await writeEditorConfigConversionResults( - dependencies, - outputPath, - conversionResults, - originalConfig, - ); - - // Assert - expect(dependencies.fileSystem.writeFile).toHaveBeenCalledWith( - outputPath, - expect.anything(), - ); - }); - - it("writes formatted output with sorted keys to file system", async () => { - // Arrange - const dependencies = createStubDependencies(); - const outputPath = "/temp"; - - const { originalConfig, conversionResults } = setupConversionEnvironment({ - originalConfig: { - "property.a": "someValue", - "property.c": 123, - "property.b": { - "unsorted.sub.property.b": false, - "unsorted.sub.property.a": false, - }, - }, - conversionResults: createEmptyEditorSettingConversionResults({ - converted: new Map([ - [ - "eslint-setting-b", - { - editorSettingName: "eslint-setting-b", - value: 42, - }, - ], - [ - "eslint-setting-a", - { - editorSettingName: "eslint-setting-a", - value: 4711, - }, - ], - ]), - }), - }); - - const expectedSortedOutput = formatJsonOutput({ - "eslint-setting-a": 4711, - "eslint-setting-b": 42, - "property.a": "someValue", - "property.b": { - "unsorted.sub.property.b": false, - "unsorted.sub.property.a": false, - }, - "property.c": 123, - }); - - // Act - await writeEditorConfigConversionResults( - dependencies, - outputPath, - conversionResults, - originalConfig, - ); - - // Assert - expect(dependencies.fileSystem.writeFile).toHaveBeenCalledWith( - expect.anything(), - expectedSortedOutput, - ); - }); -}); - -function setupConversionEnvironment( - overrides: { - conversionResults?: EditorSettingConversionResults; - originalConfig?: DeepPartial; - } = {}, -) { - return { - originalConfig: { - "typescript.tsdk": "node_modules/typescript/lib", - "editor.tabSize": 4, - "editor.codeActionsOnSave": { - "source.organizeImports": false, - }, - }, - conversionResults: createEmptyEditorSettingConversionResults({ - converted: new Map([ - [ - "tslint-editor-setting-one", - { - editorSettingName: "tslint-editor-setting-one", - value: 42, - }, - ], - ]), - }), - ...overrides, - }; -} diff --git a/src/converters/lintConfigs/writeEditorConfigConversionResults.ts b/src/converters/lintConfigs/writeEditorConfigConversionResults.ts deleted file mode 100644 index 66d158e09..000000000 --- a/src/converters/lintConfigs/writeEditorConfigConversionResults.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { FileSystem } from "../../adapters/fileSystem"; -import { EditorConfiguration } from "../../input/editorConfiguration"; -import { DeepPartial } from "../../input/findReportedConfiguration"; -import { EditorSettingConversionResults } from "../editorConfigs/convertEditorSettings"; -import { formatOutput } from "./formatting/formatOutput"; - -export type WriteConversionResultsDependencies = { - fileSystem: Pick; -}; - -export const writeEditorConfigConversionResults = async ( - dependencies: WriteConversionResultsDependencies, - outputPath: string, - conversionResults: EditorSettingConversionResults, - originalConfiguration: DeepPartial, -) => { - const output = { - ...originalConfiguration, - ...formatConvertedSettings(conversionResults), - }; - - return await dependencies.fileSystem.writeFile(outputPath, formatOutput(outputPath, output)); -}; - -export const formatConvertedSettings = (conversionResults: EditorSettingConversionResults) => { - const output: Record = {}; - const sortedEntries = Array.from(conversionResults.converted).sort(([nameA], [nameB]) => - nameA.localeCompare(nameB), - ); - - for (const [name, setting] of sortedEntries) { - output[name] = setting.value; - } - - return output; -}; diff --git a/src/errors/conversionError.ts b/src/errors/conversionError.ts index 472d0203d..e37195004 100644 --- a/src/errors/conversionError.ts +++ b/src/errors/conversionError.ts @@ -1,11 +1,12 @@ import { EOL } from "os"; -import { EditorSetting } from "../converters/editorConfigs/types"; import { TSLintRuleOptions } from "../converters/lintConfigs/rules/types"; import { ErrorSummary } from "./errorSummary"; -export class ConversionError implements ErrorSummary { - private constructor(private readonly summary: string) {} +export class ConversionError extends Error implements ErrorSummary { + private constructor(private readonly summary: string) { + super(summary); + } public static forMerger(eslintRule: string) { return new ConversionError( @@ -22,12 +23,6 @@ export class ConversionError implements ErrorSummary { ); } - public static forSettingError(error: Error, editorSetting: EditorSetting) { - return new ConversionError( - `${editorSetting.editorSettingName} threw an error during conversion: ${error.stack}${EOL}`, - ); - } - public getSummary(): string { return this.summary; } diff --git a/src/input/importer.ts b/src/input/importer.ts index be3a622bf..aa2af57e6 100644 --- a/src/input/importer.ts +++ b/src/input/importer.ts @@ -1,8 +1,8 @@ import * as path from "path"; -import stripJsonComments from "strip-json-comments"; import { FileSystem } from "../adapters/fileSystem"; import { NativeImporter } from "../adapters/nativeImporter"; +import { parseJson } from "../utils"; export type ImporterDependencies = { fileSystem: Pick; @@ -31,7 +31,7 @@ export const importer = async ( } try { - return JSON.parse(stripJsonComments(rawJsonContents)); + return parseJson(rawJsonContents); } catch (error) { return error; } diff --git a/src/reporting.ts b/src/reporting.ts index b41f04875..579ff8cf0 100644 --- a/src/reporting.ts +++ b/src/reporting.ts @@ -2,7 +2,6 @@ import chalk from "chalk"; import { EOL } from "os"; import { Logger } from "./adapters/logger"; -import { ErrorSummary } from "./errors/errorSummary"; import { ResultWithStatus, ResultStatus } from "./types"; export const logErrorResult = (result: ResultWithStatus, logger: Logger) => { @@ -31,24 +30,25 @@ export const logErrorResult = (result: ResultWithStatus, logger: Logger) => { export const logSuccessfulConversions = ( conversionTypeName: string, - converted: Map, + action: string, + quantity: number, logger: Logger, ) => { - logger.stdout.write(chalk.greenBright(`${EOL}✨ ${converted.size}`)); + logger.stdout.write(chalk.greenBright(`${EOL}✨ ${quantity}`)); logger.stdout.write( - converted.size === 1 - ? chalk.green(` ${conversionTypeName} replaced with its ESLint equivalent.`) - : chalk.green(` ${conversionTypeName}s replaced with their ESLint equivalents.`), + quantity === 1 + ? chalk.green(` ${conversionTypeName} ${action} with its ESLint equivalent.`) + : chalk.green(` ${conversionTypeName}s ${action} with their ESLint equivalents.`), ); logger.stdout.write(chalk.greenBright(` ✨${EOL}`)); }; -export const logFailedConversions = (failed: ErrorSummary[], logger: Logger) => { +export const logFailedConversions = (failed: string[], logger: Logger) => { logger.stderr.write(`${chalk.redBright(`${EOL}❌ ${failed.length}`)}`); 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((fail) => fail.getSummary()).join("\n\n") + "\n\n"); + logger.info.write(failed.join("\n\n") + "\n\n"); logger.stderr.write(chalk.red(` Check ${logger.debugFileName} for details.${EOL}`)); }; diff --git a/src/types.ts b/src/types.ts index a25919786..8763f2659 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,9 +10,9 @@ export type TSLintToESLintSettings = { comments?: true | string | string[]; /** - * Original Editor configuration file path, such as `.vscode/settings.json`. + * Original Editor configuration file path(s), such as `.vscode/settings.json`. */ - editor?: string; + editor?: string | string[]; /** * Original ESLint configuration file path, such as `.eslintrc.js`. diff --git a/src/utils.ts b/src/utils.ts index 2adbd4447..798c4c0e2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import stripJsonComments from "strip-json-comments"; + export const isDefined = (item: Item | undefined): item is Item => item !== undefined; export const isError = (item: Item | Error): item is Error => item instanceof Error; @@ -52,3 +54,5 @@ export const uniqueFromSources = (...sources: (T | T[] | undefined)[]) => { return Array.from(new Set(items)); }; + +export const parseJson = (text: string) => JSON.parse(stripJsonComments(text));