UNPKG

20.4 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to enforce concise object methods and properties.
3 * @author Jamund Ferguson
4 */
5
6"use strict";
7
8const 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// Requirements
19//------------------------------------------------------------------------------
20const astUtils = require("./utils/ast-utils");
21
22//------------------------------------------------------------------------------
23// Rule Definition
24//------------------------------------------------------------------------------
25module.exports = {
26 meta: {
27 type: "suggestion",
28
29 docs: {
30 description: "require or disallow method and property shorthand syntax for object literals",
31 category: "ECMAScript 6",
32 recommended: false,
33 url: "https://eslint.org/docs/rules/object-shorthand"
34 },
35
36 fixable: "code",
37
38 schema: {
39 anyOf: [
40 {
41 type: "array",
42 items: [
43 {
44 enum: ["always", "methods", "properties", "never", "consistent", "consistent-as-needed"]
45 }
46 ],
47 minItems: 0,
48 maxItems: 1
49 },
50 {
51 type: "array",
52 items: [
53 {
54 enum: ["always", "methods", "properties"]
55 },
56 {
57 type: "object",
58 properties: {
59 avoidQuotes: {
60 type: "boolean"
61 }
62 },
63 additionalProperties: false
64 }
65 ],
66 minItems: 0,
67 maxItems: 2
68 },
69 {
70 type: "array",
71 items: [
72 {
73 enum: ["always", "methods"]
74 },
75 {
76 type: "object",
77 properties: {
78 ignoreConstructors: {
79 type: "boolean"
80 },
81 avoidQuotes: {
82 type: "boolean"
83 },
84 avoidExplicitReturnArrows: {
85 type: "boolean"
86 }
87 },
88 additionalProperties: false
89 }
90 ],
91 minItems: 0,
92 maxItems: 2
93 }
94 ]
95 }
96 },
97
98 create(context) {
99 const APPLY = context.options[0] || OPTIONS.always;
100 const APPLY_TO_METHODS = APPLY === OPTIONS.methods || APPLY === OPTIONS.always;
101 const APPLY_TO_PROPS = APPLY === OPTIONS.properties || APPLY === OPTIONS.always;
102 const APPLY_NEVER = APPLY === OPTIONS.never;
103 const APPLY_CONSISTENT = APPLY === OPTIONS.consistent;
104 const APPLY_CONSISTENT_AS_NEEDED = APPLY === OPTIONS.consistentAsNeeded;
105
106 const PARAMS = context.options[1] || {};
107 const IGNORE_CONSTRUCTORS = PARAMS.ignoreConstructors;
108 const AVOID_QUOTES = PARAMS.avoidQuotes;
109 const AVOID_EXPLICIT_RETURN_ARROWS = !!PARAMS.avoidExplicitReturnArrows;
110 const sourceCode = context.getSourceCode();
111
112 //--------------------------------------------------------------------------
113 // Helpers
114 //--------------------------------------------------------------------------
115
116 const CTOR_PREFIX_REGEX = /[^_$0-9]/u;
117
118 /**
119 * Determines if the first character of the name is a capital letter.
120 * @param {string} name The name of the node to evaluate.
121 * @returns {boolean} True if the first character of the property name is a capital letter, false if not.
122 * @private
123 */
124 function isConstructor(name) {
125 const match = CTOR_PREFIX_REGEX.exec(name);
126
127 // Not a constructor if name has no characters apart from '_', '$' and digits e.g. '_', '$$', '_8'
128 if (!match) {
129 return false;
130 }
131
132 const firstChar = name.charAt(match.index);
133
134 return firstChar === firstChar.toUpperCase();
135 }
136
137 /**
138 * Determines if the property can have a shorthand form.
139 * @param {ASTNode} property Property AST node
140 * @returns {boolean} True if the property can have a shorthand form
141 * @private
142 *
143 */
144 function canHaveShorthand(property) {
145 return (property.kind !== "set" && property.kind !== "get" && property.type !== "SpreadElement" && property.type !== "SpreadProperty" && property.type !== "ExperimentalSpreadProperty");
146 }
147
148 /**
149 * Checks whether a node is a string literal.
150 * @param {ASTNode} node - Any AST node.
151 * @returns {boolean} `true` if it is a string literal.
152 */
153 function isStringLiteral(node) {
154 return node.type === "Literal" && typeof node.value === "string";
155 }
156
157 /**
158 * Determines if the property is a shorthand or not.
159 * @param {ASTNode} property Property AST node
160 * @returns {boolean} True if the property is considered shorthand, false if not.
161 * @private
162 *
163 */
164 function isShorthand(property) {
165
166 // property.method is true when `{a(){}}`.
167 return (property.shorthand || property.method);
168 }
169
170 /**
171 * Determines if the property's key and method or value are named equally.
172 * @param {ASTNode} property Property AST node
173 * @returns {boolean} True if the key and value are named equally, false if not.
174 * @private
175 *
176 */
177 function isRedundant(property) {
178 const value = property.value;
179
180 if (value.type === "FunctionExpression") {
181 return !value.id; // Only anonymous should be shorthand method.
182 }
183 if (value.type === "Identifier") {
184 return astUtils.getStaticPropertyName(property) === value.name;
185 }
186
187 return false;
188 }
189
190 /**
191 * Ensures that an object's properties are consistently shorthand, or not shorthand at all.
192 * @param {ASTNode} node Property AST node
193 * @param {boolean} checkRedundancy Whether to check longform redundancy
194 * @returns {void}
195 *
196 */
197 function checkConsistency(node, checkRedundancy) {
198
199 // We are excluding getters/setters and spread properties as they are considered neither longform nor shorthand.
200 const properties = node.properties.filter(canHaveShorthand);
201
202 // Do we still have properties left after filtering the getters and setters?
203 if (properties.length > 0) {
204 const shorthandProperties = properties.filter(isShorthand);
205
206 /*
207 * If we do not have an equal number of longform properties as
208 * shorthand properties, we are using the annotations inconsistently
209 */
210 if (shorthandProperties.length !== properties.length) {
211
212 // We have at least 1 shorthand property
213 if (shorthandProperties.length > 0) {
214 context.report({ node, message: "Unexpected mix of shorthand and non-shorthand properties." });
215 } else if (checkRedundancy) {
216
217 /*
218 * If all properties of the object contain a method or value with a name matching it's key,
219 * all the keys are redundant.
220 */
221 const canAlwaysUseShorthand = properties.every(isRedundant);
222
223 if (canAlwaysUseShorthand) {
224 context.report({ node, message: "Expected shorthand for all properties." });
225 }
226 }
227 }
228 }
229 }
230
231 /**
232 * Fixes a FunctionExpression node by making it into a shorthand property.
233 * @param {SourceCodeFixer} fixer The fixer object
234 * @param {ASTNode} node A `Property` node that has a `FunctionExpression` or `ArrowFunctionExpression` as its value
235 * @returns {Object} A fix for this node
236 */
237 function makeFunctionShorthand(fixer, node) {
238 const firstKeyToken = node.computed
239 ? sourceCode.getFirstToken(node, astUtils.isOpeningBracketToken)
240 : sourceCode.getFirstToken(node.key);
241 const lastKeyToken = node.computed
242 ? sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isClosingBracketToken)
243 : sourceCode.getLastToken(node.key);
244 const keyText = sourceCode.text.slice(firstKeyToken.range[0], lastKeyToken.range[1]);
245 let keyPrefix = "";
246
247 if (sourceCode.commentsExistBetween(lastKeyToken, node.value)) {
248 return null;
249 }
250
251 if (node.value.async) {
252 keyPrefix += "async ";
253 }
254 if (node.value.generator) {
255 keyPrefix += "*";
256 }
257
258 if (node.value.type === "FunctionExpression") {
259 const functionToken = sourceCode.getTokens(node.value).find(token => token.type === "Keyword" && token.value === "function");
260 const tokenBeforeParams = node.value.generator ? sourceCode.getTokenAfter(functionToken) : functionToken;
261
262 return fixer.replaceTextRange(
263 [firstKeyToken.range[0], node.range[1]],
264 keyPrefix + keyText + sourceCode.text.slice(tokenBeforeParams.range[1], node.value.range[1])
265 );
266 }
267 const arrowToken = sourceCode.getTokenBefore(node.value.body, { filter: token => token.value === "=>" });
268 const tokenBeforeArrow = sourceCode.getTokenBefore(arrowToken);
269 const hasParensAroundParameters = tokenBeforeArrow.type === "Punctuator" && tokenBeforeArrow.value === ")";
270 const oldParamText = sourceCode.text.slice(sourceCode.getFirstToken(node.value, node.value.async ? 1 : 0).range[0], tokenBeforeArrow.range[1]);
271 const newParamText = hasParensAroundParameters ? oldParamText : `(${oldParamText})`;
272
273 return fixer.replaceTextRange(
274 [firstKeyToken.range[0], node.range[1]],
275 keyPrefix + keyText + newParamText + sourceCode.text.slice(arrowToken.range[1], node.value.range[1])
276 );
277
278 }
279
280 /**
281 * Fixes a FunctionExpression node by making it into a longform property.
282 * @param {SourceCodeFixer} fixer The fixer object
283 * @param {ASTNode} node A `Property` node that has a `FunctionExpression` as its value
284 * @returns {Object} A fix for this node
285 */
286 function makeFunctionLongform(fixer, node) {
287 const firstKeyToken = node.computed ? sourceCode.getTokens(node).find(token => token.value === "[") : sourceCode.getFirstToken(node.key);
288 const lastKeyToken = node.computed ? sourceCode.getTokensBetween(node.key, node.value).find(token => token.value === "]") : sourceCode.getLastToken(node.key);
289 const keyText = sourceCode.text.slice(firstKeyToken.range[0], lastKeyToken.range[1]);
290 let functionHeader = "function";
291
292 if (node.value.async) {
293 functionHeader = `async ${functionHeader}`;
294 }
295 if (node.value.generator) {
296 functionHeader = `${functionHeader}*`;
297 }
298
299 return fixer.replaceTextRange([node.range[0], lastKeyToken.range[1]], `${keyText}: ${functionHeader}`);
300 }
301
302 /*
303 * To determine whether a given arrow function has a lexical identifier (`this`, `arguments`, `super`, or `new.target`),
304 * create a stack of functions that define these identifiers (i.e. all functions except arrow functions) as the AST is
305 * traversed. Whenever a new function is encountered, create a new entry on the stack (corresponding to a different lexical
306 * scope of `this`), and whenever a function is exited, pop that entry off the stack. When an arrow function is entered,
307 * keep a reference to it on the current stack entry, and remove that reference when the arrow function is exited.
308 * When a lexical identifier is encountered, mark all the arrow functions on the current stack entry by adding them
309 * to an `arrowsWithLexicalIdentifiers` set. Any arrow function in that set will not be reported by this rule,
310 * because converting it into a method would change the value of one of the lexical identifiers.
311 */
312 const lexicalScopeStack = [];
313 const arrowsWithLexicalIdentifiers = new WeakSet();
314 const argumentsIdentifiers = new WeakSet();
315
316 /**
317 * Enters a function. This creates a new lexical identifier scope, so a new Set of arrow functions is pushed onto the stack.
318 * Also, this marks all `arguments` identifiers so that they can be detected later.
319 * @returns {void}
320 */
321 function enterFunction() {
322 lexicalScopeStack.unshift(new Set());
323 context.getScope().variables.filter(variable => variable.name === "arguments").forEach(variable => {
324 variable.references.map(ref => ref.identifier).forEach(identifier => argumentsIdentifiers.add(identifier));
325 });
326 }
327
328 /**
329 * Exits a function. This pops the current set of arrow functions off the lexical scope stack.
330 * @returns {void}
331 */
332 function exitFunction() {
333 lexicalScopeStack.shift();
334 }
335
336 /**
337 * Marks the current function as having a lexical keyword. This implies that all arrow functions
338 * in the current lexical scope contain a reference to this lexical keyword.
339 * @returns {void}
340 */
341 function reportLexicalIdentifier() {
342 lexicalScopeStack[0].forEach(arrowFunction => arrowsWithLexicalIdentifiers.add(arrowFunction));
343 }
344
345 //--------------------------------------------------------------------------
346 // Public
347 //--------------------------------------------------------------------------
348
349 return {
350 Program: enterFunction,
351 FunctionDeclaration: enterFunction,
352 FunctionExpression: enterFunction,
353 "Program:exit": exitFunction,
354 "FunctionDeclaration:exit": exitFunction,
355 "FunctionExpression:exit": exitFunction,
356
357 ArrowFunctionExpression(node) {
358 lexicalScopeStack[0].add(node);
359 },
360 "ArrowFunctionExpression:exit"(node) {
361 lexicalScopeStack[0].delete(node);
362 },
363
364 ThisExpression: reportLexicalIdentifier,
365 Super: reportLexicalIdentifier,
366 MetaProperty(node) {
367 if (node.meta.name === "new" && node.property.name === "target") {
368 reportLexicalIdentifier();
369 }
370 },
371 Identifier(node) {
372 if (argumentsIdentifiers.has(node)) {
373 reportLexicalIdentifier();
374 }
375 },
376
377 ObjectExpression(node) {
378 if (APPLY_CONSISTENT) {
379 checkConsistency(node, false);
380 } else if (APPLY_CONSISTENT_AS_NEEDED) {
381 checkConsistency(node, true);
382 }
383 },
384
385 "Property:exit"(node) {
386 const isConciseProperty = node.method || node.shorthand;
387
388 // Ignore destructuring assignment
389 if (node.parent.type === "ObjectPattern") {
390 return;
391 }
392
393 // getters and setters are ignored
394 if (node.kind === "get" || node.kind === "set") {
395 return;
396 }
397
398 // only computed methods can fail the following checks
399 if (node.computed && node.value.type !== "FunctionExpression" && node.value.type !== "ArrowFunctionExpression") {
400 return;
401 }
402
403 //--------------------------------------------------------------
404 // Checks for property/method shorthand.
405 if (isConciseProperty) {
406 if (node.method && (APPLY_NEVER || AVOID_QUOTES && isStringLiteral(node.key))) {
407 const message = APPLY_NEVER ? "Expected longform method syntax." : "Expected longform method syntax for string literal keys.";
408
409 // { x() {} } should be written as { x: function() {} }
410 context.report({
411 node,
412 message,
413 fix: fixer => makeFunctionLongform(fixer, node)
414 });
415 } else if (APPLY_NEVER) {
416
417 // { x } should be written as { x: x }
418 context.report({
419 node,
420 message: "Expected longform property syntax.",
421 fix: fixer => fixer.insertTextAfter(node.key, `: ${node.key.name}`)
422 });
423 }
424 } else if (APPLY_TO_METHODS && !node.value.id && (node.value.type === "FunctionExpression" || node.value.type === "ArrowFunctionExpression")) {
425 if (IGNORE_CONSTRUCTORS && node.key.type === "Identifier" && isConstructor(node.key.name)) {
426 return;
427 }
428 if (AVOID_QUOTES && isStringLiteral(node.key)) {
429 return;
430 }
431
432 // {[x]: function(){}} should be written as {[x]() {}}
433 if (node.value.type === "FunctionExpression" ||
434 node.value.type === "ArrowFunctionExpression" &&
435 node.value.body.type === "BlockStatement" &&
436 AVOID_EXPLICIT_RETURN_ARROWS &&
437 !arrowsWithLexicalIdentifiers.has(node.value)
438 ) {
439 context.report({
440 node,
441 message: "Expected method shorthand.",
442 fix: fixer => makeFunctionShorthand(fixer, node)
443 });
444 }
445 } else if (node.value.type === "Identifier" && node.key.name === node.value.name && APPLY_TO_PROPS) {
446
447 // {x: x} should be written as {x}
448 context.report({
449 node,
450 message: "Expected property shorthand.",
451 fix(fixer) {
452 return fixer.replaceText(node, node.value.name);
453 }
454 });
455 } else if (node.value.type === "Identifier" && node.key.type === "Literal" && node.key.value === node.value.name && APPLY_TO_PROPS) {
456 if (AVOID_QUOTES) {
457 return;
458 }
459
460 // {"x": x} should be written as {x}
461 context.report({
462 node,
463 message: "Expected property shorthand.",
464 fix(fixer) {
465 return fixer.replaceText(node, node.value.name);
466 }
467 });
468 }
469 }
470 };
471 }
472};