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

Issue:
Document converters, mergers, and how they're resolved
### Overview

For folks interested in contributing to `tslint-to-eslint-config`, having explanations of the main parts of the code base are essential. `docs/Development.md` should explain those very core principles.

In short:

* A "converter" is a function that converts a particular TSLint rule's arguments to a description of the equivalent ESLint rule(s) and arguments(s)
* A "merger" is used to figure out how to combine two groups arguments for a particular ESLint rule, which is used when multiple converters output the same ESLint rule

Pull request description:
Added architecture and testing documentation
## PR Checklist

-   [x] Addresses an existing issue: fixes #66 
-   [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/tslint-to-eslint-config/labels/status%3A%20accepting%20prs)

## Overview

Splits content other than setup instructions from `docs/Development.md` into:
-   Architecture.md: How the general app structure operates
-   Dependencies.md How functions pass and receive static dependencies
-   Testing.md: Unit and end-to-end tests

Adds comments to source code mentioning places also mentioned in docs, particularly `src/conversion/convertConfig.ts`.

Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index d51ea9b..23c49bc 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -3,18 +3,32 @@ module.exports = {
         es6: true,
         node: true,
     },
-    extends: ["airbnb-base", "eslint-config-prettier", "plugin:@typescript-eslint/recommended"],
+    extends: ["airbnb-base", "eslint-config-prettier", "plugin:@typescript-eslint/all"],
+    overrides: [
+        {
+            files: ["*.stubs.ts", "*.test.ts"],
+            rules: {
+                "@typescript-eslint/promise-function-async": 0,
+            },
+        },
+    ],
     parser: "@typescript-eslint/parser",
     parserOptions: {
         project: "tsconfig.json",
     },
     plugins: ["@typescript-eslint"],
     rules: {
+        "@typescript-eslint/consistent-type-definitions": ["error", "type"],
         "@typescript-eslint/explicit-function-return-type": 0,
+        "@typescript-eslint/generic-type-naming": 0,
         "@typescript-eslint/indent": 0,
         "@typescript-eslint/no-explicit-any": 0,
+        "@typescript-eslint/no-extra-parens": 0,
+        "@typescript-eslint/no-magic-numbers": 0,
         "@typescript-eslint/no-parameter-properties": 0,
+        "@typescript-eslint/no-type-alias": 0,
         "@typescript-eslint/no-use-before-define": 0,
+        "@typescript-eslint/prefer-for-of": 1,
         "@typescript-eslint/prefer-interface": 0,
         "default-case": 0,
         "guard-for-in": 0,
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index d1ab197..0000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
-    "env": {
-        "es6": true,
-        "node": true
-    },
-    "parser": "@typescript-eslint/parser",
-    "parserOptions": {
-        "project": "tsconfig.json",
-        "sourceType": "module"
-    },
-    "plugins": ["@typescript-eslint", "@typescript-eslint/tslint"],
-    "rules": {
-        "@typescript-eslint/array-type": "error",
-        "@typescript-eslint/interface-name-prefix": "error",
-        "@typescript-eslint/member-ordering": "off",
-        "@typescript-eslint/no-explicit-any": "off",
-        "@typescript-eslint/no-param-reassign": "off",
-        "@typescript-eslint/no-parameter-properties": "off",
-        "@typescript-eslint/no-use-before-declare": "off",
-        "@typescript-eslint/promise-function-async": "off",
-        "@typescript-eslint/unbound-method": "off",
-        "arrow-body-style": "off",
-        "default-case": "off",
-        "linebreak-style": "off",
-        "no-bitwise": "off",
-        "no-empty": "off",
-        "no-empty-functions": "off",
-        "no-magic-numbers": "off",
-        "prefer-template": "off",
-        "@typescript-eslint/tslint/config": [
-            "error",
-            {
-                "rules": {
-                    "no-implicit-dependencies": [true, "dev"],
-                    "strict-boolean-expressions": [
-                        true,
-                        "allow-boolean-or-undefined",
-                        "allow-number"
-                    ]
-                }
-            }
-        ]
-    }
-}
diff --git a/docs/Architecture.md b/docs/Architecture.md
index 7116111..610fa08 100644
--- a/docs/Architecture.md
+++ b/docs/Architecture.md
@@ -2,13 +2,47 @@
 
 ## CLI Architecture
 
--   CLI usage starts in `bin/tslint-to-eslint-config`, which immediately calls `src/cli/main.ts`.
--   CLI settings are parsed and read in `src/cli/runCli.ts`.
--   Application logic is run by `src/conversion/convertConfig.ts`.
+1.  CLI usage starts in `bin/tslint-to-eslint-config`, which immediately calls `src/cli/main.ts`.
+2.  CLI settings are parsed and read in `src/cli/runCli.ts`.
+3.  Application logic is run by `src/conversion/convertConfig.ts`.
 
-## Dependencies
+## Configuration Conversion
 
-Methods are created using a variant of [Dependency Injection](http://en.wikipedia.org/wiki/Dependency_Injection).
-Any method with dependencies that should be stubbed out during tests, such as file system bindings, logging, or other methods,
-takes in a first argument of name `dependencies`.
-Its dependencies object is manually created in `src/cli/main.ts` and bound to the method with `bind`.
+Within `src/conversion/convertConfig.ts`, the following steps occur:
+
+1. Existing configurations are read from disk
+2. TSLint rules are converted into their ESLint configurations
+3. ESLint configurations are simplified based on extended ESLint presets
+4. The simplified configuration is written to the output config file
+5. A summary of the results is printed to the user's console
+
+### Conversion Results
+
+The overall configuration generated by steps 2-3 and printed in 4-5 contains the following information:
+
+### Rule Converters
+
+Each TSLint rule should output at least one ESLint rule as the equivalent.
+"Converters" for TSLint rules are located in `src/rules/converters/`, and keyed under their names by the map in `src/rules/converters.ts`.
+
+Each converter for a TSLint rule takes an arguments object for the rule, and returns an array of objects containing:
+
+-   `rules`: At least one equivalent ESLint rule and options
+-   `notices`: Any extra info that should be printed after conversion
+-   `plugins`: Any plugins that should now be installed if not already
+
+The `rules` output is an array of objects containing:
+
+-   `ruleName`: Equivalent ESLint rule name that should be enabled
+-   `ruleArguments`: Any arguments for that ESLint rule
+
+Multiple objects must be supported because some general-use TSLint rules can only be represented by two or more ESLint rules.
+For example, TSLint's `no-banned-terms` is represented by ESLint's `no-caller` and `no-eval`.
+
+### Rule Mergers
+
+It's possible that one ESLint rule will be output by multiple converters.
+"Mergers" for those ESLint rules should take in two configurations to the same rule and output the equivalent single configuration.
+These are located in `src/rules/mergers/`, and keyed under their names by the map in `src/rules/mergers.ts`.
+
+For example, `@typescript-eslint/ban-types` spreads both arguments' `types` members into one large `types` object.
diff --git a/docs/Dependencies.md b/docs/Dependencies.md
new file mode 100644
index 0000000..8323b17
--- /dev/null
+++ b/docs/Dependencies.md
@@ -0,0 +1,64 @@
+# Dependencies
+
+Functions are created using a variant of [Dependency Injection](http://en.wikipedia.org/wiki/Dependency_Injection).
+Any method with dependencies that should be stubbed out during tests, such as file system bindings, logging, or other functions,
+takes in a first argument of name `dependencies`.
+Its dependencies object is manually created in `src/cli/main.ts` and bound to the method with `bind`.
+
+## When to Use Dependencies
+
+Most functions don't need a `dependencies` object.
+Only add one if something should be stubbed out during tests.
+
+## How to Use Dependencies
+
+Suppose your method `myMethod`, should take in a `fileSystem`, a `string`, and a `number`:
+
+1. Create a `MyMethodDependencies` type in `myMethod.ts`:
+
+    ```ts
+    // ~~~/myMethod.ts
+    export type MyMethodDependencies = {
+        fileSystem: FileSystem;
+    };
+    ```
+
+2. Add `dependencies: MyMethodDependencies` as the first argument to `myMethod`:
+
+    ```ts
+    // ~~~/myMethod.ts
+    export const myMethod = async (
+        dependencies: MyMethodDependencies,
+        argumentOne: string,
+        argumentTwo: number,
+    ) => {
+        // ...
+    };
+    ```
+
+3. In `src/cli/main.ts`, create a `myMethodDependencies: MyMethodDependencies`:
+
+    ```ts
+    // src/cli/main.ts
+    const myMethodDependencies: MyMethodDependencies = {
+        fileSystem,
+    };
+    ```
+
+4. In `src/cli/main.ts`, include `myMethod: bind(mymethod, myMethodDependencies)` in any dependencies object that requires `myMethod`:
+
+    ```ts
+    // src/cli/main.ts
+    const otherMethodDependencies: OtherMethodDependencies = {
+        myMethod: bind(myMethod, myMethodDependencies),
+    };
+    ```
+
+5. In the types of any dependencies that include `myMethod`, add `myMethod: SansDependencies<typeof myMethod>` to require the result of `bind`ing `myMethod`:
+
+    ```ts
+    // ~~~/otherMethod.ts
+    export type OtherMethodDependencies = {
+        myMethod: SansDependencies<typeof myMethod>;
+    };
+    ```
diff --git a/docs/Development.md b/docs/Development.md
index e357258..776aee9 100644
--- a/docs/Development.md
+++ b/docs/Development.md
@@ -19,7 +19,8 @@ npm i
 
 Compile with `npm run tsc` and run tests with `npm run test:unit`.
 
-## More Reading
+## Further Reading
 
--   [Architecture](./Architecture.md)
--   [Testing](./Testing.md)
+-   [Architecture](./Architecture.md): How the general app structure operates
+-   [Dependencies](./Dependencies.md): How functions pass and receive static dependencies
+-   [Testing](./Testing.md): Unit and end-to-end tests
diff --git a/package-lock.json b/package-lock.json
index 2635827..6eb0805 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
     "name": "tslint-to-eslint-config",
-    "version": "0.1.7",
+    "version": "0.1.8",
     "lockfileVersion": 1,
     "requires": true,
     "dependencies": {
@@ -4345,9 +4345,9 @@
             }
         },
         "is-path-cwd": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.1.0.tgz",
-            "integrity": "sha512-Sc5j3/YnM8tDeyCsVeKlm/0p95075DyLmDEIkSgQ7mXkrOX+uTCtmQFm0CYzVyJwcCCmO3k8qfJt17SxQwB5Zw==",
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
+            "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
             "dev": true
         },
         "is-path-in-cwd": {
@@ -5132,9 +5132,9 @@
             }
         },
         "lint-staged": {
-            "version": "9.0.2",
-            "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-9.0.2.tgz",
-            "integrity": "sha512-2W9yBRADEzcGs6j/57JA0GVzSTByYgWbcU89/Xm0s/k+4qMp8032tZYPPmP422xZ79xtUaNisJ/0U5lbuYmaTA==",
+            "version": "9.1.0",
+            "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-9.1.0.tgz",
+            "integrity": "sha512-jI+oj159MHVlsGxMFkzTPrin0Jt8ltVJWSfCPn/xM+qiIPwCimjuhq9//IiNMaNj8VIFZ8QlaAF4qecWTbUOPQ==",
             "dev": true,
             "requires": {
                 "chalk": "^2.4.2",
@@ -5171,9 +5171,9 @@
                     }
                 },
                 "execa": {
-                    "version": "2.0.2",
-                    "resolved": "https://registry.npmjs.org/execa/-/execa-2.0.2.tgz",
-                    "integrity": "sha512-CkFnhVuWj5stQUvRSeI+zAw0ME+Iprkew4HKSc491vOXLM+hKrDVn+QQoL2CIYy0CpvT0mY+MXlzPreNbuj/8A==",
+                    "version": "2.0.3",
+                    "resolved": "https://registry.npmjs.org/execa/-/execa-2.0.3.tgz",
+                    "integrity": "sha512-iM124nlyGSrXmuyZF1EMe83ESY2chIYVyDRZKgmcDynid2Q2v/+GuE7gNMl6Sy9Niwf4MC0DDxagOxeMPjuLsw==",
                     "dev": true,
                     "requires": {
                         "cross-spawn": "^6.0.5",
diff --git a/package.json b/package.json
index 6c6dbe9..b2c3ea7 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
         "eslint-plugin-import": "2.18.0",
         "husky": "3.0.0",
         "jest": "24.8.0",
-        "lint-staged": "9.0.2",
+        "lint-staged": "9.1.0",
         "prettier": "1.18.2",
         "strip-ansi": "5.2.0",
         "tslint": "5.18.0",
@@ -66,5 +66,5 @@
         "test:end-to-end:accept": "jest --config=test/jest.config.js --globals=\"{\\\"acceptTestChanges\\\": true }\"",
         "tsc": "tsc"
     },
-    "version": "0.1.7"
+    "version": "0.1.8"
 }
diff --git a/src/conversion/convertConfig.ts b/src/conversion/convertConfig.ts
index 81e8fbb..8091c96 100644
--- a/src/conversion/convertConfig.ts
+++ b/src/conversion/convertConfig.ts
@@ -14,19 +14,27 @@ export type ConvertConfigDependencies = {
     writeConversionResults: SansDependencies<typeof writeConversionResults>;
 };
 
+/**
+ * Root-level driver to convert a TSLint configuration to ESLint.
+ * @see `Architecture.md` for documentation.
+ */
 export const convertConfig = async (
     dependencies: ConvertConfigDependencies,
     settings: TSLintToESLintSettings,
 ): Promise<ResultWithStatus> => {
+    // 1. Existing configurations are read
     const originalConfigurations = await dependencies.findOriginalConfigurations(settings);
     if (originalConfigurations.status !== ResultStatus.Succeeded) {
         return originalConfigurations;
     }
 
+    // 2. TSLint rules are converted into their ESLint configurations
     const ruleConversionResults = dependencies.convertRules(
         originalConfigurations.data.tslint.rules,
     );
-    const mergedConfiguration = {
+
+    // 3. ESLint configurations are simplified based on extended ESLint presets
+    const simplifiedConfiguration = {
         ...ruleConversionResults,
         ...(await dependencies.simplifyPackageRules(
             originalConfigurations.data.eslint,
@@ -34,12 +42,15 @@ export const convertConfig = async (
         )),
     };
 
+    // 4. The simplified configuration is written to the output config file
     await dependencies.writeConversionResults(
         settings.config,
-        mergedConfiguration,
+        simplifiedConfiguration,
         originalConfigurations.data,
     );
-    dependencies.reportConversionResults(mergedConfiguration);
+
+    // 5. A summary of the results is printed to the user's console
+    dependencies.reportConversionResults(simplifiedConfiguration);
 
     return {
         status: ResultStatus.Succeeded,
diff --git a/src/input/findTypeScriptConfiguration.ts b/src/input/findTypeScriptConfiguration.ts
index f0e33ca..27badf3 100644
--- a/src/input/findTypeScriptConfiguration.ts
+++ b/src/input/findTypeScriptConfiguration.ts
@@ -19,7 +19,7 @@ export const findTypeScriptConfiguration = async (
 ): Promise<TypeScriptConfiguration | Error> => {
     const rawConfiguration = await findConfiguration<TypeScriptConfiguration>(
         dependencies.exec,
-        "cat",
+        "tsc --showConfig -p",
         config || "./tsconfig.json",
     );
 
diff --git a/src/rules/converter.ts b/src/rules/converter.ts
index 77d63fb..7acee8f 100644
--- a/src/rules/converter.ts
+++ b/src/rules/converter.ts
@@ -37,6 +37,13 @@ export type ConversionResult = {
  * An ESLint rule equivalent to a previously enabled TSLint rule.
  */
 export type ConvertedRuleChanges = {
+    /**
+     * Any arguments for that ESLint rule.
+     */
     ruleArguments?: any[];
+
+    /**
+     * Equivalent ESLint rule name that should be enabled.
+     */
     ruleName: string;
 };
diff --git a/src/rules/converters/no-banned-terms.ts b/src/rules/converters/no-banned-terms.ts
index 4e653cb..8f7760d 100644
--- a/src/rules/converters/no-banned-terms.ts
+++ b/src/rules/converters/no-banned-terms.ts
@@ -2,6 +2,7 @@ import { RuleConverter } from "../converter";
 
 export const convertNoBannedTerms: RuleConverter = () => {
     return {
+        // This is mentioned in Architecture.md as a TSLint rule with two ESLint equivalents
         rules: [
             {
                 ruleName: "no-caller",
diff --git a/src/rules/mergers/ban-types.ts b/src/rules/mergers/ban-types.ts
index d5c06e3..e6597f8 100644
--- a/src/rules/mergers/ban-types.ts
+++ b/src/rules/mergers/ban-types.ts
@@ -5,6 +5,7 @@ export const mergeBanTypes: RuleMerger = (existingOptions, newOptions) => {
         return [];
     }
 
+    // This is mentioned in Architecture.md as an ESLint rule with a merger
     return [
         {
             types: {


Test patch:
diff --git a/docs/Testing.md b/docs/Testing.md
index 0ff7b7d..f7178a5 100644
--- a/docs/Testing.md
+++ b/docs/Testing.md
@@ -18,3 +18,23 @@ Tests should include comments above each section of the "AAA" testing format:
 -   `src/cli/main.ts`
 -   `src/adapters/*.ts`
 -   `src/**/*.stubs.ts`
+
+See [Dependencies](./Dependencies.md) for how static dependencies are stubbed out in functions.
+That system is how functions can receive stubs and spies during unit tests.
+
+## End-to-End Tests
+
+```shell
+npm run test:end-to-end
+```
+
+End-to-end tests that execute the `bin/tslint-to-eslint` command and validate outputs are generated from the directories in `test/tests/`.
+Each directory there contains:
+
+-   `.eslintrc.json`: `.gitignore`d output from the most recent test run
+-   `expected.json`: Expected output ESLint configuration
+-   `stderr.txt`: Any output written to the process `stderr`
+-   `stdout.txt`: Any output written to the process `stdout`
+-   `tslint.json`: Original TSLint configuration file to convert
+
+Within each directory, a test suite will execute `bin/tslint-to-eslint` and validate the outputs match what's on disk.
diff --git a/src/input/findTypeScriptConfiguration.test.ts b/src/input/findTypeScriptConfiguration.test.ts
index ac9e8c7..befb8d3 100644
--- a/src/input/findTypeScriptConfiguration.test.ts
+++ b/src/input/findTypeScriptConfiguration.test.ts
@@ -26,7 +26,7 @@ describe("findTypeScriptConfiguration", () => {
         await findTypeScriptConfiguration(dependencies, undefined);
 
         // Assert
-        expect(dependencies.exec).toHaveBeenLastCalledWith(`cat "./tsconfig.json"`);
+        expect(dependencies.exec).toHaveBeenLastCalledWith(`tsc --showConfig -p "./tsconfig.json"`);
     });
 
     it("includes a configuration file in the TypeScript command when one is provided", async () => {
@@ -38,7 +38,9 @@ describe("findTypeScriptConfiguration", () => {
         await findTypeScriptConfiguration(dependencies, config);
 
         // Assert
-        expect(dependencies.exec).toHaveBeenLastCalledWith(`cat "./custom/tsconfig.json"`);
+        expect(dependencies.exec).toHaveBeenLastCalledWith(
+            `tsc --showConfig -p "./custom/tsconfig.json"`,
+        );
     });
 
     it("applies TypeScript defaults when none are provided", async () => {