diff --git a/jest.config.js b/jest.config.js index f329ba374..1e574609c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,7 +4,8 @@ module.exports = { "!./src/**/*.d.ts", "!./src/**/*.stubs.ts", "!./src/adapters/*.ts", - "!./src/rules/converters.ts", + "!./src/rules/rulesConverters.ts", + "!./src/editorSettings/editorSettingsConverters.ts", "!./src/rules/mergers.ts", "!./src/cli/main.ts", ], diff --git a/src/adapters/fileSystem.stub.ts b/src/adapters/fileSystem.stub.ts new file mode 100644 index 000000000..eb1fdcc00 --- /dev/null +++ b/src/adapters/fileSystem.stub.ts @@ -0,0 +1,11 @@ +export const createStubFileSystem = ({ data = {}, exists = true } = {}) => ({ + fileExists: jest.fn().mockReturnValue(exists), + readFile: jest.fn().mockReturnValue(Promise.resolve(data)), + writeFile: jest.fn(), +}); + +export const createStubThrowingFileSystem = ({ err = "" } = {}) => ({ + fileExists: jest.fn().mockRejectedValue(Promise.resolve(new Error(err))), + readFile: jest.fn().mockRejectedValue(Promise.resolve(new Error(err))), + writeFile: jest.fn().mockRejectedValue(Promise.resolve(new Error(err))), +}); diff --git a/src/cli/main.ts b/src/cli/main.ts index efd20eb6c..b3adcd29b 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -5,11 +5,15 @@ 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 { convertConfig, ConvertConfigDependencies } from "../conversion/convertConfig"; +import { + convertEditorConfig, + ConvertEditorConfigDependencies, +} from "../conversion/convertEditorConfig"; import { removeExtendsDuplicatedRules } from "../creation/simplification/removeExtendsDuplicatedRules"; import { - RetrieveExtendsValuesDependencies, retrieveExtendsValues, + RetrieveExtendsValuesDependencies, } from "../creation/simplification/retrieveExtendsValues"; import { simplifyPackageRules, @@ -19,30 +23,43 @@ import { writeConversionResults, WriteConversionResultsDependencies, } from "../creation/writeConversionResults"; +import { writeConversionResults as writeEditorConfigConversionResults } from "../creation/writeEditorConfigConversionResults"; +import { + convertEditorSettings, + ConvertEditorSettingsDependencies, +} from "../editorSettings/convertEditorSettings"; +import { editorSettingsConverters } from "../editorSettings/editorSettingsConverters"; +import { + findEditorConfiguration, + FindEditorConfigurationDependencies, +} from "../input/findEditorConfiguration"; +import { findESLintConfiguration } from "../input/findESLintConfiguration"; import { findOriginalConfigurations, FindOriginalConfigurationsDependencies, } from "../input/findOriginalConfigurations"; import { findPackagesConfiguration } from "../input/findPackagesConfiguration"; -import { findESLintConfiguration } from "../input/findESLintConfiguration"; import { findTSLintConfiguration } from "../input/findTSLintConfiguration"; import { findTypeScriptConfiguration } from "../input/findTypeScriptConfiguration"; import { importer, ImporterDependencies } from "../input/importer"; import { mergeLintConfigurations } from "../input/mergeLintConfigurations"; -import { - reportConversionResults, - ReportConversionResultsDependencies, -} from "../reporting/reportConversionResults"; -import { converters } from "../rules/converters"; -import { convertRules } from "../rules/convertRules"; +import { ReportConversionResultsDependencies } from "../reporting/dependencies"; +import { reportConversionResults } from "../reporting/reportConversionResults"; +import { reportEditorSettingConversionResults } from "../reporting/reportEditorSettingConversionResults"; +import { convertRules, ConvertRulesDependencies } from "../rules/convertRules"; import { mergers } from "../rules/mergers"; +import { rulesConverters } from "../rules/rulesConverters"; import { runCli, RunCliDependencies } from "./runCli"; -const convertRulesDependencies = { - converters, +const convertRulesDependencies: ConvertRulesDependencies = { + converters: rulesConverters, mergers, }; +const convertEditorSettingsDependencies: ConvertEditorSettingsDependencies = { + converters: editorSettingsConverters, +}; + const nativeImporterDependencies: ImporterDependencies = { fileSystem: fsFileSystem, getCwd: () => process.cwd(), @@ -56,6 +73,10 @@ const findConfigurationDependencies = { importer: boundImporter, }; +const findEditorConfigurationDependencies: FindEditorConfigurationDependencies = { + importer: boundImporter, +}; + const findOriginalConfigurationsDependencies: FindOriginalConfigurationsDependencies = { findESLintConfiguration: bind(findESLintConfiguration, findConfigurationDependencies), findPackagesConfiguration: bind(findPackagesConfiguration, findConfigurationDependencies), @@ -81,6 +102,19 @@ const writeConversionResultsDependencies: WriteConversionResultsDependencies = { fileSystem: fsFileSystem, }; +const convertEditorConfigDependencies: ConvertEditorConfigDependencies = { + findEditorConfiguration: bind(findEditorConfiguration, findEditorConfigurationDependencies), + convertEditorSettings: bind(convertEditorSettings, convertEditorSettingsDependencies), + reportConversionResults: bind( + reportEditorSettingConversionResults, + reportConversionResultsDependencies, + ), + writeConversionResults: bind( + writeEditorConfigConversionResults, + writeConversionResultsDependencies, + ), +}; + const convertConfigDependencies: ConvertConfigDependencies = { convertRules: bind(convertRules, convertRulesDependencies), findOriginalConfigurations: bind( @@ -93,7 +127,10 @@ const convertConfigDependencies: ConvertConfigDependencies = { }; const runCliDependencies: RunCliDependencies = { - convertConfig: bind(convertConfig, convertConfigDependencies), + convertConfigs: [ + bind(convertConfig, convertConfigDependencies), + bind(convertEditorConfig, convertEditorConfigDependencies), + ], logger: processLogger, }; diff --git a/src/cli/runCli.test.ts b/src/cli/runCli.test.ts index 99fe90db7..c163ce6d1 100644 --- a/src/cli/runCli.test.ts +++ b/src/cli/runCli.test.ts @@ -8,9 +8,11 @@ import { runCli, RunCliDependencies } from "./runCli"; const createStubArgv = (argv: string[] = []) => ["node", "some/path/bin/file", ...argv]; const createStubRunCliDependencies = ( - overrides: Partial> = {}, + overrides: Partial> = {}, ) => ({ - convertConfig: async (): Promise => ({ status: ResultStatus.Succeeded }), + convertConfigs: [ + async (): Promise => ({ status: ResultStatus.Succeeded }), + ], logger: createStubLogger(), ...overrides, }); @@ -32,7 +34,7 @@ describe("runCli", () => { // Arrange const message = "Oh no"; const dependencies = createStubRunCliDependencies({ - convertConfig: () => Promise.reject(new Error(message)), + convertConfigs: [() => Promise.reject(new Error(message))], }); // Act @@ -49,11 +51,13 @@ describe("runCli", () => { // Arrange const complaint = "too much unit testing coverage"; const dependencies = createStubRunCliDependencies({ - convertConfig: () => - Promise.resolve({ - complaints: [complaint], - status: ResultStatus.ConfigurationError, - }), + convertConfigs: [ + () => + Promise.resolve({ + complaints: [complaint], + status: ResultStatus.ConfigurationError, + }), + ], }); // Act @@ -72,11 +76,13 @@ describe("runCli", () => { // Arrange const error = new Error("too much unit testing coverage"); const dependencies = createStubRunCliDependencies({ - convertConfig: () => - Promise.resolve({ - errors: [error], - status: ResultStatus.Failed, - }), + convertConfigs: [ + () => + Promise.resolve({ + errors: [error], + status: ResultStatus.Failed, + }), + ], }); // Act @@ -98,11 +104,13 @@ describe("runCli", () => { new Error("too much branch coverage"), ]; const dependencies = createStubRunCliDependencies({ - convertConfig: () => - Promise.resolve({ - errors, - status: ResultStatus.Failed, - }), + convertConfigs: [ + () => + Promise.resolve({ + errors, + status: ResultStatus.Failed, + }), + ], }); // Act @@ -133,12 +141,14 @@ describe("runCli", () => { it("default output should be .eslintrc.js", async () => { let defaultConfig; const dependencies = createStubRunCliDependencies({ - convertConfig: parsedArgs => { - defaultConfig = parsedArgs.config; - return Promise.resolve({ - status: ResultStatus.Succeeded, - }); - }, + convertConfigs: [ + parsedArgs => { + defaultConfig = parsedArgs.config; + return Promise.resolve({ + status: ResultStatus.Succeeded, + }); + }, + ], }); const status = await runCli(dependencies, createStubArgv()); diff --git a/src/cli/runCli.ts b/src/cli/runCli.ts index 147891d02..0a1cba644 100644 --- a/src/cli/runCli.ts +++ b/src/cli/runCli.ts @@ -1,15 +1,16 @@ import chalk from "chalk"; -import { EOL } from "os"; import { Command } from "commander"; +import { EOL } from "os"; import { version } from "../../package.json"; import { Logger } from "../adapters/logger"; import { SansDependencies } from "../binding"; import { convertConfig } from "../conversion/convertConfig"; -import { TSLintToESLintSettings, TSLintToESLintResult, ResultStatus } from "../types"; +import { DEFAULT_VSCODE_SETTINGS_PATH } from "../input/vsCodeSettings"; +import { ResultStatus, ResultWithStatus, TSLintToESLintSettings } from "../types"; export type RunCliDependencies = { - convertConfig: SansDependencies; + convertConfigs: SansDependencies[]; logger: Logger; }; @@ -24,6 +25,11 @@ export const runCli = async ( .option("--package [package]", "package configuration file to convert using") .option("--tslint [tslint]", "tslint configuration file to convert using") .option("--typescript [typescript]", "typescript configuration file to convert using") + .option( + "--editor [editor]", + "editor configuration file to convert using", + DEFAULT_VSCODE_SETTINGS_PATH, + ) .option("-V --version", "output the package version"); const parsedArgv = { @@ -36,10 +42,26 @@ export const runCli = async ( return ResultStatus.Succeeded; } - let result: TSLintToESLintResult; + for (const convertConfig of dependencies.convertConfigs) { + const result = await tryConvertConfig(convertConfig, parsedArgv); + if (result.status !== ResultStatus.Succeeded) { + logErrorResult(result, dependencies); + return result.status; + } + } + + dependencies.logger.stdout.write(chalk.greenBright("✅ All is well! ✅\n")); + return ResultStatus.Succeeded; +}; + +const tryConvertConfig = async ( + config: SansDependencies, + argv: Partial, +): Promise => { + let result: ResultWithStatus; try { - result = await dependencies.convertConfig(parsedArgv); + result = await config(argv as TSLintToESLintSettings); } catch (error) { result = { errors: [error as Error], @@ -47,11 +69,11 @@ export const runCli = async ( }; } - switch (result.status) { - case ResultStatus.Succeeded: - dependencies.logger.stdout.write(chalk.greenBright("✅ All is well! ✅\n")); - break; + return result; +}; +const logErrorResult = (result: ResultWithStatus, dependencies: RunCliDependencies) => { + switch (result.status) { case ResultStatus.ConfigurationError: dependencies.logger.stderr.write(chalk.redBright("❌ ")); dependencies.logger.stderr.write(chalk.red("Could not start tslint-to-eslint:")); @@ -72,6 +94,4 @@ export const runCli = async ( } break; } - - return result.status; }; diff --git a/src/conversion/conversionResults.stubs.ts b/src/conversion/conversionResults.stubs.ts index 57249adb2..b3a6ffdfc 100644 --- a/src/conversion/conversionResults.stubs.ts +++ b/src/conversion/conversionResults.stubs.ts @@ -1,3 +1,4 @@ +import { EditorSettingConversionResults } from "../editorSettings/convertEditorSettings"; import { RuleConversionResults } from "../rules/convertRules"; export const createEmptyConversionResults = ( @@ -9,3 +10,12 @@ export const createEmptyConversionResults = ( plugins: new Set(), ...overrides, }); + +export const createEmptySettingConversionResults = ( + overrides: Partial = {}, +): EditorSettingConversionResults => ({ + converted: new Map(), + failed: [], + missing: [], + ...overrides, +}); diff --git a/src/conversion/convertEditorConfig.test.ts b/src/conversion/convertEditorConfig.test.ts new file mode 100644 index 000000000..c6493256f --- /dev/null +++ b/src/conversion/convertEditorConfig.test.ts @@ -0,0 +1,132 @@ +import { EditorSetting } from "../editorSettings/types"; +import { FailedResult, ResultStatus } from "../types"; +import { createEmptySettingConversionResults } from "./conversionResults.stubs"; +import { convertEditorConfig, ConvertEditorConfigDependencies } from "./convertEditorConfig"; + +const stubSettings = { + config: "./eslintrc.js", + editor: "./my-editor/settings.json", +}; + +const createStubDependencies = ( + overrides: Partial = {}, +): ConvertEditorConfigDependencies => ({ + convertEditorSettings: jest.fn(), + findEditorConfiguration: jest.fn().mockResolvedValue({}), + reportConversionResults: jest.fn(), + writeConversionResults: jest.fn().mockReturnValue(Promise.resolve()), + ...overrides, +}); + +describe("convertEditorConfig", () => { + it("returns the failure result when finding the original configurations fails", async () => { + // Arrange + const error = new Error(); + const findError: FailedResult = { + errors: [error], + status: ResultStatus.Failed, + }; + + const dependencies = createStubDependencies({ + findEditorConfiguration: async () => error, + }); + + // Act + const result = await convertEditorConfig(dependencies, stubSettings); + + // Assert + expect(result).toEqual(findError); + }); + + it("returns the failure result when writing to the configuration file fails", async () => { + // Arrange + const fileWriteError = new Error(); + const dependencies = createStubDependencies({ + writeConversionResults: jest.fn().mockResolvedValueOnce(fileWriteError), + }); + + // 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", + "editor.tabSize": 4, + "editor.codeActionsOnSave": { + "source.organizeImports": false, + }, + }; + + const dependencies = createStubDependencies({ + findEditorConfiguration: jest.fn().mockResolvedValue(originalConfig), + }); + + // Act + await convertEditorConfig(dependencies, stubSettings); + + // Assert + expect(dependencies.convertEditorSettings).toHaveBeenCalledWith(originalConfig); + }); + + it("reports conversion results when settings are converted successfully", async () => { + // Arrange + const conversionResults = createEmptySettingConversionResults({ + converted: new Map([ + [ + "tslint-editor-setting-one", + { + editorSettingName: "tslint-editor-setting-one", + value: 42, + }, + ], + ]), + }); + + const dependencies = createStubDependencies({ + convertEditorSettings: jest.fn().mockReturnValue(conversionResults), + }); + + // Act + await convertEditorConfig(dependencies, stubSettings); + + // Assert + expect(dependencies.reportConversionResults).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); + + // Assert + expect(result).toEqual({ + status: ResultStatus.Succeeded, + }); + }); + + it("uses VS Code default settings path if editor config parameter is undefined", async () => { + // Arrange + const expectedEditorPath = ".vscode/settings.json"; + const settings = { + config: "./eslintrc.js", + }; + + const dependencies = createStubDependencies(); + + // Act + await convertEditorConfig(dependencies, settings); + + // Assert + expect(dependencies.findEditorConfiguration).toHaveBeenCalledWith(expectedEditorPath); + }); +}); diff --git a/src/conversion/convertEditorConfig.ts b/src/conversion/convertEditorConfig.ts new file mode 100644 index 000000000..2d78ba630 --- /dev/null +++ b/src/conversion/convertEditorConfig.ts @@ -0,0 +1,56 @@ +import { SansDependencies } from "../binding"; +import { writeConversionResults } from "../creation/writeEditorConfigConversionResults"; +import { convertEditorSettings } from "../editorSettings/convertEditorSettings"; +import { findEditorConfiguration } from "../input/findEditorConfiguration"; +import { DEFAULT_VSCODE_SETTINGS_PATH } from "../input/vsCodeSettings"; +import { reportEditorSettingConversionResults } from "../reporting/reportEditorSettingConversionResults"; +import { ResultStatus, ResultWithStatus, TSLintToESLintSettings } from "../types"; + +export type ConvertEditorConfigDependencies = { + convertEditorSettings: SansDependencies; + findEditorConfiguration: SansDependencies; + reportConversionResults: SansDependencies; + writeConversionResults: SansDependencies; +}; + +/** + * Root-level driver to convert an editor configuration. + */ +export const convertEditorConfig = async ( + dependencies: ConvertEditorConfigDependencies, + settings: TSLintToESLintSettings, +): Promise => { + const editorConfigPath = settings.editor ? settings.editor : DEFAULT_VSCODE_SETTINGS_PATH; + const originalEditorConfiguration = await dependencies.findEditorConfiguration( + editorConfigPath, + ); + if (originalEditorConfiguration instanceof Error) { + return { + errors: [originalEditorConfiguration], + status: ResultStatus.Failed, + }; + } + + const settingConversionResults = dependencies.convertEditorSettings( + originalEditorConfiguration, + ); + + const outputPath = editorConfigPath; + const fileWriteError = await dependencies.writeConversionResults( + outputPath, + settingConversionResults, + originalEditorConfiguration, + ); + if (fileWriteError !== undefined) { + return { + errors: [fileWriteError], + status: ResultStatus.Failed, + }; + } + + dependencies.reportConversionResults(settingConversionResults); + + return { + status: ResultStatus.Succeeded, + }; +}; diff --git a/src/creation/writeEditorConfigConversionResults.test.ts b/src/creation/writeEditorConfigConversionResults.test.ts new file mode 100644 index 000000000..4a09d8ecb --- /dev/null +++ b/src/creation/writeEditorConfigConversionResults.test.ts @@ -0,0 +1,118 @@ +import { createStubFileSystem } from "../adapters/fileSystem.stub"; +import { createEmptySettingConversionResults } from "../conversion/conversionResults.stubs"; +import { EditorSettingConversionResults } from "../editorSettings/convertEditorSettings"; +import { EditorSetting } from "../editorSettings/types"; +import { EditorConfiguration } from "../input/editorConfiguration"; +import { DeepPartial } from "../input/findReportedConfiguration"; +import { formatJsonOutput } from "./formatting/formatters/formatJsonOutput"; +import { + writeConversionResults, + 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 writeConversionResults(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: createEmptySettingConversionResults({ + 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 writeConversionResults(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: createEmptySettingConversionResults({ + converted: new Map([ + [ + "tslint-editor-setting-one", + { + editorSettingName: "tslint-editor-setting-one", + value: 42, + }, + ], + ]), + }), + ...overrides, + }; +} diff --git a/src/creation/writeEditorConfigConversionResults.ts b/src/creation/writeEditorConfigConversionResults.ts new file mode 100644 index 000000000..0d9deba50 --- /dev/null +++ b/src/creation/writeEditorConfigConversionResults.ts @@ -0,0 +1,36 @@ +import { FileSystem } from "../adapters/fileSystem"; +import { EditorSettingConversionResults } from "../editorSettings/convertEditorSettings"; +import { EditorConfiguration } from "../input/editorConfiguration"; +import { DeepPartial } from "../input/findReportedConfiguration"; +import { formatOutput } from "./formatting/formatOutput"; + +export type WriteConversionResultsDependencies = { + fileSystem: Pick; +}; + +export const writeConversionResults = 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: { [i: string]: string | any[] } = {}; + 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/editorSettings/convertEditorSetting.test.ts b/src/editorSettings/convertEditorSetting.test.ts new file mode 100644 index 000000000..e5b60c939 --- /dev/null +++ b/src/editorSettings/convertEditorSetting.test.ts @@ -0,0 +1,73 @@ +import { ConversionError } from "../errors/conversionError"; +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, + ); + + // 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, + ); + + // 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); + + // Assert + expect(result).toEqual(ConversionError.forSettingError(error, tsLintSetting)); + }); +}); diff --git a/src/editorSettings/convertEditorSetting.ts b/src/editorSettings/convertEditorSetting.ts new file mode 100644 index 000000000..0843a51c9 --- /dev/null +++ b/src/editorSettings/convertEditorSetting.ts @@ -0,0 +1,19 @@ +import { ConversionError } from "../errors/conversionError"; +import { EditorSettingConverter } from "./converter"; +import { EditorSetting } from "./types"; + +export const convertEditorSetting = ( + editorSetting: EditorSetting, + converters: Map, +) => { + const converter = converters.get(editorSetting.editorSettingName); + if (converter === undefined) { + return undefined; + } + + try { + return converter(editorSetting); + } catch (error) { + return ConversionError.forSettingError(error, editorSetting); + } +}; diff --git a/src/editorSettings/convertEditorSettings.test.ts b/src/editorSettings/convertEditorSettings.test.ts new file mode 100644 index 000000000..b1d5bd890 --- /dev/null +++ b/src/editorSettings/convertEditorSettings.test.ts @@ -0,0 +1,146 @@ +import { ConversionError } from "../errors/conversionError"; +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 { converted, missing, failed } = convertEditorSettings( + { converters }, + editorConfiguration, + ); + + // Assert + expect(converted.size).toEqual(0); + expect(missing.length).toEqual(0); + expect(failed.length).toEqual(0); + }); + + it("skips a configuration if not an editor setting", () => { + // Arrange + const conversionResult: EditorSettingConversionResult = { + settings: [ + { + editorSettingName: "editor.eslint-setting-a", + value: "a", + }, + ], + }; + + const { editorSetting, converters } = setupConversionEnvironment(conversionResult); + + const editorConfiguration = { + notAnEditorSetting: "a", + [editorSetting.editorSettingName]: editorSetting, + notAnEditorSettingEither: "b", + }; + + // Act + const { converted, missing, failed } = convertEditorSettings( + { converters }, + editorConfiguration, + ); + + // Assert + expect(converted.size).toEqual(1); + expect(missing.length).toEqual(0); + expect(failed.length).toEqual(0); + }); + + it("marks a setting as missing when its converter returns undefined", () => { + // Arrange + const { editorSetting, converters } = setupConversionEnvironment(); + + // Act + const { missing } = convertEditorSettings( + { converters }, + { [editorSetting.editorSettingName]: editorSetting }, + ); + + // Assert + expect(missing).toEqual([{ 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 { failed } = convertEditorSettings( + { converters }, + { [editorSetting.editorSettingName]: editorSetting }, + ); + + // Assert + expect(failed).toEqual([conversionError]); + }); + + it("marks a converted setting name as converted when a conversion has settings", () => { + // Arrange + const conversionResult: EditorSettingConversionResult = { + settings: [ + { + editorSettingName: "editor.eslint-setting-a", + value: "a", + }, + ], + }; + const { editorSetting, converters } = setupConversionEnvironment(conversionResult); + + // Act + const { converted } = convertEditorSettings( + { converters }, + { [editorSetting.editorSettingName]: editorSetting.value }, + ); + + // Assert + expect(converted).toEqual( + new Map([ + [ + "editor.eslint-setting-a", + { + editorSettingName: "editor.eslint-setting-a", + value: "a", + }, + ], + ]), + ); + }); +}); + +function setupConversionEnvironment(conversionResult?: EditorSettingConversionResult) { + const editorSetting = createSampleEditorSetting(); + const converters = createConverters(editorSetting, conversionResult); + + return { editorSetting, converters }; +} + +function createSampleEditorSetting(): EditorSetting { + return { + editorSettingName: "editor.tslint-editor-setting-a", + 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/editorSettings/convertEditorSettings.ts b/src/editorSettings/convertEditorSettings.ts new file mode 100644 index 000000000..809adaec8 --- /dev/null +++ b/src/editorSettings/convertEditorSettings.ts @@ -0,0 +1,57 @@ +import { ConversionError } from "../errors/conversionError"; +import { ErrorSummary } from "../errors/errorSummary"; +import { convertEditorSetting } from "./convertEditorSetting"; +import { EditorSettingConverter } from "./converter"; +import { EditorSetting } from "./types"; + +const EDITOR_SETTINGS_PREFIX = "editor."; + +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, +): 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 (!configurationName.startsWith(EDITOR_SETTINGS_PREFIX)) { + continue; + } + + const editorSetting = { editorSettingName: configurationName, value }; + const conversion = convertEditorSetting(editorSetting, dependencies.converters); + + 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/editorSettings/converter.ts b/src/editorSettings/converter.ts new file mode 100644 index 000000000..b3121aa03 --- /dev/null +++ b/src/editorSettings/converter.ts @@ -0,0 +1,34 @@ +import { ConversionError } from "../errors/conversionError"; +import { EditorSetting } from "./types"; + +/** + * Attempts to convert a TSLint editor setting into the ESLint equivalents. + */ +export type EditorSettingConverter = ( + tslintEditorSetting: EditorSetting, +) => ConversionError | EditorSettingConversionResult; + +/** + * 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/editorSettings/converters/editor-code-actions-on-save.ts b/src/editorSettings/converters/editor-code-actions-on-save.ts new file mode 100644 index 000000000..e8f114b5d --- /dev/null +++ b/src/editorSettings/converters/editor-code-actions-on-save.ts @@ -0,0 +1,24 @@ +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/editorSettings/converters/tests/editor-code-actions-on-save.test.ts b/src/editorSettings/converters/tests/editor-code-actions-on-save.test.ts new file mode 100644 index 000000000..425de1a6e --- /dev/null +++ b/src/editorSettings/converters/tests/editor-code-actions-on-save.test.ts @@ -0,0 +1,74 @@ +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, + }, + }); + + 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, + }, + }); + + 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", + }, + }); + + expect(result).toEqual({ + settings: [ + { + editorSettingName: "editor.codeActionsOnSave", + value: { + "one-property": 42, + "another-property": "foo", + }, + }, + { + editorSettingName: "eslint.autoFixOnSave", + value: true, + }, + ], + }); + }); +}); diff --git a/src/editorSettings/editorSettingsConverters.ts b/src/editorSettings/editorSettingsConverters.ts new file mode 100644 index 000000000..ec36c0e3a --- /dev/null +++ b/src/editorSettings/editorSettingsConverters.ts @@ -0,0 +1,8 @@ +import { convertEditorCodeActionsOnSave } from "./converters/editor-code-actions-on-save"; + +/** + * Keys TSLint property names in editor settings to their ESLint editor settings converters. + */ +export const editorSettingsConverters = new Map([ + ["editor.codeActionsOnSave", convertEditorCodeActionsOnSave], +]); diff --git a/src/editorSettings/types.ts b/src/editorSettings/types.ts new file mode 100644 index 000000000..2c8ff4d54 --- /dev/null +++ b/src/editorSettings/types.ts @@ -0,0 +1,4 @@ +export type EditorSetting = { + editorSettingName: string; + value: any; +}; diff --git a/src/errors/conversionError.ts b/src/errors/conversionError.ts index 313247a4c..c2766c8f7 100644 --- a/src/errors/conversionError.ts +++ b/src/errors/conversionError.ts @@ -1,5 +1,6 @@ import { EOL } from "os"; +import { EditorSetting } from "../editorSettings/types"; import { TSLintRuleOptions } from "../rules/types"; import { ErrorSummary } from "./errorSummary"; @@ -21,6 +22,12 @@ 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/editorConfiguration.ts b/src/input/editorConfiguration.ts new file mode 100644 index 000000000..817030b42 --- /dev/null +++ b/src/input/editorConfiguration.ts @@ -0,0 +1 @@ +export type EditorConfiguration = { [key: string]: any }; diff --git a/src/input/findEditorConfiguration.test.ts b/src/input/findEditorConfiguration.test.ts new file mode 100644 index 000000000..2e6617b8a --- /dev/null +++ b/src/input/findEditorConfiguration.test.ts @@ -0,0 +1,80 @@ +import { + findEditorConfiguration, + FindEditorConfigurationDependencies, +} from "./findEditorConfiguration"; + +const stubConfigPath = "temp/"; + +export const createStubImporter = (filePath = "") => + jest.fn().mockReturnValue(Promise.resolve(filePath)); + +const createStubDependencies = (overrides: Partial = {}) => ({ + importer: createStubImporter(stubConfigPath), + ...overrides, +}); + +describe("findEditorConfiguration", () => { + 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 findEditorConfiguration(dependencies, stubConfigPath); + + // Assert + expect(result).toEqual( + expect.objectContaining({ + message, + }), + ); + }); + + it("reads from the given configuration path when one is provided", async () => { + // Arrange + const configPath = "/thePath"; + const dependencies = createStubDependencies(); + + // Act + await findEditorConfiguration(dependencies, configPath); + + // Assert + expect(dependencies.importer).toHaveBeenLastCalledWith(configPath); + }); + + it("defaults to VS Code editor settings path when config path isn't provided", async () => { + // Arrange + const dependencies = createStubDependencies(); + + // Act + await findEditorConfiguration(dependencies, undefined); + + // Assert + expect(dependencies.importer).toHaveBeenLastCalledWith(".vscode/settings.json"); + }); + + it("parses object from configuration path when read successfully", async () => { + // Arrange + const originalConfig = { + "typescript.tsdk": "node_modules/typescript/lib", + "editor.tabSize": 4, + "editor.codeActionsOnSave": { + "source.organizeImports": false, + }, + }; + + const dependencies = createStubDependencies({ + importer: async () => originalConfig, + }); + + // Act + const result = await findEditorConfiguration(dependencies, stubConfigPath); + + // Assert + expect(result).toEqual(originalConfig); + }); +}); diff --git a/src/input/findEditorConfiguration.ts b/src/input/findEditorConfiguration.ts new file mode 100644 index 000000000..7e1f34fef --- /dev/null +++ b/src/input/findEditorConfiguration.ts @@ -0,0 +1,23 @@ +import { SansDependencies } from "../binding"; +import { EditorConfiguration } from "./editorConfiguration"; +import { findRawConfiguration } from "./findRawConfiguration"; +import { DeepPartial } from "./findReportedConfiguration"; +import { importer } from "./importer"; +import { DEFAULT_VSCODE_SETTINGS_PATH } from "./vsCodeSettings"; + +export type FindEditorConfigurationDependencies = { + importer: SansDependencies; +}; + +export const findEditorConfiguration = async ( + dependencies: FindEditorConfigurationDependencies, + config: string | undefined, +): Promise | Error> => { + const filePath = config || DEFAULT_VSCODE_SETTINGS_PATH; + const rawConfiguration = await findRawConfiguration>( + dependencies.importer, + filePath, + ); + + return rawConfiguration; +}; diff --git a/src/input/vsCodeSettings.ts b/src/input/vsCodeSettings.ts new file mode 100644 index 000000000..6eef5583d --- /dev/null +++ b/src/input/vsCodeSettings.ts @@ -0,0 +1 @@ +export const DEFAULT_VSCODE_SETTINGS_PATH = ".vscode/settings.json"; diff --git a/src/reporting/dependencies.ts b/src/reporting/dependencies.ts new file mode 100644 index 000000000..77b3b6f1d --- /dev/null +++ b/src/reporting/dependencies.ts @@ -0,0 +1,5 @@ +import { Logger } from "../adapters/logger"; + +export type ReportConversionResultsDependencies = { + logger: Logger; +}; diff --git a/src/reporting/reportConversionResults.ts b/src/reporting/reportConversionResults.ts index b8c9288f4..581aa3a58 100644 --- a/src/reporting/reportConversionResults.ts +++ b/src/reporting/reportConversionResults.ts @@ -2,20 +2,22 @@ import chalk from "chalk"; import { EOL } from "os"; import { Logger } from "../adapters/logger"; -import { ErrorSummary } from "../errors/errorSummary"; import { RuleConversionResults } from "../rules/convertRules"; -import { TSLintRuleOptions, ESLintRuleOptions } from "../rules/types"; - -export type ReportConversionResultsDependencies = { - logger: Logger; -}; +import { ESLintRuleOptions, TSLintRuleOptions } from "../rules/types"; +import { ReportConversionResultsDependencies } from "./dependencies"; +import { + logFailedConversions, + logMissingConversionTarget, + logMissingPlugins, + logSuccessfulConversions, +} from "./reportOutputs"; export const reportConversionResults = ( dependencies: ReportConversionResultsDependencies, ruleConversionResults: RuleConversionResults, ) => { if (ruleConversionResults.converted.size !== 0) { - logSuccessfulConversions(ruleConversionResults.converted, dependencies.logger); + logSuccessfulConversions("rule", ruleConversionResults.converted, dependencies.logger); logNotices(ruleConversionResults.converted, dependencies.logger); } @@ -24,7 +26,16 @@ export const reportConversionResults = ( } if (ruleConversionResults.missing.length !== 0) { - logMissingRules(ruleConversionResults.missing, dependencies.logger); + const missingSettingOutputMapping = (setting: TSLintRuleOptions) => + `${setting.ruleName} does not yet have an ESLint equivalent.${EOL}`; + const additionalWarnings = [`defaulting to eslint-plugin-tslint for these rules`]; + logMissingConversionTarget( + "rule", + missingSettingOutputMapping, + ruleConversionResults.missing, + dependencies.logger, + additionalWarnings, + ); } if (ruleConversionResults.plugins.size !== 0) { @@ -32,64 +43,6 @@ export const reportConversionResults = ( } }; -const logSuccessfulConversions = (converted: Map, logger: Logger) => { - logger.stdout.write(chalk.greenBright(`✨ ${converted.size}`)); - logger.stdout.write( - converted.size === 1 - ? chalk.green(" rule replaced with its ESLint equivalent.") - : chalk.green(" rules replaced with their ESLint equivalents."), - ); - logger.stdout.write(chalk.greenBright(` ✨${EOL}`)); -}; - -const logFailedConversions = (failed: ErrorSummary[], logger: Logger) => { - logger.stderr.write(`${chalk.redBright(`💀 ${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(failed => failed.getSummary()).join("")); - - logger.stderr.write(chalk.gray(`Check ${logger.debugFileName} for details.${EOL}`)); -}; - -const logMissingRules = (missing: TSLintRuleOptions[], logger: Logger) => { - logger.stdout.write(chalk.yellowBright(`️👀 ${missing.length}`)); - logger.stdout.write( - chalk.yellow( - missing.length === 1 - ? " rule does not yet have an ESLint equivalent" - : " rules do not yet have ESLint equivalents", - ), - ); - logger.stdout.write( - chalk.yellow( - " (see generated log file); defaulting to eslint-plugin-tslint for these rules.", - ), - ); - logger.stdout.write(chalk.yellowBright(` 👀${EOL}`)); - - logger.info.write( - missing - .map(({ ruleName }) => `${ruleName} does not yet have an ESLint equivalent.${EOL}`) - .join(""), - ); -}; - -const logMissingPlugins = (plugins: Set, logger: Logger) => { - logger.stdout.write(chalk.cyanBright(`⚡ ${plugins.size}`)); - logger.stdout.write(chalk.cyan(" package")); - logger.stdout.write(chalk.cyan(plugins.size === 1 ? " is" : "s are")); - logger.stdout.write(chalk.cyan(` required for new ESLint rules.`)); - logger.stdout.write(chalk.cyanBright(` ⚡${EOL}`)); - - logger.stdout.write( - Array.from(plugins) - .map(pluginName => `\t${chalk.cyanBright(pluginName)}${EOL}`) - .join(""), - ); -}; - type RuleWithNotices = { notices: any[]; ruleName: string; diff --git a/src/reporting/reportEditorSettingConversionResults.test.ts b/src/reporting/reportEditorSettingConversionResults.test.ts new file mode 100644 index 000000000..c16c65fd0 --- /dev/null +++ b/src/reporting/reportEditorSettingConversionResults.test.ts @@ -0,0 +1,161 @@ +import { EOL } from "os"; + +import { createStubLogger, expectEqualWrites } from "../adapters/logger.stubs"; +import { createEmptySettingConversionResults } from "../conversion/conversionResults.stubs"; +import { EditorSetting } from "../editorSettings/types"; +import { reportEditorSettingConversionResults } from "./reportEditorSettingConversionResults"; + +describe("reportEditorSettingConversionResults", () => { + it("logs a successful conversion when there is one converted editor setting", () => { + // Arrange + const conversionResults = createEmptySettingConversionResults({ + 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, + `✨ 1 editor setting replaced with its ESLint equivalent. ✨${EOL}`, + ); + }); + + it("logs successful conversions when there are multiple converted settings", () => { + // Arrange + const conversionResults = createEmptySettingConversionResults({ + 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, + `✨ 2 editor settings replaced with their ESLint equivalents. ✨${EOL}`, + ); + }); + + it("logs a failed conversion when there is one failed conversion", () => { + // Arrange + const conversionResults = createEmptySettingConversionResults({ + 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 = createEmptySettingConversionResults({ + 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 = createEmptySettingConversionResults({ + missing: [ + { + editorSettingName: "tslint-editor-setting-one", + }, + ], + }); + + const logger = createStubLogger(); + + // Act + reportEditorSettingConversionResults({ logger }, conversionResults); + + // Assert + expectEqualWrites( + logger.stdout.write, + "👀 1 editor setting does not yet have an ESLint equivalent (see generated log file). 👀", + ); + expectEqualWrites( + logger.info.write, + "tslint-editor-setting-one does not yet have an ESLint equivalent.", + ); + }); + + it("logs missing settings when there are missing settings", () => { + // Arrange + const conversionResults = createEmptySettingConversionResults({ + 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 do not yet have ESLint equivalents (see generated log file). 👀", + ); + expectEqualWrites( + logger.info.write, + "tslint-editor-setting-one does not yet have an ESLint equivalent.", + "tslint-editor-setting-two does not yet have an ESLint equivalent.", + ); + }); +}); diff --git a/src/reporting/reportEditorSettingConversionResults.ts b/src/reporting/reportEditorSettingConversionResults.ts new file mode 100644 index 000000000..12ebf1a29 --- /dev/null +++ b/src/reporting/reportEditorSettingConversionResults.ts @@ -0,0 +1,39 @@ +import { EOL } from "os"; + +import { EditorSettingConversionResults } from "../editorSettings/convertEditorSettings"; +import { EditorSetting } from "../editorSettings/types"; +import { ReportConversionResultsDependencies } from "./dependencies"; +import { + logFailedConversions, + logMissingConversionTarget, + logSuccessfulConversions, +} from "./reportOutputs"; + +export const reportEditorSettingConversionResults = ( + dependencies: ReportConversionResultsDependencies, + 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) { + const missingEditorSettingOutputMapping = ( + editorSetting: Pick, + ) => `${editorSetting.editorSettingName} does not yet have an ESLint equivalent.${EOL}`; + logMissingConversionTarget( + "editor setting", + missingEditorSettingOutputMapping, + editorSettingConversionResults.missing, + dependencies.logger, + ); + } +}; diff --git a/src/reporting/reportOutputs.ts b/src/reporting/reportOutputs.ts new file mode 100644 index 000000000..67af19d65 --- /dev/null +++ b/src/reporting/reportOutputs.ts @@ -0,0 +1,78 @@ +import chalk from "chalk"; +import { EOL } from "os"; + +import { Logger } from "../adapters/logger"; +import { EditorSetting } from "../editorSettings/types"; +import { ErrorSummary } from "../errors/errorSummary"; +import { ESLintRuleOptions } from "../rules/types"; + +export type EditorSettingEntry = Pick; + +export const logSuccessfulConversions = ( + conversionTypeName: string, + converted: Map, + logger: Logger, +) => { + logger.stdout.write(chalk.greenBright(`✨ ${converted.size}`)); + logger.stdout.write( + converted.size === 1 + ? chalk.green(` ${conversionTypeName} replaced with its ESLint equivalent.`) + : chalk.green(` ${conversionTypeName}s replaced with their ESLint equivalents.`), + ); + logger.stdout.write(chalk.greenBright(` ✨${EOL}`)); +}; + +export const logFailedConversions = (failed: ErrorSummary[], logger: Logger) => { + logger.stderr.write(`${chalk.redBright(`💀 ${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(failed => failed.getSummary()).join("")); + + logger.stderr.write(chalk.gray(`Check ${logger.debugFileName} for details.${EOL}`)); +}; + +export const logMissingConversionTarget = ( + conversionTypeName: string, + missingOutputMapping: (missing: T) => string, + missing: T[], + logger: Logger, + additionalWarnings: string[] = [], +) => { + logger.stdout.write(chalk.yellowBright(`️👀 ${missing.length}`)); + logger.stdout.write( + chalk.yellow( + missing.length === 1 + ? ` ${conversionTypeName} does not yet have an ESLint equivalent` + : ` ${conversionTypeName}s do not yet have ESLint equivalents`, + ), + ); + logger.stdout.write(chalk.yellow(` (see generated log file)`)); + + if (additionalWarnings.length > 0) { + logger.stdout.write(chalk.yellow("; ")); + } + for (const warning of additionalWarnings) { + logger.stdout.write(chalk.yellow(warning)); + } + logger.stdout.write(chalk.yellow(".")); + + logger.stdout.write(chalk.yellowBright(` 👀${EOL}`)); + + logger.info.write(missing.map(conversion => missingOutputMapping(conversion)).join("")); +}; + +export const logMissingPlugins = (plugins: Set, logger: Logger) => { + logger.stdout.write(chalk.cyanBright(`⚡ ${plugins.size}`)); + logger.stdout.write(chalk.cyan(" package")); + logger.stdout.write(chalk.cyan(plugins.size === 1 ? " is" : "s are")); + logger.stdout.write(chalk.cyan(` required for new ESLint rules.`)); + logger.stdout.write(chalk.cyanBright(` ⚡${EOL}`)); + + logger.stdout.write( + Array.from(plugins) + .map(pluginName => `\t${chalk.cyanBright(pluginName)}${EOL}`) + .join(""), + ); +}; diff --git a/src/rules/converters.ts b/src/rules/rulesConverters.ts similarity index 99% rename from src/rules/converters.ts rename to src/rules/rulesConverters.ts index 69aa7860d..084e91c22 100644 --- a/src/rules/converters.ts +++ b/src/rules/rulesConverters.ts @@ -134,7 +134,7 @@ import { convertVariableName } from "./converters/variable-name"; /** * Keys TSLint rule names to their ESLint rule converters. */ -export const converters = new Map([ +export const rulesConverters = new Map([ ["adjacent-overload-signatures", convertAdjacentOverloadSignatures], ["align", convertAlign], ["array-type", convertArrayType], diff --git a/src/types.ts b/src/types.ts index 474eb3f46..b4d7f7cc4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,6 +23,11 @@ export type TSLintToESLintSettings = { * Original TypeScript configuration file path, such as `tsconfig.json`. */ typescript?: string; + + /** + * Original Editor configuration file path, such as `.vscode/settings.json`. + */ + editor?: string; }; export type TSLintToESLintResult = ResultWithStatus;