diff --git a/docs/rules/generator-star-spacing.md b/docs/rules/generator-star-spacing.md index 91fa1213bbd..efa7a5a4e26 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/rules/generator-star-spacing.js b/lib/rules/generator-star-spacing.js index 9836e4ead97..5c612022476 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/tests/lib/rules/generator-star-spacing.js b/tests/lib/rules/generator-star-spacing.js index 34075485eab..5108091ff61 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" + }] } ]