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

Issue:
Conflict between tslint:align and @typescript-eslint/indent
I'm running into a conflict between @typescript-eslint/indent and the align rule in @typescript-eslint/tslint/config, specifically with regard to the alignment of parameters on my class constructors.

### 🐛 Bug Report

-   `tslint-to-eslint-config` version: 0.2.5
-   ESLint version: 6.3.0
-   Node version: 10.15.3

#### Actual Behavior

Expected indentation of x spaces but found x  @typescript-eslint/indent
or
parameters are not aligned (tslint:align)  @typescript-eslint/tslint/config

#### Expected Behavior

I don't really mind which convention is accepted as long as its consistent.

#### Reproduction

```ts
constructor(private router: Router,
            private authenticationService: AuthenticationService,
            private tokenService: TokenService) {}
```

```ts
constructor(private router: Router,
  private authenticationService: AuthenticationService,
  private tokenService: TokenService) { }
```

```JSON
{
    "rules": {
        "@typescript-eslint/indent": [
            "error",
            2
        ],
        "@typescript-eslint/tslint/config": [
            "error",
            {
                "rulesDirectory": [
                    "**MyPath**"
                ],
                "rules": {
                    "align": [
                        true,
                        "parameters",
                        "statements"
                    ]
                }
            }
        ]
    }
}
```


Pull request:
Add align rule converter + indent merger
<!--
👋 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 #149
-   [x] That issue was marked as [`status: accepting prs`](https://github.com/typescript-eslint/tslint-to-eslint-config/labels/status%3A%20accepting%20prs)

## Overview

The TSLint `align` converter maps rule arguments to the equivalent options for `@typescript-eslint-indent` rule supporting the "first" option, which aligns all parameters / arguments / elements / properties with the first one in each declaration.

Next, a merger is required for the `@typescript-eslint/indent` since two TSLint rules are mapped to it: `align` and `indent`.

There is a small issue where the object option cannot be supplied without the indent size option. This is a problem since TSLint's `align` rule does not concern itself with the indent size. I decided to specify the default indent size of `4` when converting the indent rule. Correspondingly, the `indent` merger ignores indent size values of `4` when merging rules.

Docs:
[TSLint `align`](https://palantir.github.io/tslint/rules/align/)
[`@typescript-eslint/indent`](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/indent.md)

Patch:
diff --git a/src/rules/converters.ts b/src/rules/converters.ts
index fd44418..7c88cb1 100644
--- a/src/rules/converters.ts
+++ b/src/rules/converters.ts
@@ -1,4 +1,5 @@
 import { convertAdjacentOverloadSignatures } from "./converters/adjacent-overload-signatures";
+import { convertAlign } from "./converters/align";
 import { convertArrayType } from "./converters/array-type";
 import { convertArrowParens } from "./converters/arrow-parens";
 import { convertArrowReturnShorthand } from "./converters/arrow-return-shorthand";
@@ -131,6 +132,7 @@ import { convertVariableName } from "./converters/variable-name";
  */
 export const converters = new Map([
     ["adjacent-overload-signatures", convertAdjacentOverloadSignatures],
+    ["align", convertAlign],
     ["array-type", convertArrayType],
     ["arrow-parens", convertArrowParens],
     ["arrow-return-shorthand", convertArrowReturnShorthand],
diff --git a/src/rules/converters/align.ts b/src/rules/converters/align.ts
new file mode 100644
index 0000000..84f387f
--- /dev/null
+++ b/src/rules/converters/align.ts
@@ -0,0 +1,38 @@
+import { RuleConverter } from "../converter";
+
+export const convertAlign: RuleConverter = tslintRule => {
+    const alignArguments = tslintRule.ruleArguments.includes("arguments");
+    const alignElements = tslintRule.ruleArguments.includes("elements");
+    const alignMembers = tslintRule.ruleArguments.includes("members");
+    const alignParameters = tslintRule.ruleArguments.includes("parameters");
+    // "statements" alignment is enforced by base indent rule
+
+    const objectOption = {
+        ...(alignArguments && {
+            CallExpression: { arguments: "first" },
+        }),
+        ...(alignElements && {
+            ArrayExpression: "first",
+        }),
+        ...(alignMembers && {
+            ObjectExpression: "first",
+        }),
+        ...(alignParameters && {
+            FunctionDeclaration: { parameters: "first" },
+            FunctionExpression: { parameters: "first" },
+        }),
+    };
+
+    // TSLint's "align" rule doesn't care about indent size but "indent" rule requires
+    // specifying the indent size before the object option. Use the default value of 4.
+    const ruleArguments = tslintRule.ruleArguments.length === 0 ? undefined : [4, objectOption];
+
+    return {
+        rules: [
+            {
+                ruleName: "@typescript-eslint/indent",
+                ...{ ruleArguments },
+            },
+        ],
+    };
+};
diff --git a/src/rules/mergers.ts b/src/rules/mergers.ts
index d323224..446d686 100644
--- a/src/rules/mergers.ts
+++ b/src/rules/mergers.ts
@@ -1,10 +1,12 @@
 import { mergeBanTypes } from "./mergers/ban-types";
+import { mergeIndent } from "./mergers/indent";
 import { mergeNoCaller } from "./mergers/no-caller";
 import { mergeNoEval } from "./mergers/no-eval";
 import { mergeNoUnnecessaryTypeAssertion } from "./mergers/no-unnecessary-type-assertion";
 
 export const mergers = new Map([
     ["@typescript-eslint/ban-types", mergeBanTypes],
+    ["@typescript-eslint/indent", mergeIndent],
     ["@typescript-eslint/no-unnecessary-type-assertion", mergeNoUnnecessaryTypeAssertion],
     ["no-caller", mergeNoCaller],
     ["no-eval", mergeNoEval],
diff --git a/src/rules/mergers/indent.ts b/src/rules/mergers/indent.ts
new file mode 100644
index 0000000..c9b8ae6
--- /dev/null
+++ b/src/rules/mergers/indent.ts
@@ -0,0 +1,42 @@
+import { RuleMerger } from "../merger";
+
+const ESLINT_INDENT_DEFAULT = 4;
+
+export const mergeIndent: RuleMerger = (existingOptions, newOptions) => {
+    if (existingOptions === undefined && newOptions === undefined) {
+        return [];
+    }
+
+    // Resolve indent size
+    let indentSize = ESLINT_INDENT_DEFAULT; // default
+    for (const options of [existingOptions, newOptions]) {
+        if (
+            options === undefined ||
+            options.length === 0 ||
+            options[0] === ESLINT_INDENT_DEFAULT // ignore default
+        ) {
+            continue;
+        }
+        indentSize = options[0];
+    }
+
+    // Resolve object option
+    let objectOption = null;
+    for (const options of [existingOptions, newOptions]) {
+        if (options === undefined || options.length < 2 || options[1] === undefined) {
+            continue;
+        }
+        objectOption = {
+            ...(objectOption || {}),
+            ...options[1],
+        };
+    }
+
+    if (indentSize === ESLINT_INDENT_DEFAULT && objectOption === null) {
+        return [];
+    } else if (objectOption === null) {
+        return [indentSize];
+    }
+
+    return [indentSize, objectOption];
+};


Test patch:
diff --git a/src/rules/converters/tests/align.test.ts b/src/rules/converters/tests/align.test.ts
new file mode 100644
index 0000000..eeabe3a
--- /dev/null
+++ b/src/rules/converters/tests/align.test.ts
@@ -0,0 +1,122 @@
+import { convertAlign } from "../align";
+
+describe(convertAlign, () => {
+    test("conversion without arguments", () => {
+        const result = convertAlign({
+            ruleArguments: [],
+        });
+
+        expect(result).toEqual({
+            rules: [
+                {
+                    ruleName: "@typescript-eslint/indent",
+                },
+            ],
+        });
+    });
+
+    test("conversion with align arguments", () => {
+        const result = convertAlign({
+            ruleArguments: ["arguments"],
+        });
+
+        expect(result).toEqual({
+            rules: [
+                {
+                    ruleName: "@typescript-eslint/indent",
+                    ruleArguments: [
+                        4,
+                        {
+                            CallExpression: { arguments: "first" },
+                        },
+                    ],
+                },
+            ],
+        });
+    });
+
+    test("conversion with align elements", () => {
+        const result = convertAlign({
+            ruleArguments: ["elements"],
+        });
+
+        expect(result).toEqual({
+            rules: [
+                {
+                    ruleName: "@typescript-eslint/indent",
+                    ruleArguments: [
+                        4,
+                        {
+                            ArrayExpression: "first",
+                        },
+                    ],
+                },
+            ],
+        });
+    });
+
+    test("conversion with align members", () => {
+        const result = convertAlign({
+            ruleArguments: ["members"],
+        });
+
+        expect(result).toEqual({
+            rules: [
+                {
+                    ruleName: "@typescript-eslint/indent",
+                    ruleArguments: [
+                        4,
+                        {
+                            ObjectExpression: "first",
+                        },
+                    ],
+                },
+            ],
+        });
+    });
+
+    test("conversion with align parameters", () => {
+        const result = convertAlign({
+            ruleArguments: ["parameters"],
+        });
+
+        expect(result).toEqual({
+            rules: [
+                {
+                    ruleName: "@typescript-eslint/indent",
+                    ruleArguments: [
+                        4,
+                        {
+                            FunctionDeclaration: { parameters: "first" },
+                            FunctionExpression: { parameters: "first" },
+                        },
+                    ],
+                },
+            ],
+        });
+    });
+
+    test("conversion with align all", () => {
+        const result = convertAlign({
+            ruleArguments: ["parameters", "elements", "arguments", "members"],
+        });
+
+        expect(result).toEqual({
+            rules: [
+                {
+                    ruleName: "@typescript-eslint/indent",
+                    ruleArguments: [
+                        4,
+                        {
+                            ArrayExpression: "first",
+                            CallExpression: { arguments: "first" },
+                            FunctionDeclaration: { parameters: "first" },
+                            FunctionExpression: { parameters: "first" },
+                            ObjectExpression: "first",
+                        },
+                    ],
+                },
+            ],
+        });
+    });
+});
diff --git a/src/rules/mergers/tests/indent.test.ts b/src/rules/mergers/tests/indent.test.ts
new file mode 100644
index 0000000..db54aea
--- /dev/null
+++ b/src/rules/mergers/tests/indent.test.ts
@@ -0,0 +1,60 @@
+import { mergeIndent } from "../indent";
+
+describe(mergeIndent, () => {
+    test("neither options existing", () => {
+        const result = mergeIndent(undefined, undefined);
+
+        expect(result).toEqual([]);
+    });
+
+    test("original indent size existing", () => {
+        const result = mergeIndent([2], undefined);
+
+        expect(result).toEqual([2]);
+    });
+
+    test("new indent size existing", () => {
+        const result = mergeIndent(undefined, [2]);
+
+        expect(result).toEqual([2]);
+    });
+
+    test("both indent sizes existing", () => {
+        const result = mergeIndent([1], [2]);
+
+        expect(result).toEqual([2]);
+    });
+
+    test("default indent sizes existing", () => {
+        const result = mergeIndent([4], [4]);
+
+        expect(result).toEqual([]);
+    });
+
+    test("original object option existing", () => {
+        const result = mergeIndent([4, { ArrayExpression: "first" }], [2]);
+
+        expect(result).toEqual([2, { ArrayExpression: "first" }]);
+    });
+
+    test("new object option existing", () => {
+        const result = mergeIndent([2], [4, { ArrayExpression: "first" }]);
+
+        expect(result).toEqual([2, { ArrayExpression: "first" }]);
+    });
+
+    test("both object option existing", () => {
+        const result = mergeIndent(
+            [4, { ObjectExpression: "first" }],
+            [4, { ArrayExpression: "first" }],
+        );
+
+        expect(result).toEqual([
+            4,
+            {
+                ArrayExpression: "first",
+                ObjectExpression: "first",
+            },
+        ]);
+    });
+});