Repository: eslint/eslint
Pull request #11648 (2019-04-24 04:34:19)

Build: add node 12






Patch:
diff --git a/.markdownlint.yml b/.markdownlint.yml
new file mode 100644
index 000000000..7bbf13588
--- /dev/null
+++ b/.markdownlint.yml
@@ -0,0 +1,20 @@
+default: true
+
+# Exclusions for deliberate/widespread violations
+MD001: false     # Header levels should only increment by one level at a time
+MD002: false     # First header should be a h1 header
+MD007:           # Unordered list indentation
+    indent: 4
+MD012: false     # Multiple consecutive blank lines
+MD013: false     # Line length
+MD014: false     # Dollar signs used before commands without showing output
+MD019: false     # Multiple spaces after hash on atx style header
+MD021: false     # Multiple spaces inside hashes on closed atx style header
+MD024: false     # Multiple headers with the same content
+MD026: false     # Trailing punctuation in header
+MD029: false     # Ordered list item prefix
+MD030: false     # Spaces after list markers
+MD033: false     # Allow inline HTML
+MD034: false     # Bare URL used
+MD040: false     # Fenced code blocks should have a language specified
+MD041: false     # First line in file should be a top level header
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 03cd84ead..40aa2a8a4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,7 +3,7 @@ node_js:
     - "8"
     - "10"
     - "11"
-
+    - "12"
 branches:
     only:
     - master
diff --git a/Makefile.js b/Makefile.js
index 08e102564..2aea2619d 100644
--- a/Makefile.js
+++ b/Makefile.js
@@ -385,29 +385,7 @@ function getFirstVersionOfDeletion(filePath) {
  * @private
  */
 function lintMarkdown(files) {
-    const config = {
-            default: true,
-
-            // Exclusions for deliberate/widespread violations
-            MD001: false, // Header levels should only increment by one level at a time
-            MD002: false, // First header should be a h1 header
-            MD007: { // Unordered list indentation
-                indent: 4
-            },
-            MD012: false, // Multiple consecutive blank lines
-            MD013: false, // Line length
-            MD014: false, // Dollar signs used before commands without showing output
-            MD019: false, // Multiple spaces after hash on atx style header
-            MD021: false, // Multiple spaces inside hashes on closed atx style header
-            MD024: false, // Multiple headers with the same content
-            MD026: false, // Trailing punctuation in header
-            MD029: false, // Ordered list item prefix
-            MD030: false, // Spaces after list markers
-            MD033: false, // Allow inline HTML
-            MD034: false, // Bare URL used
-            MD040: false, // Fenced code blocks should have a language specified
-            MD041: false // First line in file should be a top level header
-        },
+    const config = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "./.markdownlint.yml"), "utf8")),
         result = markdownlint.sync({
             files,
             config,
diff --git a/README.md b/README.md
index ab6f2aa52..27a6592a4 100644
--- a/README.md
+++ b/README.md
@@ -245,7 +245,7 @@ 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.gitkraken.com/"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Flogo.clearbit.com%2Fgitkraken.com&height=96" alt="GitKraken" height="96"></a> <a href="https://code.facebook.com/projects/"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Fres.cloudinary.com%2Fopencollective%2Fimage%2Fupload%2Fv1508519428%2FS9gk78AS_400x400_fulq2l.jpg&height=96" alt="Facebook Open Source" height="96"></a> <a href="https://badoo.com/team?utm_source=eslint"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Fopencollective-production.s3-us-west-1.amazonaws.com%2Fbbdb9cc0-3b5d-11e9-9537-ad85092287b8.png&height=96" alt="Badoo" height="96"></a> <a href="http://salesforce.com"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Flogo.clearbit.com%2Fsalesforce.com&height=96" alt="Salesforce" height="96"></a></p><h3>Silver Sponsors</h3>
+<p><a href="https://www.gitkraken.com/"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Flogo.clearbit.com%2Fgitkraken.com&height=96" alt="GitKraken" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Fopencollective-production.s3-us-west-1.amazonaws.com%2F098e3bd0-4d57-11e8-9324-0f6cc1f92bf1.png&height=96" alt="Airbnb" height="96"></a> <a href="https://code.facebook.com/projects/"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Fres.cloudinary.com%2Fopencollective%2Fimage%2Fupload%2Fv1508519428%2FS9gk78AS_400x400_fulq2l.jpg&height=96" alt="Facebook Open Source" height="96"></a> <a href="https://badoo.com/team?utm_source=eslint"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Fopencollective-production.s3-us-west-1.amazonaws.com%2Fbbdb9cc0-3b5d-11e9-9537-ad85092287b8.png&height=96" alt="Badoo" height="96"></a> <a href="http://salesforce.com"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Flogo.clearbit.com%2Fsalesforce.com&height=96" alt="Salesforce" height="96"></a></p><h3>Silver Sponsors</h3>
 <p><a href="https://www.ampproject.org/"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Fopencollective-production.s3-us-west-1.amazonaws.com%2F68ed8b70-ebf2-11e6-9958-cb7e79408c56.png&height=96" alt="AMP Project" height="64"></a></p><h3>Bronze Sponsors</h3>
 <p><a href="http://faithlife.com/ref/about"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Flogo.clearbit.com%2Ffaithlife.com&height=96" alt="Faithlife" height="32"></a> <a href="https://jsheroes.io/"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Flogo.clearbit.com%2Fjsheroes.io&height=96" alt="JSHeroes " height="32"></a> <a href="http://www.firesticktricks.com"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Flogo.clearbit.com%2Ffiresticktricks.com&height=96" alt="Fire Stick Tricks" height="32"></a> <a href="https://www.marfeel.com/"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Fopencollective-production.s3-us-west-1.amazonaws.com%2F431b60c0-6062-11e9-b486-e1bb2d3f0d94.png&height=96" alt="Marfeel" height="32"></a> <a href="https://usave.co.uk/utilities/broadband/"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Flogo.clearbit.com%2Fusave.co.uk&height=96" alt="Usave" height="32"></a> <a href="https://tekhattan.com"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Fopencollective-production.s3-us-west-1.amazonaws.com%2F31835450-5a98-11e9-adb1-e17fccd4c65e.png&height=96" alt="TekHattan" height="32"></a></p>
 <!--sponsorsend-->
diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md
index d189d53ad..c49863501 100644
--- a/docs/developer-guide/working-with-rules.md
+++ b/docs/developer-guide/working-with-rules.md
@@ -169,6 +169,13 @@ This method returns the scope which has the following types:
 **※2** Only if the `for` statement defines the iteration variable as a block-scoped variable (E.g., `for (let i = 0;;) {}`).<br>
 **※3** The scope of the closest ancestor node which has own scope. If the closest ancestor node has multiple scopes then it chooses the innermost scope (E.g., the `Program` node has a `global` scope and a `module` scope if `Program#sourceType` is `"module"`. The innermost scope is the `module` scope.).
 
+The returned value is a [`Scope` object](scope-manager-interface.md) defined by the `eslint-scope` package. The `Variable` objects of global variables have some additional properties.
+
+* `variable.writeable` (`boolean | undefined`) ... If `true`, this global variable can be assigned arbitrary value. If `false`, this global variable is read-only.
+* `variable.eslintExplicitGlobal` (`boolean | undefined`) ... If `true`, this global variable was defined by a `/* globals */` directive comment in the source code file.
+* `variable.eslintExplicitGlobalComments` (`Comment[] | undefined`) ... The array of `/* globals */` directive comments which defined this global variable in the source code file. This property is `undefined` if there are no `/* globals */` directive comments.
+* `variable.eslintImplicitGlobalSetting` (`"readonly" | "writable" | undefined`) ... The configured value in config files. This can be different from `variable.writeable` if there are `/* globals */` directive comments.
+
 ### context.report()
 
 The main method you'll use is `context.report()`, which publishes a warning or error (depending on the configuration being used). This method accepts a single argument, which is an object containing the following properties:
diff --git a/docs/rules/no-redeclare.md b/docs/rules/no-redeclare.md
index d0d71e5de..c3b8422cf 100644
--- a/docs/rules/no-redeclare.md
+++ b/docs/rules/no-redeclare.md
@@ -27,7 +27,7 @@ a = 10;
 
 ## Options
 
-This rule takes one optional argument, an object with a boolean property `"builtinGlobals"`. It defaults to `false`.
+This rule takes one optional argument, an object with a boolean property `"builtinGlobals"`. It defaults to `true`.
 If set to `true`, this rule also checks redeclaration of built-in globals, such as `Object`, `Array`, `Number`...
 
 ### builtinGlobals
diff --git a/lib/config/config-ops.js b/lib/config/config-ops.js
index 48a8302d9..beca8ca2c 100644
--- a/lib/config/config-ops.js
+++ b/lib/config/config-ops.js
@@ -385,14 +385,14 @@ module.exports = {
             case "true":
             case "writeable":
             case "writable":
-                return "writeable";
+                return "writable";
 
             case null:
             case false:
             case "false":
             case "readable":
             case "readonly":
-                return "readable";
+                return "readonly";
 
             default:
                 throw new Error(`'${configuredValue}' is not a valid configuration for a global (use 'readonly', 'writable', or 'off')`);
diff --git a/lib/linter.js b/lib/linter.js
index bd3d8b235..f2297529b 100644
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -70,39 +70,41 @@ const commentParser = new ConfigCommentParser();
  * @param {{exportedVariables: Object, enabledGlobals: Object}} commentDirectives Directives from comment configuration
  * @returns {void}
  */
-function addDeclaredGlobals(globalScope, configGlobals, commentDirectives) {
-    const mergedGlobalsInfo = Object.assign(
-        {},
+function addDeclaredGlobals(globalScope, configGlobals, { exportedVariables, enabledGlobals }) {
+
+    // Define configured global variables.
+    for (const id of new Set([...Object.keys(configGlobals), ...Object.keys(enabledGlobals)])) {
 
         /*
          * `ConfigOps.normalizeConfigGlobal` will throw an error if a configured global value is invalid. However, these errors would
          * typically be caught when validating a config anyway (validity for inline global comments is checked separately).
          */
-        lodash.mapValues(configGlobals, value => ({ sourceComment: null, value: ConfigOps.normalizeConfigGlobal(value) })),
-        lodash.mapValues(commentDirectives.enabledGlobals, ({ comment, value }) => ({ sourceComment: comment, value }))
-    );
+        const configValue = configGlobals[id] === void 0 ? void 0 : ConfigOps.normalizeConfigGlobal(configGlobals[id]);
+        const commentValue = enabledGlobals[id] && enabledGlobals[id].value;
+        const value = commentValue || configValue;
+        const sourceComments = enabledGlobals[id] && enabledGlobals[id].comments;
 
-    Object.keys(mergedGlobalsInfo)
-        .filter(name => mergedGlobalsInfo[name].value !== "off")
-        .forEach(name => {
-            let variable = globalScope.set.get(name);
-
-            if (!variable) {
-                variable = new eslintScope.Variable(name, globalScope);
-                if (mergedGlobalsInfo[name].sourceComment === null) {
-                    variable.eslintExplicitGlobal = false;
-                } else {
-                    variable.eslintExplicitGlobal = true;
-                    variable.eslintExplicitGlobalComment = mergedGlobalsInfo[name].sourceComment;
-                }
-                globalScope.variables.push(variable);
-                globalScope.set.set(name, variable);
-            }
-            variable.writeable = (mergedGlobalsInfo[name].value === "writeable");
-        });
+        if (value === "off") {
+            continue;
+        }
+
+        let variable = globalScope.set.get(id);
+
+        if (!variable) {
+            variable = new eslintScope.Variable(id, globalScope);
+
+            globalScope.variables.push(variable);
+            globalScope.set.set(id, variable);
+        }
+
+        variable.eslintImplicitGlobalSetting = configValue;
+        variable.eslintExplicitGlobal = sourceComments !== void 0;
+        variable.eslintExplicitGlobalComments = sourceComments;
+        variable.writeable = (value === "writable");
+    }
 
     // mark all exported variables as such
-    Object.keys(commentDirectives.exportedVariables).forEach(name => {
+    Object.keys(exportedVariables).forEach(name => {
         const variable = globalScope.set.get(name);
 
         if (variable) {
@@ -157,7 +159,7 @@ function createDisableDirectives(type, loc, value) {
  * @param {string} filename The file being checked.
  * @param {ASTNode} ast The top node of the AST.
  * @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules
- * @returns {{configuredRules: Object, enabledGlobals: Object, exportedVariables: Object, problems: Problem[], disableDirectives: DisableDirective[]}}
+ * @returns {{configuredRules: Object, enabledGlobals: {value:string,comment:Token}[], exportedVariables: Object, problems: Problem[], disableDirectives: DisableDirective[]}}
  * A collection of the directive comments that were found, along with any problems that occurred when parsing
  */
 function getDirectiveComments(filename, ast, ruleMapper) {
@@ -201,11 +203,8 @@ function getDirectiveComments(filename, ast, ruleMapper) {
                     break;
 
                 case "globals":
-                case "global": {
-                    const updatedGlobals = commentParser.parseStringConfig(directiveValue, comment);
-
-                    Object.keys(updatedGlobals).forEach(globalName => {
-                        const { value } = updatedGlobals[globalName];
+                case "global":
+                    for (const [id, { value }] of Object.entries(commentParser.parseStringConfig(directiveValue, comment))) {
                         let normalizedValue;
 
                         try {
@@ -221,13 +220,21 @@ function getDirectiveComments(filename, ast, ruleMapper) {
                                 endColumn: comment.loc.end.column + 1,
                                 nodeType: null
                             });
-                            return;
+                            continue;
                         }
 
-                        enabledGlobals[globalName] = { comment, value: normalizedValue };
-                    });
+                        if (enabledGlobals[id]) {
+                            enabledGlobals[id].comments.push(comment);
+                            enabledGlobals[id].value = normalizedValue;
+                        } else {
+                            enabledGlobals[id] = {
+                                comments: [comment],
+                                value: normalizedValue
+                            };
+                        }
+                    }
                     break;
-                }
+
                 case "eslint-disable":
                     disableDirectives.push(...createDisableDirectives("disable", comment.loc.start, directiveValue));
                     break;
diff --git a/lib/rules/no-redeclare.js b/lib/rules/no-redeclare.js
index 4d689cc61..6adde0f97 100644
--- a/lib/rules/no-redeclare.js
+++ b/lib/rules/no-redeclare.js
@@ -5,6 +5,12 @@
 
 "use strict";
 
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const astUtils = require("../util/ast-utils");
+
 //------------------------------------------------------------------------------
 // Rule Definition
 //------------------------------------------------------------------------------
@@ -20,11 +26,17 @@ module.exports = {
             url: "https://eslint.org/docs/rules/no-redeclare"
         },
 
+        messages: {
+            redeclared: "'{{id}}' is already defined.",
+            redeclaredAsBuiltin: "'{{id}}' is already defined as a built-in global variable.",
+            redeclaredBySyntax: "'{{id}}' is already defined by a variable declaration."
+        },
+
         schema: [
             {
                 type: "object",
                 properties: {
-                    builtinGlobals: { type: "boolean", default: false }
+                    builtinGlobals: { type: "boolean", default: true }
                 },
                 additionalProperties: false
             }
@@ -33,72 +45,128 @@ module.exports = {
 
     create(context) {
         const options = {
-            builtinGlobals: context.options[0] && context.options[0].builtinGlobals
+            builtinGlobals: Boolean(
+                context.options.length === 0 ||
+                context.options[0].builtinGlobals
+            )
         };
+        const sourceCode = context.getSourceCode();
 
         /**
-         * Find variables in a given scope and flag redeclared ones.
-         * @param {Scope} scope - An eslint-scope scope object.
-         * @returns {void}
-         * @private
+         * Iterate declarations of a given variable.
+         * @param {escope.variable} variable The variable object to iterate declarations.
+         * @returns {IterableIterator<{type:string,node:ASTNode,loc:SourceLocation}>} The declarations.
          */
-        function findVariablesInScope(scope) {
-            scope.variables.forEach(variable => {
-                const hasBuiltin = options.builtinGlobals && "writeable" in variable;
-                const count = (hasBuiltin ? 1 : 0) + variable.identifiers.length;
+        function *iterateDeclarations(variable) {
+            if (options.builtinGlobals && (
+                variable.eslintImplicitGlobalSetting === "readonly" ||
+                variable.eslintImplicitGlobalSetting === "writable"
+            )) {
+                yield { type: "builtin" };
+            }
 
-                if (count >= 2) {
-                    variable.identifiers.sort((a, b) => a.range[1] - b.range[1]);
+            for (const id of variable.identifiers) {
+                yield { type: "syntax", node: id, loc: id.loc };
+            }
 
-                    for (let i = (hasBuiltin ? 0 : 1), l = variable.identifiers.length; i < l; i++) {
-                        context.report({ node: variable.identifiers[i], message: "'{{a}}' is already defined.", data: { a: variable.name } });
-                    }
+            if (variable.eslintExplicitGlobalComments) {
+                for (const comment of variable.eslintExplicitGlobalComments) {
+                    yield {
+                        type: "comment",
+                        node: comment,
+                        loc: astUtils.getNameLocationInGlobalDirectiveComment(
+                            sourceCode,
+                            comment,
+                            variable.name
+                        )
+                    };
                 }
-            });
-
+            }
         }
 
         /**
-         * Find variables in the current scope.
-         * @param {ASTNode} node - The Program node.
+         * Find variables in a given scope and flag redeclared ones.
+         * @param {Scope} scope - An eslint-scope scope object.
          * @returns {void}
          * @private
          */
-        function checkForGlobal(node) {
-            const scope = context.getScope(),
-                parserOptions = context.parserOptions,
-                ecmaFeatures = parserOptions.ecmaFeatures || {};
-
-            // Nodejs env or modules has a special scope.
-            if (ecmaFeatures.globalReturn || node.sourceType === "module") {
-                findVariablesInScope(scope.childScopes[0]);
-            } else {
-                findVariablesInScope(scope);
+        function findVariablesInScope(scope) {
+            for (const variable of scope.variables) {
+                const [
+                    declaration,
+                    ...extraDeclarations
+                ] = iterateDeclarations(variable);
+
+                if (extraDeclarations.length === 0) {
+                    continue;
+                }
+
+                /*
+                 * If the type of a declaration is different from the type of
+                 * the first declaration, it shows the location of the first
+                 * declaration.
+                 */
+                const detailMessageId = declaration.type === "builtin"
+                    ? "redeclaredAsBuiltin"
+                    : "redeclaredBySyntax";
+                const data = { id: variable.name };
+
+                // Report extra declarations.
+                for (const { type, node, loc } of extraDeclarations) {
+                    const messageId = type === declaration.type
+                        ? "redeclared"
+                        : detailMessageId;
+
+                    context.report({ node, loc, messageId, data });
+                }
             }
         }
 
         /**
          * Find variables in the current scope.
+         * @param {ASTNode} node The node of the current scope.
          * @returns {void}
          * @private
          */
-        function checkForBlock() {
-            findVariablesInScope(context.getScope());
-        }
+        function checkForBlock(node) {
+            const scope = context.getScope();
 
-        if (context.parserOptions.ecmaVersion >= 6) {
-            return {
-                Program: checkForGlobal,
-                BlockStatement: checkForBlock,
-                SwitchStatement: checkForBlock
-            };
+            /*
+             * In ES5, some node type such as `BlockStatement` doesn't have that scope.
+             * `scope.block` is a different node in such a case.
+             */
+            if (scope.block === node) {
+                findVariablesInScope(scope);
+            }
         }
+
         return {
-            Program: checkForGlobal,
+            Program() {
+                const scope = context.getScope();
+
+                findVariablesInScope(scope);
+
+                // Node.js or ES modules has a special scope.
+                if (
+                    scope.type === "global" &&
+                    scope.childScopes[0] &&
+
+                    // The special scope's block is the Program node.
+                    scope.block === scope.childScopes[0].block
+                ) {
+                    findVariablesInScope(scope.childScopes[0]);
+                }
+            },
+
             FunctionDeclaration: checkForBlock,
             FunctionExpression: checkForBlock,
-            ArrowFunctionExpression: checkForBlock
-        };
+            ArrowFunctionExpression: checkForBlock,
 
+            BlockStatement: checkForBlock,
+            ForStatement: checkForBlock,
+            ForInStatement: checkForBlock,
+            ForOfStatement: checkForBlock,
+            SwitchStatement: checkForBlock
+        };
     }
 };
diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js
index 1482a0238..8e32951bd 100644
--- a/lib/rules/no-unused-vars.js
+++ b/lib/rules/no-unused-vars.js
@@ -9,7 +9,6 @@
 // Requirements
 //------------------------------------------------------------------------------
 
-const lodash = require("lodash");
 const astUtils = require("../util/ast-utils");
 
 //------------------------------------------------------------------------------
@@ -588,39 +587,6 @@ module.exports = {
             return unusedVars;
         }
 
-        /**
-         * Gets the index of a given variable name in a given comment.
-         * @param {eslint-scope.Variable} variable - A variable to get.
-         * @param {ASTNode} comment - A comment node which includes the variable name.
-         * @returns {number} The index of the variable name's location.
-         * @private
-         */
-        function getColumnInComment(variable, comment) {
-            const namePattern = new RegExp(`[\\s,]${lodash.escapeRegExp(variable.name)}(?:$|[\\s,:])`, "gu");
-
-            // To ignore the first text "global".
-            namePattern.lastIndex = comment.value.indexOf("global") + 6;
-
-            // Search a given variable name.
-            const match = namePattern.exec(comment.value);
-
-            return match ? match.index + 1 : 0;
-        }
-
-        /**
-         * Creates the correct location of a given variables.
-         * The location is at its name string in a `/*global` comment.
-         *
-         * @param {eslint-scope.Variable} variable - A variable to get its location.
-         * @returns {{line: number, column: number}} The location object for the variable.
-         * @private
-         */
-        function getLocation(variable) {
-            const comment = variable.eslintExplicitGlobalComment;
-
-            return sourceCode.getLocFromIndex(comment.range[0] + 2 + getColumnInComment(variable, comment));
-        }
-
         //--------------------------------------------------------------------------
         // Public
         //--------------------------------------------------------------------------
@@ -632,14 +598,8 @@ module.exports = {
                 for (let i = 0, l = unusedVars.length; i < l; ++i) {
                     const unusedVar = unusedVars[i];
 
-                    if (unusedVar.eslintExplicitGlobal) {
-                        context.report({
-                            node: programNode,
-                            loc: getLocation(unusedVar),
-                            message: getDefinedMessage(unusedVar),
-                            data: unusedVar
-                        });
-                    } else if (unusedVar.defs.length > 0) {
+                    // Report the first declaration.
+                    if (unusedVar.defs.length > 0) {
                         context.report({
                             node: unusedVar.identifiers[0],
                             message: unusedVar.references.some(ref => ref.isWrite())
@@ -647,6 +607,17 @@ module.exports = {
                                 : getDefinedMessage(unusedVar),
                             data: unusedVar
                         });
+
+                    // If there are no regular declaration, report the first `/*globals*/` comment directive.
+                    } else if (unusedVar.eslintExplicitGlobalComments) {
+                        const directiveComment = unusedVar.eslintExplicitGlobalComments[0];
+
+                        context.report({
+                            node: programNode,
+                            loc: astUtils.getNameLocationInGlobalDirectiveComment(sourceCode, directiveComment, unusedVar.name),
+                            message: getDefinedMessage(unusedVar),
+                            data: unusedVar
+                        });
                     }
                 }
             }
diff --git a/lib/util/ast-utils.js b/lib/util/ast-utils.js
index 85205a75d..0f9ba290a 100644
--- a/lib/util/ast-utils.js
+++ b/lib/util/ast-utils.js
@@ -11,6 +11,7 @@
 
 const esutils = require("esutils");
 const espree = require("espree");
+const lodash = require("lodash");
 
 //------------------------------------------------------------------------------
 // Helpers
@@ -1342,5 +1343,29 @@ module.exports = {
         }
 
         return false;
+    },
+
+    /**
+     * Get the `loc` object of a given name in a `/*globals` directive comment.
+     * @param {SourceCode} sourceCode The source code to convert index to loc.
+     * @param {Comment} comment The `/*globals` directive comment which include the name.
+     * @param {string} name The name to find.
+     * @returns {SourceLocation} The `loc` object.
+     */
+    getNameLocationInGlobalDirectiveComment(sourceCode, comment, name) {
+        const namePattern = new RegExp(`[\\s,]${lodash.escapeRegExp(name)}(?:$|[\\s,:])`, "gu");
+
+        // To ignore the first text "global".
+        namePattern.lastIndex = comment.value.indexOf("global") + 6;
+
+        // Search a given variable name.
+        const match = namePattern.exec(comment.value);
+
+        // Convert the index to loc.
+        return sourceCode.getLocFromIndex(
+            comment.range[0] +
+            "/*".length +
+            (match ? match.index + 1 : 0)
+        );
     }
 };
diff --git a/package.json b/package.json
index d2ea8a004..c99b1559f 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,16 @@
     "profile": "beefy tests/bench/bench.js --open -- -t brfs -t ./tests/bench/xform-rules.js -r espree",
     "coveralls": "cat ./coverage/lcov.info | coveralls"
   },
+  "gitHooks": {
+    "pre-commit": "lint-staged"
+  },
+  "lint-staged": {
+    "*.js": [
+      "node ./bin/eslint.js --fix",
+      "git add"
+    ],
+    "*.md": "markdownlint"
+  },
   "files": [
     "LICENSE",
     "README.md",
@@ -75,6 +85,7 @@
     "@babel/core": "^7.4.3",
     "@babel/polyfill": "^7.4.3",
     "@babel/preset-env": "^7.4.3",
+    "acorn": "^6.1.1",
     "babel-loader": "^8.0.5",
     "beefy": "^2.1.8",
     "brfs": "^2.0.2",
@@ -98,6 +109,7 @@
     "karma-mocha-reporter": "^2.2.3",
     "karma-webpack": "^4.0.0-rc.6",
     "leche": "^2.2.3",
+    "lint-staged": "^8.1.5",
     "load-perf": "^0.2.0",
     "markdownlint": "^0.13.0",
     "mocha": "^6.1.2",
@@ -111,7 +123,8 @@
     "temp": "^0.9.0",
     "through": "^2.3.8",
     "webpack": "^4.29.6",
-    "webpack-cli": "^3.3.0"
+    "webpack-cli": "^3.3.0",
+    "yorkie": "^2.0.0"
   },
   "keywords": [
     "ast",


Test patch:
diff --git a/tests/lib/config/config-initializer.js b/tests/lib/config/config-initializer.js
index d306db1b1..377633ec6 100644
--- a/tests/lib/config/config-initializer.js
+++ b/tests/lib/config/config-initializer.js
@@ -70,7 +70,7 @@ describe("configInitializer", () => {
 
     // copy into clean area so as not to get "infected" by this project's .eslintrc files
     before(() => {
-        fixtureDir = `${os.tmpdir()}/eslint/fixtures/config-initializer`;
+        fixtureDir = path.join(os.tmpdir(), "eslint/fixtures/config-initializer");
         sh.mkdir("-p", fixtureDir);
         sh.cp("-r", "./tests/fixtures/config-initializer/.", fixtureDir);
         fixtureDir = fs.realpathSync(fixtureDir);
diff --git a/tests/lib/config/config-ops.js b/tests/lib/config/config-ops.js
index ffa65660d..6a1f0b219 100644
--- a/tests/lib/config/config-ops.js
+++ b/tests/lib/config/config-ops.js
@@ -902,16 +902,16 @@ describe("ConfigOps", () => {
     describe("normalizeConfigGlobal", () => {
         [
             ["off", "off"],
-            [true, "writeable"],
-            ["true", "writeable"],
-            [false, "readable"],
-            ["false", "readable"],
-            [null, "readable"],
-            ["writeable", "writeable"],
-            ["writable", "writeable"],
-            ["readable", "readable"],
-            ["readonly", "readable"],
-            ["writable", "writeable"]
+            [true, "writable"],
+            ["true", "writable"],
+            [false, "readonly"],
+            ["false", "readonly"],
+            [null, "readonly"],
+            ["writeable", "writable"],
+            ["writable", "writable"],
+            ["readable", "readonly"],
+            ["readonly", "readonly"],
+            ["writable", "writable"]
         ].forEach(([input, output]) => {
             it(util.inspect(input), () => {
                 assert.strictEqual(ConfigOps.normalizeConfigGlobal(input), output);
diff --git a/tests/lib/linter.js b/tests/lib/linter.js
index bf8812c7b..3df167111 100644
--- a/tests/lib/linter.js
+++ b/tests/lib/linter.js
@@ -3469,18 +3469,18 @@ describe("Linter", () => {
 
                             const foo = getVariable(scope, "foo");
 
-                            assert.strictEqual(true, foo.eslintExplicitGlobal);
-                            assert.strictEqual(comments[0], foo.eslintExplicitGlobalComment);
+                            assert.strictEqual(foo.eslintExplicitGlobal, true);
+                            assert.strictEqual(foo.eslintExplicitGlobalComments[0], comments[0]);
 
                             const bar = getVariable(scope, "bar");
 
-                            assert.strictEqual(true, bar.eslintExplicitGlobal);
-                            assert.strictEqual(comments[1], bar.eslintExplicitGlobalComment);
+                            assert.strictEqual(bar.eslintExplicitGlobal, true);
+                            assert.strictEqual(bar.eslintExplicitGlobalComments[0], comments[1]);
 
                             const baz = getVariable(scope, "baz");
 
-                            assert.strictEqual(true, baz.eslintExplicitGlobal);
-                            assert.strictEqual(comments[1], baz.eslintExplicitGlobalComment);
+                            assert.strictEqual(baz.eslintExplicitGlobal, true);
+                            assert.strictEqual(baz.eslintExplicitGlobalComments[0], comments[1]);
 
                             ok = true;
                         }
diff --git a/tests/lib/rules/no-redeclare.js b/tests/lib/rules/no-redeclare.js
index 23a45f021..15d8275d0 100644
--- a/tests/lib/rules/no-redeclare.js
+++ b/tests/lib/rules/no-redeclare.js
@@ -9,13 +9,15 @@
 // Requirements
 //------------------------------------------------------------------------------
 
-const rule = require("../../../lib/rules/no-redeclare"),
-    RuleTester = require("../../../lib/testers/rule-tester");
+const path = require("path");
+const rule = require("../../../lib/rules/no-redeclare");
+const RuleTester = require("../../../lib/testers/rule-tester");
 
 //------------------------------------------------------------------------------
 // Tests
 //------------------------------------------------------------------------------
 
+const looseParserPath = path.resolve(__dirname, "../../tools/loose-parser.js");
 const ruleTester = new RuleTester();
 
 ruleTester.run("no-redeclare", rule, {
@@ -28,11 +30,9 @@ ruleTester.run("no-redeclare", rule, {
                 ecmaVersion: 6
             }
         },
-        "var Object = 0;",
         { code: "var Object = 0;", options: [{ builtinGlobals: false }] },
         { code: "var Object = 0;", options: [{ builtinGlobals: true }], parserOptions: { sourceType: "module" } },
         { code: "var Object = 0;", options: [{ builtinGlobals: true }], parserOptions: { ecmaFeatures: { globalReturn: true } } },
-        { code: "var top = 0;", env: { browser: true } },
         { code: "var top = 0;", options: [{ builtinGlobals: true }] },
         { code: "var top = 0;", options: [{ builtinGlobals: true }], parserOptions: { ecmaFeatures: { globalReturn: true } }, env: { browser: true } },
         { code: "var top = 0;", options: [{ builtinGlobals: true }], parserOptions: { sourceType: "module" }, env: { browser: true } },
@@ -40,6 +40,32 @@ ruleTester.run("no-redeclare", rule, {
             code: "var self = 1",
             options: [{ builtinGlobals: true }],
             env: { browser: false }
+        },
+
+        // Comments and built-ins.
+        {
+            code: "/*globals Array */",
+            options: [{ builtinGlobals: false }]
+        },
+        {
+            code: "/*globals a */",
+            options: [{ builtinGlobals: false }],
+            globals: { a: "readonly" }
+        },
+        {
+            code: "/*globals a */",
+            options: [{ builtinGlobals: false }],
+            globals: { a: "writable" }
+        },
+        {
+            code: "/*globals a:off */",
+            options: [{ builtinGlobals: true }],
+            globals: { a: "readonly" }
+        },
+        {
+            code: "/*globals a */",
+            options: [{ builtinGlobals: true }],
+            globals: { a: "off" }
         }
     ],
     invalid: [
@@ -57,13 +83,13 @@ ruleTester.run("no-redeclare", rule, {
         {
             code: "var Object = 0;",
             options: [{ builtinGlobals: true }],
-            errors: [{ message: "'Object' is already defined.", type: "Identifier" }]
+            errors: [{ message: "'Object' is already defined as a built-in global variable.", type: "Identifier" }]
         },
         {
             code: "var top = 0;",
             options: [{ builtinGlobals: true }],
             env: { browser: true },
-            errors: [{ message: "'top' is already defined.", type: "Identifier" }]
+            errors: [{ message: "'top' is already defined as a built-in global variable.", type: "Identifier" }]
         },
         {
             code: "var a; var {a = 0, b: Object = 0} = {};",
@@ -71,7 +97,7 @@ ruleTester.run("no-redeclare", rule, {
             parserOptions: { ecmaVersion: 6 },
             errors: [
                 { message: "'a' is already defined.", type: "Identifier" },
-                { message: "'Object' is already defined.", type: "Identifier" }
+                { message: "'Object' is already defined as a built-in global variable.", type: "Identifier" }
             ]
         },
         {
@@ -98,13 +124,203 @@ ruleTester.run("no-redeclare", rule, {
                 { message: "'a' is already defined.", type: "Identifier" }
             ]
         },
-
-        // Notifications of readonly are moved from no-undef: https://github.com/eslint/eslint/issues/4504
         {
             code: "/*global b:false*/ var b = 1;",
             options: [{ builtinGlobals: true }],
             errors: [
-                { message: "'b' is already defined.", type: "Identifier" }
+                { message: "'b' is already defined by a variable declaration.", type: "Block" }
+            ]
+        },
+        {
+            code: "/*global b:true*/ var b = 1;",
+            options: [{ builtinGlobals: true }],
+            errors: [
+                { message: "'b' is already defined by a variable declaration.", type: "Block" }
+            ]
+        },
+        {
+            code: "function f() { var a; var a; }",
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "function f(a) { var a; }",
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "function f() { var a; if (test) { var a; } }",
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "for (var a, a;;);",
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+
+        {
+            code: "var Object = 0;",
+            errors: [
+                { message: "'Object' is already defined as a built-in global variable.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "var top = 0;",
+            env: { browser: true },
+            errors: [
+                { message: "'top' is already defined as a built-in global variable.", type: "Identifier" }
+            ]
+        },
+
+        // let/const
+        {
+            code: "let a; let a;",
+            parser: looseParserPath,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "let a; let a;",
+            parser: looseParserPath,
+            parserOptions: { ecmaVersion: 2015, sourceType: "module" },
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "let a; let a;",
+            parser: looseParserPath,
+            parserOptions: { ecmaVersion: 2015, ecmaFeatures: { globalReturn: true } },
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "let a; const a = 0;",
+            parser: looseParserPath,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "const a = 0; const a = 0;",
+            parser: looseParserPath,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "if (test) { let a; let a; }",
+            parser: looseParserPath,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "switch (test) { case 0: let a; let a; }",
+            parser: looseParserPath,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "for (let a, a;;);",
+            parser: looseParserPath,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "for (let [a, a] in xs);",
+            parser: looseParserPath,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "for (let [a, a] of xs);",
+            parser: looseParserPath,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "function f() { let a; let a; }",
+            parser: looseParserPath,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "function f(a) { let a; }",
+            parser: looseParserPath,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "function f() { if (test) { let a; let a; } }",
+            parser: looseParserPath,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [
+                { message: "'a' is already defined.", type: "Identifier" }
+            ]
+        },
+
+        // Comments and built-ins.
+        {
+            code: "/*globals Array */",
+            options: [{ builtinGlobals: true }],
+            errors: [
+                { message: "'Array' is already defined as a built-in global variable.", type: "Block" }
+            ]
+        },
+        {
+            code: "/*globals a */",
+            options: [{ builtinGlobals: true }],
+            globals: { a: "readonly" },
+            errors: [
+                { message: "'a' is already defined as a built-in global variable.", type: "Block" }
+            ]
+        },
+        {
+            code: "/*globals a */",
+            options: [{ builtinGlobals: true }],
+            globals: { a: "writable" },
+            errors: [
+                { message: "'a' is already defined as a built-in global variable.", type: "Block" }
+            ]
+        },
+        {
+            code: "/*globals a */ /*globals a */",
+            errors: [
+                { message: "'a' is already defined.", type: "Block", column: 26 }
+            ]
+        },
+        {
+            code: "/*globals a */ /*globals a */ var a = 0",
+            options: [{ builtinGlobals: true }],
+            globals: { a: "writable" },
+            errors: [
+                { message: "'a' is already defined as a built-in global variable.", type: "Block", column: 11 },
+                { message: "'a' is already defined as a built-in global variable.", type: "Block", column: 26 },
+                { message: "'a' is already defined as a built-in global variable.", type: "Identifier", column: 35 }
             ]
         }
     ]
diff --git a/tests/tools/loose-parser.js b/tests/tools/loose-parser.js
new file mode 100644
index 000000000..abb3aeb5b
--- /dev/null
+++ b/tests/tools/loose-parser.js
@@ -0,0 +1,31 @@
+/**
+ * @fileoverview Define a custom parser to ignore recoverable syntax errors.
+ * @author Toru Nagashima <https://github.com/mysticatea>
+ *
+ * no-redeclare rule uses this parser to check redeclarations.
+ */
+"use strict";
+
+const acorn = require("acorn");
+const espree = require("espree/lib/espree");
+
+// eslint-disable-next-line valid-jsdoc
+/**
+ * Define the parser which ignores recoverable errors.
+ * @returns {(parser:acorn.Parser) => acorn.Parser} The function that defines loose parser.
+ */
+function loose() {
+    return Parser => class LooseParser extends Parser {
+        raiseRecoverable() { // eslint-disable-line class-methods-use-this
+            // ignore
+        }
+    };
+}
+
+const LooseEspree = acorn.Parser.extend(espree(), loose());
+
+module.exports = {
+    parse(code, options) {
+        return new LooseEspree(options, code).parse();
+    }
+};