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

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


Pull request:
Implement no-submodule-imports converter
<!--
👋 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 #175
-   [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 based on the rule definition at TSLint, also it requires a plugin to be installed.

Patch:
diff --git a/src/rules/converters.ts b/src/rules/converters.ts
index f609001..fdd5f49 100644
--- a/src/rules/converters.ts
+++ b/src/rules/converters.ts
@@ -31,6 +31,7 @@ import { convertNewlinePerChainedCall } from "./converters/newline-per-chained-c
 import { convertNewParens } from "./converters/new-parens";
 import { convertNoAngleBracketTypeAssertion } from "./converters/no-angle-bracket-type-assertion";
 import { convertNoArg } from "./converters/no-arg";
+import { convertNoAsyncWithoutAwait } from "./converters/no-async-without-await";
 import { convertNoBannedTerms } from "./converters/no-banned-terms";
 import { convertNoBitwise } from "./converters/no-bitwise";
 import { convertNoConditionalAssignment } from "./converters/no-conditional-assignment";
@@ -62,6 +63,7 @@ import { convertNoMisusedNew } from "./converters/no-misused-new";
 import { convertNoMultilineString } from "./converters/no-multiline-string";
 import { convertNoNamespace } from "./converters/no-namespace";
 import { convertNoNonNullAssertion } from "./converters/no-non-null-assertion";
+import { convertNoNullKeyword } from "./converters/no-null-keyword";
 import { convertNoObjectLiteralTypeAssertion } from "./converters/no-object-literal-type-assertion";
 import { convertNoOctalLiteral } from "./converters/no-octal-literal";
 import { convertNoParameterProperties } from "./converters/no-parameter-properties";
@@ -70,9 +72,11 @@ import { convertNoReference } from "./converters/no-reference";
 import { convertNoRegexSpaces } from "./converters/no-regex-spaces";
 import { convertNoRequireImports } from "./converters/no-require-imports";
 import { convertNoReturnAwait } from "./converters/no-return-await";
+import { convertNoShadowedVariable } from "./converters/no-shadowed-variable";
 import { convertNoSparseArrays } from "./converters/no-sparse-arrays";
 import { convertNoStringLiteral } from "./converters/no-string-literal";
 import { convertNoStringThrow } from "./converters/no-string-throw";
+import { convertNoSubmoduleImports } from "./converters/no-submodule-imports";
 import { convertNoSwitchCaseFallThrough } from "./converters/no-switch-case-fall-through";
 import { convertNoThisAssignment } from "./converters/no-this-assignment";
 import { convertNoTrailingWhitespace } from "./converters/no-trailing-whitespace";
@@ -153,6 +157,7 @@ export const converters = new Map([
     ["no-angle-bracket-type-assertion", convertNoAngleBracketTypeAssertion],
     ["no-any", convertNoExplicitAny],
     ["no-arg", convertNoArg],
+    ["no-async-without-await", convertNoAsyncWithoutAwait],
     ["no-banned-terms", convertNoBannedTerms],
     ["no-bitwise", convertNoBitwise],
     ["no-conditional-assignment", convertNoConditionalAssignment],
@@ -183,6 +188,7 @@ export const converters = new Map([
     ["no-multiline-string", convertNoMultilineString],
     ["no-namespace", convertNoNamespace],
     ["no-non-null-assertion", convertNoNonNullAssertion],
+    ["no-null-keyword", convertNoNullKeyword],
     ["no-object-literal-type-assertion", convertNoObjectLiteralTypeAssertion],
     ["no-octal-literal", convertNoOctalLiteral],
     ["no-parameter-properties", convertNoParameterProperties],
@@ -191,9 +197,11 @@ export const converters = new Map([
     ["no-regex-spaces", convertNoRegexSpaces],
     ["no-require-imports", convertNoRequireImports],
     ["no-return-await", convertNoReturnAwait],
+    ["no-shadowed-variable", convertNoShadowedVariable],
     ["no-sparse-arrays", convertNoSparseArrays],
     ["no-string-literal", convertNoStringLiteral],
     ["no-string-throw", convertNoStringThrow],
+    ["no-submodule-imports", convertNoSubmoduleImports],
     ["no-switch-case-fall-through", convertNoSwitchCaseFallThrough],
     ["no-this-assignment", convertNoThisAssignment],
     ["no-trailing-whitespace", convertNoTrailingWhitespace],
@@ -245,7 +253,6 @@ export const converters = new Map([
     // ["ban", convertBan], // no-restricted-properties
     // ["import-blacklist", convertImportBlacklist], // no-restricted-imports
     // ["no-duplicate-variable", convertNoDuplicateVariable], // no-redeclare
-    // ["no-shadowed-variable", convertNoShadowedVariable], // no-shadow
     // ["no-unused-expression", convertNoUnusedExpression], // no-unused-expressions
     // ["space-within-parens", convertSpaceWithinParens], // space-in-parens
     // ["variable-name", convertVariableName], // a bunch of rules...
diff --git a/src/rules/converters/no-async-without-await.ts b/src/rules/converters/no-async-without-await.ts
new file mode 100644
index 0000000..7f8a5ff
--- /dev/null
+++ b/src/rules/converters/no-async-without-await.ts
@@ -0,0 +1,11 @@
+import { RuleConverter } from "../converter";
+
+export const convertNoAsyncWithoutAwait: RuleConverter = () => {
+    return {
+        rules: [
+            {
+                ruleName: "@typescript-eslint/require-await",
+            },
+        ],
+    };
+};
diff --git a/src/rules/converters/no-null-keyword.ts b/src/rules/converters/no-null-keyword.ts
new file mode 100644
index 0000000..e982058
--- /dev/null
+++ b/src/rules/converters/no-null-keyword.ts
@@ -0,0 +1,13 @@
+import { RuleConverter } from "../converter";
+
+export const convertNoNullKeyword: RuleConverter = () => {
+    return {
+        notices: ["Null types will no longer be handled."],
+        rules: [
+            {
+                ruleName: "no-null/no-null",
+            },
+        ],
+        plugins: ["no-null"],
+    };
+};
diff --git a/src/rules/converters/no-shadowed-variable.ts b/src/rules/converters/no-shadowed-variable.ts
new file mode 100644
index 0000000..b6722f2
--- /dev/null
+++ b/src/rules/converters/no-shadowed-variable.ts
@@ -0,0 +1,49 @@
+import { RuleConverter } from "../converter";
+
+const SELECTIVE_DISABLE_NOTICE =
+    "ESLint does not support selectively disabling shadowed declaration checks " +
+    "depending on the type of declaration, so all kinds of declarations are checked.";
+
+const UNDERSCORE_DISABLE_NOTICE =
+    "ESLint does not support disabling shadowed variable checks based on " +
+    "whether their names start with underscore or not, please use 'allow' in eslint configuration to " +
+    "provide specific variable names you want to disable the rule for.";
+
+export const convertNoShadowedVariable: RuleConverter = tslintRule => {
+    const ruleArguments: { hoist: "all" | "never" }[] = [];
+    const notices: string[] = [];
+
+    if (tslintRule.ruleArguments.length === 0 || !(tslintRule.ruleArguments[0] instanceof Object)) {
+        ruleArguments.push({ hoist: "all" });
+    } else {
+        const config: Record<string, boolean> = tslintRule.ruleArguments[0];
+
+        if (config.underscore === false) {
+            notices.push(UNDERSCORE_DISABLE_NOTICE);
+        }
+
+        if (config.temporalDeadZone === false) {
+            ruleArguments.push({ hoist: "never" });
+        } else {
+            ruleArguments.push({ hoist: "all" });
+        }
+
+        const hasUnsupportedDisables = Object.entries(config).some(
+            ([key, value]) => value === false && key !== "underscore" && key !== "temporalDeadZone",
+        );
+
+        if (hasUnsupportedDisables) {
+            notices.push(SELECTIVE_DISABLE_NOTICE);
+        }
+    }
+
+    return {
+        rules: [
+            {
+                ...(notices.length > 0 && { notices }),
+                ...(ruleArguments.length > 0 && { ruleArguments }),
+                ruleName: "no-shadow",
+            },
+        ],
+    };
+};
diff --git a/src/rules/converters/no-submodule-imports.ts b/src/rules/converters/no-submodule-imports.ts
new file mode 100644
index 0000000..31ffc9f
--- /dev/null
+++ b/src/rules/converters/no-submodule-imports.ts
@@ -0,0 +1,35 @@
+import { RuleConverter } from "../converter";
+
+export const convertNoSubmoduleImports: RuleConverter = tslintRule => {
+    const ruleArguments: string[] = [];
+
+    if (
+        !(
+            tslintRule.ruleArguments.length === 0 ||
+            tslintRule.ruleArguments[0] === false ||
+            tslintRule.ruleArguments.length < 2
+        )
+    ) {
+        ruleArguments.push(
+            ...tslintRule.ruleArguments.map(ruleArg => {
+                if (typeof ruleArg === "string") {
+                    return ruleArg.concat("/*");
+                }
+                return ruleArg;
+            }),
+        );
+    }
+
+    return {
+        rules: [
+            {
+                ruleName: "import/no-internal-modules",
+                ...{
+                    ruleArguments:
+                        ruleArguments.length > 0 ? [{ allow: ruleArguments }] : undefined,
+                },
+            },
+        ],
+        plugins: ["eslint-plugin-import"],
+    };
+};
diff --git a/src/rules/mergers.ts b/src/rules/mergers.ts
index 583f3d4..d323224 100644
--- a/src/rules/mergers.ts
+++ b/src/rules/mergers.ts
@@ -1,7 +1,11 @@
 import { mergeBanTypes } from "./mergers/ban-types";
+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/no-unnecessary-type-assertion", mergeNoUnnecessaryTypeAssertion],
+    ["no-caller", mergeNoCaller],
+    ["no-eval", mergeNoEval],
 ]);
diff --git a/src/rules/mergers/no-caller.ts b/src/rules/mergers/no-caller.ts
new file mode 100644
index 0000000..1d2450f
--- /dev/null
+++ b/src/rules/mergers/no-caller.ts
@@ -0,0 +1,6 @@
+import { RuleMerger } from "../merger";
+
+export const mergeNoCaller: RuleMerger = () => {
+    // no-caller rule does not accept any options
+    return [];
+};
diff --git a/src/rules/mergers/no-eval.ts b/src/rules/mergers/no-eval.ts
new file mode 100644
index 0000000..59cf5af
--- /dev/null
+++ b/src/rules/mergers/no-eval.ts
@@ -0,0 +1,28 @@
+import { RuleMerger } from "../merger";
+
+export const mergeNoEval: RuleMerger = (existingOptions, newOptions) => {
+    if (existingOptions === undefined && newOptions === undefined) {
+        return [];
+    }
+
+    let allowIndirect = true;
+
+    for (const options of [existingOptions, newOptions]) {
+        if (
+            options === undefined ||
+            options.length === 0 ||
+            options[0].allowIndirect === undefined
+        ) {
+            allowIndirect = false;
+            break;
+        }
+
+        allowIndirect = allowIndirect && options[0].allowIndirect;
+    }
+
+    return [
+        {
+            ...(allowIndirect && { allowIndirect }),
+        },
+    ];
+};


Test patch:
diff --git a/src/rules/converters/tests/no-async-without-await.test.ts b/src/rules/converters/tests/no-async-without-await.test.ts
new file mode 100644
index 0000000..1a6084c
--- /dev/null
+++ b/src/rules/converters/tests/no-async-without-await.test.ts
@@ -0,0 +1,17 @@
+import { convertNoAsyncWithoutAwait } from "../no-async-without-await";
+
+describe(convertNoAsyncWithoutAwait, () => {
+    test("conversion without arguments", () => {
+        const result = convertNoAsyncWithoutAwait({
+            ruleArguments: [],
+        });
+
+        expect(result).toEqual({
+            rules: [
+                {
+                    ruleName: "@typescript-eslint/require-await",
+                },
+            ],
+        });
+    });
+});
diff --git a/src/rules/converters/tests/no-null-keyword.test.ts b/src/rules/converters/tests/no-null-keyword.test.ts
new file mode 100644
index 0000000..ec140de
--- /dev/null
+++ b/src/rules/converters/tests/no-null-keyword.test.ts
@@ -0,0 +1,19 @@
+import { convertNoNullKeyword } from "../no-null-keyword";
+
+describe(convertNoNullKeyword, () => {
+    test("conversion without arguments", () => {
+        const result = convertNoNullKeyword({
+            ruleArguments: [],
+        });
+
+        expect(result).toEqual({
+            notices: ["Null types will no longer be handled."],
+            rules: [
+                {
+                    ruleName: "no-null/no-null",
+                },
+            ],
+            plugins: ["no-null"],
+        });
+    });
+});
diff --git a/src/rules/converters/tests/no-shadowed-variable.test.ts b/src/rules/converters/tests/no-shadowed-variable.test.ts
new file mode 100644
index 0000000..6a1f676
--- /dev/null
+++ b/src/rules/converters/tests/no-shadowed-variable.test.ts
@@ -0,0 +1,85 @@
+import { convertNoShadowedVariable } from "../no-shadowed-variable";
+
+describe(convertNoShadowedVariable, () => {
+    test("conversion without parameter", () => {
+        const result = convertNoShadowedVariable({
+            ruleArguments: [],
+        });
+
+        expect(result).toEqual({
+            rules: [
+                {
+                    ruleArguments: [{ hoist: "all" }],
+                    ruleName: "no-shadow",
+                },
+            ],
+        });
+    });
+
+    test("conversion with non-object parameter", () => {
+        const result = convertNoShadowedVariable({
+            ruleArguments: ["will-be-ignored"],
+        });
+
+        expect(result).toEqual({
+            rules: [
+                {
+                    ruleArguments: [{ hoist: "all" }],
+                    ruleName: "no-shadow",
+                },
+            ],
+        });
+    });
+
+    test("conversion with empty parameter object", () => {
+        const result = convertNoShadowedVariable({
+            ruleArguments: [{}],
+        });
+
+        expect(result).toEqual({
+            rules: [
+                {
+                    ruleArguments: [{ hoist: "all" }],
+                    ruleName: "no-shadow",
+                },
+            ],
+        });
+    });
+
+    test("conversion with disabled 'temporalDeadZone'", () => {
+        const result = convertNoShadowedVariable({
+            ruleArguments: [{ temporalDeadZone: false }],
+        });
+
+        expect(result).toEqual({
+            rules: [
+                {
+                    ruleArguments: [{ hoist: "never" }],
+                    ruleName: "no-shadow",
+                },
+            ],
+        });
+    });
+
+    test("conversion with disabled declaration types", () => {
+        const result = convertNoShadowedVariable({
+            ruleArguments: [{ class: false, underscore: false }],
+        });
+
+        expect(result).toEqual({
+            rules: [
+                {
+                    notices: [
+                        "ESLint does not support disabling shadowed variable checks based on " +
+                            "whether their names start with underscore or not, please use 'allow' in eslint configuration to " +
+                            "provide specific variable names you want to disable the rule for.",
+                        "ESLint does not support selectively disabling shadowed declaration checks " +
+                            "depending on the type of declaration, so all kinds of declarations are checked.",
+                    ],
+                    ruleArguments: [{ hoist: "all" }],
+                    ruleName: "no-shadow",
+                },
+            ],
+        });
+    });
+});
diff --git a/src/rules/converters/tests/no-submodule-imports.test.ts b/src/rules/converters/tests/no-submodule-imports.test.ts
new file mode 100644
index 0000000..dcb26c4
--- /dev/null
+++ b/src/rules/converters/tests/no-submodule-imports.test.ts
@@ -0,0 +1,34 @@
+import { convertNoSubmoduleImports } from "../no-submodule-imports";
+
+describe(convertNoSubmoduleImports, () => {
+    test("conversion without arguments", () => {
+        const result = convertNoSubmoduleImports({
+            ruleArguments: [],
+        });
+
+        expect(result).toEqual({
+            rules: [
+                {
+                    ruleName: "import/no-internal-modules",
+                },
+            ],
+            plugins: ["eslint-plugin-import"],
+        });
+    });
+
+    test("conversion with arguments", () => {
+        const result = convertNoSubmoduleImports({
+            ruleArguments: [true, "rxjs"],
+        });
+
+        expect(result).toEqual({
+            rules: [
+                {
+                    ruleName: "import/no-internal-modules",
+                    ruleArguments: [{ allow: [true, "rxjs/*"] }],
+                },
+            ],
+            plugins: ["eslint-plugin-import"],
+        });
+    });
+});
diff --git a/src/rules/mergers/tests/no-caller.test.ts b/src/rules/mergers/tests/no-caller.test.ts
new file mode 100644
index 0000000..adee30e
--- /dev/null
+++ b/src/rules/mergers/tests/no-caller.test.ts
@@ -0,0 +1,9 @@
+import { mergeNoCaller } from "../no-caller";
+
+describe(mergeNoCaller, () => {
+    test("neither options existing", () => {
+        const result = mergeNoCaller(undefined, undefined);
+
+        expect(result).toEqual([]);
+    });
+});
diff --git a/src/rules/mergers/tests/no-eval.test.ts b/src/rules/mergers/tests/no-eval.test.ts
new file mode 100644
index 0000000..a47a581
--- /dev/null
+++ b/src/rules/mergers/tests/no-eval.test.ts
@@ -0,0 +1,45 @@
+import { mergeNoEval } from "../no-eval";
+
+describe(mergeNoEval, () => {
+    test("neither options existing", () => {
+        const result = mergeNoEval(undefined, undefined);
+
+        expect(result).toEqual([]);
+    });
+
+    test("neither allowIndirect existing", () => {
+        const result = mergeNoEval([{}], [{}]);
+
+        expect(result).toEqual([{}]);
+    });
+
+    test("original allowIndirect existing", () => {
+        const result = mergeNoEval([{ allowIndirect: true }], [{}]);
+
+        expect(result).toEqual([{}]);
+    });
+
+    test("new allowIndirect existing", () => {
+        const result = mergeNoEval([{}], [{ allowIndirect: true }]);
+
+        expect(result).toEqual([{}]);
+    });
+
+    test("original allowIndirect is false but new allowIndirect is true", () => {
+        const result = mergeNoEval([{ allowIndirect: false }], [{ allowIndirect: true }]);
+
+        expect(result).toEqual([{}]);
+    });
+
+    test("original allowIndirect is true but new allowIndirect is false", () => {
+        const result = mergeNoEval([{ allowIndirect: true }], [{ allowIndirect: false }]);
+
+        expect(result).toEqual([{}]);
+    });
+
+    test("both allowIndirect are true", () => {
+        const result = mergeNoEval([{ allowIndirect: true }], [{ allowIndirect: true }]);
+
+        expect(result).toEqual([{ allowIndirect: true }]);
+    });
+});