Repository: eslint/eslint
Pull request #9186 (2017-08-30 20:17:28)

Chore: Make parseJsonConfig() a pure function in Linter
<!--
    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] Other, please explain:

<!--
    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)**

This updates the `parseJsonConfig` function in `Linter` to return a result object rather than pushing problems to a provided list of messages. The goal of this change is to give the `verify` method sole control of the `messages` array. This will make the code easier to follow (since the array won't be mutated from a distance), and it will also allow the array to be replaced with a local variable rather than an instance property.

(refs #9161)

**Is there anything you'd like reviewers to focus on?**

Nothing in particular


eslintbot: LGTM

Patch:
diff --git a/docs/rules/generator-star-spacing.md b/docs/rules/generator-star-spacing.md
index 91fa1213b..efa7a5a4e 100644
--- a/docs/rules/generator-star-spacing.md
+++ b/docs/rules/generator-star-spacing.md
@@ -75,6 +75,27 @@ An example of shorthand configuration:
 "generator-star-spacing": ["error", "after"]
 ```
 
+Additionally, this rule allows further configurability via overrides per function type.
+
+* `named` provides overrides for named functions
+* `anonymous` provides overrides for anonymous functions
+* `method` provides overrides for class methods or property function shorthand
+
+An example of a configuration with overrides:
+
+```json
+"generator-star-spacing": ["error", {
+    "before": false,
+    "after": true,
+    "anonymous": "neither",
+    "method": {"before": true, "after": true}
+}]
+```
+
+In the example configuration above, the top level "before" and "after" options define the default behavior of
+the rule, while the "anonymous", "method", and "static" options override the default behavior.
+Overrides can be either an object with "before" and "after", or a shorthand string as above.
+
 ## Examples
 
 ### before
@@ -137,6 +158,46 @@ var anonymous = function*() {};
 var shorthand = { *generator() {} };
 ```
 
+Examples of **incorrect** code for this rule with overrides present:
+
+```js
+/*eslint generator-star-spacing: ["error", {
+    "before": false,
+    "after": true,
+    "anonymous": "neither",
+    "method": {"before": true, "after": true}
+}]*/
+/*eslint-env es6*/
+
+function * generator() {}
+
+var anonymous = function* () {};
+
+var shorthand = { *generator() {} };
+
+class Class { static* method() {} }
+```
+
+Examples of **correct** code for this rule with overrides present:
+
+```js
+/*eslint generator-star-spacing: ["error", {
+    "before": false,
+    "after": true,
+    "anonymous": "neither",
+    "method": {"before": true, "after": true}
+}]*/
+/*eslint-env es6*/
+
+function* generator() {}
+
+var anonymous = function*() {};
+
+var shorthand = { * generator() {} };
+
+class Class { static * method() {} }
+```
+
 ## When Not To Use It
 
 If your project will not be using generators or you are not concerned with spacing consistency, you do not need this rule.
diff --git a/docs/rules/prefer-arrow-callback.md b/docs/rules/prefer-arrow-callback.md
index 4942295a5..3335e73a2 100644
--- a/docs/rules/prefer-arrow-callback.md
+++ b/docs/rules/prefer-arrow-callback.md
@@ -1,77 +1,100 @@
-# Suggest using arrow functions as callbacks. (prefer-arrow-callback)
+# Require using arrow functions for callbacks (prefer-arrow-callback)
 
-Arrow functions are suited to callbacks, because:
+Arrow functions can be an attractive alternative to function expressions for callbacks or function arguments.
 
-- `this` keywords in arrow functions bind to the upper scope's.
-- The notation of the arrow function is shorter than function expression's.
+For example, arrow functions are automatically bound to their surrounding scope/context. This provides an alternative to the pre-ES6 standard of explicitly binding function expressions to achieve similar behavior.
+
+Additionally, arrow functions are:
+
+- less verbose, and easier to reason about.
+
+- bound lexically regardless of where or when they are invoked.
 
 ## Rule Details
 
-This rule is aimed to flag usage of function expressions in an argument list.
+This rule locates function expressions used as callbacks or function arguments. An error will be produced for any that could be replaced by an arrow function without changing the result.
 
-The following patterns are considered problems:
+The following examples **will** be flagged:
 
 ```js
-/*eslint prefer-arrow-callback: "error"*/
+/* eslint prefer-arrow-callback: "error" */
 
-foo(function(a) { return a; });
-foo(function() { return this.a; }.bind(this));
+foo(function(a) { return a; }); // ERROR
+// prefer: foo(a => a)
+
+foo(function() { return this.a; }.bind(this)); // ERROR
+// prefer: foo(() => this.a)
 ```
 
-The following patterns are not considered problems:
+Instances where an arrow function would not produce identical results will be ignored.
+
+The following examples **will not** be flagged:
 
 ```js
-/*eslint prefer-arrow-callback: "error"*/
-/*eslint-env es6*/
+/* eslint prefer-arrow-callback: "error" */
+/* eslint-env es6 */
+
+// arrow function callback
+foo(a => a); // OK
 
-foo(a => a);
-foo(function*() { yield; });
+// generator as callback
+foo(function*() { yield; }); // OK
 
-// this is not a callback.
-var foo = function foo(a) { return a; };
+// function expression not used as callback or function argument
+var foo = function foo(a) { return a; }; // OK
 
-// using `this` without `.bind(this)`.
-foo(function() { return this.a; });
+// unbound function expression callback
+foo(function() { return this.a; }); // OK
 
-// recursively.
-foo(function bar(n) { return n && n + bar(n - 1); });
+// recursive named function callback
+foo(function bar(n) { return n && n + bar(n - 1); }); // OK
 ```
 
 ## Options
 
-This rule takes one optional argument, an object which is an options object.
+Access further control over this rule's behavior via an options object.
+
+Default: `{ allowNamedFunctions: false, allowUnboundThis: true }`
 
 ### allowNamedFunctions
 
-This is a `boolean` option and it is `false` by default. When set to `true`, the rule doesn't warn on named functions used as callbacks.
+By default `{ "allowNamedFunctions": false }`, this `boolean` option prohibits using named functions as callbacks or function arguments.
+
+Changing this value to `true` will reverse this option's behavior by allowing use of named functions without restriction.
 
-Examples of **correct** code for the `{ "allowNamedFunctions": true }` option:
+`{ "allowNamedFunctions": true }` **will not** flag the following example:
 
 ```js
-/*eslint prefer-arrow-callback: ["error", { "allowNamedFunctions": true }]*/
+/* eslint prefer-arrow-callback: [ "error", { "allowNamedFunctions": true } ] */
 
 foo(function bar() {});
 ```
 
 ### allowUnboundThis
 
-This is a `boolean` option and it is `true` by default. When set to `false`, this option allows the use of `this` without restriction and checks for dynamically assigned `this` values such as when using `Array.prototype.map` with a `context` argument. Normally, the rule will flag the use of `this` whenever a function does not use `bind()` to specify the value of `this` constantly.
+By default `{ "allowUnboundThis": true }`, this `boolean` option allows function expressions containing `this` to be used as callbacks, as long as the function in question has not been explicitly bound.
+
+When set to `false` this option prohibits the use of function expressions as callbacks or function arguments entirely, without exception.
 
-Examples of **incorrect** code for the `{ "allowUnboundThis": false }` option:
+`{ "allowNamedFunctions": false }` **will** flag the following examples:
 
 ```js
-/*eslint prefer-arrow-callback: ["error", { "allowUnboundThis": false }]*/
-/*eslint-env es6*/
+/* eslint prefer-arrow-callback: [ "error", { "allowUnboundThis": false } ] */
+/* eslint-env es6 */
 
 foo(function() { this.a; });
 
 foo(function() { (() => this); });
 
-someArray.map(function (itm) { return this.doSomething(itm); }, someObject);
+someArray.map(function(itm) { return this.doSomething(itm); }, someObject);
 ```
 
 ## When Not To Use It
 
-This rule should not be used in ES3/5 environments.
+- In environments that have not yet adopted ES6 language features (ES3/5).
+
+- In ES6+ environments that allow the use of function expressions when describing callbacks or function arguments.
+
+## Further Reading
 
-In ES2015 (ES6) or later, if you don't want to be notified about function expressions in an argument list, you can safely disable this rule.
+- [More on ES6 arrow functions]('https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions')
diff --git a/lib/config/config-initializer.js b/lib/config/config-initializer.js
index d344fa0ac..11d441029 100644
--- a/lib/config/config-initializer.js
+++ b/lib/config/config-initializer.js
@@ -65,6 +65,7 @@ function writeFile(config, format) {
  * @param {string} moduleName The module name to get.
  * @returns {Object} The peer dependencies of the given module.
  * This object is the object of `peerDependencies` field of `package.json`.
+ * Returns null if npm was not found.
  */
 function getPeerDependencies(moduleName) {
     let result = getPeerDependencies.cache.get(moduleName);
@@ -356,7 +357,8 @@ function hasESLintVersionConflict(answers) {
     // Get the required range of ESLint version.
     const configName = getStyleGuideName(answers);
     const moduleName = `eslint-config-${configName}@latest`;
-    const requiredESLintVersionRange = getPeerDependencies(moduleName).eslint;
+    const peerDependencies = getPeerDependencies(moduleName) || {};
+    const requiredESLintVersionRange = peerDependencies.eslint;
 
     if (!requiredESLintVersionRange) {
         return false;
diff --git a/lib/linter.js b/lib/linter.js
index f84b6e6ad..c0ae742d2 100755
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -87,10 +87,9 @@ function parseBooleanConfig(string, comment) {
  * Parses a JSON-like config.
  * @param {string} string The string to parse.
  * @param {Object} location Start line and column of comments for potential error message.
- * @param {Object[]} messages The messages queue for potential error message.
- * @returns {Object} Result map object
+ * @returns {({success: true, config: Object}|{success: false, error: Problem})} Result map object
  */
-function parseJsonConfig(string, location, messages) {
+function parseJsonConfig(string, location) {
     let items = {};
 
     // Parses a JSON-like comment by the same way as parsing CLI option.
@@ -102,7 +101,10 @@ function parseJsonConfig(string, location, messages) {
         //     "no-alert: 2 no-console: 2" --> {"no-alert": "2 no-console: 2"}
         // Should ignore that case as well.
         if (ConfigOps.isEverySeverityValid(items)) {
-            return items;
+            return {
+                success: true,
+                config: items
+            };
         }
     } catch (ex) {
 
@@ -116,20 +118,25 @@ function parseJsonConfig(string, location, messages) {
     try {
         items = JSON.parse(`{${string}}`);
     } catch (ex) {
-
-        messages.push({
-            ruleId: null,
-            fatal: true,
-            severity: 2,
-            source: null,
-            message: `Failed to parse JSON from '${string}': ${ex.message}`,
-            line: location.start.line,
-            column: location.start.column + 1
-        });
+        return {
+            success: false,
+            error: {
+                ruleId: null,
+                fatal: true,
+                severity: 2,
+                source: null,
+                message: `Failed to parse JSON from '${string}': ${ex.message}`,
+                line: location.start.line,
+                column: location.start.column + 1
+            }
+        };
 
     }
 
-    return items;
+    return {
+        success: true,
+        config: items
+    };
 }
 
 /**
@@ -316,7 +323,8 @@ function enableReporting(reportingConfig, start, rulesToEnable) {
  * @param {ASTNode} ast The top node of the AST.
  * @param {Object} config The existing configuration data.
  * @param {Linter} linterContext Linter context object
- * @returns {Object} Modified config object
+ * @returns {{config: Object, problems: Problem[]}} Modified config object, along with any problems encountered
+ * while parsing config comments
  */
 function modifyConfigsFromComments(filename, ast, config, linterContext) {
 
@@ -327,7 +335,7 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) {
         env: {}
     };
     const commentRules = {};
-    const messages = linterContext.messages;
+    const problems = [];
     const reportingConfig = linterContext.reportingConfig;
 
     ast.comments.forEach(comment => {
@@ -362,14 +370,19 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) {
                         break;
 
                     case "eslint": {
-                        const items = parseJsonConfig(value, comment.loc, messages);
+                        const parseResult = parseJsonConfig(value, comment.loc);
+
+                        if (parseResult.success) {
+                            Object.keys(parseResult.config).forEach(name => {
+                                const ruleValue = parseResult.config[name];
 
-                        Object.keys(items).forEach(name => {
-                            const ruleValue = items[name];
+                                validator.validateRuleOptions(name, ruleValue, `${filename} line ${comment.loc.start.line}`, linterContext.rules);
+                                commentRules[name] = ruleValue;
+                            });
+                        } else {
+                            problems.push(parseResult.error);
+                        }
 
-                            validator.validateRuleOptions(name, ruleValue, `${filename} line ${comment.loc.start.line}`, linterContext.rules);
-                            commentRules[name] = ruleValue;
-                        });
                         break;
                     }
 
@@ -397,14 +410,17 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) {
     });
     Object.assign(commentConfig.rules, commentRules);
 
-    return ConfigOps.merge(config, commentConfig);
+    return {
+        config: ConfigOps.merge(config, commentConfig),
+        problems
+    };
 }
 
 /**
  * Check if message of rule with ruleId should be ignored in location
  * @param  {Object[]} reportingConfig  Collection of ignore records
  * @param  {string} ruleId   Id of rule
- * @param  {Object} location Location of message
+ * @param  {Object} location 1-indexed location of message
  * @returns {boolean}          True if message should be ignored, false otherwise
  */
 function isDisabledByReportingConfig(reportingConfig, ruleId, location) {
@@ -414,8 +430,8 @@ function isDisabledByReportingConfig(reportingConfig, ruleId, location) {
         const ignore = reportingConfig[i];
 
         if ((!ignore.rule || ignore.rule === ruleId) &&
-            (location.line > ignore.start.line || (location.line === ignore.start.line && location.column >= ignore.start.column)) &&
-            (!ignore.end || (location.line < ignore.end.line || (location.line === ignore.end.line && location.column <= ignore.end.column)))) {
+            (location.line > ignore.start.line || (location.line === ignore.start.line && location.column > ignore.start.column)) &&
+            (!ignore.end || (location.line < ignore.end.line || (location.line === ignore.end.line && location.column - 1 <= ignore.end.column)))) {
             return true;
         }
     }
@@ -747,7 +763,6 @@ class Linter {
     constructor() {
         this.messages = [];
         this.currentConfig = null;
-        this.currentScopes = null;
         this.scopeManager = null;
         this.currentFilename = null;
         this.traverser = null;
@@ -766,7 +781,6 @@ class Linter {
     reset() {
         this.messages = [];
         this.currentConfig = null;
-        this.currentScopes = null;
         this.scopeManager = null;
         this.traverser = null;
         this.reportingConfig = [];
@@ -866,7 +880,10 @@ class Linter {
 
         // parse global comments and modify config
         if (allowInlineConfig !== false) {
-            config = modifyConfigsFromComments(this.currentFilename, this.sourceCode.ast, config, this);
+            const modifyConfigResult = modifyConfigsFromComments(this.currentFilename, this.sourceCode.ast, config, this);
+
+            config = modifyConfigResult.config;
+            modifyConfigResult.problems.forEach(problem => this.messages.push(problem));
         }
 
         // ensure that severities are normalized in the config
@@ -892,7 +909,20 @@ class Linter {
                     parserOptions: config.parserOptions,
                     parserPath: config.parser,
                     parserServices,
-                    settings: config.settings
+                    settings: config.settings,
+
+                    /**
+                     * This is used to avoid breaking rules that used to monkeypatch the `Linter#report` method
+                     * by using the `_linter` property on rule contexts.
+                     *
+                     * This should be removed in a major release after we create a better way to
+                     * lint for unused disable comments.
+                     * https://github.com/eslint/eslint/issues/9193
+                     */
+                    _linter: {
+                        report() {},
+                        on: emitter.on.bind(emitter)
+                    }
                 }
             )
         );
@@ -928,6 +958,24 @@ class Linter {
                                 if (!isDisabledByReportingConfig(this.reportingConfig, ruleId, problem)) {
                                     this.messages.push(problem);
                                 }
+
+                                /*
+                                 * This is used to avoid breaking rules that used monkeypatch Linter, and relied on
+                                 * `linter.report` getting called with report info every time a rule reports a problem.
+                                 * To continue to support this, make sure that `context._linter.report` is called every
+                                 * time a problem is reported by a rule, even though `context._linter` is no longer a
+                                 * `Linter` instance.
+                                 *
+                                 * This should be removed in a major release after we create a better way to
+                                 * lint for unused disable comments.
+                                 * https://github.com/eslint/eslint/issues/9193
+                                 */
+                                sharedTraversalContext._linter.report( // eslint-disable-line no-underscore-dangle
+                                    ruleId,
+                                    severity,
+                                    { loc: { start: { line: problem.line, column: problem.column - 1 } } },
+                                    problem.message
+                                );
                             }
                         ])
                     }
@@ -969,10 +1017,8 @@ class Linter {
             fallback: Traverser.getKeys
         });
 
-        this.currentScopes = this.scopeManager.scopes;
-
         // augment global scope with declared global variables
-        addDeclaredGlobals(this.sourceCode.ast, this.currentScopes[0], this.currentConfig, this.environments);
+        addDeclaredGlobals(this.sourceCode.ast, this.scopeManager.scopes[0], this.currentConfig, this.environments);
 
         const eventGenerator = new CodePathAnalyzer(new NodeEventGenerator(emitter));
 
@@ -1063,7 +1109,7 @@ class Linter {
 
         }
 
-        return this.currentScopes[0];
+        return this.scopeManager.scopes[0];
     }
 
     /**
diff --git a/lib/report-translator.js b/lib/report-translator.js
index fe59d9a75..9c4ab8b9a 100644
--- a/lib/report-translator.js
+++ b/lib/report-translator.js
@@ -41,7 +41,7 @@ function normalizeMultiArgReportCall() {
 
     // If there is one argument, it is considered to be a new-style call already.
     if (arguments.length === 1) {
-        return Object.assign({}, arguments[0]);
+        return arguments[0];
     }
 
     // If the second argument is a string, the arguments are interpreted as [node, message, data, fix].
@@ -67,7 +67,7 @@ function normalizeMultiArgReportCall() {
 /**
  * Asserts that either a loc or a node was provided, and the node is valid if it was provided.
  * @param {MessageDescriptor} descriptor A descriptor to validate
- * @returns {MessageDescriptor} The same descriptor
+ * @returns {void}
  * @throws AssertionError if neither a node nor a loc was provided, or if the node is not an object
  */
 function assertValidNodeInfo(descriptor) {
@@ -76,46 +76,39 @@ function assertValidNodeInfo(descriptor) {
     } else {
         assert(descriptor.loc, "Node must be provided when reporting error if location is not provided");
     }
-
-    return descriptor;
 }
 
 /**
  * Normalizes a MessageDescriptor to always have a `loc` with `start` and `end` properties
- * @param {MessageDescriptor} descriptor A descriptor for the report from a rule. This descriptor may be mutated
- * by this function.
- * @returns {MessageDescriptor} The updated MessageDescriptor that infers the `start` and `end` properties from
- * the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor.
+ * @param {MessageDescriptor} descriptor A descriptor for the report from a rule.
+ * @returns {{start: Location, end: (Location|null)}} An updated location that infers the `start` and `end` properties
+ * from the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor.
  */
 function normalizeReportLoc(descriptor) {
     if (descriptor.loc) {
         if (descriptor.loc.start) {
-            return descriptor;
+            return descriptor.loc;
         }
-        return Object.assign(descriptor, { loc: { start: descriptor.loc, end: null } });
+        return { start: descriptor.loc, end: null };
     }
-
-    return Object.assign(descriptor, { loc: descriptor.node.loc });
+    return descriptor.node.loc;
 }
 
 /**
  * Interpolates data placeholders in report messages
- * @param {MessageDescriptor} descriptor The report message descriptor. This descriptor may be mutated
- * by this function.
- * @returns {MessageDescriptor} An new descriptor with a message containing the interpolated data
+ * @param {MessageDescriptor} descriptor The report message descriptor.
+ * @returns {string} The interpolated message for the descriptor
  */
 function normalizeMessagePlaceholders(descriptor) {
     if (!descriptor.data) {
-        return descriptor;
+        return descriptor.message;
     }
-    return Object.assign(descriptor, {
-        message: descriptor.message.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (fullMatch, term) => {
-            if (term in descriptor.data) {
-                return descriptor.data[term];
-            }
-
-            return fullMatch;
-        })
+    return descriptor.message.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (fullMatch, term) => {
+        if (term in descriptor.data) {
+            return descriptor.data[term];
+        }
+
+        return fullMatch;
     });
 }
 
@@ -134,7 +127,7 @@ function compareFixesByRange(a, b) {
  * Merges the given fixes array into one.
  * @param {Fix[]} fixes The fixes to merge.
  * @param {SourceCode} sourceCode The source code object to get the text between fixes.
- * @returns {void}
+ * @returns {{text: string, range: [number, number]}} The merged fixes
  */
 function mergeFixes(fixes, sourceCode) {
     if (fixes.length === 0) {
@@ -169,14 +162,13 @@ function mergeFixes(fixes, sourceCode) {
 /**
  * Gets one fix object from the given descriptor.
  * If the descriptor retrieves multiple fixes, this merges those to one.
- * @param {MessageDescriptor} descriptor The report descriptor. This descriptor may be mutated
- * by this function.
+ * @param {MessageDescriptor} descriptor The report descriptor.
  * @param {SourceCode} sourceCode The source code object to get text between fixes.
- * @returns {MessageDescriptor} The updated descriptor.
+ * @returns {({text: string, range: [number, number]}|null)} The fix for the descriptor
  */
 function normalizeFixes(descriptor, sourceCode) {
     if (typeof descriptor.fix !== "function") {
-        return Object.assign(descriptor, { fix: null });
+        return null;
     }
 
     // @type {null | Fix | Fix[] | IterableIterator<Fix>}
@@ -184,16 +176,22 @@ function normalizeFixes(descriptor, sourceCode) {
 
     // Merge to one.
     if (fix && Symbol.iterator in fix) {
-        return Object.assign(descriptor, { fix: mergeFixes(Array.from(fix), sourceCode) });
+        return mergeFixes(Array.from(fix), sourceCode);
     }
-    return Object.assign(descriptor, { fix });
+    return fix;
 }
 
 /**
  * Creates information about the report from a descriptor
- * @param {MessageDescriptor} descriptor The message descriptor
- * @param {string} ruleId The rule ID of the problem
- * @param {(0|1|2)} severity The severity of the problem
+ * @param {{
+ *     ruleId: string,
+ *     severity: (0|1|2),
+ *     node: (ASTNode|null),
+ *     message: string,
+ *     loc: {start: SourceLocation, end: (SourceLocation|null)},
+ *     fix: ({text: string, range: [number, number]}|null),
+ *     sourceLines: string[]
+ * }} options Information about the problem
  * @returns {function(...args): {
  *      ruleId: string,
  *      severity: (0|1|2),
@@ -207,23 +205,24 @@ function normalizeFixes(descriptor, sourceCode) {
  *      fix: ({text: string, range: [number, number]}|null)
  * }} Information about the report
  */
-function createProblemFromDescriptor(descriptor, ruleId, severity) {
+function createProblem(options) {
     const problem = {
-        ruleId,
-        severity,
-        message: descriptor.message,
-        line: descriptor.loc.start.line,
-        column: descriptor.loc.start.column + 1,
-        nodeType: descriptor.node && descriptor.node.type || null
+        ruleId: options.ruleId,
+        severity: options.severity,
+        message: options.message,
+        line: options.loc.start.line,
+        column: options.loc.start.column + 1,
+        nodeType: options.node && options.node.type || null,
+        source: options.sourceLines[options.loc.start.line - 1] || ""
     };
 
-    if (descriptor.loc.end) {
-        problem.endLine = descriptor.loc.end.line;
-        problem.endColumn = descriptor.loc.end.column + 1;
+    if (options.loc.end) {
+        problem.endLine = options.loc.end.line;
+        problem.endColumn = options.loc.end.column + 1;
     }
 
-    if (descriptor.fix) {
-        problem.fix = descriptor.fix;
+    if (options.fix) {
+        problem.fix = options.fix;
     }
 
     return problem;
@@ -261,14 +260,15 @@ module.exports = function createReportTranslator(metadata) {
         const descriptor = normalizeMultiArgReportCall.apply(null, arguments);
 
         assertValidNodeInfo(descriptor);
-        normalizeReportLoc(descriptor);
-        normalizeMessagePlaceholders(descriptor);
-        normalizeFixes(descriptor, metadata.sourceCode);
-
-        const problem = createProblemFromDescriptor(descriptor, metadata.ruleId, metadata.severity);
-
-        problem.source = metadata.sourceCode.lines[problem.line - 1] || "";
 
-        return problem;
+        return createProblem({
+            ruleId: metadata.ruleId,
+            severity: metadata.severity,
+            node: descriptor.node,
+            message: normalizeMessagePlaceholders(descriptor),
+            loc: normalizeReportLoc(descriptor),
+            fix: normalizeFixes(descriptor, metadata.sourceCode),
+            sourceLines: metadata.sourceCode.lines
+        });
     };
 };
diff --git a/lib/rules/generator-star-spacing.js b/lib/rules/generator-star-spacing.js
index 9836e4ead..5c6120224 100644
--- a/lib/rules/generator-star-spacing.js
+++ b/lib/rules/generator-star-spacing.js
@@ -9,6 +9,22 @@
 // Rule Definition
 //------------------------------------------------------------------------------
 
+const OVERRIDE_SCHEMA = {
+    oneOf: [
+        {
+            enum: ["before", "after", "both", "neither"]
+        },
+        {
+            type: "object",
+            properties: {
+                before: { type: "boolean" },
+                after: { type: "boolean" }
+            },
+            additionalProperties: false
+        }
+    ]
+};
+
 module.exports = {
     meta: {
         docs: {
@@ -29,7 +45,10 @@ module.exports = {
                         type: "object",
                         properties: {
                             before: { type: "boolean" },
-                            after: { type: "boolean" }
+                            after: { type: "boolean" },
+                            named: OVERRIDE_SCHEMA,
+                            anonymous: OVERRIDE_SCHEMA,
+                            method: OVERRIDE_SCHEMA
                         },
                         additionalProperties: false
                     }
@@ -40,16 +59,39 @@ module.exports = {
 
     create(context) {
 
-        const mode = (function(option) {
-            if (!option || typeof option === "string") {
-                return {
-                    before: { before: true, after: false },
-                    after: { before: false, after: true },
-                    both: { before: true, after: true },
-                    neither: { before: false, after: false }
-                }[option || "before"];
+        const optionDefinitions = {
+            before: { before: true, after: false },
+            after: { before: false, after: true },
+            both: { before: true, after: true },
+            neither: { before: false, after: false }
+        };
+
+        /**
+         * Returns resolved option definitions based on an option and defaults
+         * 
+         * @param {any} option - The option object or string value
+         * @param {Object} defaults - The defaults to use if options are not present
+         * @returns {Object} the resolved object definition
+         */
+        function optionToDefinition(option, defaults) {
+            if (!option) {
+                return defaults;
             }
-            return option;
+
+            return typeof option === "string"
+                ? optionDefinitions[option]
+                : Object.assign({}, defaults, option);
+        }
+
+        const modes = (function(option) {
+            option = option || {};
+            const defaults = optionToDefinition(option, optionDefinitions.before);
+
+            return {
+                named: optionToDefinition(option.named, defaults),
+                anonymous: optionToDefinition(option.anonymous, defaults),
+                method: optionToDefinition(option.method, defaults)
+            };
         }(context.options[0]));
 
         const sourceCode = context.getSourceCode();
@@ -79,6 +121,8 @@ module.exports = {
 
         /**
          * Checks the spacing between two tokens before or after the star token.
+         * 
+         * @param {string} kind Either "named", "anonymous", or "method"
          * @param {string} side Either "before" or "after".
          * @param {Token} leftToken `function` keyword token if side is "before", or
          *     star token if side is "after".
@@ -86,10 +130,10 @@ module.exports = {
          *     token if side is "after".
          * @returns {void}
          */
-        function checkSpacing(side, leftToken, rightToken) {
-            if (!!(rightToken.range[0] - leftToken.range[1]) !== mode[side]) {
+        function checkSpacing(kind, side, leftToken, rightToken) {
+            if (!!(rightToken.range[0] - leftToken.range[1]) !== modes[kind][side]) {
                 const after = leftToken.value === "*";
-                const spaceRequired = mode[side];
+                const spaceRequired = modes[kind][side];
                 const node = after ? leftToken : rightToken;
                 const type = spaceRequired ? "Missing" : "Unexpected";
                 const message = "{{type}} space {{side}} *.";
@@ -117,6 +161,7 @@ module.exports = {
 
         /**
          * Enforces the spacing around the star if node is a generator function.
+         * 
          * @param {ASTNode} node A function expression or declaration node.
          * @returns {void}
          */
@@ -126,17 +171,23 @@ module.exports = {
             }
 
             const starToken = getStarToken(node);
-
-            // Only check before when preceded by `function`|`static` keyword
             const prevToken = sourceCode.getTokenBefore(starToken);
+            const nextToken = sourceCode.getTokenAfter(starToken);
 
-            if (prevToken.value === "function" || prevToken.value === "static") {
-                checkSpacing("before", prevToken, starToken);
+            let kind = "named";
+
+            if (node.parent.type === "MethodDefinition" || (node.parent.type === "Property" && node.parent.method)) {
+                kind = "method";
+            } else if (!node.id) {
+                kind = "anonymous";
             }
 
-            const nextToken = sourceCode.getTokenAfter(starToken);
+            // Only check before when preceded by `function`|`static` keyword
+            if (!(kind === "method" && starToken === sourceCode.getFirstToken(node.parent))) {
+                checkSpacing(kind, "before", prevToken, starToken);
+            }
 
-            checkSpacing("after", starToken, nextToken);
+            checkSpacing(kind, "after", starToken, nextToken);
         }
 
         return {
diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js
index 3ed278d54..05940d593 100644
--- a/lib/rules/no-unused-vars.js
+++ b/lib/rules/no-unused-vars.js
@@ -64,8 +64,6 @@ module.exports = {
     create(context) {
         const sourceCode = context.getSourceCode();
 
-        const DEFINED_MESSAGE = "'{{name}}' is defined but never used.";
-        const ASSIGNED_MESSAGE = "'{{name}}' is assigned a value but never used.";
         const REST_PROPERTY_TYPE = /^(?:Experimental)?RestProperty$/;
 
         const config = {
@@ -100,6 +98,49 @@ module.exports = {
             }
         }
 
+        /**
+         * Generate the warning message about the variable being
+         * defined and unused, including the ignore pattern if configured.
+         * @param {Variable} unusedVar - eslint-scope variable object.
+         * @returns {string} The warning message to be used with this unused variable.
+         */
+        function getDefinedMessage(unusedVar) {
+            let type;
+            let pattern;
+
+            if (config.varsIgnorePattern) {
+                type = "vars";
+                pattern = config.varsIgnorePattern.toString();
+            }
+
+            if (unusedVar.defs && unusedVar.defs[0] && unusedVar.defs[0].type) {
+                const defType = unusedVar.defs[0].type;
+
+                if (defType === "CatchClause" && config.caughtErrorsIgnorePattern) {
+                    type = "args";
+                    pattern = config.caughtErrorsIgnorePattern.toString();
+                } else if (defType === "Parameter" && config.argsIgnorePattern) {
+                    type = "args";
+                    pattern = config.argsIgnorePattern.toString();
+                }
+            }
+
+            const additional = type ? ` Allowed unused ${type} must match ${pattern}.` : "";
+
+            return `'{{name}}' is defined but never used.${additional}`;
+        }
+
+        /**
+         * Generate the warning message about the variable being
+         * assigned and unused, including the ignore pattern if configured.
+         * @returns {string} The warning message to be used with this unused variable.
+         */
+        function getAssignedMessage() {
+            const additional = config.varsIgnorePattern ? ` Allowed unused vars must match ${config.varsIgnorePattern.toString()}.` : "";
+
+            return `'{{name}}' is assigned a value but never used.${additional}`;
+        }
+
         //--------------------------------------------------------------------------
         // Helpers
         //--------------------------------------------------------------------------
@@ -586,13 +627,15 @@ module.exports = {
                         context.report({
                             node: programNode,
                             loc: getLocation(unusedVar),
-                            message: DEFINED_MESSAGE,
+                            message: getDefinedMessage(unusedVar),
                             data: unusedVar
                         });
                     } else if (unusedVar.defs.length > 0) {
                         context.report({
                             node: unusedVar.identifiers[0],
-                            message: unusedVar.references.some(ref => ref.isWrite()) ? ASSIGNED_MESSAGE : DEFINED_MESSAGE,
+                            message: unusedVar.references.some(ref => ref.isWrite())
+                                ? getAssignedMessage()
+                                : getDefinedMessage(unusedVar),
                             data: unusedVar
                         });
                     }
diff --git a/lib/rules/prefer-arrow-callback.js b/lib/rules/prefer-arrow-callback.js
index de7c33940..7565e445f 100644
--- a/lib/rules/prefer-arrow-callback.js
+++ b/lib/rules/prefer-arrow-callback.js
@@ -132,7 +132,7 @@ function hasDuplicateParams(paramsList) {
 module.exports = {
     meta: {
         docs: {
-            description: "require arrow functions as callbacks",
+            description: "require using arrow functions for callbacks",
             category: "ECMAScript 6",
             recommended: false
         },
diff --git a/lib/util/npm-util.js b/lib/util/npm-util.js
index 4f488c012..6c431e039 100644
--- a/lib/util/npm-util.js
+++ b/lib/util/npm-util.js
@@ -53,22 +53,39 @@ function installSyncSaveDev(packages) {
     if (!Array.isArray(packages)) {
         packages = [packages];
     }
-    spawn.sync("npm", ["i", "--save-dev"].concat(packages), { stdio: "inherit" });
+    const npmProcess = spawn.sync("npm", ["i", "--save-dev"].concat(packages),
+        { stdio: "inherit" });
+    const error = npmProcess.error;
+
+    if (error && error.code === "ENOENT") {
+        const pluralS = packages.length > 1 ? "s" : "";
+
+        log.error(`Could not execute npm. Please install the following package${pluralS} with your package manager of choice: ${packages.join(", ")}`);
+    }
 }
 
 /**
  * Fetch `peerDependencies` of the given package by `npm show` command.
  * @param {string} packageName The package name to fetch peerDependencies.
- * @returns {Object} Gotten peerDependencies.
+ * @returns {Object} Gotten peerDependencies. Returns null if npm was not found.
  */
 function fetchPeerDependencies(packageName) {
-    const fetchedText = spawn.sync(
+    const npmProcess = spawn.sync(
         "npm",
         ["show", "--json", packageName, "peerDependencies"],
         { encoding: "utf8" }
-    ).stdout.trim();
+    );
+
+    const error = npmProcess.error;
+
+    if (error && error.code === "ENOENT") {
+        return null;
+    }
+    const fetchedText = npmProcess.stdout.trim();
 
     return JSON.parse(fetchedText || "{}");
+
+
 }
 
 /**


Test patch:
diff --git a/tests/lib/linter.js b/tests/lib/linter.js
index c175d3310..2096f5a63 100644
--- a/tests/lib/linter.js
+++ b/tests/lib/linter.js
@@ -1548,6 +1548,33 @@ describe("Linter", () => {
             assert.equal(messages.length, 0);
         });
 
+        it("should report a violation when the report is right before the comment", () => {
+            const code = " /* eslint-disable */ ";
+
+            linter.defineRule("checker", context => ({
+                Program() {
+                    context.report({ loc: { line: 1, column: 0 }, message: "foo" });
+                }
+            }));
+            const problems = linter.verify(code, { rules: { checker: "error" } });
+
+            assert.strictEqual(problems.length, 1);
+            assert.strictEqual(problems[0].message, "foo");
+        });
+
+        it("should not report a violation when the report is right at the start of the comment", () => {
+            const code = " /* eslint-disable */ ";
+
+            linter.defineRule("checker", context => ({
+                Program() {
+                    context.report({ loc: { line: 1, column: 1 }, message: "foo" });
+                }
+            }));
+            const problems = linter.verify(code, { rules: { checker: "error" } });
+
+            assert.strictEqual(problems.length, 0);
+        });
+
         it("rules should not change initial config", () => {
             const config = { rules: { "test-plugin/test-rule": 2 } };
             const codeA = "/*eslint test-plugin/test-rule: 0*/ var a = \"trigger violation\";";
@@ -1563,6 +1590,31 @@ describe("Linter", () => {
         });
     });
 
+    describe("when evaluating rules that monkeypatch Linter", () => {
+        it("should call `context._linter.report` appropriately", () => {
+            linter.defineRule("foo", context => ({
+                Program() {
+                    context.report({ loc: { line: 5, column: 4 }, message: "foo" });
+                }
+            }));
+
+            const spy = sandbox.spy((ruleId, severity, node, message) => {
+                assert.strictEqual(ruleId, "foo");
+                assert.strictEqual(severity, 2);
+                assert.deepEqual(node.loc, { start: { line: 5, column: 4 } });
+                assert.strictEqual(message, "foo");
+            });
+
+            linter.defineRule("bar", context => {
+                context._linter.report = spy; // eslint-disable-line no-underscore-dangle
+                return {};
+            });
+
+            linter.verify("foo", { rules: { foo: "error", bar: "error" } });
+            assert(spy.calledOnce);
+        });
+    });
+
     describe("when evaluating code with comments to enable and disable all reporting", () => {
         it("should report a violation", () => {
 
diff --git a/tests/lib/rules/generator-star-spacing.js b/tests/lib/rules/generator-star-spacing.js
index 34075485e..5108091ff 100644
--- a/tests/lib/rules/generator-star-spacing.js
+++ b/tests/lib/rules/generator-star-spacing.js
@@ -404,6 +404,56 @@ ruleTester.run("generator-star-spacing", rule, {
             options: [{ before: false, after: false }]
         },
 
+        // full configurability
+        {
+            code: "function * foo(){}",
+            options: [{ before: false, after: false, named: "both" }]
+        },
+        {
+            code: "var foo = function * (){};",
+            options: [{ before: false, after: false, anonymous: "both" }]
+        },
+        {
+            code: "class Foo { * foo(){} }",
+            options: [{ before: false, after: false, method: "both" }]
+        },
+        {
+            code: "var foo = { * foo(){} }",
+            options: [{ before: false, after: false, method: "both" }]
+        },
+        {
+            code: "var foo = { bar: function * () {} }",
+            options: [{ before: false, after: false, anonymous: "both" }]
+        },
+        {
+            code: "class Foo { static * foo(){} }",
+            options: [{ before: false, after: false, method: "both" }]
+        },
+
+        // default to top level "before"
+        {
+            code: "function *foo(){}",
+            options: [{ method: "both" }]
+        },
+
+        // don't apply unrelated override
+        {
+            code: "function*foo(){}",
+            options: [{ before: false, after: false, method: "both" }]
+        },
+
+        // ensure using object-type override works
+        {
+            code: "function * foo(){}",
+            options: [{ before: false, after: false, named: { before: true, after: true } }]
+        },
+
+        // unspecified option uses default
+        {
+            code: "function *foo(){}",
+            options: [{ before: false, after: false, named: { before: true } }]
+        },
+
         // https://github.com/eslint/eslint/issues/7101#issuecomment-246080531
         {
             code: "async function foo() { }",
@@ -1168,6 +1218,124 @@ ruleTester.run("generator-star-spacing", rule, {
                 message: "Unexpected space after *.",
                 type: "Punctuator"
             }]
+        },
+
+        // full configurability
+        {
+            code: "function*foo(){}",
+            output: "function * foo(){}",
+            options: [{ before: false, after: false, named: "both" }],
+            errors: [{
+                message: "Missing space before *.",
+                type: "Punctuator"
+            }, {
+                message: "Missing space after *.",
+                type: "Punctuator"
+            }]
+        },
+        {
+            code: "var foo = function*(){};",
+            output: "var foo = function * (){};",
+            options: [{ before: false, after: false, anonymous: "both" }],
+            errors: [{
+                message: "Missing space before *.",
+                type: "Punctuator"
+            }, {
+                message: "Missing space after *.",
+                type: "Punctuator"
+            }]
+        },
+        {
+            code: "class Foo { *foo(){} }",
+            output: "class Foo { * foo(){} }",
+            options: [{ before: false, after: false, method: "both" }],
+            errors: [{
+                message: "Missing space after *.",
+                type: "Punctuator"
+            }]
+        },
+        {
+            code: "var foo = { *foo(){} }",
+            output: "var foo = { * foo(){} }",
+            options: [{ before: false, after: false, method: "both" }],
+            errors: [{
+                message: "Missing space after *.",
+                type: "Punctuator"
+            }]
+        },
+        {
+            code: "var foo = { bar: function*() {} }",
+            output: "var foo = { bar: function * () {} }",
+            options: [{ before: false, after: false, anonymous: "both" }],
+            errors: [{
+                message: "Missing space before *.",
+                type: "Punctuator"
+            }, {
+                message: "Missing space after *.",
+                type: "Punctuator"
+            }]
+        },
+        {
+            code: "class Foo { static*foo(){} }",
+            output: "class Foo { static * foo(){} }",
+            options: [{ before: false, after: false, method: "both" }],
+            errors: [{
+                message: "Missing space before *.",
+                type: "Punctuator"
+            }, {
+                message: "Missing space after *.",
+                type: "Punctuator"
+            }]
+        },
+
+        // default to top level "before"
+        {
+            code: "function*foo(){}",
+            output: "function *foo(){}",
+            options: [{ method: "both" }],
+            errors: [{
+                message: "Missing space before *.",
+                type: "Punctuator"
+            }]
+        },
+
+        // don't apply unrelated override
+        {
+            code: "function * foo(){}",
+            output: "function*foo(){}",
+            options: [{ before: false, after: false, method: "both" }],
+            errors: [{
+                message: "Unexpected space before *.",
+                type: "Punctuator"
+            }, {
+                message: "Unexpected space after *.",
+                type: "Punctuator"
+            }]
+        },
+
+        // ensure using object-type override works
+        {
+            code: "function*foo(){}",
+            output: "function * foo(){}",
+            options: [{ before: false, after: false, named: { before: true, after: true } }],
+            errors: [{
+                message: "Missing space before *.",
+                type: "Punctuator"
+            }, {
+                message: "Missing space after *.",
+                type: "Punctuator"
+            }]
+        },
+
+        // unspecified option uses default
+        {
+            code: "function*foo(){}",
+            output: "function *foo(){}",
+            options: [{ before: false, after: false, named: { before: true } }],
+            errors: [{
+                message: "Missing space before *.",
+                type: "Punctuator"
+            }]
         }
 
     ]
diff --git a/tests/lib/rules/no-unused-vars.js b/tests/lib/rules/no-unused-vars.js
index 4e2849fe1..b7ccc3844 100644
--- a/tests/lib/rules/no-unused-vars.js
+++ b/tests/lib/rules/no-unused-vars.js
@@ -326,11 +326,37 @@ ruleTester.run("no-unused-vars", rule, {
         { code: "/*exported x*/ var { x, y } = z", parserOptions: { ecmaVersion: 6 }, errors: [assignedError("y")] },
 
         // ignore pattern
-        { code: "var _a; var b;", options: [{ vars: "all", varsIgnorePattern: "^_" }], errors: [{ message: "'b' is defined but never used.", line: 1, column: 13 }] },
-        { code: "var a; function foo() { var _b; var c_; } foo();", options: [{ vars: "local", varsIgnorePattern: "^_" }], errors: [{ message: "'c_' is defined but never used.", line: 1, column: 37 }] },
-        { code: "function foo(a, _b) { } foo();", options: [{ args: "all", argsIgnorePattern: "^_" }], errors: [{ message: "'a' is defined but never used.", line: 1, column: 14 }] },
-        { code: "function foo(a, _b, c) { return a; } foo();", options: [{ args: "after-used", argsIgnorePattern: "^_" }], errors: [{ message: "'c' is defined but never used.", line: 1, column: 21 }] },
-        { code: "var [ firstItemIgnored, secondItem ] = items;", options: [{ vars: "all", varsIgnorePattern: "[iI]gnored" }], parserOptions: { ecmaVersion: 6 }, errors: [{ message: "'secondItem' is assigned a value but never used.", line: 1, column: 25 }] },
+        {
+            code: "var _a; var b;",
+            options: [{ vars: "all", varsIgnorePattern: "^_" }],
+            errors: [{ message: "'b' is defined but never used. Allowed unused vars must match /^_/.", line: 1, column: 13 }]
+        },
+        {
+            code: "var a; function foo() { var _b; var c_; } foo();",
+            options: [{ vars: "local", varsIgnorePattern: "^_" }],
+            errors: [{ message: "'c_' is defined but never used. Allowed unused vars must match /^_/.", line: 1, column: 37 }]
+        },
+        {
+            code: "function foo(a, _b) { } foo();",
+            options: [{ args: "all", argsIgnorePattern: "^_" }],
+            errors: [{ message: "'a' is defined but never used. Allowed unused args must match /^_/.", line: 1, column: 14 }]
+        },
+        {
+            code: "function foo(a, _b, c) { return a; } foo();",
+            options: [{ args: "after-used", argsIgnorePattern: "^_" }],
+            errors: [{ message: "'c' is defined but never used. Allowed unused args must match /^_/.", line: 1, column: 21 }]
+        },
+        {
+            code: "function foo(_a) { } foo();",
+            options: [{ args: "all", argsIgnorePattern: "[iI]gnored" }],
+            errors: [{ message: "'_a' is defined but never used. Allowed unused args must match /[iI]gnored/.", line: 1, column: 14 }]
+        },
+        {
+            code: "var [ firstItemIgnored, secondItem ] = items;",
+            options: [{ vars: "all", varsIgnorePattern: "[iI]gnored" }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [{ message: "'secondItem' is assigned a value but never used. Allowed unused vars must match /[iI]gnored/.", line: 1, column: 25 }]
+        },
 
         // for-in loops (see #2342)
         { code: "(function(obj) { var name; for ( name in obj ) { i(); return; } })({});", errors: [{ message: "'name' is assigned a value but never used.", line: 1, column: 22 }] },
@@ -491,14 +517,14 @@ ruleTester.run("no-unused-vars", rule, {
         {
             code: "try{}catch(err){};",
             options: [{ caughtErrors: "all", caughtErrorsIgnorePattern: "^ignore" }],
-            errors: [{ message: "'err' is defined but never used." }]
+            errors: [{ message: "'err' is defined but never used. Allowed unused args must match /^ignore/." }]
         },
 
         // multiple try catch with one success
         {
             code: "try{}catch(ignoreErr){}try{}catch(err){};",
             options: [{ caughtErrors: "all", caughtErrorsIgnorePattern: "^ignore" }],
-            errors: [{ message: "'err' is defined but never used." }]
+            errors: [{ message: "'err' is defined but never used. Allowed unused args must match /^ignore/." }]
         },
 
         // multiple try catch both fail
@@ -506,8 +532,8 @@ ruleTester.run("no-unused-vars", rule, {
             code: "try{}catch(error){}try{}catch(err){};",
             options: [{ caughtErrors: "all", caughtErrorsIgnorePattern: "^ignore" }],
             errors: [
-                { message: "'error' is defined but never used." },
-                { message: "'err' is defined but never used." }
+                { message: "'error' is defined but never used. Allowed unused args must match /^ignore/." },
+                { message: "'err' is defined but never used. Allowed unused args must match /^ignore/." }
             ]
         },
 
@@ -578,25 +604,25 @@ ruleTester.run("no-unused-vars", rule, {
         {
             code: "(function(a, b, c) {})",
             options: [{ argsIgnorePattern: "c" }],
-            errors: [{ message: "'b' is defined but never used." }]
+            errors: [{ message: "'b' is defined but never used. Allowed unused args must match /c/." }]
         },
         {
             code: "(function(a, b, {c, d}) {})",
             options: [{ argsIgnorePattern: "[cd]" }],
             parserOptions: { ecmaVersion: 6 },
-            errors: [{ message: "'b' is defined but never used." }]
+            errors: [{ message: "'b' is defined but never used. Allowed unused args must match /[cd]/." }]
         },
         {
             code: "(function(a, b, {c, d}) {})",
             options: [{ argsIgnorePattern: "c" }],
             parserOptions: { ecmaVersion: 6 },
-            errors: [{ message: "'d' is defined but never used." }]
+            errors: [{ message: "'d' is defined but never used. Allowed unused args must match /c/." }]
         },
         {
             code: "(function(a, b, {c, d}) {})",
             options: [{ argsIgnorePattern: "d" }],
             parserOptions: { ecmaVersion: 6 },
-            errors: [{ message: "'c' is defined but never used." }]
+            errors: [{ message: "'c' is defined but never used. Allowed unused args must match /d/." }]
         },
         {
             code: "/*global\rfoo*/",
diff --git a/tests/lib/util/npm-util.js b/tests/lib/util/npm-util.js
index 466d4b839..fa9a0d199 100644
--- a/tests/lib/util/npm-util.js
+++ b/tests/lib/util/npm-util.js
@@ -170,7 +170,7 @@ describe("npmUtil", () => {
 
     describe("installSyncSaveDev()", () => {
         it("should invoke npm to install a single desired package", () => {
-            const stub = sandbox.stub(spawn, "sync");
+            const stub = sandbox.stub(spawn, "sync").returns({ stdout: "" });
 
             npmUtil.installSyncSaveDev("desired-package");
             assert(stub.calledOnce);
@@ -180,7 +180,7 @@ describe("npmUtil", () => {
         });
 
         it("should accept an array of packages to install", () => {
-            const stub = sandbox.stub(spawn, "sync");
+            const stub = sandbox.stub(spawn, "sync").returns({ stdout: "" });
 
             npmUtil.installSyncSaveDev(["first-package", "second-package"]);
             assert(stub.calledOnce);
@@ -188,6 +188,18 @@ describe("npmUtil", () => {
             assert.deepEqual(stub.firstCall.args[1], ["i", "--save-dev", "first-package", "second-package"]);
             stub.restore();
         });
+
+        it("should log an error message if npm throws ENOENT error", () => {
+            const logErrorStub = sandbox.stub(log, "error");
+            const npmUtilStub = sandbox.stub(spawn, "sync").returns({ error: { code: "ENOENT" } });
+
+            npmUtil.installSyncSaveDev("some-package");
+
+            assert(logErrorStub.calledOnce);
+
+            logErrorStub.restore();
+            npmUtilStub.restore();
+        });
     });
 
     describe("fetchPeerDependencies()", () => {
@@ -200,5 +212,15 @@ describe("npmUtil", () => {
             assert.deepEqual(stub.firstCall.args[1], ["show", "--json", "desired-package", "peerDependencies"]);
             stub.restore();
         });
+
+        it("should return null if npm throws ENOENT error", () => {
+            const stub = sandbox.stub(spawn, "sync").returns({ error: { code: "ENOENT" } });
+
+            const peerDependencies = npmUtil.fetchPeerDependencies("desired-package");
+
+            assert.isNull(peerDependencies);
+
+            stub.restore();
+        });
     });
 });