Spaces:
Runtime error
Runtime error
| /** | |
| * @fileoverview Rule to enforce getter and setter pairs in objects and classes. | |
| * @author Gyandeep Singh | |
| */ | |
| ; | |
| //------------------------------------------------------------------------------ | |
| // 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"; | |
| } | |
| /** | |
| * Checks whether or not a given node is an argument of a specified method call. | |
| * @param {ASTNode} node A node to check. | |
| * @param {number} index An expected index of the node in arguments. | |
| * @param {string} object An expected name of the object of the method. | |
| * @param {string} property An expected name of the method. | |
| * @returns {boolean} `true` if the node is an argument of the specified method call. | |
| */ | |
| function isArgumentOfMethodCall(node, index, object, property) { | |
| const parent = node.parent; | |
| return ( | |
| parent.type === "CallExpression" && | |
| astUtils.isSpecificMemberAccess(parent.callee, object, property) && | |
| parent.arguments[index] === node | |
| ); | |
| } | |
| /** | |
| * Checks whether or not a given node is a property descriptor. | |
| * @param {ASTNode} node A node to check. | |
| * @returns {boolean} `true` if the node is a property descriptor. | |
| */ | |
| function isPropertyDescriptor(node) { | |
| // Object.defineProperty(obj, "foo", {set: ...}) | |
| if ( | |
| isArgumentOfMethodCall(node, 2, "Object", "defineProperty") || | |
| isArgumentOfMethodCall(node, 2, "Reflect", "defineProperty") | |
| ) { | |
| return true; | |
| } | |
| /* | |
| * Object.defineProperties(obj, {foo: {set: ...}}) | |
| * Object.create(proto, {foo: {set: ...}}) | |
| */ | |
| const grandparent = node.parent.parent; | |
| return ( | |
| grandparent.type === "ObjectExpression" && | |
| (isArgumentOfMethodCall(grandparent, 1, "Object", "create") || | |
| isArgumentOfMethodCall( | |
| grandparent, | |
| 1, | |
| "Object", | |
| "defineProperties", | |
| )) | |
| ); | |
| } | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
| /** @type {import('../types').Rule.RuleModule} */ | |
| module.exports = { | |
| meta: { | |
| type: "suggestion", | |
| defaultOptions: [ | |
| { | |
| enforceForTSTypes: false, | |
| enforceForClassMembers: true, | |
| getWithoutSet: false, | |
| setWithoutGet: true, | |
| }, | |
| ], | |
| docs: { | |
| description: | |
| "Enforce getter and setter pairs in objects and classes", | |
| recommended: false, | |
| url: "https://eslint.org/docs/latest/rules/accessor-pairs", | |
| }, | |
| schema: [ | |
| { | |
| type: "object", | |
| properties: { | |
| getWithoutSet: { | |
| type: "boolean", | |
| }, | |
| setWithoutGet: { | |
| type: "boolean", | |
| }, | |
| enforceForClassMembers: { | |
| type: "boolean", | |
| }, | |
| enforceForTSTypes: { | |
| type: "boolean", | |
| }, | |
| }, | |
| additionalProperties: false, | |
| }, | |
| ], | |
| messages: { | |
| missingGetterInPropertyDescriptor: | |
| "Getter is not present in property descriptor.", | |
| missingSetterInPropertyDescriptor: | |
| "Setter is not present in property descriptor.", | |
| missingGetterInObjectLiteral: | |
| "Getter is not present for {{ name }}.", | |
| missingSetterInObjectLiteral: | |
| "Setter is not present for {{ name }}.", | |
| missingGetterInClass: "Getter is not present for class {{ name }}.", | |
| missingSetterInClass: "Setter is not present for class {{ name }}.", | |
| missingGetterInType: "Getter is not present for type {{ name }}.", | |
| missingSetterInType: "Setter is not present for type {{ name }}.", | |
| }, | |
| }, | |
| create(context) { | |
| const [ | |
| { | |
| getWithoutSet: checkGetWithoutSet, | |
| setWithoutGet: checkSetWithoutGet, | |
| enforceForClassMembers, | |
| enforceForTSTypes, | |
| }, | |
| ] = context.options; | |
| const sourceCode = context.sourceCode; | |
| /** | |
| * Reports the given node. | |
| * @param {ASTNode} node The node to report. | |
| * @param {string} messageKind "missingGetter" or "missingSetter". | |
| * @returns {void} | |
| * @private | |
| */ | |
| function report(node, messageKind) { | |
| if (node.type === "Property") { | |
| context.report({ | |
| node, | |
| messageId: `${messageKind}InObjectLiteral`, | |
| loc: astUtils.getFunctionHeadLoc(node.value, sourceCode), | |
| data: { | |
| name: astUtils.getFunctionNameWithKind(node.value), | |
| }, | |
| }); | |
| } else if (node.type === "MethodDefinition") { | |
| context.report({ | |
| node, | |
| messageId: `${messageKind}InClass`, | |
| loc: astUtils.getFunctionHeadLoc(node.value, sourceCode), | |
| data: { | |
| name: astUtils.getFunctionNameWithKind(node.value), | |
| }, | |
| }); | |
| } else if (node.type === "TSMethodSignature") { | |
| context.report({ | |
| node, | |
| messageId: `${messageKind}InType`, | |
| loc: astUtils.getFunctionHeadLoc(node, sourceCode), | |
| data: { | |
| name: astUtils.getFunctionNameWithKind(node), | |
| }, | |
| }); | |
| } else { | |
| context.report({ | |
| node, | |
| messageId: `${messageKind}InPropertyDescriptor`, | |
| }); | |
| } | |
| } | |
| /** | |
| * Reports each of the nodes in the given list using the same messageId. | |
| * @param {ASTNode[]} nodes Nodes to report. | |
| * @param {string} messageKind "missingGetter" or "missingSetter". | |
| * @returns {void} | |
| * @private | |
| */ | |
| function reportList(nodes, messageKind) { | |
| for (const node of nodes) { | |
| report(node, messageKind); | |
| } | |
| } | |
| /** | |
| * Checks accessor pairs in the given list of nodes. | |
| * @param {ASTNode[]} nodes The list to check. | |
| * @returns {void} | |
| * @private | |
| */ | |
| function checkList(nodes) { | |
| const accessors = []; | |
| let found = false; | |
| for (let i = 0; i < nodes.length; i++) { | |
| const node = nodes[i]; | |
| if (isAccessorKind(node)) { | |
| // Creates a new `AccessorData` object for the given getter or setter node. | |
| const name = astUtils.getStaticPropertyName(node); | |
| const key = | |
| name !== null ? name : sourceCode.getTokens(node.key); | |
| // Merges the given `AccessorData` object into the given accessors list. | |
| for (let j = 0; j < accessors.length; j++) { | |
| const accessor = accessors[j]; | |
| if (areEqualKeys(accessor.key, key)) { | |
| accessor.getters.push( | |
| ...(node.kind === "get" ? [node] : []), | |
| ); | |
| accessor.setters.push( | |
| ...(node.kind === "set" ? [node] : []), | |
| ); | |
| found = true; | |
| break; | |
| } | |
| } | |
| if (!found) { | |
| accessors.push({ | |
| key, | |
| getters: node.kind === "get" ? [node] : [], | |
| setters: node.kind === "set" ? [node] : [], | |
| }); | |
| } | |
| found = false; | |
| } | |
| } | |
| for (const { getters, setters } of accessors) { | |
| if (checkSetWithoutGet && setters.length && !getters.length) { | |
| reportList(setters, "missingGetter"); | |
| } | |
| if (checkGetWithoutSet && getters.length && !setters.length) { | |
| reportList(getters, "missingSetter"); | |
| } | |
| } | |
| } | |
| /** | |
| * Checks accessor pairs in an object literal. | |
| * @param {ASTNode} node `ObjectExpression` node to check. | |
| * @returns {void} | |
| * @private | |
| */ | |
| function checkObjectLiteral(node) { | |
| checkList(node.properties.filter(p => p.type === "Property")); | |
| } | |
| /** | |
| * Checks accessor pairs in a property descriptor. | |
| * @param {ASTNode} node Property descriptor `ObjectExpression` node to check. | |
| * @returns {void} | |
| * @private | |
| */ | |
| function checkPropertyDescriptor(node) { | |
| const namesToCheck = new Set( | |
| node.properties | |
| .filter( | |
| p => | |
| p.type === "Property" && | |
| p.kind === "init" && | |
| !p.computed, | |
| ) | |
| .map(({ key }) => key.name), | |
| ); | |
| const hasGetter = namesToCheck.has("get"); | |
| const hasSetter = namesToCheck.has("set"); | |
| if (checkSetWithoutGet && hasSetter && !hasGetter) { | |
| report(node, "missingGetter"); | |
| } | |
| if (checkGetWithoutSet && hasGetter && !hasSetter) { | |
| report(node, "missingSetter"); | |
| } | |
| } | |
| /** | |
| * Checks the given object expression as an object literal and as a possible property descriptor. | |
| * @param {ASTNode} node `ObjectExpression` node to check. | |
| * @returns {void} | |
| * @private | |
| */ | |
| function checkObjectExpression(node) { | |
| checkObjectLiteral(node); | |
| if (isPropertyDescriptor(node)) { | |
| checkPropertyDescriptor(node); | |
| } | |
| } | |
| /** | |
| * Checks the given class body. | |
| * @param {ASTNode} node `ClassBody` node to check. | |
| * @returns {void} | |
| * @private | |
| */ | |
| function checkClassBody(node) { | |
| const methodDefinitions = node.body.filter( | |
| m => m.type === "MethodDefinition", | |
| ); | |
| checkList(methodDefinitions.filter(m => m.static)); | |
| checkList(methodDefinitions.filter(m => !m.static)); | |
| } | |
| /** | |
| * Checks the given type. | |
| * @param {ASTNode} node `TSTypeLiteral` or `TSInterfaceBody` node to check. | |
| * @returns {void} | |
| * @private | |
| */ | |
| function checkType(node) { | |
| const members = | |
| node.type === "TSTypeLiteral" ? node.members : node.body; | |
| const methodDefinitions = members.filter( | |
| m => m.type === "TSMethodSignature", | |
| ); | |
| checkList(methodDefinitions); | |
| } | |
| const listeners = {}; | |
| if (checkSetWithoutGet || checkGetWithoutSet) { | |
| listeners.ObjectExpression = checkObjectExpression; | |
| if (enforceForClassMembers) { | |
| listeners.ClassBody = checkClassBody; | |
| } | |
| if (enforceForTSTypes) { | |
| listeners["TSTypeLiteral, TSInterfaceBody"] = checkType; | |
| } | |
| } | |
| return listeners; | |
| }, | |
| }; | |