Repository: typescript-eslint/tslint-to-eslint-config
Pull request #601 (2020-06-01 00:33:23)

improve type safety with regards to  config files
<!--
👋 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

-   [ ] Addresses an existing issue: fixes #000
-   [ ] 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. -->
Last week I said I would look deeper at the poor `DeepPartial<T>` type in https://github.com/typescript-eslint/tslint-to-eslint-config/pull/589

It seems that this type needs to touch a number of config related places and there are several places where incomplete configurations might have slipped through but were fine according to tsc. 


Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index 981a771..02905f9 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -23,6 +23,7 @@ module.exports = {
         "@typescript-eslint/explicit-function-return-type": "off",
         "@typescript-eslint/explicit-module-boundary-types": "off",
         "@typescript-eslint/generic-type-naming": "off",
+        "@typescript-eslint/naming-convention": "off",
         "@typescript-eslint/init-declarations": "off",
         "@typescript-eslint/method-signature-style": "off",
         "@typescript-eslint/no-explicit-any": "off",
diff --git a/package-lock.json b/package-lock.json
index e577702..3c00a65 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3808,12 +3808,12 @@
             "dev": true
         },
         "@typescript-eslint/eslint-plugin": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.0.0.tgz",
-            "integrity": "sha512-lcZ0M6jD4cqGccYOERKdMtg+VWpoq3NSnWVxpc/AwAy0zhkUYVioOUZmfNqiNH8/eBNGhCn6HXd6mKIGRgNc1Q==",
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.1.0.tgz",
+            "integrity": "sha512-D52KwdgkjYc+fmTZKW7CZpH5ZBJREJKZXRrveMiRCmlzZ+Rw9wRVJ1JAmHQ9b/+Ehy1ZeaylofDB9wwXUt83wg==",
             "dev": true,
             "requires": {
-                "@typescript-eslint/experimental-utils": "3.0.0",
+                "@typescript-eslint/experimental-utils": "3.1.0",
                 "functional-red-black-tree": "^1.0.1",
                 "regexpp": "^3.0.0",
                 "semver": "^7.3.2",
@@ -3838,33 +3838,33 @@
             }
         },
         "@typescript-eslint/experimental-utils": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.0.0.tgz",
-            "integrity": "sha512-BN0vmr9N79M9s2ctITtChRuP1+Dls0x/wlg0RXW1yQ7WJKPurg6X3Xirv61J2sjPif4F8SLsFMs5Nzte0WYoTQ==",
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.1.0.tgz",
+            "integrity": "sha512-Zf8JVC2K1svqPIk1CB/ehCiWPaERJBBokbMfNTNRczCbQSlQXaXtO/7OfYz9wZaecNvdSvVADt6/XQuIxhC79w==",
             "dev": true,
             "requires": {
                 "@types/json-schema": "^7.0.3",
-                "@typescript-eslint/typescript-estree": "3.0.0",
+                "@typescript-eslint/typescript-estree": "3.1.0",
                 "eslint-scope": "^5.0.0",
                 "eslint-utils": "^2.0.0"
             }
         },
         "@typescript-eslint/parser": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.0.0.tgz",
-            "integrity": "sha512-8RRCA9KLxoFNO0mQlrLZA0reGPd/MsobxZS/yPFj+0/XgMdS8+mO8mF3BDj2ZYQj03rkayhSJtF1HAohQ3iylw==",
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.1.0.tgz",
+            "integrity": "sha512-NcDSJK8qTA2tPfyGiPes9HtVKLbksmuYjlgGAUs7Ld2K0swdWibnCq9IJx9kJN8JJdgUJSorFiGaPHBgH81F/Q==",
             "dev": true,
             "requires": {
                 "@types/eslint-visitor-keys": "^1.0.0",
-                "@typescript-eslint/experimental-utils": "3.0.0",
-                "@typescript-eslint/typescript-estree": "3.0.0",
+                "@typescript-eslint/experimental-utils": "3.1.0",
+                "@typescript-eslint/typescript-estree": "3.1.0",
                 "eslint-visitor-keys": "^1.1.0"
             }
         },
         "@typescript-eslint/typescript-estree": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.0.0.tgz",
-            "integrity": "sha512-nevQvHyNghsfLrrByzVIH4ZG3NROgJ8LZlfh3ddwPPH4CH7W4GAiSx5qu+xHuX5pWsq6q/eqMc1io840ZhAnUg==",
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.1.0.tgz",
+            "integrity": "sha512-+4nfYauqeQvK55PgFrmBWFVYb6IskLyOosYEmhH3mSVhfBp9AIJnjExdgDmKWoOBHRcPM8Ihfm2BFpZf0euUZQ==",
             "dev": true,
             "requires": {
                 "debug": "^4.1.1",
diff --git a/package.json b/package.json
index f9354d5..03cc8dd 100644
--- a/package.json
+++ b/package.json
@@ -28,8 +28,8 @@
         "@types/glob": "7.1.1",
         "@types/jest": "25.2.3",
         "@types/node": "12.12.21",
-        "@typescript-eslint/eslint-plugin": "3.0.0",
-        "@typescript-eslint/parser": "3.0.0",
+        "@typescript-eslint/eslint-plugin": "3.1.0",
+        "@typescript-eslint/parser": "3.1.0",
         "babel-jest": "26.0.1",
         "eslint": "7.1.0",
         "husky": "4.2.5",
diff --git a/src/creation/eslint/createEnv.ts b/src/creation/eslint/createEnv.ts
index a02b617..771cd67 100644
--- a/src/creation/eslint/createEnv.ts
+++ b/src/creation/eslint/createEnv.ts
@@ -6,12 +6,12 @@ export const createEnv = ({
 }: Pick<AllOriginalConfigurations, "packages" | "typescript">) => {
     const browser =
         typescript === undefined ||
-        typescript.compilerOptions.lib === undefined ||
+        typescript.compilerOptions?.lib === undefined ||
         typescript.compilerOptions.lib.includes("dom");
 
     const es6 =
         typescript === undefined ||
-        !["es3", "es5"].includes(typescript.compilerOptions.target.toLowerCase());
+        !["es3", "es5"].includes(typescript.compilerOptions?.target?.toLowerCase() ?? "");
 
     const node =
         packages === undefined ||
diff --git a/src/input/findESLintConfiguration.ts b/src/input/findESLintConfiguration.ts
index 5b301d2..34175ea 100644
--- a/src/input/findESLintConfiguration.ts
+++ b/src/input/findESLintConfiguration.ts
@@ -9,10 +9,10 @@ import { OriginalConfigurations } from "./findOriginalConfigurations";
 import { importer } from "./importer";
 
 export type ESLintConfiguration = {
-    env: Record<string, boolean>;
-    extends: string | string[];
-    globals?: Record<string, boolean>;
-    rules: ESLintConfigurationRules;
+    env?: Record<string, boolean | undefined>;
+    extends?: string | string[];
+    globals?: Record<string, boolean | undefined>;
+    rules?: ESLintConfigurationRules;
 };
 
 export type ESLintConfigurationRules = {
@@ -43,7 +43,7 @@ export const findESLintConfiguration = async (
         findRawConfiguration<ESLintConfiguration>(dependencies.importer, filePath, {
             extends: [],
         }),
-        findReportedConfiguration<Partial<ESLintConfiguration>>(
+        findReportedConfiguration<ESLintConfiguration>(
             dependencies.exec,
             "eslint --print-config",
             filePath,
diff --git a/src/input/findOriginalConfigurations.ts b/src/input/findOriginalConfigurations.ts
index 4364c47..654d7c5 100644
--- a/src/input/findOriginalConfigurations.ts
+++ b/src/input/findOriginalConfigurations.ts
@@ -14,6 +14,7 @@ import {
 } from "./findTypeScriptConfiguration";
 import { findTSLintConfiguration, TSLintConfiguration } from "./findTSLintConfiguration";
 import { mergeLintConfigurations } from "./mergeLintConfigurations";
+import { DeepPartial } from "./findReportedConfiguration";
 
 export type FindOriginalConfigurationsDependencies = {
     findESLintConfiguration: SansDependencies<typeof findESLintConfiguration>;
@@ -35,7 +36,7 @@ export type OriginalConfigurations<Configuration> = {
     /**
      * Raw import results from `import`ing the configuration file.
      */
-    raw: Partial<Configuration>;
+    raw: DeepPartial<Configuration>;
 };
 
 export type AllOriginalConfigurations = {
diff --git a/src/input/findPackagesConfiguration.ts b/src/input/findPackagesConfiguration.ts
index 42c79b1..5e614e0 100644
--- a/src/input/findPackagesConfiguration.ts
+++ b/src/input/findPackagesConfiguration.ts
@@ -4,8 +4,8 @@ import {
 } from "./findReportedConfiguration";
 
 export type PackagesConfiguration = {
-    dependencies: Record<string, string>;
-    devDependencies: Record<string, string>;
+    dependencies: Record<string, string | undefined>;
+    devDependencies: Record<string, string | undefined>;
 };
 
 export const findPackagesConfiguration = async (
diff --git a/src/input/findReportedConfiguration.ts b/src/input/findReportedConfiguration.ts
index 915e99b..df199f5 100644
--- a/src/input/findReportedConfiguration.ts
+++ b/src/input/findReportedConfiguration.ts
@@ -1,7 +1,7 @@
 import { Exec } from "../adapters/exec";
 
 export type DeepPartial<T> = {
-    [P in keyof T]?: T[P] extends Record<P, T[P]> ? DeepPartial<T[P]> : T[P];
+    [P in keyof T]?: T[P] extends Record<string, unknown> ? DeepPartial<T[P]> : T[P];
 };
 
 export type FindReportedConfigurationDependencies = {
diff --git a/src/input/findTSLintConfiguration.ts b/src/input/findTSLintConfiguration.ts
index 0219baa..e932c23 100644
--- a/src/input/findTSLintConfiguration.ts
+++ b/src/input/findTSLintConfiguration.ts
@@ -8,7 +8,7 @@ import { importer } from "./importer";
 export type TSLintConfiguration = {
     extends?: string[];
     rulesDirectory?: string[];
-    rules: TSLintConfigurationRules;
+    rules?: TSLintConfigurationRules;
 };
 
 export type TSLintConfigurationRules = Record<string, any>;
diff --git a/src/input/findTypeScriptConfiguration.ts b/src/input/findTypeScriptConfiguration.ts
index f9c423a..afc46e3 100644
--- a/src/input/findTypeScriptConfiguration.ts
+++ b/src/input/findTypeScriptConfiguration.ts
@@ -4,9 +4,9 @@ import {
 } from "./findReportedConfiguration";
 
 export type TypeScriptConfiguration = {
-    compilerOptions: {
+    compilerOptions?: {
         lib?: string[];
-        target: string;
+        target?: string;
     };
 };
 
diff --git a/src/input/mergeLintConfigurations.ts b/src/input/mergeLintConfigurations.ts
index de2bb80..711a6d2 100644
--- a/src/input/mergeLintConfigurations.ts
+++ b/src/input/mergeLintConfigurations.ts
@@ -10,7 +10,7 @@ export const mergeLintConfigurations = (
         return tslint;
     }
 
-    const mappedConfig = eslint.full.rules["@typescript-eslint/tslint/config"];
+    const mappedConfig = eslint.full.rules?.["@typescript-eslint/tslint/config"];
     if (!(mappedConfig instanceof Array) || mappedConfig[0] === "off") {
         return tslint;
     }
diff --git a/src/rules/convertRules.ts b/src/rules/convertRules.ts
index e4a7781..29db1b5 100644
--- a/src/rules/convertRules.ts
+++ b/src/rules/convertRules.ts
@@ -22,66 +22,67 @@ export type RuleConversionResults = {
 
 export const convertRules = (
     dependencies: ConvertRulesDependencies,
-    rawTslintRules: TSLintConfigurationRules,
+    rawTslintRules?: TSLintConfigurationRules,
 ): RuleConversionResults => {
     const converted = new Map<string, ESLintRuleOptions>();
     const failed: ConversionError[] = [];
     const missing: TSLintRuleOptions[] = [];
     const plugins = new Set<string>();
 
-    for (const [ruleName, value] of Object.entries(rawTslintRules)) {
-        const tslintRule = formatRawTslintRule(ruleName, value);
-        const conversion = convertRule(tslintRule, dependencies.converters);
+    if (rawTslintRules !== undefined) {
+        for (const [ruleName, value] of Object.entries(rawTslintRules)) {
+            const tslintRule = formatRawTslintRule(ruleName, value);
+            const conversion = convertRule(tslintRule, dependencies.converters);
 
-        if (conversion === undefined) {
-            if (tslintRule.ruleSeverity !== "off") {
-                missing.push(tslintRule);
-            }
-
-            continue;
-        }
+            if (conversion === undefined) {
+                if (tslintRule.ruleSeverity !== "off") {
+                    missing.push(tslintRule);
+                }
 
-        if (conversion instanceof ConversionError) {
-            failed.push(conversion);
-            continue;
-        }
-
-        for (const changes of conversion.rules) {
-            const existingConversion = converted.get(changes.ruleName);
-            const newConversion = {
-                ...changes,
-                ruleSeverity: convertTSLintRuleSeverity(tslintRule.ruleSeverity),
-            };
+                continue;
+            }
 
-            if (existingConversion === undefined) {
-                converted.set(changes.ruleName, newConversion);
+            if (conversion instanceof ConversionError) {
+                failed.push(conversion);
                 continue;
             }
 
-            const merger = dependencies.mergers.get(changes.ruleName);
-            if (merger === undefined) {
-                failed.push(ConversionError.forMerger(changes.ruleName));
-            } else {
-                const existingNotices = existingConversion.notices ?? [];
-                const newNotices = newConversion.notices ?? [];
+            for (const changes of conversion.rules) {
+                const existingConversion = converted.get(changes.ruleName);
+                const newConversion = {
+                    ...changes,
+                    ruleSeverity: convertTSLintRuleSeverity(tslintRule.ruleSeverity),
+                };
+
+                if (existingConversion === undefined) {
+                    converted.set(changes.ruleName, newConversion);
+                    continue;
+                }
+
+                const merger = dependencies.mergers.get(changes.ruleName);
+                if (merger === undefined) {
+                    failed.push(ConversionError.forMerger(changes.ruleName));
+                } else {
+                    const existingNotices = existingConversion.notices ?? [];
+                    const newNotices = newConversion.notices ?? [];
 
-                converted.set(changes.ruleName, {
-                    ...existingConversion,
-                    ruleArguments: merger(
-                        existingConversion.ruleArguments,
-                        newConversion.ruleArguments,
-                    ),
-                    notices: Array.from(new Set([...existingNotices, ...newNotices])),
-                });
+                    converted.set(changes.ruleName, {
+                        ...existingConversion,
+                        ruleArguments: merger(
+                            existingConversion.ruleArguments,
+                            newConversion.ruleArguments,
+                        ),
+                        notices: Array.from(new Set([...existingNotices, ...newNotices])),
+                    });
+                }
             }
-        }
 
-        if (conversion.plugins !== undefined) {
-            for (const newPlugin of conversion.plugins) {
-                plugins.add(newPlugin);
+            if (conversion.plugins !== undefined) {
+                for (const newPlugin of conversion.plugins) {
+                    plugins.add(newPlugin);
+                }
             }
         }
     }
-
     return { converted, failed, missing, plugins };
 };


Test patch:
diff --git a/src/creation/eslint/createEnv.test.ts b/src/creation/eslint/createEnv.test.ts
index c648ca5..a2cfe27 100644
--- a/src/creation/eslint/createEnv.test.ts
+++ b/src/creation/eslint/createEnv.test.ts
@@ -63,6 +63,20 @@ describe("createEnv", () => {
         });
     });
 
+    it("handles an environment where typescript configuration files are mostly undefined", () => {
+        // Arrange
+        const packages = undefined;
+        const typescript = {};
+
+        // Act
+        const env = createEnv({ packages, typescript });
+
+        // Assert
+        expect(env).not.toContain({
+            browser: expect.any(Boolean),
+        });
+    });
+
     it("returns browser as true if a typescript lib is dom", () => {
         // Arrange
         const packages = undefined;
diff --git a/src/rules/convertRules.test.ts b/src/rules/convertRules.test.ts
index d542a40..1d22011 100644
--- a/src/rules/convertRules.test.ts
+++ b/src/rules/convertRules.test.ts
@@ -5,6 +5,19 @@ import { RuleConverter, ConversionResult } from "./converter";
 import { RuleMerger } from "./merger";
 
 describe("convertRules", () => {
+    it("doesn't crash when passed an undefined configuration", () => {
+        // Arrange
+        const { converters, mergers } = setupConversionEnvironment({
+            ruleSeverity: "off",
+        });
+
+        // Act
+        const { missing } = convertRules({ converters, mergers }, undefined);
+
+        // Assert
+        expect(missing).toEqual([]);
+    });
+
     it("doesn't marks a disabled rule as missing when its converter returns undefined", () => {
         // Arrange
         const { tslintRule, converters, mergers } = setupConversionEnvironment({