1 | /**
|
2 | * @fileoverview Rule to enforce concise object methods and properties.
|
3 | * @author Jamund Ferguson
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | const OPTIONS = {
|
9 | always: "always",
|
10 | never: "never",
|
11 | methods: "methods",
|
12 | properties: "properties",
|
13 | consistent: "consistent",
|
14 | consistentAsNeeded: "consistent-as-needed"
|
15 | };
|
16 |
|
17 | //------------------------------------------------------------------------------
|
18 | // Rule Definition
|
19 | //------------------------------------------------------------------------------
|
20 | module.exports = {
|
21 | meta: {
|
22 | docs: {
|
23 | description: "require or disallow method and property shorthand syntax for object literals",
|
24 | category: "ECMAScript 6",
|
25 | recommended: false
|
26 | },
|
27 |
|
28 | fixable: "code",
|
29 |
|
30 | schema: {
|
31 | anyOf: [
|
32 | {
|
33 | type: "array",
|
34 | items: [
|
35 | {
|
36 | enum: ["always", "methods", "properties", "never", "consistent", "consistent-as-needed"]
|
37 | }
|
38 | ],
|
39 | minItems: 0,
|
40 | maxItems: 1
|
41 | },
|
42 | {
|
43 | type: "array",
|
44 | items: [
|
45 | {
|
46 | enum: ["always", "methods", "properties"]
|
47 | },
|
48 | {
|
49 | type: "object",
|
50 | properties: {
|
51 | avoidQuotes: {
|
52 | type: "boolean"
|
53 | }
|
54 | },
|
55 | additionalProperties: false
|
56 | }
|
57 | ],
|
58 | minItems: 0,
|
59 | maxItems: 2
|
60 | },
|
61 | {
|
62 | type: "array",
|
63 | items: [
|
64 | {
|
65 | enum: ["always", "methods"]
|
66 | },
|
67 | {
|
68 | type: "object",
|
69 | properties: {
|
70 | ignoreConstructors: {
|
71 | type: "boolean"
|
72 | },
|
73 | avoidQuotes: {
|
74 | type: "boolean"
|
75 | }
|
76 | },
|
77 | additionalProperties: false
|
78 | }
|
79 | ],
|
80 | minItems: 0,
|
81 | maxItems: 2
|
82 | }
|
83 | ]
|
84 | }
|
85 | },
|
86 |
|
87 | create(context) {
|
88 | const APPLY = context.options[0] || OPTIONS.always;
|
89 | const APPLY_TO_METHODS = APPLY === OPTIONS.methods || APPLY === OPTIONS.always;
|
90 | const APPLY_TO_PROPS = APPLY === OPTIONS.properties || APPLY === OPTIONS.always;
|
91 | const APPLY_NEVER = APPLY === OPTIONS.never;
|
92 | const APPLY_CONSISTENT = APPLY === OPTIONS.consistent;
|
93 | const APPLY_CONSISTENT_AS_NEEDED = APPLY === OPTIONS.consistentAsNeeded;
|
94 |
|
95 | const PARAMS = context.options[1] || {};
|
96 | const IGNORE_CONSTRUCTORS = PARAMS.ignoreConstructors;
|
97 | const AVOID_QUOTES = PARAMS.avoidQuotes;
|
98 |
|
99 | //--------------------------------------------------------------------------
|
100 | // Helpers
|
101 | //--------------------------------------------------------------------------
|
102 |
|
103 | /**
|
104 | * Determines if the first character of the name is a capital letter.
|
105 | * @param {string} name The name of the node to evaluate.
|
106 | * @returns {boolean} True if the first character of the property name is a capital letter, false if not.
|
107 | * @private
|
108 | */
|
109 | function isConstructor(name) {
|
110 | const firstChar = name.charAt(0);
|
111 |
|
112 | return firstChar === firstChar.toUpperCase();
|
113 | }
|
114 |
|
115 | /**
|
116 | * Determines if the property is not a getter and a setter.
|
117 | * @param {ASTNode} property Property AST node
|
118 | * @returns {boolean} True if the property is not a getter and a setter, false if it is.
|
119 | * @private
|
120 | **/
|
121 | function isNotGetterOrSetter(property) {
|
122 | return (property.kind !== "set" && property.kind !== "get");
|
123 | }
|
124 |
|
125 | /**
|
126 | * Checks whether a node is a string literal.
|
127 | * @param {ASTNode} node - Any AST node.
|
128 | * @returns {boolean} `true` if it is a string literal.
|
129 | */
|
130 | function isStringLiteral(node) {
|
131 | return node.type === "Literal" && typeof node.value === "string";
|
132 | }
|
133 |
|
134 | /**
|
135 | * Determines if the property is a shorthand or not.
|
136 | * @param {ASTNode} property Property AST node
|
137 | * @returns {boolean} True if the property is considered shorthand, false if not.
|
138 | * @private
|
139 | **/
|
140 | function isShorthand(property) {
|
141 |
|
142 | // property.method is true when `{a(){}}`.
|
143 | return (property.shorthand || property.method);
|
144 | }
|
145 |
|
146 | /**
|
147 | * Determines if the property's key and method or value are named equally.
|
148 | * @param {ASTNode} property Property AST node
|
149 | * @returns {boolean} True if the key and value are named equally, false if not.
|
150 | * @private
|
151 | **/
|
152 | function isRedudant(property) {
|
153 | return (property.key && (
|
154 |
|
155 | // A function expression
|
156 | property.value && property.value.id && property.value.id.name === property.key.name ||
|
157 |
|
158 | // A property
|
159 | property.value && property.value.name === property.key.name
|
160 | ));
|
161 | }
|
162 |
|
163 | /**
|
164 | * Ensures that an object's properties are consistently shorthand, or not shorthand at all.
|
165 | * @param {ASTNode} node Property AST node
|
166 | * @param {boolean} checkRedundancy Whether to check longform redundancy
|
167 | * @returns {void}
|
168 | **/
|
169 | function checkConsistency(node, checkRedundancy) {
|
170 |
|
171 | // We are excluding getters and setters as they are considered neither longform nor shorthand.
|
172 | const properties = node.properties.filter(isNotGetterOrSetter);
|
173 |
|
174 | // Do we still have properties left after filtering the getters and setters?
|
175 | if (properties.length > 0) {
|
176 | const shorthandProperties = properties.filter(isShorthand);
|
177 |
|
178 | // If we do not have an equal number of longform properties as
|
179 | // shorthand properties, we are using the annotations inconsistently
|
180 | if (shorthandProperties.length !== properties.length) {
|
181 |
|
182 | // We have at least 1 shorthand property
|
183 | if (shorthandProperties.length > 0) {
|
184 | context.report(node, "Unexpected mix of shorthand and non-shorthand properties.");
|
185 | } else if (checkRedundancy) {
|
186 |
|
187 | // If all properties of the object contain a method or value with a name matching it's key,
|
188 | // all the keys are redudant.
|
189 | const canAlwaysUseShorthand = properties.every(isRedudant);
|
190 |
|
191 | if (canAlwaysUseShorthand) {
|
192 | context.report(node, "Expected shorthand for all properties.");
|
193 | }
|
194 | }
|
195 | }
|
196 | }
|
197 | }
|
198 |
|
199 | //--------------------------------------------------------------------------
|
200 | // Public
|
201 | //--------------------------------------------------------------------------
|
202 |
|
203 | return {
|
204 | ObjectExpression(node) {
|
205 | if (APPLY_CONSISTENT) {
|
206 | checkConsistency(node, false);
|
207 | } else if (APPLY_CONSISTENT_AS_NEEDED) {
|
208 | checkConsistency(node, true);
|
209 | }
|
210 | },
|
211 |
|
212 | Property(node) {
|
213 | const isConciseProperty = node.method || node.shorthand;
|
214 |
|
215 | // Ignore destructuring assignment
|
216 | if (node.parent.type === "ObjectPattern") {
|
217 | return;
|
218 | }
|
219 |
|
220 | // getters and setters are ignored
|
221 | if (node.kind === "get" || node.kind === "set") {
|
222 | return;
|
223 | }
|
224 |
|
225 | // only computed methods can fail the following checks
|
226 | if (node.computed && node.value.type !== "FunctionExpression") {
|
227 | return;
|
228 | }
|
229 |
|
230 | //--------------------------------------------------------------
|
231 | // Checks for property/method shorthand.
|
232 | if (isConciseProperty) {
|
233 |
|
234 | // if we're "never" and concise we should warn now
|
235 | if (APPLY_NEVER) {
|
236 | const type = node.method ? "method" : "property";
|
237 |
|
238 | context.report({
|
239 | node,
|
240 | message: "Expected longform {{type}} syntax.",
|
241 | data: {
|
242 | type
|
243 | },
|
244 | fix(fixer) {
|
245 | if (node.method) {
|
246 | if (node.value.generator) {
|
247 | return fixer.replaceTextRange([node.range[0], node.key.range[1]], `${node.key.name}: function*`);
|
248 | }
|
249 |
|
250 | return fixer.insertTextAfter(node.key, ": function");
|
251 | }
|
252 |
|
253 | return fixer.insertTextAfter(node.key, `: ${node.key.name}`);
|
254 | }
|
255 | });
|
256 | }
|
257 |
|
258 | // {'xyz'() {}} should be written as {'xyz': function() {}}
|
259 | if (AVOID_QUOTES && isStringLiteral(node.key)) {
|
260 | context.report({
|
261 | node,
|
262 | message: "Expected longform method syntax for string literal keys.",
|
263 | fix(fixer) {
|
264 | if (node.computed) {
|
265 | return fixer.insertTextAfterRange([node.key.range[0], node.key.range[1] + 1], ": function");
|
266 | }
|
267 |
|
268 | return fixer.insertTextAfter(node.key, ": function");
|
269 | }
|
270 | });
|
271 | }
|
272 |
|
273 | return;
|
274 | }
|
275 |
|
276 | //--------------------------------------------------------------
|
277 | // Checks for longform properties.
|
278 | if (node.value.type === "FunctionExpression" && !node.value.id && APPLY_TO_METHODS) {
|
279 | if (IGNORE_CONSTRUCTORS && isConstructor(node.key.name)) {
|
280 | return;
|
281 | }
|
282 | if (AVOID_QUOTES && isStringLiteral(node.key)) {
|
283 | return;
|
284 | }
|
285 |
|
286 | // {[x]: function(){}} should be written as {[x]() {}}
|
287 | if (node.computed) {
|
288 | context.report({
|
289 | node,
|
290 | message: "Expected method shorthand.",
|
291 | fix(fixer) {
|
292 | if (node.value.generator) {
|
293 | return fixer.replaceTextRange(
|
294 | [node.key.range[0], node.value.range[0] + "function*".length],
|
295 | `*[${node.key.name}]`
|
296 | );
|
297 | }
|
298 |
|
299 | return fixer.removeRange([node.key.range[1] + 1, node.value.range[0] + "function".length]);
|
300 | }
|
301 | });
|
302 | return;
|
303 | }
|
304 |
|
305 | // {x: function(){}} should be written as {x() {}}
|
306 | context.report({
|
307 | node,
|
308 | message: "Expected method shorthand.",
|
309 | fix(fixer) {
|
310 | if (node.value.generator) {
|
311 | return fixer.replaceTextRange(
|
312 | [node.key.range[0], node.value.range[0] + "function*".length],
|
313 | `*${node.key.name}`
|
314 | );
|
315 | }
|
316 |
|
317 | return fixer.removeRange([node.key.range[1], node.value.range[0] + "function".length]);
|
318 | }
|
319 | });
|
320 | } else if (node.value.type === "Identifier" && node.key.name === node.value.name && APPLY_TO_PROPS) {
|
321 |
|
322 | // {x: x} should be written as {x}
|
323 | context.report({
|
324 | node,
|
325 | message: "Expected property shorthand.",
|
326 | fix(fixer) {
|
327 | return fixer.replaceText(node, node.value.name);
|
328 | }
|
329 | });
|
330 | } else if (node.value.type === "Identifier" && node.key.type === "Literal" && node.key.value === node.value.name && APPLY_TO_PROPS) {
|
331 | if (AVOID_QUOTES) {
|
332 | return;
|
333 | }
|
334 |
|
335 | // {"x": x} should be written as {x}
|
336 | context.report({
|
337 | node,
|
338 | message: "Expected property shorthand.",
|
339 | fix(fixer) {
|
340 | return fixer.replaceText(node, node.value.name);
|
341 | }
|
342 | });
|
343 | }
|
344 | }
|
345 | };
|
346 | }
|
347 | };
|