Repository: eslint/eslint
Pull request #9189 (2017-08-31 00:36:19)

Chore: avoid mutating report descriptors in report-translator
<!--
    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 helper functions in `report-translator` to avoid mutating their argument.

Previously, these helper functions were designed to mutate their argument to avoid repeatedly copying properties. However, if the helper functions only return the data that they relate to (e.g. the `message` property) rather than returning an entire descriptor, it's not necessary to copy any properties.

**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/lib/linter.js b/lib/linter.js
index f84b6e6ad..7b3117d92 100755
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -747,7 +747,6 @@ class Linter {
     constructor() {
         this.messages = [];
         this.currentConfig = null;
-        this.currentScopes = null;
         this.scopeManager = null;
         this.currentFilename = null;
         this.traverser = null;
@@ -766,7 +765,6 @@ class Linter {
     reset() {
         this.messages = [];
         this.currentConfig = null;
-        this.currentScopes = null;
         this.scopeManager = null;
         this.traverser = null;
         this.reportingConfig = [];
@@ -969,10 +967,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 +1059,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
                         });
                     }


Test patch:
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*/",