Repository: typescript-eslint/tslint-to-eslint-config (ts)

Issue:
Missing converter: trailing-comma
### 💥 Missing Converter

-   `tslint-to-eslint-config` version: 1.2.0
-   `typescript-eslint` version: 2.3.2

#### TSLint Rule Name

`trailing-comma`


Pull request:
add converter trailing-comma
<!--
👋 Hi, thanks for sending a PR to tslint-to-eslint-config! 💖
Please fill out all fields below to ensure your PR is reviewed quickly.
-->

## PR Checklist

-   [x] Addresses an existing issue: fixes #186
-   [x] That issue was marked as [`status: accepting prs`](https://github.com/typescript-eslint/tslint-to-eslint-config/labels/status%3A%20accepting%20prs)

## Overview

<!-- Brief description of what is changed and how the code change does that. -->
Add converter for trailing comma

TSLint ref
[https://palantir.github.io/tslint/rules/trailing-comma/](https://palantir.github.io/tslint/rules/trailing-comma/)

ESLint ref
[https://eslint.org/docs/rules/comma-dangle](https://eslint.org/docs/rules/comma-dangle)

Patch:
diff --git a/src/rules/converters.ts b/src/rules/converters.ts
index 7690278..4a4ce1a 100644
--- a/src/rules/converters.ts
+++ b/src/rules/converters.ts
@@ -118,6 +118,7 @@ import { convertSpaceBeforeFunctionParen } from "./converters/space-before-funct
 import { convertSpaceWithinParens } from "./converters/space-within-parens";
 import { convertStrictBooleanExpressions } from "./converters/strict-boolean-expressions";
 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";
@@ -253,6 +254,7 @@ export const converters = new Map([
     ["space-within-parens", convertSpaceWithinParens],
     ["strict-boolean-expressions", convertStrictBooleanExpressions],
     ["switch-default", convertSwitchDefault],
+    ["trailing-comma", convertTrailingComma],
     ["triple-equals", convertTripleEquals],
     ["type-literal-delimiter", convertTypeLiteralDelimiter],
     ["typedef-whitespace", convertTypedefWhitespace],
diff --git a/src/rules/converters/trailing-comma.ts b/src/rules/converters/trailing-comma.ts
new file mode 100644
index 0000000..e94d455
--- /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;
+};


Test patch:
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 0000000..244146b
--- /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,
+                        },
+                    ],
+                });
+            });
+        });
+    });
+});