Repository: eslint/eslint
Pull request #8998 (2017-07-25 06:37:40)

Fix: avoid reporting the entire AST for missing rules
<!--
    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] Bug fix

**Tell us about your environment**

* **ESLint Version:** master
* **Node Version:** 8.2.1
* **npm Version:** 5.3.0

**What parser (default, Babel-ESLint, etc.) are you using?**

default

**Please show your full configuration:**

```yml
rules:
  nonexistent-rule: error
```

**What did you do? Please include the actual source code causing the issue.**

```js
foo;
bar;
baz;
qux;
```

**What did you expect to happen?**

I expected an error to be reported at the top of the file due to the nonexistent rule.

**What actually happened? Please include the actual, raw output from ESLint.**

An error was reported, spanning the entire file.

<details>
<summary>Output of JSON formatter</summary>

```json
[{"filePath":"<text>","messages":[{"ruleId":"nonexistent-rule","severity":2,"message":"Definition for rule 'nonexistent-rule' was not found","line":1,"column":1,"nodeType":"Program","source":"foo;","endLine":4,"endColumn":5}],"errorCount":1,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"foo;\nbar;\nbaz;\nqux;"}]
```
</details>

This was an unintended side-effect of b0c63f0a928cceff7cf6f060262dd7ac4bc0f8dc.

<!--
    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 (http://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)**

Messages for missing rules are only intended to appear at the top of a file. However, the messages have always been implemented as `context.report` calls on the `Program` node, and due to b0c63f0a928cceff7cf6f060262dd7ac4bc0f8dc, this results in the entire AST range being reported. This commit updates the missing-rule messages to report issues at the top of the file.

**Is there anything you'd like reviewers to focus on?**

Nothing in particular

eslintbot: LGTM

Patch:
diff --git a/.eslintrc.yml b/.eslintrc.yml
index 645e328d9..7c8224968 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -10,5 +10,6 @@ extends:
 rules:
     eslint-plugin/consistent-output: "error"
     eslint-plugin/no-identical-tests: "error"
+    eslint-plugin/prefer-output-null: "error"
     eslint-plugin/prefer-placeholders: "error"
     eslint-plugin/report-message-format: ["error", '[^a-z].*\.$']
diff --git a/Makefile.js b/Makefile.js
index 3e04619d1..06cbef959 100644
--- a/Makefile.js
+++ b/Makefile.js
@@ -115,7 +115,7 @@ function validateJsonFile(filePath) {
  */
 function fileType(extension) {
     return function(filename) {
-        return filename.substring(filename.lastIndexOf(".") + 1) === extension;
+        return filename.slice(filename.lastIndexOf(".") + 1) === extension;
     };
 }
 
diff --git a/lib/config/config-file.js b/lib/config/config-file.js
index 091a24f34..34cc0d914 100644
--- a/lib/config/config-file.js
+++ b/lib/config/config-file.js
@@ -502,8 +502,8 @@ function resolve(filePath, relativeTo) {
 
     if (filePath.startsWith("plugin:")) {
         const configFullName = filePath;
-        const pluginName = filePath.substr(7, filePath.lastIndexOf("/") - 7);
-        const configName = filePath.substr(filePath.lastIndexOf("/") + 1, filePath.length - filePath.lastIndexOf("/") - 1);
+        const pluginName = filePath.slice(7, filePath.lastIndexOf("/"));
+        const configName = filePath.slice(filePath.lastIndexOf("/") + 1);
 
         normalizedPackageName = normalizePackageName(pluginName, "eslint-plugin");
         debug(`Attempting to resolve ${normalizedPackageName}`);
diff --git a/lib/config/plugins.js b/lib/config/plugins.js
index adfd8a1bb..9884f3603 100644
--- a/lib/config/plugins.js
+++ b/lib/config/plugins.js
@@ -43,7 +43,7 @@ class Plugins {
      * @returns {string} The name of the plugin without prefix.
      */
     static removePrefix(pluginName) {
-        return pluginName.startsWith(PLUGIN_NAME_PREFIX) ? pluginName.substring(PLUGIN_NAME_PREFIX.length) : pluginName;
+        return pluginName.startsWith(PLUGIN_NAME_PREFIX) ? pluginName.slice(PLUGIN_NAME_PREFIX.length) : pluginName;
     }
 
     /**
diff --git a/lib/linter.js b/lib/linter.js
index ce759756e..bb17ac216 100755
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -70,8 +70,8 @@ function parseBooleanConfig(string, comment) {
         let value;
 
         if (pos !== -1) {
-            value = name.substring(pos + 1, name.length);
-            name = name.substring(0, pos);
+            value = name.slice(pos + 1);
+            name = name.slice(0, pos);
         }
 
         items[name] = {
@@ -338,7 +338,7 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) {
         const match = /^(eslint(-\w+){0,3}|exported|globals?)(\s|$)/.exec(value);
 
         if (match) {
-            value = value.substring(match.index + match[1].length);
+            value = value.slice(match.index + match[1].length);
 
             if (comment.type === "Block") {
                 switch (match[1]) {
@@ -519,8 +519,11 @@ function createStubRule(message) {
      */
     function createRuleModule(context) {
         return {
-            Program(node) {
-                context.report(node, message);
+            Program() {
+                context.report({
+                    loc: { line: 1, column: 0 },
+                    message
+                });
             }
         };
     }
@@ -1288,13 +1291,18 @@ const externalMethods = {
 Object.keys(externalMethods).forEach(methodName => {
     const exMethodName = externalMethods[methodName];
 
-    // All functions expected to have less arguments than 5.
-    Linter.prototype[methodName] = function(a, b, c, d, e) {
-        if (this.sourceCode) {
-            return this.sourceCode[exMethodName](a, b, c, d, e);
-        }
-        return null;
-    };
+    // Applies the SourceCode methods to the Linter prototype
+    Object.defineProperty(Linter.prototype, methodName, {
+        value() {
+            if (this.sourceCode) {
+                return this.sourceCode[exMethodName].apply(this.sourceCode, arguments);
+            }
+            return null;
+        },
+        configurable: true,
+        writable: true,
+        enumerable: false
+    });
 });
 
 module.exports = Linter;
diff --git a/lib/rules/indent-legacy.js b/lib/rules/indent-legacy.js
index f686c18ee..89d98f833 100644
--- a/lib/rules/indent-legacy.js
+++ b/lib/rules/indent-legacy.js
@@ -729,7 +729,7 @@ module.exports = {
                 if (!parentVarNode || parentVarNode.loc.start.line !== node.loc.start.line) {
                     if (parent.type !== "VariableDeclarator" || parentVarNode === parentVarNode.parent.declarations[0]) {
                         if (parent.type === "VariableDeclarator" && parentVarNode.loc.start.line === parent.loc.start.line) {
-                            nodeIndent = nodeIndent + (indentSize * options.VariableDeclarator[parentVarNode.parent.kind]);
+                            nodeIndent += (indentSize * options.VariableDeclarator[parentVarNode.parent.kind]);
                         } else if (parent.type === "ObjectExpression" || parent.type === "ArrayExpression") {
                             const parentElements = node.parent.type === "ObjectExpression" ? node.parent.properties : node.parent.elements;
 
@@ -765,7 +765,7 @@ module.exports = {
                         }
                     }
                 } else if (!parentVarNode && !isFirstArrayElementOnSameLine(parent) && parent.type !== "MemberExpression" && parent.type !== "ExpressionStatement" && parent.type !== "AssignmentExpression" && parent.type !== "Property") {
-                    nodeIndent = nodeIndent + indentSize;
+                    nodeIndent += indentSize;
                 }
 
                 checkFirstNodeLineIndent(node, nodeIndent);
diff --git a/lib/rules/no-multi-spaces.js b/lib/rules/no-multi-spaces.js
index 30d650c3a..fb4e017fd 100644
--- a/lib/rules/no-multi-spaces.js
+++ b/lib/rules/no-multi-spaces.js
@@ -116,7 +116,7 @@ module.exports = {
         function formatReportedCommentValue(token) {
             const valueLines = token.value.split("\n");
             const value = valueLines[0];
-            const formattedValue = `${value.substring(0, 12)}...`;
+            const formattedValue = `${value.slice(0, 12)}...`;
 
             return valueLines.length === 1 && value.length <= 12 ? value : formattedValue;
         }
diff --git a/lib/rules/space-infix-ops.js b/lib/rules/space-infix-ops.js
index ad514b28f..b92c889c7 100644
--- a/lib/rules/space-infix-ops.js
+++ b/lib/rules/space-infix-ops.js
@@ -112,7 +112,7 @@ module.exports = {
             const nonSpacedNode = getFirstNonSpacedToken(leftNode, rightNode);
 
             if (nonSpacedNode) {
-                if (!(int32Hint && sourceCode.getText(node).substr(-2) === "|0")) {
+                if (!(int32Hint && sourceCode.getText(node).endsWith("|0"))) {
                     report(node, nonSpacedNode);
                 }
             }
diff --git a/package.json b/package.json
index 984592b1f..462170b0d 100644
--- a/package.json
+++ b/package.json
@@ -84,7 +84,7 @@
     "coveralls": "^2.13.1",
     "dateformat": "^2.0.0",
     "ejs": "^2.5.6",
-    "eslint-plugin-eslint-plugin": "^0.7.4",
+    "eslint-plugin-eslint-plugin": "^0.8.0",
     "eslint-plugin-node": "^5.1.0",
     "eslint-release": "^0.10.1",
     "eslump": "1.6.0",
diff --git a/packages/eslint-config-eslint/default.yml b/packages/eslint-config-eslint/default.yml
index cdeac9ad4..f1d848ca3 100644
--- a/packages/eslint-config-eslint/default.yml
+++ b/packages/eslint-config-eslint/default.yml
@@ -10,7 +10,7 @@ rules:
     indent: ["error", 4, {SwitchCase: 1}]
     block-spacing: "error"
     brace-style: ["error", "1tbs"]
-    camelcase: ["error", { properties: "never" }]
+    camelcase: "error"
     callback-return: ["error", ["cb", "callback", "next"]]
     class-methods-use-this: "error"
     comma-dangle: "error"
@@ -20,6 +20,7 @@ rules:
     consistent-return: "error"
     curly: ["error", "all"]
     default-case: "error"
+    dot-location: ["error", "property"]
     dot-notation: ["error", { allowKeywords: true }]
     eol-last: "error"
     eqeqeq: "error"
@@ -27,7 +28,9 @@ rules:
     func-call-spacing: "error"
     func-style: ["error", "declaration"]
     generator-star-spacing: "error"
+    getter-return: "error"
     guard-for-in: "error"
+    handle-callback-err: ["error", "err"]
     key-spacing: ["error", { beforeColon: false, afterColon: true }]
     keyword-spacing: "error"
     lines-around-comment: ["error", {
@@ -41,7 +44,9 @@ rules:
     new-parens: "error"
     no-alert: "error"
     no-array-constructor: "error"
+    no-buffer-constructor: "error"
     no-caller: "error"
+    no-catch-shadow: "error"
     no-confusing-arrow: "error"
     no-console: "error"
     no-delete-var: "error"
@@ -59,6 +64,7 @@ rules:
     no-labels: "error"
     no-lone-blocks: "error"
     no-loop-func: "error"
+    no-mixed-requires: "error"
     no-mixed-spaces-and-tabs: ["error", false]
     no-multi-spaces: "error"
     no-multi-str: "error"
@@ -67,19 +73,28 @@ rules:
     no-new: "error"
     no-new-func: "error"
     no-new-object: "error"
+    no-new-require: "error"
     no-new-wrappers: "error"
     no-octal: "error"
     no-octal-escape: "error"
+    no-path-concat: "error"
     no-process-exit: "error"
     no-proto: "error"
     no-redeclare: "error"
+    no-restricted-properties: [
+        "error",
+        { property: "substring", message: "Use .slice instead of .substring." },
+        { property: "substr", message: "Use .slice instead of .substr." }
+    ]
     no-return-assign: "error"
     no-script-url: "error"
     no-self-assign: "error"
+    no-self-compare: "error"
     no-sequences: "error"
     no-shadow: "error"
     no-shadow-restricted-names: "error"
     no-tabs: "error"
+    no-throw-literal: "error"
     no-trailing-spaces: "error"
     no-undef: ["error", {typeof: true}]
     no-undef-init: "error"
@@ -90,16 +105,20 @@ rules:
     no-unused-expressions: "error"
     no-unused-vars: ["error", {vars: "all", args: "after-used"}]
     no-use-before-define: "error"
+    no-useless-call: "error"
     no-useless-computed-key: "error"
     no-useless-concat: "error"
     no-useless-constructor: "error"
     no-useless-escape: "error"
+    no-useless-rename: "error"
     no-useless-return: "error"
+    no-whitespace-before-property: "error"
     no-with: "error"
     no-var: "error"
     object-curly-spacing: ["error", "always"]
     object-shorthand: "error"
     one-var-declaration-per-line: "error"
+    operator-assignment: "error"
     operator-linebreak: "error"
     padding-line-between-statements: [
         "error",
@@ -116,6 +135,7 @@ rules:
     ]
     prefer-arrow-callback: "error"
     prefer-const: "error"
+    prefer-numeric-literals: "error"
     prefer-promise-reject-errors: "error"
     prefer-template: "error"
     quotes: ["error", "double"]
@@ -124,6 +144,7 @@ rules:
     require-jsdoc: "error"
     semi: "error"
     semi-spacing: ["error", {before: false, after: true}]
+    semi-style: "error"
     space-before-blocks: "error"
     space-before-function-paren: ["error", "never"]
     space-in-parens: "error"
@@ -131,8 +152,11 @@ rules:
     space-unary-ops: ["error", {words: true, nonwords: false}]
     spaced-comment: ["error", "always", { exceptions: ["-"]}]
     strict: ["error", "global"]
+    switch-colon-spacing: "error"
+    symbol-description: "error"
     template-curly-spacing: ["error", "never"]
     template-tag-spacing: "error"
+    unicode-bom: "error"
     valid-jsdoc: ["error", {
         prefer: { "return": "returns"},
         preferType: {
@@ -146,10 +170,3 @@ rules:
     wrap-iife: "error"
     yield-star-spacing: "error"
     yoda: ["error", "never"]
-
-    # Previously on by default in node environment
-    no-catch-shadow: "off"
-    no-mixed-requires: "error"
-    no-new-require: "error"
-    no-path-concat: "error"
-    handle-callback-err: ["error", "err"]


Test patch:
diff --git a/tests/lib/ignored-paths.js b/tests/lib/ignored-paths.js
index 1ef1f29cd..f09eab1d7 100644
--- a/tests/lib/ignored-paths.js
+++ b/tests/lib/ignored-paths.js
@@ -85,7 +85,7 @@ function countDefaultPatterns(ignoredPaths) {
     let count = ignoredPaths.defaultPatterns.length;
 
     if (!ignoredPaths.options || (ignoredPaths.options.dotfiles !== true)) {
-        count = count + 2; // Two patterns for ignoring dotfiles
+        count += 2; // Two patterns for ignoring dotfiles
     }
     return count;
 }
diff --git a/tests/lib/linter.js b/tests/lib/linter.js
index d52292abc..d9244bde1 100644
--- a/tests/lib/linter.js
+++ b/tests/lib/linter.js
@@ -2697,7 +2697,19 @@ describe("Linter", () => {
 
         it("should report multiple missing rules", () => {
             assert.isArray(resultsMultiple);
-            assert.equal(resultsMultiple[1].ruleId, "barfoo");
+
+            assert.deepEqual(
+                resultsMultiple[1],
+                {
+                    ruleId: "barfoo",
+                    message: "Definition for rule 'barfoo' was not found",
+                    line: 1,
+                    column: 1,
+                    severity: 1,
+                    source: "var answer = 6 * 7;",
+                    nodeType: void 0
+                }
+            );
         });
     });
 
diff --git a/tests/lib/rules/getter-return.js b/tests/lib/rules/getter-return.js
index 82542411c..13018bdea 100644
--- a/tests/lib/rules/getter-return.js
+++ b/tests/lib/rules/getter-return.js
@@ -51,14 +51,14 @@ ruleTester.run("getter-return", rule, {
         // option: {allowImplicit: false}
         { code: "Object.defineProperty(foo, \"bar\", { get: function () {return true;}});" },
         { code: "Object.defineProperty(foo, \"bar\", { get: function () { ~function (){ return true; }();return true;}});" },
-        { code: "Object.defineProperies(foo, { bar: { get: function () {return true;}} });" },
-        { code: "Object.defineProperies(foo, { bar: { get: function () { ~function (){ return true; }(); return true;}} });" },
+        { code: "Object.defineProperties(foo, { bar: { get: function () {return true;}} });" },
+        { code: "Object.defineProperties(foo, { bar: { get: function () { ~function (){ return true; }(); return true;}} });" },
 
         // option: {allowImplicit: true}
         { code: "Object.defineProperty(foo, \"bar\", { get: function () {return true;}});", options },
         { code: "Object.defineProperty(foo, \"bar\", { get: function (){return;}});", options },
-        { code: "Object.defineProperies(foo, { bar: { get: function () {return true;}} });", options },
-        { code: "Object.defineProperies(foo, { bar: { get: function () {return;}} });", options },
+        { code: "Object.defineProperties(foo, { bar: { get: function () {return true;}} });", options },
+        { code: "Object.defineProperties(foo, { bar: { get: function () {return;}} });", options },
 
         // not getter.
         { code: "var get = function(){};" },
@@ -97,9 +97,9 @@ ruleTester.run("getter-return", rule, {
         { code: "Object.defineProperty(foo, \"bar\", { get: function (){}});", errors: [{ noReturnMessage }] },
         { code: "Object.defineProperty(foo, \"bar\", { get: function (){if(bar) {return true;}}});", errors: [{ message: "Expected method 'get' to always return a value." }] },
         { code: "Object.defineProperty(foo, \"bar\", { get: function (){ ~function () { return true; }()}});", errors: [{ noReturnMessage }] },
-        { code: "Object.defineProperies(foo, { bar: { get: function () {}} });", options, errors: [{ noReturnMessage }] },
-        { code: "Object.defineProperies(foo, { bar: { get: function (){if(bar) {return true;}}}});", options, errors: [{ message: "Expected method 'get' to always return a value." }] },
-        { code: "Object.defineProperies(foo, { bar: { get: function () {~function () { return true; }()}} });", options, errors: [{ noReturnMessage }] },
+        { code: "Object.defineProperties(foo, { bar: { get: function () {}} });", options, errors: [{ noReturnMessage }] },
+        { code: "Object.defineProperties(foo, { bar: { get: function (){if(bar) {return true;}}}});", options, errors: [{ message: "Expected method 'get' to always return a value." }] },
+        { code: "Object.defineProperties(foo, { bar: { get: function () {~function () { return true; }()}} });", options, errors: [{ noReturnMessage }] },
 
         // option: {allowImplicit: true}
         { code: "Object.defineProperty(foo, \"bar\", { get: function (){}});", options, errors: [{ message: "Expected to return a value in method 'get'." }] }