diff --git a/src/rules/converters.ts b/src/rules/converters.ts index d21e68bea..cee212ff5 100644 --- a/src/rules/converters.ts +++ b/src/rules/converters.ts @@ -113,6 +113,7 @@ import { convertSemicolon } from "./converters/semicolon"; import { convertSpaceBeforeFunctionParen } from "./converters/space-before-function-paren"; import { convertSpaceWithinParens } from "./converters/space-within-parens"; import { convertSwitchDefault } from "./converters/switch-default"; +import { convertTrailingComma } from "./converters/trailing-comma"; import { convertTripleEquals } from "./converters/triple-equals"; import { convertTypedefWhitespace } from "./converters/typedef-whitespace"; import { convertTypeLiteralDelimiter } from "./converters/type-literal-delimiter"; @@ -243,6 +244,7 @@ export const converters = new Map([ ["space-before-function-paren", convertSpaceBeforeFunctionParen], ["space-within-parens", convertSpaceWithinParens], ["switch-default", convertSwitchDefault], + ["trailing-comma", convertTrailingComma], ["triple-equals", convertTripleEquals], ["type-literal-delimiter", convertTypeLiteralDelimiter], ["typedef-whitespace", convertTypedefWhitespace], diff --git a/src/rules/converters/tests/trailing-comma.test.ts b/src/rules/converters/tests/trailing-comma.test.ts new file mode 100644 index 000000000..244146be6 --- /dev/null +++ b/src/rules/converters/tests/trailing-comma.test.ts @@ -0,0 +1,359 @@ +import { convertTrailingComma } from "../trailing-comma"; + +describe(convertTrailingComma, () => { + test("conversion without arguments", () => { + const result = convertTrailingComma({ + ruleArguments: [], + }); + + expect(result).toEqual({ + rules: [ + { + ruleName: "comma-dangle", + }, + ], + }); + }); + + describe("conversion with arguments using string values", () => { + const testCases = [ + { + argument: { + singleline: "never", + }, + expectedRuleArguments: [], + }, + { + argument: { + singleline: "always", + }, + expectedRuleArguments: [], + }, + { + argument: { + multiline: "never", + }, + expectedRuleArguments: [], + }, + { + argument: { + multiline: "always", + }, + expectedRuleArguments: ["always-multiline"], + }, + { + argument: { + singleline: "never", + multiline: "never", + }, + expectedRuleArguments: [], + }, + { + argument: { + singleline: "never", + multiline: "always", + }, + expectedRuleArguments: ["always-multiline"], + }, + { + argument: { + singleline: "always", + multiline: "never", + }, + expectedRuleArguments: [], + }, + { + argument: { + singleline: "always", + multiline: "always", + }, + expectedRuleArguments: ["always"], + }, + ]; + + testCases.forEach(testCase => { + test(`conversion with arguments ${JSON.stringify(testCase.argument)}`, () => { + const result = convertTrailingComma({ + ruleArguments: [testCase.argument], + }); + + expect(result).toEqual({ + rules: [ + { + ruleName: "comma-dangle", + ...(testCase.expectedRuleArguments.length !== 0 && { + ruleArguments: testCase.expectedRuleArguments, + }), + }, + ], + }); + }); + }); + }); + + describe("conversion with arguments using object values", () => { + const testCases = [ + { + argument: { + singleline: "never", + multiline: { + objects: "always", + arrays: "always", + functions: "always", + imports: "always", + exports: "always", + }, + }, + expectedRuleArguments: [ + { + arrays: "always-multiline", + objects: "always-multiline", + functions: "always-multiline", + imports: "always-multiline", + exports: "always-multiline", + }, + ], + }, + { + argument: { + singleline: "always", + multiline: { + objects: "always", + arrays: "always", + functions: "always", + imports: "always", + exports: "always", + }, + }, + expectedRuleArguments: [ + { + arrays: "always", + objects: "always", + functions: "always", + imports: "always", + exports: "always", + }, + ], + }, + { + argument: { + singleline: { + objects: "never", + arrays: "never", + functions: "never", + imports: "never", + exports: "never", + }, + }, + expectedRuleArguments: [ + { + arrays: "never", + objects: "never", + functions: "never", + imports: "never", + exports: "never", + }, + ], + }, + { + argument: { + singleline: { + objects: "never", + arrays: "never", + functions: "never", + }, + multiline: { + objects: "never", + arrays: "never", + functions: "never", + }, + }, + expectedRuleArguments: [ + { + arrays: "never", + objects: "never", + functions: "never", + }, + ], + }, + { + argument: { + multiline: { + objects: "always", + arrays: "always", + functions: "always", + imports: "always", + exports: "always", + }, + }, + expectedRuleArguments: [ + { + arrays: "always-multiline", + objects: "always-multiline", + functions: "always-multiline", + imports: "always-multiline", + exports: "always-multiline", + }, + ], + }, + { + argument: { + singleline: { + objects: "always", + arrays: "always", + functions: "always", + imports: "always", + exports: "always", + }, + multiline: { + objects: "always", + arrays: "always", + functions: "always", + imports: "always", + exports: "always", + }, + }, + expectedRuleArguments: [ + { + arrays: "always", + objects: "always", + functions: "always", + imports: "always", + exports: "always", + }, + ], + }, + { + argument: { + singleline: { + objects: "always", + arrays: "always", + }, + multiline: { + objects: "always", + arrays: "always", + functions: "always", + }, + }, + expectedRuleArguments: [ + { + arrays: "always", + objects: "always", + functions: "always-multiline", + }, + ], + }, + ]; + + testCases.forEach(testCase => { + test(`conversion with arguments ${JSON.stringify(testCase.argument)}`, () => { + const result = convertTrailingComma({ + ruleArguments: [testCase.argument], + }); + + expect(result).toEqual({ + rules: [ + { + ruleName: "comma-dangle", + ...(testCase.expectedRuleArguments.length && { + ruleArguments: testCase.expectedRuleArguments, + }), + }, + ], + }); + }); + }); + }); + + describe("conversion with not supported config", () => { + const testCases = [ + { + argument: { + esSpecCompliant: true, + }, + expectedRuleArguments: [], + expectedNotices: ["ESLint does not support config property esSpecCompliant"], + }, + { + argument: { + singleline: { + typeLiterals: "ignore", + }, + }, + expectedRuleArguments: [{}], + expectedNotices: ["ESLint does not support config property typeLiterals"], + }, + { + argument: { + multiline: { + typeLiterals: "ignore", + }, + }, + expectedRuleArguments: [{}], + expectedNotices: ["ESLint does not support config property typeLiterals"], + }, + { + argument: { + esSpecCompliant: true, + singleline: { + typeLiterals: "always", + }, + }, + expectedRuleArguments: [{}], + expectedNotices: [ + "ESLint does not support config property esSpecCompliant", + "ESLint does not support config property typeLiterals", + ], + }, + { + argument: { + esSpecCompliant: false, + multiline: { + typeLiterals: "always-multiline", + }, + }, + expectedRuleArguments: [{}], + expectedNotices: [ + "ESLint does not support config property esSpecCompliant", + "ESLint does not support config property typeLiterals", + ], + }, + { + argument: { + esSpecCompliant: false, + singleline: { + typeLiterals: "ignore", + }, + multiline: { + typeLiterals: "ignore", + }, + }, + expectedRuleArguments: [{}], + expectedNotices: [ + "ESLint does not support config property esSpecCompliant", + "ESLint does not support config property typeLiterals", + ], + }, + ]; + + testCases.forEach(testCase => { + test(`conversion with arguments ${JSON.stringify(testCase.argument)}`, () => { + const result = convertTrailingComma({ + ruleArguments: [testCase.argument], + }); + + expect(result).toEqual({ + rules: [ + { + ruleName: "comma-dangle", + ...(testCase.expectedRuleArguments.length && { + ruleArguments: testCase.expectedRuleArguments, + }), + notices: testCase.expectedNotices, + }, + ], + }); + }); + }); + }); +}); diff --git a/src/rules/converters/trailing-comma.ts b/src/rules/converters/trailing-comma.ts new file mode 100644 index 000000000..e94d455ed --- /dev/null +++ b/src/rules/converters/trailing-comma.ts @@ -0,0 +1,178 @@ +import { RuleConverter } from "../converter"; + +const unsupportedKeyInEsLint = "typeLiterals"; + +export const convertTrailingComma: RuleConverter = tslintRule => { + const eslintArgs = tslintRule.ruleArguments.length + ? collectArguments(tslintRule.ruleArguments) + : undefined; + + const notices = tslintRule.ruleArguments.length + ? collectNotices(tslintRule.ruleArguments) + : undefined; + + return { + rules: [ + { + ruleName: "comma-dangle", + ...(eslintArgs && { ruleArguments: [eslintArgs] }), + ...(notices && notices.length && { notices }), + }, + ], + }; +}; + +function collectArguments(args: TSLintArg[]): ESLintArgValue | undefined { + const tslintArg = args[0]; + const { singleline, multiline } = tslintArg; + + if (typeof singleline === "object" || typeof multiline === "object") { + const keys = mergePropertyKeys(singleline, multiline); + const single = singleline && mapToObjectConfig(singleline); + const multi = multiline && mapToObjectConfig(multiline); + + return keys.reduce( + (acc, key) => ({ + ...acc, + ...collectKeys(key as TSLintObjectKey, single, multi), + }), + {}, + ); + } + + if ((singleline === undefined || singleline === "never") && multiline === "always") { + return "always-multiline"; + } + + if (singleline === "always" && multiline === "always") { + return "always"; + } + + return; +} + +function mergePropertyKeys( + singleline: TSLintArgValue | undefined, + multiline: TSLintArgValue | undefined, +): string[] { + const getKeysIfObject = (field: TSLintArgValue | undefined): string[] => + typeof field === "object" ? Object.keys(field) : []; + const singlelineKeys = getKeysIfObject(singleline); + const multilineKeys = getKeysIfObject(multiline); + + const uniqueKeys = [...new Set([...singlelineKeys, ...multilineKeys])]; + + return uniqueKeys.filter(field => field !== unsupportedKeyInEsLint); +} + +function collectKeys( + key: TSLintObjectKey, + singleline: TSLintObject | undefined, + multiline: TSLintObject | undefined, +): { [key: string]: ESLintStringValue } { + const hasSingleline = Boolean(singleline); + const hasSinglelineAndFieldExist = Boolean(singleline && singleline[key]); + const hasSinglelineAlways = Boolean(singleline && singleline[key] === "always"); + const hasMultilineAlways = Boolean(multiline && multiline[key] === "always"); + + if (!hasSingleline && hasMultilineAlways) { + return { + [key]: "always-multiline", + }; + } + + if (!hasSinglelineAndFieldExist && hasMultilineAlways) { + return { + [key]: "always-multiline", + }; + } + + if (!hasSinglelineAlways && hasMultilineAlways) { + return { + [key]: "always-multiline", + }; + } + + if (hasSinglelineAlways && hasMultilineAlways) { + return { + [key]: "always", + }; + } + + return { + [key]: "never", + }; +} + +function mapToObjectConfig(value: TSLintArgValue): TSLintObject { + return typeof value === "string" + ? { + arrays: value, + objects: value, + functions: value, + imports: value, + exports: value, + } + : value; +} + +function collectNotices(args: TSLintArg[]): string[] { + const tslintArg = args[0]; + + return [buildNoticeForEsSpecCompliant(tslintArg), buildNoticeForTypeLiterals(tslintArg)].filter( + Boolean, + ); +} + +function buildNoticeForEsSpecCompliant(arg: TSLintArg): string { + const unsupportedConfigKey = "esSpecCompliant"; + + if (Object.keys(arg).includes(unsupportedConfigKey)) { + return `ESLint does not support config property ${unsupportedConfigKey}`; + } + + return ""; +} + +function buildNoticeForTypeLiterals(arg: TSLintArg): string { + const { singleline, multiline } = arg; + const hasTypeLiterals = (field: any) => + typeof field === "object" && Object.keys(field).includes(unsupportedKeyInEsLint); + + if (hasTypeLiterals(singleline) || hasTypeLiterals(multiline)) { + return `ESLint does not support config property ${unsupportedKeyInEsLint}`; + } + + return ""; +} + +type TSLintArg = { + singleline?: TSLintArgValue; + multiline?: TSLintArgValue; + esSpecCompliant?: boolean; +}; + +type TSLintArgValue = TSLintStringValue | TSLintObject; +type TSLintObjectKey = keyof TSLintObject; + +type TSLintObject = { + arrays?: TSLintStringValueForObject; + objects?: TSLintStringValueForObject; + functions?: TSLintStringValueForObject; + imports?: TSLintStringValueForObject; + exports?: TSLintStringValueForObject; + typeLiterals?: TSLintStringValueForObject; +}; +type TSLintStringValue = "always" | "never"; +type TSLintStringValueForObject = TSLintStringValue | "ignore"; + +// ESLint +type ESLintArgValue = ESLintStringValue | ESLintObject; +type ESLintStringValue = "never" | "always" | "always-multiline" | "only-multiline" | "ignore"; +type ESLintObject = { + arrays?: ESLintStringValue; + objects?: ESLintStringValue; + functions?: ESLintStringValue; + imports?: ESLintStringValue; + exports?: ESLintStringValue; +};