]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to require grouped accessor pairs in object literals and classes | |
3 | * @author Milos Djermanovic | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | //------------------------------------------------------------------------------ | |
9 | // Requirements | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | const astUtils = require("./utils/ast-utils"); | |
13 | ||
14 | //------------------------------------------------------------------------------ | |
15 | // Typedefs | |
16 | //------------------------------------------------------------------------------ | |
17 | ||
18 | /** | |
19 | * Property name if it can be computed statically, otherwise the list of the tokens of the key node. | |
20 | * @typedef {string|Token[]} Key | |
21 | */ | |
22 | ||
23 | /** | |
24 | * Accessor nodes with the same key. | |
25 | * @typedef {Object} AccessorData | |
26 | * @property {Key} key Accessor's key | |
27 | * @property {ASTNode[]} getters List of getter nodes. | |
28 | * @property {ASTNode[]} setters List of setter nodes. | |
29 | */ | |
30 | ||
31 | //------------------------------------------------------------------------------ | |
32 | // Helpers | |
33 | //------------------------------------------------------------------------------ | |
34 | ||
35 | /** | |
36 | * Checks whether or not the given lists represent the equal tokens in the same order. | |
37 | * Tokens are compared by their properties, not by instance. | |
38 | * @param {Token[]} left First list of tokens. | |
39 | * @param {Token[]} right Second list of tokens. | |
40 | * @returns {boolean} `true` if the lists have same tokens. | |
41 | */ | |
42 | function areEqualTokenLists(left, right) { | |
43 | if (left.length !== right.length) { | |
44 | return false; | |
45 | } | |
46 | ||
47 | for (let i = 0; i < left.length; i++) { | |
48 | const leftToken = left[i], | |
49 | rightToken = right[i]; | |
50 | ||
51 | if (leftToken.type !== rightToken.type || leftToken.value !== rightToken.value) { | |
52 | return false; | |
53 | } | |
54 | } | |
55 | ||
56 | return true; | |
57 | } | |
58 | ||
59 | /** | |
60 | * Checks whether or not the given keys are equal. | |
61 | * @param {Key} left First key. | |
62 | * @param {Key} right Second key. | |
63 | * @returns {boolean} `true` if the keys are equal. | |
64 | */ | |
65 | function areEqualKeys(left, right) { | |
66 | if (typeof left === "string" && typeof right === "string") { | |
67 | ||
68 | // Statically computed names. | |
69 | return left === right; | |
70 | } | |
71 | if (Array.isArray(left) && Array.isArray(right)) { | |
72 | ||
73 | // Token lists. | |
74 | return areEqualTokenLists(left, right); | |
75 | } | |
76 | ||
77 | return false; | |
78 | } | |
79 | ||
80 | /** | |
81 | * Checks whether or not a given node is of an accessor kind ('get' or 'set'). | |
82 | * @param {ASTNode} node A node to check. | |
83 | * @returns {boolean} `true` if the node is of an accessor kind. | |
84 | */ | |
85 | function isAccessorKind(node) { | |
86 | return node.kind === "get" || node.kind === "set"; | |
87 | } | |
88 | ||
89 | //------------------------------------------------------------------------------ | |
90 | // Rule Definition | |
91 | //------------------------------------------------------------------------------ | |
92 | ||
93 | module.exports = { | |
94 | meta: { | |
95 | type: "suggestion", | |
96 | ||
97 | docs: { | |
98 | description: "require grouped accessor pairs in object literals and classes", | |
eb39fafa DC |
99 | recommended: false, |
100 | url: "https://eslint.org/docs/rules/grouped-accessor-pairs" | |
101 | }, | |
102 | ||
103 | schema: [ | |
104 | { | |
105 | enum: ["anyOrder", "getBeforeSet", "setBeforeGet"] | |
106 | } | |
107 | ], | |
108 | ||
109 | messages: { | |
110 | notGrouped: "Accessor pair {{ formerName }} and {{ latterName }} should be grouped.", | |
111 | invalidOrder: "Expected {{ latterName }} to be before {{ formerName }}." | |
112 | } | |
113 | }, | |
114 | ||
115 | create(context) { | |
116 | const order = context.options[0] || "anyOrder"; | |
117 | const sourceCode = context.getSourceCode(); | |
118 | ||
119 | /** | |
120 | * Reports the given accessor pair. | |
121 | * @param {string} messageId messageId to report. | |
122 | * @param {ASTNode} formerNode getter/setter node that is defined before `latterNode`. | |
123 | * @param {ASTNode} latterNode getter/setter node that is defined after `formerNode`. | |
124 | * @returns {void} | |
125 | * @private | |
126 | */ | |
127 | function report(messageId, formerNode, latterNode) { | |
128 | context.report({ | |
129 | node: latterNode, | |
130 | messageId, | |
131 | loc: astUtils.getFunctionHeadLoc(latterNode.value, sourceCode), | |
132 | data: { | |
133 | formerName: astUtils.getFunctionNameWithKind(formerNode.value), | |
134 | latterName: astUtils.getFunctionNameWithKind(latterNode.value) | |
135 | } | |
136 | }); | |
137 | } | |
138 | ||
139 | /** | |
140 | * Creates a new `AccessorData` object for the given getter or setter node. | |
141 | * @param {ASTNode} node A getter or setter node. | |
142 | * @returns {AccessorData} New `AccessorData` object that contains the given node. | |
143 | * @private | |
144 | */ | |
145 | function createAccessorData(node) { | |
146 | const name = astUtils.getStaticPropertyName(node); | |
147 | const key = (name !== null) ? name : sourceCode.getTokens(node.key); | |
148 | ||
149 | return { | |
150 | key, | |
151 | getters: node.kind === "get" ? [node] : [], | |
152 | setters: node.kind === "set" ? [node] : [] | |
153 | }; | |
154 | } | |
155 | ||
156 | /** | |
157 | * Merges the given `AccessorData` object into the given accessors list. | |
158 | * @param {AccessorData[]} accessors The list to merge into. | |
159 | * @param {AccessorData} accessorData The object to merge. | |
160 | * @returns {AccessorData[]} The same instance with the merged object. | |
161 | * @private | |
162 | */ | |
163 | function mergeAccessorData(accessors, accessorData) { | |
164 | const equalKeyElement = accessors.find(a => areEqualKeys(a.key, accessorData.key)); | |
165 | ||
166 | if (equalKeyElement) { | |
167 | equalKeyElement.getters.push(...accessorData.getters); | |
168 | equalKeyElement.setters.push(...accessorData.setters); | |
169 | } else { | |
170 | accessors.push(accessorData); | |
171 | } | |
172 | ||
173 | return accessors; | |
174 | } | |
175 | ||
176 | /** | |
177 | * Checks accessor pairs in the given list of nodes. | |
178 | * @param {ASTNode[]} nodes The list to check. | |
179 | * @param {Function} shouldCheck – Predicate that returns `true` if the node should be checked. | |
180 | * @returns {void} | |
181 | * @private | |
182 | */ | |
183 | function checkList(nodes, shouldCheck) { | |
184 | const accessors = nodes | |
185 | .filter(shouldCheck) | |
186 | .filter(isAccessorKind) | |
187 | .map(createAccessorData) | |
188 | .reduce(mergeAccessorData, []); | |
189 | ||
190 | for (const { getters, setters } of accessors) { | |
191 | ||
192 | // Don't report accessor properties that have duplicate getters or setters. | |
193 | if (getters.length === 1 && setters.length === 1) { | |
194 | const [getter] = getters, | |
195 | [setter] = setters, | |
196 | getterIndex = nodes.indexOf(getter), | |
197 | setterIndex = nodes.indexOf(setter), | |
198 | formerNode = getterIndex < setterIndex ? getter : setter, | |
199 | latterNode = getterIndex < setterIndex ? setter : getter; | |
200 | ||
201 | if (Math.abs(getterIndex - setterIndex) > 1) { | |
202 | report("notGrouped", formerNode, latterNode); | |
203 | } else if ( | |
204 | (order === "getBeforeSet" && getterIndex > setterIndex) || | |
205 | (order === "setBeforeGet" && getterIndex < setterIndex) | |
206 | ) { | |
207 | report("invalidOrder", formerNode, latterNode); | |
208 | } | |
209 | } | |
210 | } | |
211 | } | |
212 | ||
213 | return { | |
214 | ObjectExpression(node) { | |
215 | checkList(node.properties, n => n.type === "Property"); | |
216 | }, | |
217 | ClassBody(node) { | |
218 | checkList(node.body, n => n.type === "MethodDefinition" && !n.static); | |
219 | checkList(node.body, n => n.type === "MethodDefinition" && n.static); | |
220 | } | |
221 | }; | |
222 | } | |
223 | }; |