From 312a88f2230082d898b7d8d82f8af63cb352e55a Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 22 Nov 2019 19:02:37 +0100 Subject: [PATCH] New: Add grouped-accessor-pairs rule (fixes #12277) (#12331) * New: Add grouped-accessor-pairs rule (fixes #12277) * Update docs/rules/grouped-accessor-pairs.md Co-Authored-By: Jordan Harband * Fix JSDoc * Add related rules --- docs/rules/grouped-accessor-pairs.md | 326 ++++++++++++++++ lib/rules/grouped-accessor-pairs.js | 224 +++++++++++ lib/rules/index.js | 1 + tests/lib/rules/grouped-accessor-pairs.js | 440 ++++++++++++++++++++++ tools/rule-types.json | 1 + 5 files changed, 992 insertions(+) create mode 100644 docs/rules/grouped-accessor-pairs.md create mode 100644 lib/rules/grouped-accessor-pairs.js create mode 100644 tests/lib/rules/grouped-accessor-pairs.js diff --git a/docs/rules/grouped-accessor-pairs.md b/docs/rules/grouped-accessor-pairs.md new file mode 100644 index 00000000000..60848d1d0aa --- /dev/null +++ b/docs/rules/grouped-accessor-pairs.md @@ -0,0 +1,326 @@ +# Require grouped accessor pairs in object literals and classes (grouped-accessor-pairs) + +A getter and setter for the same property don't necessarily have to be defined adjacent to each other. + +For example, the following statements would create the same object: + +```js +var o = { + get a() { + return this.val; + }, + set a(value) { + this.val = value; + }, + b: 1 +}; + +var o = { + get a() { + return this.val; + }, + b: 1, + set a(value) { + this.val = value; + } +}; +``` + +While it is allowed to define the pair for a getter or a setter anywhere in an object or class definition, it's considered a best practice to group accessor functions for the same property. + +In other words, if a property has a getter and a setter, the setter should be defined right after the getter, or vice versa. + +## Rule Details + +This rule requires grouped definitions of accessor functions for the same property in object literals, class declarations and class expressions. + +Optionally, this rule can also enforce consistent order (`getBeforeSet` or `setBeforeGet`). + +This rule does not enforce the existence of the pair for a getter or a setter. See [accessor-pairs](accessor-pairs.md) if you also want to enforce getter/setter pairs. + +Examples of **incorrect** code for this rule: + +```js +/*eslint grouped-accessor-pairs: "error"*/ + +var foo = { + get a() { + return this.val; + }, + b: 1, + set a(value) { + this.val = value; + } +}; + +var bar = { + set b(value) { + this.val = value; + }, + a: 1, + get b() { + return this.val; + } +} + +class Foo { + set a(value) { + this.val = value; + } + b(){} + get a() { + return this.val; + } +} + +const Bar = class { + static get a() { + return this.val; + } + b(){} + static set a(value) { + this.val = value; + } +} +``` + +Examples of **correct** code for this rule: + +```js +/*eslint grouped-accessor-pairs: "error"*/ + +var foo = { + get a() { + return this.val; + }, + set a(value) { + this.val = value; + }, + b: 1 +}; + +var bar = { + set b(value) { + this.val = value; + }, + get b() { + return this.val; + }, + a: 1 +} + +class Foo { + set a(value) { + this.val = value; + } + get a() { + return this.val; + } + b(){} +} + +const Bar = class { + static get a() { + return this.val; + } + static set a(value) { + this.val = value; + } + b(){} +} +``` + +## Options + +This rule has a string option: + +* `"anyOrder"` (default) does not enforce order. +* `"getBeforeSet"` if a property has both getter and setter, requires the getter to be defined before the setter. +* `"setBeforeGet"` if a property has both getter and setter, requires the setter to be defined before the getter. + +### getBeforeSet + +Examples of **incorrect** code for this rule with the `"getBeforeSet"` option: + +```js +/*eslint grouped-accessor-pairs: ["error", "getBeforeSet"]*/ + +var foo = { + set a(value) { + this.val = value; + }, + get a() { + return this.val; + } +}; + +class Foo { + set a(value) { + this.val = value; + } + get a() { + return this.val; + } +} + +const Bar = class { + static set a(value) { + this.val = value; + } + static get a() { + return this.val; + } +} +``` + +Examples of **correct** code for this rule with the `"getBeforeSet"` option: + +```js +/*eslint grouped-accessor-pairs: ["error", "getBeforeSet"]*/ + +var foo = { + get a() { + return this.val; + }, + set a(value) { + this.val = value; + } +}; + +class Foo { + get a() { + return this.val; + } + set a(value) { + this.val = value; + } +} + +const Bar = class { + static get a() { + return this.val; + } + static set a(value) { + this.val = value; + } +} +``` + +### setBeforeGet + +Examples of **incorrect** code for this rule with the `"setBeforeGet"` option: + +```js +/*eslint grouped-accessor-pairs: ["error", "setBeforeGet"]*/ + +var foo = { + get a() { + return this.val; + }, + set a(value) { + this.val = value; + } +}; + +class Foo { + get a() { + return this.val; + } + set a(value) { + this.val = value; + } +} + +const Bar = class { + static get a() { + return this.val; + } + static set a(value) { + this.val = value; + } +} +``` + +Examples of **correct** code for this rule with the `"setBeforeGet"` option: + +```js +/*eslint grouped-accessor-pairs: ["error", "setBeforeGet"]*/ + +var foo = { + set a(value) { + this.val = value; + }, + get a() { + return this.val; + } +}; + +class Foo { + set a(value) { + this.val = value; + } + get a() { + return this.val; + } +} + +const Bar = class { + static set a(value) { + this.val = value; + } + static get a() { + return this.val; + } +} +``` + +## Known Limitations + +Due to the limits of static analysis, this rule does not account for possible side effects and in certain cases +might require or miss to require grouping or order for getters/setters that have a computed key, like in the following example: + +```js +/*eslint grouped-accessor-pairs: "error"*/ + +var a = 1; + +// false warning (false positive) +var foo = { + get [a++]() { + return this.val; + }, + b: 1, + set [a++](value) { + this.val = value; + } +}; + +// missed warning (false negative) +var bar = { + get [++a]() { + return this.val; + }, + b: 1, + set [a](value) { + this.val = value; + } +}; +``` + +Also, this rule does not report any warnings for properties that have duplicate getters or setters. + +See [no-dupe-keys](no-dupe-keys.md) if you also want to disallow duplicate keys in object literals. + +See [no-dupe-class-members](no-dupe-class-members.md) if you also want to disallow duplicate names in class definitions. + +## Further Reading + +* [Object Setters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set) +* [Object Getters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) +* [Classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) + +## Related Rules + +* [accessor-pairs](accessor-pairs.md) +* [no-dupe-keys](no-dupe-keys.md) +* [no-dupe-class-members](no-dupe-class-members.md) diff --git a/lib/rules/grouped-accessor-pairs.js b/lib/rules/grouped-accessor-pairs.js new file mode 100644 index 00000000000..a790f83750b --- /dev/null +++ b/lib/rules/grouped-accessor-pairs.js @@ -0,0 +1,224 @@ +/** + * @fileoverview Rule to require grouped accessor pairs in object literals and classes + * @author Milos Djermanovic + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("./utils/ast-utils"); + +//------------------------------------------------------------------------------ +// Typedefs +//------------------------------------------------------------------------------ + +/** + * Property name if it can be computed statically, otherwise the list of the tokens of the key node. + * @typedef {string|Token[]} Key + */ + +/** + * Accessor nodes with the same key. + * @typedef {Object} AccessorData + * @property {Key} key Accessor's key + * @property {ASTNode[]} getters List of getter nodes. + * @property {ASTNode[]} setters List of setter nodes. + */ + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether or not the given lists represent the equal tokens in the same order. + * Tokens are compared by their properties, not by instance. + * @param {Token[]} left First list of tokens. + * @param {Token[]} right Second list of tokens. + * @returns {boolean} `true` if the lists have same tokens. + */ +function areEqualTokenLists(left, right) { + if (left.length !== right.length) { + return false; + } + + for (let i = 0; i < left.length; i++) { + const leftToken = left[i], + rightToken = right[i]; + + if (leftToken.type !== rightToken.type || leftToken.value !== rightToken.value) { + return false; + } + } + + return true; +} + +/** + * Checks whether or not the given keys are equal. + * @param {Key} left First key. + * @param {Key} right Second key. + * @returns {boolean} `true` if the keys are equal. + */ +function areEqualKeys(left, right) { + if (typeof left === "string" && typeof right === "string") { + + // Statically computed names. + return left === right; + } + if (Array.isArray(left) && Array.isArray(right)) { + + // Token lists. + return areEqualTokenLists(left, right); + } + + return false; +} + +/** + * Checks whether or not a given node is of an accessor kind ('get' or 'set'). + * @param {ASTNode} node A node to check. + * @returns {boolean} `true` if the node is of an accessor kind. + */ +function isAccessorKind(node) { + return node.kind === "get" || node.kind === "set"; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: "suggestion", + + docs: { + description: "require grouped accessor pairs in object literals and classes", + category: "Best Practices", + recommended: false, + url: "https://eslint.org/docs/rules/grouped-accessor-pairs" + }, + + schema: [ + { + enum: ["anyOrder", "getBeforeSet", "setBeforeGet"] + } + ], + + messages: { + notGrouped: "Accessor pair {{ formerName }} and {{ latterName }} should be grouped.", + invalidOrder: "Expected {{ latterName }} to be before {{ formerName }}." + } + }, + + create(context) { + const order = context.options[0] || "anyOrder"; + const sourceCode = context.getSourceCode(); + + /** + * Reports the given accessor pair. + * @param {string} messageId messageId to report. + * @param {ASTNode} formerNode getter/setter node that is defined before `latterNode`. + * @param {ASTNode} latterNode getter/setter node that is defined after `formerNode`. + * @returns {void} + * @private + */ + function report(messageId, formerNode, latterNode) { + context.report({ + node: latterNode, + messageId, + loc: astUtils.getFunctionHeadLoc(latterNode.value, sourceCode), + data: { + formerName: astUtils.getFunctionNameWithKind(formerNode.value), + latterName: astUtils.getFunctionNameWithKind(latterNode.value) + } + }); + } + + /** + * Creates a new `AccessorData` object for the given getter or setter node. + * @param {ASTNode} node A getter or setter node. + * @returns {AccessorData} New `AccessorData` object that contains the given node. + * @private + */ + function createAccessorData(node) { + const name = astUtils.getStaticPropertyName(node); + const key = (name !== null) ? name : sourceCode.getTokens(node.key); + + return { + key, + getters: node.kind === "get" ? [node] : [], + setters: node.kind === "set" ? [node] : [] + }; + } + + /** + * Merges the given `AccessorData` object into the given accessors list. + * @param {AccessorData[]} accessors The list to merge into. + * @param {AccessorData} accessorData The object to merge. + * @returns {AccessorData[]} The same instance with the merged object. + * @private + */ + function mergeAccessorData(accessors, accessorData) { + const equalKeyElement = accessors.find(a => areEqualKeys(a.key, accessorData.key)); + + if (equalKeyElement) { + equalKeyElement.getters.push(...accessorData.getters); + equalKeyElement.setters.push(...accessorData.setters); + } else { + accessors.push(accessorData); + } + + return accessors; + } + + /** + * Checks accessor pairs in the given list of nodes. + * @param {ASTNode[]} nodes The list to check. + * @param {Function} shouldCheck – Predicate that returns `true` if the node should be checked. + * @returns {void} + * @private + */ + function checkList(nodes, shouldCheck) { + const accessors = nodes + .filter(shouldCheck) + .filter(isAccessorKind) + .map(createAccessorData) + .reduce(mergeAccessorData, []); + + for (const { getters, setters } of accessors) { + + // Don't report accessor properties that have duplicate getters or setters. + if (getters.length === 1 && setters.length === 1) { + const [getter] = getters, + [setter] = setters, + getterIndex = nodes.indexOf(getter), + setterIndex = nodes.indexOf(setter), + formerNode = getterIndex < setterIndex ? getter : setter, + latterNode = getterIndex < setterIndex ? setter : getter; + + if (Math.abs(getterIndex - setterIndex) > 1) { + report("notGrouped", formerNode, latterNode); + } else if ( + (order === "getBeforeSet" && getterIndex > setterIndex) || + (order === "setBeforeGet" && getterIndex < setterIndex) + ) { + report("invalidOrder", formerNode, latterNode); + } + } + } + } + + return { + ObjectExpression(node) { + checkList(node.properties, n => n.type === "Property"); + }, + ClassBody(node) { + checkList(node.body, n => n.type === "MethodDefinition" && !n.static); + checkList(node.body, n => n.type === "MethodDefinition" && n.static); + } + }; + } +}; diff --git a/lib/rules/index.js b/lib/rules/index.js index 5e75c2366b7..d3fbe412080 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -52,6 +52,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({ "generator-star-spacing": () => require("./generator-star-spacing"), "getter-return": () => require("./getter-return"), "global-require": () => require("./global-require"), + "grouped-accessor-pairs": () => require("./grouped-accessor-pairs"), "guard-for-in": () => require("./guard-for-in"), "handle-callback-err": () => require("./handle-callback-err"), "id-blacklist": () => require("./id-blacklist"), diff --git a/tests/lib/rules/grouped-accessor-pairs.js b/tests/lib/rules/grouped-accessor-pairs.js new file mode 100644 index 00000000000..c395e6ec9b4 --- /dev/null +++ b/tests/lib/rules/grouped-accessor-pairs.js @@ -0,0 +1,440 @@ +/** + * @fileoverview Tests for the grouped-accessor-pairs rule + * @author Milos Djermanovic + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/grouped-accessor-pairs"); +const { RuleTester } = require("../../../lib/rule-tester"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } }); + +ruleTester.run("grouped-accessor-pairs", rule, { + valid: [ + + // no accessors + "({})", + "({ a })", + "({ a(){}, b(){}, a(){} })", + "({ a: 1, b: 2 })", + "({ a, ...b, c: 1 })", + "({ a, b, ...a })", + "({ a: 1, [b]: 2, a: 3, [b]: 4 })", + "({ a: function get(){}, b, a: function set(foo){} })", + "({ get(){}, a, set(){} })", + "class A {}", + "(class { a(){} })", + "class A { a(){} [b](){} a(){} [b](){} }", + "(class { a(){} b(){} static a(){} static b(){} })", + "class A { get(){} a(){} set(){} }", + + // no accessor pairs + "({ get a(){} })", + "({ set a(foo){} })", + "({ a: 1, get b(){}, c, ...d })", + "({ get a(){}, get b(){}, set c(foo){}, set d(foo){} })", + "({ get a(){}, b: 1, set c(foo){} })", + "({ set a(foo){}, b: 1, a: 2 })", + "({ get a(){}, b: 1, a })", + "({ set a(foo){}, b: 1, a(){} })", + "({ get a(){}, b: 1, set [a](foo){} })", + "({ set a(foo){}, b: 1, get 'a '(){} })", + "({ get a(){}, b: 1, ...a })", + "({ set a(foo){}, b: 1 }, { get a(){} })", + "({ get a(){}, b: 1, ...{ set a(foo){} } })", + { + code: "({ set a(foo){}, get b(){} })", + options: ["getBeforeSet"] + }, + { + code: "({ get a(){}, set b(foo){} })", + options: ["setBeforeGet"] + }, + "class A { get a(){} }", + "(class { set a(foo){} })", + "class A { static set a(foo){} }", + "(class { static get a(){} })", + "class A { a(){} set b(foo){} c(){} }", + "(class { a(){} get b(){} c(){} })", + "class A { get a(){} static get b(){} set c(foo){} static set d(bar){} }", + "(class { get a(){} b(){} a(foo){} })", + "class A { static set a(foo){} b(){} static a(){} }", + "(class { get a(){} static b(){} set [a](foo){} })", + "class A { static set a(foo){} b(){} static get ' a'(){} }", + "(class { set a(foo){} b(){} static get a(){} })", + "class A { static set a(foo){} b(){} get a(){} }", + "(class { get a(){} }, class { b(){} set a(foo){} })", + + // correct grouping + "({ get a(){}, set a(foo){} })", + "({ a: 1, set b(foo){}, get b(){}, c: 2 })", + "({ get a(){}, set a(foo){}, set b(bar){}, get b(){} })", + "({ get [a](){}, set [a](foo){} })", + "({ set a(foo){}, get 'a'(){} })", + "({ a: 1, b: 2, get a(){}, set a(foo){}, c: 3, a: 4 })", + "({ get a(){}, set a(foo){}, set b(bar){} })", + "({ get a(){}, get b(){}, set b(bar){} })", + "class A { get a(){} set a(foo){} }", + "(class { set a(foo){} get a(){} })", + "class A { static set a(foo){} static get a(){} }", + "(class { static get a(){} static set a(foo){} })", + "class A { a(){} set b(foo){} get b(){} c(){} get d(){} set d(bar){} }", + "(class { set a(foo){} get a(){} get b(){} set b(bar){} })", + "class A { static set [a](foo){} static get [a](){} }", + "(class { get a(){} set [`a`](foo){} })", + "class A { static get a(){} static set a(foo){} set a(bar){} static get a(){} }", + "(class { static get a(){} get a(){} set a(foo){} })", + + // correct order + { + code: "({ get a(){}, set a(foo){} })", + options: ["anyOrder"] + }, + { + code: "({ set a(foo){}, get a(){} })", + options: ["anyOrder"] + }, + { + code: "({ get a(){}, set a(foo){} })", + options: ["getBeforeSet"] + }, + { + code: "({ set a(foo){}, get a(){} })", + options: ["setBeforeGet"] + }, + { + code: "class A { get a(){} set a(foo){} }", + options: ["anyOrder"] + }, + { + code: "(class { set a(foo){} get a(){} })", + options: ["anyOrder"] + }, + { + code: "class A { get a(){} set a(foo){} }", + options: ["getBeforeSet"] + }, + { + code: "(class { static set a(foo){} static get a(){} })", + options: ["setBeforeGet"] + }, + + // ignores properties with duplicate getters/setters + "({ get a(){}, b: 1, get a(){} })", + "({ set a(foo){}, b: 1, set a(foo){} })", + "({ get a(){}, b: 1, set a(foo){}, c: 2, get a(){} })", + "({ set a(foo){}, b: 1, set 'a'(bar){}, c: 2, get a(){} })", + "class A { get [a](){} b(){} get [a](){} c(){} set [a](foo){} }", + "(class { static set a(foo){} b(){} static get a(){} static c(){} static set a(bar){} })" + ], + + invalid: [ + + // basic grouping tests with full messages + { + code: "({ get a(){}, b:1, set a(foo){} })", + errors: [{ message: "Accessor pair getter 'a' and setter 'a' should be grouped.", type: "Property", column: 20 }] + }, + { + code: "({ set 'abc'(foo){}, b:1, get 'abc'(){} })", + errors: [{ message: "Accessor pair setter 'abc' and getter 'abc' should be grouped.", type: "Property", column: 27 }] + }, + { + code: "({ get [a](){}, b:1, set [a](foo){} })", + errors: [{ message: "Accessor pair getter and setter should be grouped.", type: "Property", column: 22 }] + }, + { + code: "class A { get abc(){} b(){} set abc(foo){} }", + errors: [{ message: "Accessor pair getter 'abc' and setter 'abc' should be grouped.", type: "MethodDefinition", column: 29 }] + }, + { + code: "(class { set abc(foo){} b(){} get abc(){} })", + errors: [{ message: "Accessor pair setter 'abc' and getter 'abc' should be grouped.", type: "MethodDefinition", column: 31 }] + }, + { + code: "class A { static set a(foo){} b(){} static get a(){} }", + errors: [{ message: "Accessor pair static setter 'a' and static getter 'a' should be grouped.", type: "MethodDefinition", column: 37 }] + }, + { + code: "(class { static get 123(){} b(){} static set 123(foo){} })", + errors: [{ message: "Accessor pair static getter '123' and static setter '123' should be grouped.", type: "MethodDefinition", column: 35 }] + }, + { + code: "class A { static get [a](){} b(){} static set [a](foo){} }", + errors: [{ message: "Accessor pair static getter and static setter should be grouped.", type: "MethodDefinition", column: 36 }] + }, + + // basic ordering tests with full messages + { + code: "({ set a(foo){}, get a(){} })", + options: ["getBeforeSet"], + errors: [{ message: "Expected getter 'a' to be before setter 'a'.", type: "Property", column: 18 }] + }, + { + code: "({ get 123(){}, set 123(foo){} })", + options: ["setBeforeGet"], + errors: [{ message: "Expected setter '123' to be before getter '123'.", type: "Property", column: 17 }] + }, + { + code: "({ get [a](){}, set [a](foo){} })", + options: ["setBeforeGet"], + errors: [{ message: "Expected setter to be before getter.", type: "Property", column: 17 }] + }, + { + code: "class A { set abc(foo){} get abc(){} }", + options: ["getBeforeSet"], + errors: [{ message: "Expected getter 'abc' to be before setter 'abc'.", type: "MethodDefinition", column: 26 }] + }, + { + code: "(class { get [`abc`](){} set [`abc`](foo){} })", + options: ["setBeforeGet"], + errors: [{ message: "Expected setter 'abc' to be before getter 'abc'.", type: "MethodDefinition", column: 26 }] + }, + { + code: "class A { static get a(){} static set a(foo){} }", + options: ["setBeforeGet"], + errors: [{ message: "Expected static setter 'a' to be before static getter 'a'.", type: "MethodDefinition", column: 28 }] + }, + { + code: "(class { static set 'abc'(foo){} static get 'abc'(){} })", + options: ["getBeforeSet"], + errors: [{ message: "Expected static getter 'abc' to be before static setter 'abc'.", type: "MethodDefinition", column: 34 }] + }, + { + code: "class A { static set [abc](foo){} static get [abc](){} }", + options: ["getBeforeSet"], + errors: [{ message: "Expected static getter to be before static setter.", type: "MethodDefinition", column: 35 }] + }, + + // ordering option does not affect the grouping check + { + code: "({ get a(){}, b: 1, set a(foo){} })", + options: ["anyOrder"], + errors: [{ messageId: "notGrouped", data: { formerName: "getter 'a'", latterName: "setter 'a'" }, type: "Property" }] + }, + { + code: "({ get a(){}, b: 1, set a(foo){} })", + options: ["setBeforeGet"], + errors: [{ messageId: "notGrouped", data: { formerName: "getter 'a'", latterName: "setter 'a'" }, type: "Property" }] + }, + { + code: "({ get a(){}, b: 1, set a(foo){} })", + options: ["getBeforeSet"], + errors: [{ messageId: "notGrouped", data: { formerName: "getter 'a'", latterName: "setter 'a'" }, type: "Property" }] + }, + { + code: "class A { set a(foo){} b(){} get a(){} }", + options: ["getBeforeSet"], + errors: [{ messageId: "notGrouped", data: { formerName: "setter 'a'", latterName: "getter 'a'" }, type: "MethodDefinition" }] + }, + { + code: "(class { static set a(foo){} b(){} static get a(){} })", + options: ["setBeforeGet"], + errors: [{ messageId: "notGrouped", data: { formerName: "static setter 'a'", latterName: "static getter 'a'" }, type: "MethodDefinition" }] + }, + + // various kinds of keys + { + code: "({ get 'abc'(){}, d(){}, set 'abc'(foo){} })", + errors: [{ messageId: "notGrouped", data: { formerName: "getter 'abc'", latterName: "setter 'abc'" }, type: "Property" }] + }, + { + code: "({ set ''(foo){}, get [''](){} })", + options: ["getBeforeSet"], + errors: [{ messageId: "invalidOrder", data: { formerName: "setter ''", latterName: "getter ''" }, type: "Property" }] + }, + { + code: "class A { set abc(foo){} get 'abc'(){} }", + options: ["getBeforeSet"], + errors: [{ messageId: "invalidOrder", data: { formerName: "setter 'abc'", latterName: "getter 'abc'" }, type: "MethodDefinition" }] + }, + { + code: "(class { set [`abc`](foo){} get abc(){} })", + options: ["getBeforeSet"], + errors: [{ messageId: "invalidOrder", data: { formerName: "setter 'abc'", latterName: "getter 'abc'" }, type: "MethodDefinition" }] + }, + { + code: "({ set ['abc'](foo){}, get [`abc`](){} })", + options: ["getBeforeSet"], + errors: [{ messageId: "invalidOrder", data: { formerName: "setter 'abc'", latterName: "getter 'abc'" }, type: "Property" }] + }, + { + code: "({ set 123(foo){}, get [123](){} })", + options: ["getBeforeSet"], + errors: [{ messageId: "invalidOrder", data: { formerName: "setter '123'", latterName: "getter '123'" }, type: "Property" }] + }, + { + code: "class A { static set '123'(foo){} static get 123(){} }", + options: ["getBeforeSet"], + errors: [{ messageId: "invalidOrder", data: { formerName: "static setter '123'", latterName: "static getter '123'" }, type: "MethodDefinition" }] + }, + { + code: "(class { set [a+b](foo){} get [a+b](){} })", + options: ["getBeforeSet"], + errors: [{ messageId: "invalidOrder", data: { formerName: "setter", latterName: "getter" }, type: "MethodDefinition" }] + }, + { + code: "({ set [f(a)](foo){}, get [f(a)](){} })", + options: ["getBeforeSet"], + errors: [{ messageId: "invalidOrder", data: { formerName: "setter", latterName: "getter" }, type: "Property" }] + }, + + // multiple invalid + { + code: "({ get a(){}, b: 1, set a(foo){}, set c(foo){}, d(){}, get c(){} })", + errors: [ + { messageId: "notGrouped", data: { formerName: "getter 'a'", latterName: "setter 'a'" }, type: "Property", column: 21 }, + { messageId: "notGrouped", data: { formerName: "setter 'c'", latterName: "getter 'c'" }, type: "Property", column: 56 } + ] + }, + { + code: "({ get a(){}, set b(foo){}, set a(bar){}, get b(){} })", + errors: [ + { messageId: "notGrouped", data: { formerName: "getter 'a'", latterName: "setter 'a'" }, type: "Property", column: 29 }, + { messageId: "notGrouped", data: { formerName: "setter 'b'", latterName: "getter 'b'" }, type: "Property", column: 43 } + ] + }, + { + code: "({ get a(){}, set [a](foo){}, set a(bar){}, get [a](){} })", + errors: [ + { messageId: "notGrouped", data: { formerName: "getter 'a'", latterName: "setter 'a'" }, type: "Property", column: 31 }, + { messageId: "notGrouped", data: { formerName: "setter", latterName: "getter" }, type: "Property", column: 45 } + ] + }, + { + code: "({ a(){}, set b(foo){}, ...c, get b(){}, set c(bar){}, get c(){} })", + options: ["getBeforeSet"], + errors: [ + { messageId: "notGrouped", data: { formerName: "setter 'b'", latterName: "getter 'b'" }, type: "Property", column: 31 }, + { messageId: "invalidOrder", data: { formerName: "setter 'c'", latterName: "getter 'c'" }, type: "Property", column: 56 } + ] + }, + { + code: "({ set [a](foo){}, get [a](){}, set [-a](bar){}, get [-a](){} })", + options: ["getBeforeSet"], + errors: [ + { messageId: "invalidOrder", data: { formerName: "setter", latterName: "getter" }, type: "Property", column: 20 }, + { messageId: "invalidOrder", data: { formerName: "setter", latterName: "getter" }, type: "Property", column: 50 } + ] + }, + { + code: "class A { get a(){} constructor (){} set a(foo){} get b(){} static c(){} set b(bar){} }", + errors: [ + { messageId: "notGrouped", data: { formerName: "getter 'a'", latterName: "setter 'a'" }, type: "MethodDefinition", column: 38 }, + { messageId: "notGrouped", data: { formerName: "getter 'b'", latterName: "setter 'b'" }, type: "MethodDefinition", column: 74 } + ] + }, + { + code: "(class { set a(foo){} static get a(){} get a(){} static set a(bar){} })", + errors: [ + { messageId: "notGrouped", data: { formerName: "setter 'a'", latterName: "getter 'a'" }, type: "MethodDefinition", column: 40 }, + { messageId: "notGrouped", data: { formerName: "static getter 'a'", latterName: "static setter 'a'" }, type: "MethodDefinition", column: 50 } + ] + }, + { + code: "class A { get a(){} set a(foo){} static get b(){} static set b(bar){} }", + options: ["setBeforeGet"], + errors: [ + { messageId: "invalidOrder", data: { formerName: "getter 'a'", latterName: "setter 'a'" }, type: "MethodDefinition", column: 21 }, + { messageId: "invalidOrder", data: { formerName: "static getter 'b'", latterName: "static setter 'b'" }, type: "MethodDefinition", column: 51 } + ] + }, + { + code: "(class { set [a+b](foo){} get [a-b](){} get [a+b](){} set [a-b](bar){} })", + errors: [ + { messageId: "notGrouped", data: { formerName: "setter", latterName: "getter" }, type: "MethodDefinition", column: 41 }, + { messageId: "notGrouped", data: { formerName: "getter", latterName: "setter" }, type: "MethodDefinition", column: 55 } + ] + }, + + // combinations of valid and invalid + { + code: "({ get a(){}, set a(foo){}, get b(){}, c: function(){}, set b(bar){} })", + errors: [{ messageId: "notGrouped", data: { formerName: "getter 'b'", latterName: "setter 'b'" }, type: "Property", column: 57 }] + }, + { + code: "({ get a(){}, get b(){}, set a(foo){} })", + errors: [{ messageId: "notGrouped", data: { formerName: "getter 'a'", latterName: "setter 'a'" }, type: "Property", column: 26 }] + }, + { + code: "({ set a(foo){}, get [a](){}, get a(){} })", + errors: [{ messageId: "notGrouped", data: { formerName: "setter 'a'", latterName: "getter 'a'" }, type: "Property", column: 31 }] + }, + { + code: "({ set [a](foo){}, set a(bar){}, get [a](){} })", + errors: [{ messageId: "notGrouped", data: { formerName: "setter", latterName: "getter" }, type: "Property", column: 34 }] + }, + { + code: "({ get a(){}, set a(foo){}, set b(bar){}, get b(){} })", + options: ["getBeforeSet"], + errors: [{ messageId: "invalidOrder", data: { formerName: "setter 'b'", latterName: "getter 'b'" }, type: "Property", column: 43 }] + }, + { + code: "class A { get a(){} static set b(foo){} static get b(){} set a(foo){} }", + errors: [{ messageId: "notGrouped", data: { formerName: "getter 'a'", latterName: "setter 'a'" }, type: "MethodDefinition", column: 58 }] + }, + { + code: "(class { static get a(){} set a(foo){} static set a(bar){} })", + errors: [{ messageId: "notGrouped", data: { formerName: "static getter 'a'", latterName: "static setter 'a'" }, type: "MethodDefinition", column: 40 }] + }, + { + code: "class A { set a(foo){} get a(){} static get a(){} static set a(bar){} }", + options: ["setBeforeGet"], + errors: [{ messageId: "invalidOrder", data: { formerName: "static getter 'a'", latterName: "static setter 'a'" }, type: "MethodDefinition", column: 51 }] + }, + + // non-accessor duplicates do not affect this rule + { + code: "({ get a(){}, a: 1, set a(foo){} })", + errors: [{ messageId: "notGrouped", data: { formerName: "getter 'a'", latterName: "setter 'a'" }, type: "Property", column: 21 }] + }, + { + code: "({ a(){}, set a(foo){}, get a(){} })", + options: ["getBeforeSet"], + errors: [{ messageId: "invalidOrder", data: { formerName: "setter 'a'", latterName: "getter 'a'" }, type: "Property", column: 25 }] + }, + { + code: "class A { get a(){} a(){} set a(foo){} }", + errors: [{ messageId: "notGrouped", data: { formerName: "getter 'a'", latterName: "setter 'a'" }, type: "MethodDefinition", column: 27 }] + }, + + // full location tests + { + code: "({ get a(){},\n b: 1,\n set a(foo){}\n})", + errors: [ + { + messageId: "notGrouped", + data: { formerName: "getter 'a'", latterName: "setter 'a'" }, + type: "Property", + line: 3, + column: 5, + endLine: 3, + endColumn: 10 + } + ] + }, + { + code: "class A { static set a(foo){} b(){} static get \n a(){}\n}", + errors: [ + { + messageId: "notGrouped", + data: { formerName: "static setter 'a'", latterName: "static getter 'a'" }, + type: "MethodDefinition", + line: 1, + column: 37, + endLine: 2, + endColumn: 3 + } + ] + } + ] +}); diff --git a/tools/rule-types.json b/tools/rule-types.json index d4e2da6ddd9..7b5789fbe8d 100644 --- a/tools/rule-types.json +++ b/tools/rule-types.json @@ -39,6 +39,7 @@ "generator-star-spacing": "layout", "getter-return": "problem", "global-require": "suggestion", + "grouped-accessor-pairs": "suggestion", "guard-for-in": "suggestion", "handle-callback-err": "suggestion", "id-blacklist": "suggestion",