Repository: eslint/eslint
Pull request #12290 (2019-09-19 12:48:51)

Docs: Add supported ECMAScript version to README
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
-->

**What is the purpose of this pull request? (put an "X" next to item)**

[X] Documentation update

<!--
    If the item you've checked above has a template, please paste the template questions below and answer them. (If this pull request is addressing an issue, you can just paste a link to the issue here instead.)
-->

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (https://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

**What changes did you make? (Give an overview)**

Added `2019` in the README as a fully supported version.

**Is there anything you'd like reviewers to focus on?**

Since the sentence is 'has full support' I guess it's too early to add 2020 as well?


Patch:
diff --git a/README.md b/README.md
index 43a566c75..c553cc810 100644
--- a/README.md
+++ b/README.md
@@ -120,7 +120,7 @@ Yes, ESLint natively supports parsing JSX syntax (this must be enabled in [confi
 
 ### What ECMAScript versions does ESLint support?
 
-ESLint has full support for ECMAScript 3, 5 (default), 2015, 2016, 2017, and 2018. You can set your desired ECMAScript syntax (and other settings, like global variables or your target environments) through [configuration](https://eslint.org/docs/user-guide/configuring).
+ESLint has full support for ECMAScript 3, 5 (default), 2015, 2016, 2017, 2018, and 2019. You can set your desired ECMAScript syntax (and other settings, like global variables or your target environments) through [configuration](https://eslint.org/docs/user-guide/configuring).
 
 ### What about experimental features?
 
@@ -262,9 +262,9 @@ The following companies, organizations, and individuals support ESLint's ongoing
 <!-- NOTE: This section is autogenerated. Do not manually edit.-->
 <!--sponsorsstart-->
 <h3>Gold Sponsors</h3>
-<p><a href="https://www.shopify.com"><img src="https://images.opencollective.com/shopify/eeb91aa/logo.png" alt="Shopify" height="96"></a> <a href="http://engineering.salesforce.com"><img src="https://images.opencollective.com/salesforce/d1b37c4/logo.png" alt="Salesforce" height="96"></a> <a href="https://badoo.com/team?utm_source=eslint"><img src="https://images.opencollective.com/badoo/2826a3b/logo.png" alt="Badoo" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a> <a href="https://opensource.facebook.com"><img src="https://images.opencollective.com/fbopensource/fbb8a5b/logo.png" alt="Facebook Open Source" height="96"></a></p><h3>Silver Sponsors</h3>
+<p><a href="https://www.shopify.com"><img src="https://images.opencollective.com/shopify/eeb91aa/logo.png" alt="Shopify" height="96"></a> <a href="http://engineering.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://badoo.com/team?utm_source=eslint"><img src="https://images.opencollective.com/badoo/2826a3b/logo.png" alt="Badoo" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a> <a href="https://opensource.facebook.com"><img src="https://images.opencollective.com/fbopensource/fbb8a5b/logo.png" alt="Facebook Open Source" height="96"></a></p><h3>Silver Sponsors</h3>
 <p><a href="https://www.ampproject.org/"><img src="https://images.opencollective.com/amp/c8a3b25/logo.png" alt="AMP Project" height="64"></a></p><h3>Bronze Sponsors</h3>
-<p><a href="https://discordapp.com"><img src="https://images.opencollective.com/discordapp/7e3d9a9/logo.png" alt="Discord" height="32"></a> <a href="https://moneypug.co.uk/"><img src="https://images.opencollective.com/moneypug/45f8d53/logo.png" alt="MONEYPUG" height="32"></a> <a href="https://icons8.com"><img src="https://images.opencollective.com/icons8/0b37d14/logo.png" alt="Free Icons by Icons8" height="32"></a> <a href="https://uxplanet.org/top-ui-ux-design-agencies-user-experience-firms-8c54697e290"><img src="https://images.opencollective.com/ui-ux-design-agencies/cae5dfe/logo.png" alt="UI UX Design Agencies" height="32"></a> <a href="https://clay.global"><img src="https://images.opencollective.com/clayglobal/2468f34/logo.png" alt="clay" height="32"></a> <a href="https://www.vpsserver.com"><img src="https://images.opencollective.com/vpsservercom/logo.png" alt="VPS Server" height="32"></a> <a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://tekhattan.com"><img src="https://images.opencollective.com/tekhattan/bc73c28/logo.png" alt="TekHattan" height="32"></a> <a href="https://www.marfeel.com/"><img src="https://images.opencollective.com/marfeel/4b88e30/logo.png" alt="Marfeel" height="32"></a> <a href="http://www.firesticktricks.com"><img src="https://images.opencollective.com/fire-stick-tricks/b8fbe2c/logo.png" alt="Fire Stick Tricks" height="32"></a> <a href="https://jsheroes.io/"><img src="https://images.opencollective.com/jsheroes1/9fedf0b/logo.png" alt="JSHeroes " height="32"></a></p>
+<p><a href="https://discordapp.com"><img src="https://images.opencollective.com/discordapp/7e3d9a9/logo.png" alt="Discord" height="32"></a> <a href="https://mixpanel.com"><img src="https://images.opencollective.com/mixpanel/c35a9d7/logo.png" alt="Mixpanel" height="32"></a> <a href="https://icons8.com"><img src="https://images.opencollective.com/icons8/0b37d14/logo.png" alt="Free Icons by Icons8" height="32"></a> <a href="https://uxplanet.org/top-ui-ux-design-agencies-user-experience-firms-8c54697e290"><img src="https://images.opencollective.com/ui-ux-design-agencies/cae5dfe/logo.png" alt="UI UX Design Agencies" height="32"></a> <a href="https://clay.global"><img src="https://images.opencollective.com/clayglobal/2468f34/logo.png" alt="clay" height="32"></a> <a href="https://www.vpsserver.com"><img src="https://images.opencollective.com/vpsservercom/logo.png" alt="VPS Server" height="32"></a> <a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://tekhattan.com"><img src="https://images.opencollective.com/tekhattan/bc73c28/logo.png" alt="TekHattan" height="32"></a> <a href="https://www.marfeel.com/"><img src="https://images.opencollective.com/marfeel/4b88e30/logo.png" alt="Marfeel" height="32"></a> <a href="http://www.firesticktricks.com"><img src="https://images.opencollective.com/fire-stick-tricks/b8fbe2c/logo.png" alt="Fire Stick Tricks" height="32"></a> <a href="https://jsheroes.io/"><img src="https://images.opencollective.com/jsheroes1/9fedf0b/logo.png" alt="JSHeroes " height="32"></a></p>
 <!--sponsorsend-->
 
 ## <a name="technology-sponsors"></a>Technology Sponsors
diff --git a/bin/eslint.js b/bin/eslint.js
index 061e94767..82bcc1e03 100755
--- a/bin/eslint.js
+++ b/bin/eslint.js
@@ -16,9 +16,9 @@ require("v8-compile-cache");
 // Helpers
 //------------------------------------------------------------------------------
 
-const useStdIn = (process.argv.indexOf("--stdin") > -1),
-    init = (process.argv.indexOf("--init") > -1),
-    debug = (process.argv.indexOf("--debug") > -1);
+const useStdIn = process.argv.includes("--stdin"),
+    init = process.argv.includes("--init"),
+    debug = process.argv.includes("--debug");
 
 // must do this initialization *before* other requires in order to work
 if (debug) {
@@ -30,9 +30,9 @@ if (debug) {
 //------------------------------------------------------------------------------
 
 // now we can safely include the other modules that use debug
-const cli = require("../lib/cli"),
-    path = require("path"),
-    fs = require("fs");
+const path = require("path"),
+    fs = require("fs"),
+    cli = require("../lib/cli");
 
 //------------------------------------------------------------------------------
 // Execution
@@ -50,7 +50,6 @@ process.once("uncaughtException", err => {
         console.error("\nOops! Something went wrong! :(");
         console.error(`\nESLint: ${pkg.version}.\n\n${template(err.messageData || {})}`);
     } else {
-
         console.error(err.stack);
     }
 
diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md
index e6df77222..464d6dbd1 100644
--- a/docs/developer-guide/nodejs-api.md
+++ b/docs/developer-guide/nodejs-api.md
@@ -617,14 +617,16 @@ const cli = new CLIEngine({
     }
 });
 
-// lint the supplied text and optionally set
-// a filename that is displayed in the report
+// Lint the supplied text and optionally set a filename that is displayed in the report
 const report = cli.executeOnText("var foo = 'bar';", "foo.js");
+
+// In addition to the above, warn if the resolved file name is ignored.
+const reportAndWarnOnIgnoredFile = cli.executeOnText("var foo = 'bar';", "foo.js", true);
 ```
 
 The `report` returned from `executeOnText()` is in the same format as from `executeOnFiles()`, but there is only ever one result in `report.results`.
 
-If a filename in the optional second parameter matches a file that is configured to be ignored, then this function returns no errors or warnings. To return a warning instead, call the method with true as the optional third parameter.
+If a filename in the optional second parameter matches a file that is configured to be ignored, then this function returns no errors or warnings. The method includes an additional optional boolean third parameter. When `true`, a resolved file name that is ignored will return a warning.
 
 ### CLIEngine#addPlugin()
 
diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md
index 2a9f1b9d3..35f224b32 100644
--- a/docs/developer-guide/working-with-rules.md
+++ b/docs/developer-guide/working-with-rules.md
@@ -616,6 +616,6 @@ The thing that makes ESLint different from other linters is the ability to defin
 
 Runtime rules are written in the same format as all other rules. Create your rule as you would any other and then follow these steps:
 
-1. Place all of your runtime rules in the same directory (i.e., `eslint_rules`).
+1. Place all of your runtime rules in the same directory (e.g., `eslint_rules`).
 2. Create a [configuration file](../user-guide/configuring.md) and specify your rule ID error level under the `rules` key. Your rule will not run unless it has a value of `1` or `2` in the configuration file.
 3. Run the [command line interface](../user-guide/command-line-interface.md) using the `--rulesdir` option to specify the location of your runtime rules.
diff --git a/docs/rules/array-callback-return.md b/docs/rules/array-callback-return.md
index 58dd26c35..34ffeffce 100644
--- a/docs/rules/array-callback-return.md
+++ b/docs/rules/array-callback-return.md
@@ -20,10 +20,10 @@ This rule finds callback functions of the following methods, then checks usage o
 * [`Array.prototype.every`](https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.every)
 * [`Array.prototype.filter`](https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.filter)
 * [`Array.prototype.find`](https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.find)
-* [`Array.prototype.findIndex`](https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.findIndex )
+* [`Array.prototype.findIndex`](https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.findindex)
 * [`Array.prototype.map`](https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.map)
 * [`Array.prototype.reduce`](https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.reduce)
-* [`Array.prototype.reduceRight`](https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.reduceRight)
+* [`Array.prototype.reduceRight`](https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.reduceright)
 * [`Array.prototype.some`](https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.some)
 * [`Array.prototype.sort`](https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.sort)
 * And above of typed arrays.
diff --git a/docs/user-guide/command-line-interface.md b/docs/user-guide/command-line-interface.md
index 532d8f947..8938bc91f 100644
--- a/docs/user-guide/command-line-interface.md
+++ b/docs/user-guide/command-line-interface.md
@@ -78,6 +78,7 @@ Caching:
 
 Miscellaneous:
   --init                         Run config initialization wizard - default: false
+  --env-info                     Output execution environment information - default: false
   --debug                        Output debugging information
   -h, --help                     Show help
   -v, --version                  Output the version number
@@ -446,6 +447,10 @@ This option will start config initialization wizard. It's designed to help new u
 
 The resulting configuration file will be created in the current directory.
 
+#### `--env-info`
+
+This option outputs information about the execution environment, including the version of Node, npm, and local and global installations of ESLint. The ESLint team may ask for this information to help solve bugs.
+
 #### `--debug`
 
 This option outputs debugging information to the console. This information is useful when you're seeing a problem and having a hard time pinpointing it. The ESLint team may ask for this debugging information to help solve bugs.
diff --git a/lib/cli.js b/lib/cli.js
index c34545544..18a917cf0 100644
--- a/lib/cli.js
+++ b/lib/cli.js
@@ -20,7 +20,8 @@ const fs = require("fs"),
     mkdirp = require("mkdirp"),
     { CLIEngine } = require("./cli-engine"),
     options = require("./options"),
-    log = require("./shared/logging");
+    log = require("./shared/logging"),
+    RuntimeInfo = require("./shared/runtime-info");
 
 const debug = require("debug")("eslint:cli");
 
@@ -159,13 +160,18 @@ const cli = {
         }
 
         const files = currentOptions._;
-
         const useStdin = typeof text === "string";
 
-        if (currentOptions.version) { // version from package.json
-
-            log.info(`v${require("../package.json").version}`);
-
+        if (currentOptions.version) {
+            log.info(RuntimeInfo.version());
+        } else if (currentOptions.envInfo) {
+            try {
+                log.info(RuntimeInfo.environment());
+                return 0;
+            } catch (err) {
+                log.error(err.message);
+                return 2;
+            }
         } else if (currentOptions.printConfig) {
             if (files.length) {
                 log.error("The --print-config option must be used with exactly one file name.");
@@ -177,17 +183,13 @@ const cli = {
             }
 
             const engine = new CLIEngine(translateOptions(currentOptions));
-
             const fileConfig = engine.getConfigForFile(currentOptions.printConfig);
 
             log.info(JSON.stringify(fileConfig, null, "  "));
             return 0;
         } else if (currentOptions.help || (!files.length && !useStdin)) {
-
             log.info(options.generateHelp());
-
         } else {
-
             debug(`Running on ${useStdin ? "text" : "files"}`);
 
             if (currentOptions.fix && currentOptions.fixDryRun) {
@@ -227,9 +229,8 @@ const cli = {
 
                 return (report.errorCount || tooManyWarnings) ? 1 : 0;
             }
-            return 2;
-
 
+            return 2;
         }
 
         return 0;
diff --git a/lib/options.js b/lib/options.js
index 440773a84..83bf9afc2 100644
--- a/lib/options.js
+++ b/lib/options.js
@@ -224,6 +224,12 @@ module.exports = optionator({
             default: "false",
             description: "Run config initialization wizard"
         },
+        {
+            option: "env-info",
+            type: "Boolean",
+            default: "false",
+            description: "Output execution environment information"
+        },
         {
             option: "debug",
             type: "Boolean",
diff --git a/lib/rules/indent.js b/lib/rules/indent.js
index f44eafa71..a2fa9c4f4 100644
--- a/lib/rules/indent.js
+++ b/lib/rules/indent.js
@@ -81,6 +81,9 @@ const KNOWN_NODES = new Set([
     "WhileStatement",
     "WithStatement",
     "YieldExpression",
+    "JSXFragment",
+    "JSXOpeningFragment",
+    "JSXClosingFragment",
     "JSXIdentifier",
     "JSXNamespacedName",
     "JSXMemberExpression",
@@ -1453,6 +1456,31 @@ module.exports = {
                 offsets.setDesiredOffsets(node.name.range, firstToken, 1);
             },
 
+            JSXFragment(node) {
+                const firstOpeningToken = sourceCode.getFirstToken(node.openingFragment);
+                const firstClosingToken = sourceCode.getFirstToken(node.closingFragment);
+
+                addElementListIndent(node.children, firstOpeningToken, firstClosingToken, 1);
+            },
+
+            JSXOpeningFragment(node) {
+                const firstToken = sourceCode.getFirstToken(node);
+                const closingToken = sourceCode.getLastToken(node);
+
+                offsets.setDesiredOffsets(node.range, firstToken, 1);
+                offsets.matchOffsetOf(firstToken, closingToken);
+            },
+
+            JSXClosingFragment(node) {
+                const firstToken = sourceCode.getFirstToken(node);
+                const slashToken = sourceCode.getLastToken(node, { skip: 1 });
+                const closingToken = sourceCode.getLastToken(node);
+                const tokenToMatch = astUtils.isTokenOnSameLine(slashToken, closingToken) ? slashToken : closingToken;
+
+                offsets.setDesiredOffsets(node.range, firstToken, 1);
+                offsets.matchOffsetOf(firstToken, tokenToMatch);
+            },
+
             JSXExpressionContainer(node) {
                 const openingCurly = sourceCode.getFirstToken(node);
                 const closingCurly = sourceCode.getLastToken(node);
diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js
index c5cf79047..c6809c355 100644
--- a/lib/rules/no-extra-parens.js
+++ b/lib/rules/no-extra-parens.js
@@ -205,7 +205,14 @@ module.exports = {
             const lastToken = sourceCode.getLastToken(newExpression);
             const penultimateToken = sourceCode.getTokenBefore(lastToken);
 
-            return newExpression.arguments.length > 0 || astUtils.isOpeningParenToken(penultimateToken) && astUtils.isClosingParenToken(lastToken);
+            return newExpression.arguments.length > 0 ||
+                (
+
+                    // The expression should end with its own parens, e.g., new new foo() is not a new expression with parens
+                    astUtils.isOpeningParenToken(penultimateToken) &&
+                    astUtils.isClosingParenToken(lastToken) &&
+                    newExpression.callee.range[1] < newExpression.range[1]
+                );
         }
 
         /**
@@ -338,7 +345,7 @@ module.exports = {
             function finishReport() {
                 context.report({
                     node,
-                    loc: leftParenToken.loc.start,
+                    loc: leftParenToken.loc,
                     messageId: "unexpected",
                     fix(fixer) {
                         const parenthesizedSource = sourceCode.text.slice(leftParenToken.range[1], rightParenToken.range[0]);
@@ -887,6 +894,12 @@ module.exports = {
                     report(node.object);
                 }
 
+                if (nodeObjHasExcessParens &&
+                  node.object.type === "NewExpression" &&
+                  isNewExpressionWithParens(node.object)) {
+                    report(node.object);
+                }
+
                 if (node.computed && hasExcessParens(node.property)) {
                     report(node.property);
                 }
diff --git a/lib/rules/no-useless-rename.js b/lib/rules/no-useless-rename.js
index c1860645e..4ff65d4b8 100644
--- a/lib/rules/no-useless-rename.js
+++ b/lib/rules/no-useless-rename.js
@@ -41,6 +41,8 @@ module.exports = {
             ignoreImport = options.ignoreImport === true,
             ignoreExport = options.ignoreExport === true;
 
+        const sourceCode = context.getSourceCode();
+
         //--------------------------------------------------------------------------
         // Helpers
         //--------------------------------------------------------------------------
@@ -64,6 +66,9 @@ module.exports = {
                     type
                 },
                 fix(fixer) {
+                    if (sourceCode.commentsExistBetween(initial, result)) {
+                        return null;
+                    }
                     return fixer.replaceTextRange([
                         initial.range[0],
                         result.range[1]
diff --git a/lib/rules/prefer-numeric-literals.js b/lib/rules/prefer-numeric-literals.js
index ca7358aa0..b4113a3b3 100644
--- a/lib/rules/prefer-numeric-literals.js
+++ b/lib/rules/prefer-numeric-literals.js
@@ -96,6 +96,10 @@ module.exports = {
                         fix(fixer) {
                             const newPrefix = prefixMap[node.arguments[1].value];
 
+                            if (sourceCode.getCommentsInside(node).length) {
+                                return null;
+                            }
+
                             if (+(newPrefix + node.arguments[0].value) !== parseInt(node.arguments[0].value, node.arguments[1].value)) {
 
                                 /*
diff --git a/lib/shared/runtime-info.js b/lib/shared/runtime-info.js
new file mode 100644
index 000000000..169bbc58f
--- /dev/null
+++ b/lib/shared/runtime-info.js
@@ -0,0 +1,163 @@
+/**
+ * @fileoverview Utility to get information about the execution environment.
+ * @author Kai Cataldo
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const path = require("path");
+const spawn = require("cross-spawn");
+const { isEmpty } = require("lodash");
+const log = require("../shared/logging");
+const packageJson = require("../../package.json");
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+/**
+ * Generates and returns execution environment information.
+ * @returns {string} A string that contains execution environment information.
+ */
+function environment() {
+    const cache = new Map();
+
+    /**
+     * Checks if a path is a child of a directory.
+     * @param {string} parentPath - The parent path to check.
+     * @param {string} childPath - The path to check.
+     * @returns {boolean} Whether or not the given path is a child of a directory.
+     */
+    function isChildOfDirectory(parentPath, childPath) {
+        return !path.relative(parentPath, childPath).startsWith("..");
+    }
+
+    /**
+     * Synchronously executes a shell command and formats the result.
+     * @param {string} cmd - The command to execute.
+     * @param {Array} args - The arguments to be executed with the command.
+     * @returns {string} The version returned by the command.
+     */
+    function execCommand(cmd, args) {
+        const key = [cmd, ...args].join(" ");
+
+        if (cache.has(key)) {
+            return cache.get(key);
+        }
+
+        const process = spawn.sync(cmd, args, { encoding: "utf8" });
+
+        if (process.error) {
+            throw process.error;
+        }
+
+        const result = process.stdout.trim();
+
+        cache.set(key, result);
+        return result;
+    }
+
+    /**
+     * Normalizes a version number.
+     * @param {string} versionStr - The string to normalize.
+     * @returns {string} The normalized version number.
+     */
+    function normalizeVersionStr(versionStr) {
+        return versionStr.startsWith("v") ? versionStr : `v${versionStr}`;
+    }
+
+    /**
+     * Gets bin version.
+     * @param {string} bin - The bin to check.
+     * @returns {string} The normalized version returned by the command.
+     */
+    function getBinVersion(bin) {
+        const binArgs = ["--version"];
+
+        try {
+            return normalizeVersionStr(execCommand(bin, binArgs));
+        } catch (e) {
+            log.error(`Error finding ${bin} version running the command \`${bin} ${binArgs.join(" ")}\``);
+            throw e;
+        }
+    }
+
+    /**
+     * Gets installed npm package version.
+     * @param {string} pkg - The package to check.
+     * @param {boolean} global - Whether to check globally or not.
+     * @returns {string} The normalized version returned by the command.
+     */
+    function getNpmPackageVersion(pkg, { global = false } = {}) {
+        const npmBinArgs = ["bin", "-g"];
+        const npmLsArgs = ["ls", "--depth=0", "--json", "eslint"];
+
+        if (global) {
+            npmLsArgs.push("-g");
+        }
+
+        try {
+            const parsedStdout = JSON.parse(execCommand("npm", npmLsArgs));
+
+            /*
+             * Checking globally returns an empty JSON object, while local checks
+             * include the name and version of the local project.
+             */
+            if (isEmpty(parsedStdout) || !(parsedStdout.dependencies && parsedStdout.dependencies.eslint)) {
+                return "Not found";
+            }
+
+            const [, processBinPath] = process.argv;
+            let npmBinPath;
+
+            try {
+                npmBinPath = execCommand("npm", npmBinArgs);
+            } catch (e) {
+                log.error(`Error finding npm binary path when running command \`npm ${npmBinArgs.join(" ")}\``);
+                throw e;
+            }
+
+            const isGlobal = isChildOfDirectory(npmBinPath, processBinPath);
+            let pkgVersion = parsedStdout.dependencies.eslint.version;
+
+            if ((global && isGlobal) || (!global && !isGlobal)) {
+                pkgVersion += " (Currently used)";
+            }
+
+            return normalizeVersionStr(pkgVersion);
+        } catch (e) {
+            log.error(`Error finding ${pkg} version running the command \`npm ${npmLsArgs.join(" ")}\``);
+            throw e;
+        }
+    }
+
+    return [
+        "Environment Info:",
+        "",
+        `Node version: ${getBinVersion("node")}`,
+        `npm version: ${getBinVersion("npm")}`,
+        `Local ESLint version: ${getNpmPackageVersion("eslint", { global: false })}`,
+        `Global ESLint version: ${getNpmPackageVersion("eslint", { global: true })}`
+    ].join("\n");
+}
+
+/**
+ * Returns version of currently executing ESLint.
+ * @returns {string} The version from the currently executing ESLint's package.json.
+ */
+function version() {
+    return `v${packageJson.version}`;
+}
+
+//------------------------------------------------------------------------------
+// Public Interface
+//------------------------------------------------------------------------------
+
+module.exports = {
+    environment,
+    version
+};


Test patch:
diff --git a/tests/lib/cli.js b/tests/lib/cli.js
index baae75b53..1b98fb137 100644
--- a/tests/lib/cli.js
+++ b/tests/lib/cli.js
@@ -29,14 +29,18 @@ const proxyquire = require("proxyquire").noCallThru().noPreserveCache();
 //------------------------------------------------------------------------------
 
 describe("cli", () => {
-
     let fixtureDir;
     const log = {
         info: sinon.spy(),
         error: sinon.spy()
     };
+    const RuntimeInfo = {
+        environment: sinon.stub(),
+        version: sinon.stub()
+    };
     const cli = proxyquire("../../lib/cli", {
-        "./shared/logging": log
+        "./shared/logging": log,
+        "./shared/runtime-info": RuntimeInfo
     });
 
     /**
@@ -324,15 +328,27 @@ describe("cli", () => {
     describe("when executing with version flag", () => {
         it("should print out current version", () => {
             assert.strictEqual(cli.execute("-v"), 0);
+            assert.strictEqual(log.info.callCount, 1);
+        });
+    });
 
+    describe("when executing with env-info flag", () => {
+        it("should print out environment information", () => {
+            assert.strictEqual(cli.execute("--env-info"), 0);
             assert.strictEqual(log.info.callCount, 1);
         });
+
+        it("should print error message and return error code", () => {
+            RuntimeInfo.environment.throws("There was an error!");
+
+            assert.strictEqual(cli.execute("--env-info"), 2);
+            assert.strictEqual(log.error.callCount, 1);
+        });
     });
 
     describe("when executing with help flag", () => {
         it("should print out help", () => {
             assert.strictEqual(cli.execute("-h"), 0);
-
             assert.strictEqual(log.info.callCount, 1);
         });
     });
@@ -349,7 +365,6 @@ describe("cli", () => {
     });
 
     describe("when given a file in excluded files list", () => {
-
         it("should not process the file", () => {
             const ignorePath = getFixturePath(".eslintignore");
             const filePath = getFixturePath("passing.js");
@@ -398,7 +413,6 @@ describe("cli", () => {
     });
 
     describe("when executing a file with a shebang", () => {
-
         it("should execute without error", () => {
             const filePath = getFixturePath("shebang.js");
             const exit = cli.execute(`--no-ignore ${filePath}`);
@@ -408,7 +422,6 @@ describe("cli", () => {
     });
 
     describe("when loading a custom rule", () => {
-
         it("should return an error when rule isn't found", () => {
             const rulesPath = getFixturePath("rules", "wrong");
             const configPath = getFixturePath("rules", "eslint.json");
@@ -551,7 +564,6 @@ describe("cli", () => {
     });
 
     describe("when supplied with report output file path", () => {
-
         afterEach(() => {
             sh.rm("-rf", "tests/output");
         });
@@ -594,7 +606,6 @@ describe("cli", () => {
     });
 
     describe("when supplied with a plugin", () => {
-
         it("should pass plugins to CLIEngine", () => {
             const examplePluginName = "eslint-plugin-example";
 
diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js
index e2d6202a9..96b746392 100644
--- a/tests/lib/rules/indent.js
+++ b/tests/lib/rules/indent.js
@@ -4651,6 +4651,210 @@ ruleTester.run("indent", rule, {
                 </div>
             `,
 
+        /*
+         * JSX Fragments
+         * https://github.com/eslint/eslint/issues/12208
+         */
+        unIndent`
+            <>
+                <A />
+            </>
+        `,
+        unIndent`
+            <
+            >
+                <A />
+            </>
+        `,
+        unIndent`
+            <>
+                <A />
+            <
+            />
+        `,
+        unIndent`
+            <>
+                <A />
+            </
+            >
+        `,
+        unIndent`
+            <
+            >
+                <A />
+            </
+            >
+        `,
+        unIndent`
+            <
+            >
+                <A />
+            <
+            />
+        `,
+        unIndent`
+            < // Comment
+            >
+                <A />
+            </>
+        `,
+        unIndent`
+            <
+                // Comment
+            >
+                <A />
+            </>
+        `,
+        unIndent`
+            <
+            // Comment
+            >
+                <A />
+            </>
+        `,
+        unIndent`
+            <>
+                <A />
+            < // Comment
+            />
+        `,
+        unIndent`
+            <>
+                <A />
+            <
+                // Comment
+            />
+        `,
+        unIndent`
+            <>
+                <A />
+            <
+            // Comment
+            />
+        `,
+        unIndent`
+            <>
+                <A />
+            </ // Comment
+            >
+        `,
+        unIndent`
+            <>
+                <A />
+            </
+                // Comment
+            >
+        `,
+        unIndent`
+            <>
+                <A />
+            </
+            // Comment
+            >
+        `,
+        unIndent`
+            < /* Comment */
+            >
+                <A />
+            </>
+        `,
+        unIndent`
+            <
+                /* Comment */
+            >
+                <A />
+            </>
+        `,
+        unIndent`
+            <
+            /* Comment */
+            >
+                <A />
+            </>
+        `,
+        unIndent`
+            <
+                /*
+                 * Comment
+                 */
+            >
+                <A />
+            </>
+        `,
+        unIndent`
+            <
+            /*
+             * Comment
+             */
+            >
+                <A />
+            </>
+        `,
+        unIndent`
+            <>
+                <A />
+            < /* Comment */
+            />
+        `,
+        unIndent`
+            <>
+                <A />
+            <
+                /* Comment */ />
+        `,
+        unIndent`
+            <>
+                <A />
+            <
+            /* Comment */ />
+        `,
+        unIndent`
+            <>
+                <A />
+            <
+                /* Comment */
+            />
+        `,
+        unIndent`
+            <>
+                <A />
+            <
+            /* Comment */
+            />
+        `,
+        unIndent`
+            <>
+                <A />
+            </ /* Comment */
+            >
+        `,
+        unIndent`
+            <>
+                <A />
+            </
+                /* Comment */ >
+        `,
+        unIndent`
+            <>
+                <A />
+            </
+            /* Comment */ >
+        `,
+        unIndent`
+            <>
+                <A />
+            </
+                /* Comment */
+            >
+        `,
+        unIndent`
+            <>
+                <A />
+            </
+            /* Comment */
+            >
+        `,
+
         // https://github.com/eslint/eslint/issues/8832
         unIndent`
                 <div>
@@ -9576,6 +9780,200 @@ ruleTester.run("indent", rule, {
             `,
             errors: expectedErrors([2, 4, 0, "Punctuator"])
         },
+
+        /*
+         * JSX Fragments
+         * https://github.com/eslint/eslint/issues/12208
+         */
+        {
+            code: unIndent`
+                <>
+                <A />
+                </>
+            `,
+            output: unIndent`
+                <>
+                    <A />
+                </>
+            `,
+            errors: expectedErrors([2, 4, 0, "Punctuator"])
+        },
+        {
+            code: unIndent`
+                <
+                    >
+                    <A />
+                </>
+            `,
+            output: unIndent`
+                <
+                >
+                    <A />
+                </>
+            `,
+            errors: expectedErrors([2, 0, 4, "Punctuator"])
+        },
+        {
+            code: unIndent`
+                <>
+                    <A />
+                <
+                    />
+            `,
+            output: unIndent`
+                <>
+                    <A />
+                <
+                />
+            `,
+            errors: expectedErrors([4, 0, 4, "Punctuator"])
+        },
+        {
+            code: unIndent`
+                <>
+                    <A />
+                </
+                    >
+            `,
+            output: unIndent`
+                <>
+                    <A />
+                </
+                >
+            `,
+            errors: expectedErrors([4, 0, 4, "Punctuator"])
+        },
+        {
+            code: unIndent`
+                <
+                    >
+                    <A />
+                </
+                    >
+            `,
+            output: unIndent`
+                <
+                >
+                    <A />
+                </
+                >
+            `,
+            errors: expectedErrors([
+                [2, 0, 4, "Punctuator"],
+                [5, 0, 4, "Punctuator"]
+            ])
+        },
+        {
+            code: unIndent`
+                <
+                    >
+                    <A />
+                <
+                    />
+            `,
+            output: unIndent`
+                <
+                >
+                    <A />
+                <
+                />
+            `,
+            errors: expectedErrors([
+                [2, 0, 4, "Punctuator"],
+                [5, 0, 4, "Punctuator"]
+            ])
+        },
+        {
+            code: unIndent`
+                < // Comment
+                    >
+                    <A />
+                </>
+            `,
+            output: unIndent`
+                < // Comment
+                >
+                    <A />
+                </>
+            `,
+            errors: expectedErrors([2, 0, 4, "Punctuator"])
+        },
+        {
+            code: unIndent`
+                <>
+                    <A />
+                < // Comment
+                    />
+            `,
+            output: unIndent`
+                <>
+                    <A />
+                < // Comment
+                />
+            `,
+            errors: expectedErrors([4, 0, 4, "Punctuator"])
+        },
+        {
+            code: unIndent`
+                <>
+                    <A />
+                </ // Comment
+                    >
+            `,
+            output: unIndent`
+                <>
+                    <A />
+                </ // Comment
+                >
+            `,
+            errors: expectedErrors([4, 0, 4, "Punctuator"])
+        },
+        {
+            code: unIndent`
+                < /* Comment */
+                    >
+                    <A />
+                </>
+            `,
+            output: unIndent`
+                < /* Comment */
+                >
+                    <A />
+                </>
+            `,
+            errors: expectedErrors([2, 0, 4, "Punctuator"])
+        },
+        {
+            code: unIndent`
+                <>
+                    <A />
+                < /* Comment */
+                    />
+            `,
+            output: unIndent`
+                <>
+                    <A />
+                < /* Comment */
+                />
+            `,
+            errors: expectedErrors([4, 0, 4, "Punctuator"])
+        },
+        {
+            code: unIndent`
+                <>
+                    <A />
+                </ /* Comment */
+                    >
+            `,
+            output: unIndent`
+                <>
+                    <A />
+                </ /* Comment */
+                >
+            `,
+            errors: expectedErrors([4, 0, 4, "Punctuator"])
+        },
+
         {
             code: unIndent`
                 ({
diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js
index 788823e04..e42b5eadc 100644
--- a/tests/lib/rules/no-extra-parens.js
+++ b/tests/lib/rules/no-extra-parens.js
@@ -95,6 +95,17 @@ ruleTester.run("no-extra-parens", rule, {
         "new A()()",
         "(new A)()",
         "(new (Foo || Bar))()",
+        "(new new foo())()",
+        "new (new new foo())(bar)",
+        "(new foo).bar",
+        "(new foo)[bar]",
+        "(new foo).bar.baz",
+        "(new foo.bar).baz",
+        "(new foo).bar()",
+        "(new foo.bar).baz()",
+        "new (new foo).bar",
+        "new (new foo.bar).baz",
+        "(new new foo()).baz",
         "(2 + 3) ** 4",
         "2 ** (2 + 3)",
         "new (import(source))",
@@ -647,6 +658,11 @@ ruleTester.run("no-extra-parens", rule, {
         invalid("(foo()).bar", "foo().bar", "CallExpression"),
         invalid("(foo.bar()).baz", "foo.bar().baz", "CallExpression"),
         invalid("(foo\n.bar())\n.baz", "foo\n.bar()\n.baz", "CallExpression"),
+        invalid("(new foo()).bar", "new foo().bar", "NewExpression"),
+        invalid("(new foo()).bar()", "new foo().bar()", "NewExpression"),
+        invalid("(new foo(bar)).baz", "new foo(bar).baz", "NewExpression"),
+        invalid("(new foo.bar()).baz", "new foo.bar().baz", "NewExpression"),
+        invalid("(new foo.bar()).baz()", "new foo.bar().baz()", "NewExpression"),
 
         invalid("new (A)", "new A", "Identifier"),
         invalid("(new A())()", "new A()()", "NewExpression"),
diff --git a/tests/lib/rules/no-useless-rename.js b/tests/lib/rules/no-useless-rename.js
index 1fcc843db..58796c625 100644
--- a/tests/lib/rules/no-useless-rename.js
+++ b/tests/lib/rules/no-useless-rename.js
@@ -299,6 +299,133 @@ ruleTester.run("no-useless-rename", rule, {
             code: "export {foo as foo, bar as bar} from 'foo';",
             output: "export {foo, bar} from 'foo';",
             errors: ["Export foo unnecessarily renamed.", "Export bar unnecessarily renamed."]
+        },
+
+        // Should not autofix if it would remove comments
+        {
+            code: "({/* comment */foo: foo} = {});",
+            output: "({/* comment */foo} = {});",
+            errors: ["Destructuring assignment foo unnecessarily renamed."]
+        },
+        {
+            code: "({foo, /* comment */bar: bar} = {});",
+            output: "({foo, /* comment */bar} = {});",
+            errors: ["Destructuring assignment bar unnecessarily renamed."]
+        },
+        {
+            code: "({foo/**/ : foo} = {});",
+            output: null,
+            errors: ["Destructuring assignment foo unnecessarily renamed."]
+        },
+        {
+            code: "({foo /**/: foo} = {});",
+            output: null,
+            errors: ["Destructuring assignment foo unnecessarily renamed."]
+        },
+        {
+            code: "({foo://\nfoo} = {});",
+            output: null,
+            errors: ["Destructuring assignment foo unnecessarily renamed."]
+        },
+        {
+            code: "({foo: /**/foo} = {});",
+            output: null,
+            errors: ["Destructuring assignment foo unnecessarily renamed."]
+        },
+        {
+            code: "({foo: foo/* comment */} = {});",
+            output: "({foo/* comment */} = {});",
+            errors: ["Destructuring assignment foo unnecessarily renamed."]
+        },
+        {
+            code: "({foo: foo//comment\n,bar} = {});",
+            output: "({foo//comment\n,bar} = {});",
+            errors: ["Destructuring assignment foo unnecessarily renamed."]
+        },
+        {
+            code: "import {/* comment */foo as foo} from 'foo';",
+            output: "import {/* comment */foo} from 'foo';",
+            errors: ["Import foo unnecessarily renamed."]
+        },
+        {
+            code: "import {foo,/* comment */bar as bar} from 'foo';",
+            output: "import {foo,/* comment */bar} from 'foo';",
+            errors: ["Import bar unnecessarily renamed."]
+        },
+        {
+            code: "import {foo/**/ as foo} from 'foo';",
+            output: null,
+            errors: ["Import foo unnecessarily renamed."]
+        },
+        {
+            code: "import {foo /**/as foo} from 'foo';",
+            output: null,
+            errors: ["Import foo unnecessarily renamed."]
+        },
+        {
+            code: "import {foo //\nas foo} from 'foo';",
+            output: null,
+            errors: ["Import foo unnecessarily renamed."]
+        },
+        {
+            code: "import {foo as/**/foo} from 'foo';",
+            output: null,
+            errors: ["Import foo unnecessarily renamed."]
+        },
+        {
+            code: "import {foo as foo/* comment */} from 'foo';",
+            output: "import {foo/* comment */} from 'foo';",
+            errors: ["Import foo unnecessarily renamed."]
+        },
+        {
+            code: "import {foo as foo/* comment */,bar} from 'foo';",
+            output: "import {foo/* comment */,bar} from 'foo';",
+            errors: ["Import foo unnecessarily renamed."]
+        },
+        {
+            code: "let foo; export {/* comment */foo as foo};",
+            output: "let foo; export {/* comment */foo};",
+            errors: ["Export foo unnecessarily renamed."]
+        },
+        {
+            code: "let foo, bar; export {foo,/* comment */bar as bar};",
+            output: "let foo, bar; export {foo,/* comment */bar};",
+            errors: ["Export bar unnecessarily renamed."]
+        },
+        {
+            code: "let foo; export {foo/**/as foo};",
+            output: null,
+            errors: ["Export foo unnecessarily renamed."]
+        },
+        {
+            code: "let foo; export {foo as/**/ foo};",
+            output: null,
+            errors: ["Export foo unnecessarily renamed."]
+        },
+        {
+            code: "let foo; export {foo as /**/foo};",
+            output: null,
+            errors: ["Export foo unnecessarily renamed."]
+        },
+        {
+            code: "let foo; export {foo as//comment\n foo};",
+            output: null,
+            errors: ["Export foo unnecessarily renamed."]
+        },
+        {
+            code: "let foo; export {foo as foo/* comment*/};",
+            output: "let foo; export {foo/* comment*/};",
+            errors: ["Export foo unnecessarily renamed."]
+        },
+        {
+            code: "let foo, bar; export {foo as foo/* comment*/,bar};",
+            output: "let foo, bar; export {foo/* comment*/,bar};",
+            errors: ["Export foo unnecessarily renamed."]
+        },
+        {
+            code: "let foo, bar; export {foo as foo//comment\n,bar};",
+            output: "let foo, bar; export {foo//comment\n,bar};",
+            errors: ["Export foo unnecessarily renamed."]
         }
     ]
 });
diff --git a/tests/lib/rules/prefer-numeric-literals.js b/tests/lib/rules/prefer-numeric-literals.js
index e84017490..800047051 100644
--- a/tests/lib/rules/prefer-numeric-literals.js
+++ b/tests/lib/rules/prefer-numeric-literals.js
@@ -90,6 +90,68 @@ ruleTester.run("prefer-numeric-literals", rule, {
             code: "Number.parseInt('1️⃣3️⃣3️⃣7️⃣', 16);",
             output: null, // not fixed, javascript doesn't support emoji literals
             errors: [{ message: "Use hexadecimal literals instead of Number.parseInt()." }]
+        },
+
+        // Should not autofix if it would remove comments
+        {
+            code: "/* comment */Number.parseInt('11', 2);",
+            output: "/* comment */0b11;",
+            errors: 1
+        },
+        {
+            code: "Number/**/.parseInt('11', 2);",
+            output: null,
+            errors: 1
+        },
+        {
+            code: "Number//\n.parseInt('11', 2);",
+            output: null,
+            errors: 1
+        },
+        {
+            code: "Number./**/parseInt('11', 2);",
+            output: null,
+            errors: 1
+        },
+        {
+            code: "Number.parseInt(/**/'11', 2);",
+            output: null,
+            errors: 1
+        },
+        {
+            code: "Number.parseInt('11', /**/2);",
+            output: null,
+            errors: 1
+        },
+        {
+            code: "Number.parseInt('11', 2)/* comment */;",
+            output: "0b11/* comment */;",
+            errors: 1
+        },
+        {
+            code: "parseInt/**/('11', 2);",
+            output: null,
+            errors: 1
+        },
+        {
+            code: "parseInt(//\n'11', 2);",
+            output: null,
+            errors: 1
+        },
+        {
+            code: "parseInt('11'/**/, 2);",
+            output: null,
+            errors: 1
+        },
+        {
+            code: "parseInt('11', 2 /**/);",
+            output: null,
+            errors: 1
+        },
+        {
+            code: "parseInt('11', 2)//comment\n;",
+            output: "0b11//comment\n;",
+            errors: 1
         }
     ]
 });
diff --git a/tests/lib/shared/runtime-info.js b/tests/lib/shared/runtime-info.js
new file mode 100644
index 000000000..c82d416df
--- /dev/null
+++ b/tests/lib/shared/runtime-info.js
@@ -0,0 +1,216 @@
+/**
+ * @fileoverview Tests for RuntimeInfo util.
+ * @author Kai Cataldo
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const assert = require("chai").assert;
+const sinon = require("sinon");
+const spawn = require("cross-spawn");
+const { unIndent } = require("../_utils");
+const RuntimeInfo = require("../../../lib/shared/runtime-info");
+const log = require("../../../lib/shared/logging");
+const packageJson = require("../../../package.json");
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+/**
+ * Sets up spawn.sync() stub calls to return values and throw errors in the order in which they are given.
+ * @param {Function} stub - The stub to set up.
+ * @param {Array} returnVals - Values to be returned by subsequent stub calls.
+ * @returns {Function} The set up stub.
+ */
+function setupSpawnSyncStubReturnVals(stub, returnVals) {
+    let stubChain = stub;
+
+    for (const [i, val] of returnVals.entries()) {
+        const returnVal = val instanceof Error
+            ? { error: val }
+            : { stdout: val };
+
+        stubChain = stubChain.onCall(i).returns(returnVal);
+    }
+
+    return stubChain;
+}
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const LOCAL_ESLINT_BIN_PATH = "/Users/username/code/project/node_modules/eslint/bin/eslint.js";
+const GLOBAL_ESLINT_BIN_PATH = "/usr/local/bin/npm/node_modules/eslint/bin/eslint.js";
+const NPM_BIN_PATH = "/usr/local/bin/npm";
+
+describe("RuntimeInfo", () => {
+    describe("environment()", () => {
+        let spawnSyncStub;
+        let logErrorStub;
+        let originalProcessArgv;
+        let spawnSyncStubArgs;
+
+        beforeEach(() => {
+            spawnSyncStub = sinon.stub(spawn, "sync");
+            logErrorStub = sinon.stub(log, "error");
+            originalProcessArgv = process.argv;
+            process.argv[1] = LOCAL_ESLINT_BIN_PATH;
+            spawnSyncStubArgs = [
+                "v12.8.0",
+                "6.11.3",
+                unIndent`
+                    {
+                        "name": "project",
+                        "version": "1.0.0",
+                        "dependencies": {
+                            "eslint": {
+                                "version": "6.3.0"
+                            }
+                        }
+                    }
+                `,
+                NPM_BIN_PATH,
+                unIndent`
+                    {
+                        "dependencies": {
+                            "eslint": {
+                                "version": "5.16.0",
+                                "from": "eslint",
+                                "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.16.0.tgz"
+                            }
+                        }
+                    }
+                `
+            ];
+        });
+
+        afterEach(() => {
+            spawnSyncStub.restore();
+            logErrorStub.restore();
+            process.argv = originalProcessArgv;
+        });
+
+
+        it("should return a string containing environment information when running local installation", () => {
+            setupSpawnSyncStubReturnVals(spawnSyncStub, spawnSyncStubArgs);
+
+            assert.strictEqual(
+                RuntimeInfo.environment(),
+                unIndent`
+                    Environment Info:
+
+                    Node version: v12.8.0
+                    npm version: v6.11.3
+                    Local ESLint version: v6.3.0 (Currently used)
+                    Global ESLint version: v5.16.0
+                `
+            );
+        });
+
+        it("should return a string containing environment information when running global installation", () => {
+            setupSpawnSyncStubReturnVals(spawnSyncStub, spawnSyncStubArgs);
+            process.argv[1] = GLOBAL_ESLINT_BIN_PATH;
+
+            assert.strictEqual(
+                RuntimeInfo.environment(),
+                unIndent`
+                    Environment Info:
+
+                    Node version: v12.8.0
+                    npm version: v6.11.3
+                    Local ESLint version: v6.3.0
+                    Global ESLint version: v5.16.0 (Currently used)
+                `
+            );
+        });
+
+        it("should return a string containing environment information when not installed locally", () => {
+            spawnSyncStubArgs.splice(2, 2, unIndent`
+                {
+                    "name": "project",
+                    "version": "1.0.0"
+                }
+            `);
+            spawnSyncStubArgs.push(NPM_BIN_PATH);
+            setupSpawnSyncStubReturnVals(spawnSyncStub, spawnSyncStubArgs);
+            process.argv[1] = GLOBAL_ESLINT_BIN_PATH;
+
+            assert.strictEqual(
+                RuntimeInfo.environment(),
+                unIndent`
+                    Environment Info:
+
+                    Node version: v12.8.0
+                    npm version: v6.11.3
+                    Local ESLint version: Not found
+                    Global ESLint version: v5.16.0 (Currently used)
+                `
+            );
+        });
+
+        it("should return a string containing environment information when not installed globally", () => {
+            spawnSyncStubArgs[4] = "{}";
+            setupSpawnSyncStubReturnVals(spawnSyncStub, spawnSyncStubArgs);
+
+            assert.strictEqual(
+                RuntimeInfo.environment(),
+                unIndent`
+                    Environment Info:
+
+                    Node version: v12.8.0
+                    npm version: v6.11.3
+                    Local ESLint version: v6.3.0 (Currently used)
+                    Global ESLint version: Not found
+                `
+            );
+        });
+
+        it("log and throw an error when npm version can not be found", () => {
+            const expectedErr = new Error("npm can not be found");
+
+            spawnSyncStubArgs[1] = expectedErr;
+            setupSpawnSyncStubReturnVals(spawnSyncStub, spawnSyncStubArgs);
+
+            assert.throws(RuntimeInfo.environment, expectedErr);
+            assert.strictEqual(logErrorStub.args[0][0], "Error finding npm version running the command `npm --version`");
+        });
+
+        it("log and throw an error when npm binary path can not be found", () => {
+            const expectedErr = new Error("npm can not be found");
+
+            spawnSyncStubArgs[3] = expectedErr;
+            setupSpawnSyncStubReturnVals(spawnSyncStub, spawnSyncStubArgs);
+
+            assert.throws(RuntimeInfo.environment, expectedErr);
+            assert.strictEqual(logErrorStub.args[0][0], "Error finding npm binary path when running command `npm bin -g`");
+        });
+
+        it("log and throw an error when checking for local ESLint version when returned output of command is malformed", () => {
+            spawnSyncStubArgs[2] = "This is not JSON";
+            setupSpawnSyncStubReturnVals(spawnSyncStub, spawnSyncStubArgs);
+
+            assert.throws(RuntimeInfo.environment, "Unexpected token T in JSON at position 0");
+            assert.strictEqual(logErrorStub.args[0][0], "Error finding eslint version running the command `npm ls --depth=0 --json eslint`");
+        });
+
+        it("log and throw an error when checking for global ESLint version when returned output of command is malformed", () => {
+            spawnSyncStubArgs[4] = "This is not JSON";
+            setupSpawnSyncStubReturnVals(spawnSyncStub, spawnSyncStubArgs);
+
+            assert.throws(RuntimeInfo.environment, "Unexpected token T in JSON at position 0");
+            assert.strictEqual(logErrorStub.args[0][0], "Error finding eslint version running the command `npm ls --depth=0 --json eslint -g`");
+        });
+    });
+
+    describe("version()", () => {
+        it("should return the version of the package defined in package.json", () => {
+            assert.strictEqual(RuntimeInfo.version(), `v${packageJson.version}`);
+        });
+    });
+});