UNPKG

21.7 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 messages: {
98 expectedAllPropertiesShorthanded: "Expected shorthand for all properties.",
99 expectedLiteralMethodLongform: "Expected longform method syntax for string literal keys.",
100 expectedPropertyShorthand: "Expected property shorthand.",
101 expectedPropertyLongform: "Expected longform property syntax.",
102 expectedMethodShorthand: "Expected method shorthand.",
103 expectedMethodLongform: "Expected longform method syntax.",
104 unexpectedMix: "Unexpected mix of shorthand and non-shorthand properties."
105 }
106 },
107
108 create(context) {
109 const APPLY = context.options[0] || OPTIONS.always;
110 const APPLY_TO_METHODS = APPLY === OPTIONS.methods || APPLY === OPTIONS.always;
111 const APPLY_TO_PROPS = APPLY === OPTIONS.properties || APPLY === OPTIONS.always;
112 const APPLY_NEVER = APPLY === OPTIONS.never;
113 const APPLY_CONSISTENT = APPLY === OPTIONS.consistent;
114 const APPLY_CONSISTENT_AS_NEEDED = APPLY === OPTIONS.consistentAsNeeded;
115
116 const PARAMS = context.options[1] || {};
117 const IGNORE_CONSTRUCTORS = PARAMS.ignoreConstructors;
118 const AVOID_QUOTES = PARAMS.avoidQuotes;
119 const AVOID_EXPLICIT_RETURN_ARROWS = !!PARAMS.avoidExplicitReturnArrows;
120 const sourceCode = context.getSourceCode();
121
122 //--------------------------------------------------------------------------
123 // Helpers
124 //--------------------------------------------------------------------------
125
126 const CTOR_PREFIX_REGEX = /[^_$0-9]/u;
127
128 /**
129 * Determines if the first character of the name is a capital letter.
130 * @param {string} name The name of the node to evaluate.
131 * @returns {boolean} True if the first character of the property name is a capital letter, false if not.
132 * @private
133 */
134 function isConstructor(name) {
135 const match = CTOR_PREFIX_REGEX.exec(name);
136
137 // Not a constructor if name has no characters apart from '_', '$' and digits e.g. '_', '$$', '_8'
138 if (!match) {
139 return false;
140 }
141
142 const firstChar = name.charAt(match.index);
143
144 return firstChar === firstChar.toUpperCase();
145 }
146
147 /**
148 * Determines if the property can have a shorthand form.
149 * @param {ASTNode} property Property AST node
150 * @returns {boolean} True if the property can have a shorthand form
151 * @private
152 *
153 */
154 function canHaveShorthand(property) {
155 return (property.kind !== "set" && property.kind !== "get" && property.type !== "SpreadElement" && property.type !== "SpreadProperty" && property.type !== "ExperimentalSpreadProperty");
156 }
157
158 /**
159 * Checks whether a node is a string literal.
160 * @param {ASTNode} node Any AST node.
161 * @returns {boolean} `true` if it is a string literal.
162 */
163 function isStringLiteral(node) {
164 return node.type === "Literal" && typeof node.value === "string";
165 }
166
167 /**
168 * Determines if the property is a shorthand or not.
169 * @param {ASTNode} property Property AST node
170 * @returns {boolean} True if the property is considered shorthand, false if not.
171 * @private
172 *
173 */
174 function isShorthand(property) {
175
176 // property.method is true when `{a(){}}`.
177 return (property.shorthand || property.method);
178 }
179
180 /**
181 * Determines if the property's key and method or value are named equally.
182 * @param {ASTNode} property Property AST node
183 * @returns {boolean} True if the key and value are named equally, false if not.
184 * @private
185 *
186 */
187 function isRedundant(property) {
188 const value = property.value;
189
190 if (value.type === "FunctionExpression") {
191 return !value.id; // Only anonymous should be shorthand method.
192 }
193 if (value.type === "Identifier") {
194 return astUtils.getStaticPropertyName(property) === value.name;
195 }
196
197 return false;
198 }
199
200 /**
201 * Ensures that an object's properties are consistently shorthand, or not shorthand at all.
202 * @param {ASTNode} node Property AST node
203 * @param {boolean} checkRedundancy Whether to check longform redundancy
204 * @returns {void}
205 *
206 */
207 function checkConsistency(node, checkRedundancy) {
208
209 // We are excluding getters/setters and spread properties as they are considered neither longform nor shorthand.
210 const properties = node.properties.filter(canHaveShorthand);
211
212 // Do we still have properties left after filtering the getters and setters?
213 if (properties.length > 0) {
214 const shorthandProperties = properties.filter(isShorthand);
215
216 /*
217 * If we do not have an equal number of longform properties as
218 * shorthand properties, we are using the annotations inconsistently
219 */
220 if (shorthandProperties.length !== properties.length) {
221
222 // We have at least 1 shorthand property
223 if (shorthandProperties.length > 0) {
224 context.report({ node, messageId: "unexpectedMix" });
225 } else if (checkRedundancy) {
226
227 /*
228 * If all properties of the object contain a method or value with a name matching it's key,
229 * all the keys are redundant.
230 */
231 const canAlwaysUseShorthand = properties.every(isRedundant);
232
233 if (canAlwaysUseShorthand) {
234 context.report({ node, messageId: "expectedAllPropertiesShorthanded" });
235 }
236 }
237 }
238 }
239 }
240
241 /**
242 * Fixes a FunctionExpression node by making it into a shorthand property.
243 * @param {SourceCodeFixer} fixer The fixer object
244 * @param {ASTNode} node A `Property` node that has a `FunctionExpression` or `ArrowFunctionExpression` as its value
245 * @returns {Object} A fix for this node
246 */
247 function makeFunctionShorthand(fixer, node) {
248 const firstKeyToken = node.computed
249 ? sourceCode.getFirstToken(node, astUtils.isOpeningBracketToken)
250 : sourceCode.getFirstToken(node.key);
251 const lastKeyToken = node.computed
252 ? sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isClosingBracketToken)
253 : sourceCode.getLastToken(node.key);
254 const keyText = sourceCode.text.slice(firstKeyToken.range[0], lastKeyToken.range[1]);
255 let keyPrefix = "";
256
257 // key: /* */ () => {}
258 if (sourceCode.commentsExistBetween(lastKeyToken, node.value)) {
259 return null;
260 }
261
262 if (node.value.async) {
263 keyPrefix += "async ";
264 }
265 if (node.value.generator) {
266 keyPrefix += "*";
267 }
268
269 const fixRange = [firstKeyToken.range[0], node.range[1]];
270 const methodPrefix = keyPrefix + keyText;
271
272 if (node.value.type === "FunctionExpression") {
273 const functionToken = sourceCode.getTokens(node.value).find(token => token.type === "Keyword" && token.value === "function");
274 const tokenBeforeParams = node.value.generator ? sourceCode.getTokenAfter(functionToken) : functionToken;
275
276 return fixer.replaceTextRange(
277 fixRange,
278 methodPrefix + sourceCode.text.slice(tokenBeforeParams.range[1], node.value.range[1])
279 );
280 }
281
282 const arrowToken = sourceCode.getTokenBefore(node.value.body, astUtils.isArrowToken);
283 const fnBody = sourceCode.text.slice(arrowToken.range[1], node.value.range[1]);
284
285 let shouldAddParensAroundParameters = false;
286 let tokenBeforeParams;
287
288 if (node.value.params.length === 0) {
289 tokenBeforeParams = sourceCode.getFirstToken(node.value, astUtils.isOpeningParenToken);
290 } else {
291 tokenBeforeParams = sourceCode.getTokenBefore(node.value.params[0]);
292 }
293
294 if (node.value.params.length === 1) {
295 const hasParen = astUtils.isOpeningParenToken(tokenBeforeParams);
296 const isTokenOutsideNode = tokenBeforeParams.range[0] < node.range[0];
297
298 shouldAddParensAroundParameters = !hasParen || isTokenOutsideNode;
299 }
300
301 const sliceStart = shouldAddParensAroundParameters
302 ? node.value.params[0].range[0]
303 : tokenBeforeParams.range[0];
304 const sliceEnd = sourceCode.getTokenBefore(arrowToken).range[1];
305
306 const oldParamText = sourceCode.text.slice(sliceStart, sliceEnd);
307 const newParamText = shouldAddParensAroundParameters ? `(${oldParamText})` : oldParamText;
308
309 return fixer.replaceTextRange(
310 fixRange,
311 methodPrefix + newParamText + fnBody
312 );
313
314 }
315
316 /**
317 * Fixes a FunctionExpression node by making it into a longform property.
318 * @param {SourceCodeFixer} fixer The fixer object
319 * @param {ASTNode} node A `Property` node that has a `FunctionExpression` as its value
320 * @returns {Object} A fix for this node
321 */
322 function makeFunctionLongform(fixer, node) {
323 const firstKeyToken = node.computed ? sourceCode.getTokens(node).find(token => token.value === "[") : sourceCode.getFirstToken(node.key);
324 const lastKeyToken = node.computed ? sourceCode.getTokensBetween(node.key, node.value).find(token => token.value === "]") : sourceCode.getLastToken(node.key);
325 const keyText = sourceCode.text.slice(firstKeyToken.range[0], lastKeyToken.range[1]);
326 let functionHeader = "function";
327
328 if (node.value.async) {
329 functionHeader = `async ${functionHeader}`;
330 }
331 if (node.value.generator) {
332 functionHeader = `${functionHeader}*`;
333 }
334
335 return fixer.replaceTextRange([node.range[0], lastKeyToken.range[1]], `${keyText}: ${functionHeader}`);
336 }
337
338 /*
339 * To determine whether a given arrow function has a lexical identifier (`this`, `arguments`, `super`, or `new.target`),
340 * create a stack of functions that define these identifiers (i.e. all functions except arrow functions) as the AST is
341 * traversed. Whenever a new function is encountered, create a new entry on the stack (corresponding to a different lexical
342 * scope of `this`), and whenever a function is exited, pop that entry off the stack. When an arrow function is entered,
343 * keep a reference to it on the current stack entry, and remove that reference when the arrow function is exited.
344 * When a lexical identifier is encountered, mark all the arrow functions on the current stack entry by adding them
345 * to an `arrowsWithLexicalIdentifiers` set. Any arrow function in that set will not be reported by this rule,
346 * because converting it into a method would change the value of one of the lexical identifiers.
347 */
348 const lexicalScopeStack = [];
349 const arrowsWithLexicalIdentifiers = new WeakSet();
350 const argumentsIdentifiers = new WeakSet();
351
352 /**
353 * Enters a function. This creates a new lexical identifier scope, so a new Set of arrow functions is pushed onto the stack.
354 * Also, this marks all `arguments` identifiers so that they can be detected later.
355 * @returns {void}
356 */
357 function enterFunction() {
358 lexicalScopeStack.unshift(new Set());
359 context.getScope().variables.filter(variable => variable.name === "arguments").forEach(variable => {
360 variable.references.map(ref => ref.identifier).forEach(identifier => argumentsIdentifiers.add(identifier));
361 });
362 }
363
364 /**
365 * Exits a function. This pops the current set of arrow functions off the lexical scope stack.
366 * @returns {void}
367 */
368 function exitFunction() {
369 lexicalScopeStack.shift();
370 }
371
372 /**
373 * Marks the current function as having a lexical keyword. This implies that all arrow functions
374 * in the current lexical scope contain a reference to this lexical keyword.
375 * @returns {void}
376 */
377 function reportLexicalIdentifier() {
378 lexicalScopeStack[0].forEach(arrowFunction => arrowsWithLexicalIdentifiers.add(arrowFunction));
379 }
380
381 //--------------------------------------------------------------------------
382 // Public
383 //--------------------------------------------------------------------------
384
385 return {
386 Program: enterFunction,
387 FunctionDeclaration: enterFunction,
388 FunctionExpression: enterFunction,
389 "Program:exit": exitFunction,
390 "FunctionDeclaration:exit": exitFunction,
391 "FunctionExpression:exit": exitFunction,
392
393 ArrowFunctionExpression(node) {
394 lexicalScopeStack[0].add(node);
395 },
396 "ArrowFunctionExpression:exit"(node) {
397 lexicalScopeStack[0].delete(node);
398 },
399
400 ThisExpression: reportLexicalIdentifier,
401 Super: reportLexicalIdentifier,
402 MetaProperty(node) {
403 if (node.meta.name === "new" && node.property.name === "target") {
404 reportLexicalIdentifier();
405 }
406 },
407 Identifier(node) {
408 if (argumentsIdentifiers.has(node)) {
409 reportLexicalIdentifier();
410 }
411 },
412
413 ObjectExpression(node) {
414 if (APPLY_CONSISTENT) {
415 checkConsistency(node, false);
416 } else if (APPLY_CONSISTENT_AS_NEEDED) {
417 checkConsistency(node, true);
418 }
419 },
420
421 "Property:exit"(node) {
422 const isConciseProperty = node.method || node.shorthand;
423
424 // Ignore destructuring assignment
425 if (node.parent.type === "ObjectPattern") {
426 return;
427 }
428
429 // getters and setters are ignored
430 if (node.kind === "get" || node.kind === "set") {
431 return;
432 }
433
434 // only computed methods can fail the following checks
435 if (node.computed && node.value.type !== "FunctionExpression" && node.value.type !== "ArrowFunctionExpression") {
436 return;
437 }
438
439 //--------------------------------------------------------------
440 // Checks for property/method shorthand.
441 if (isConciseProperty) {
442 if (node.method && (APPLY_NEVER || AVOID_QUOTES && isStringLiteral(node.key))) {
443 const messageId = APPLY_NEVER ? "expectedMethodLongform" : "expectedLiteralMethodLongform";
444
445 // { x() {} } should be written as { x: function() {} }
446 context.report({
447 node,
448 messageId,
449 fix: fixer => makeFunctionLongform(fixer, node)
450 });
451 } else if (APPLY_NEVER) {
452
453 // { x } should be written as { x: x }
454 context.report({
455 node,
456 messageId: "expectedPropertyLongform",
457 fix: fixer => fixer.insertTextAfter(node.key, `: ${node.key.name}`)
458 });
459 }
460 } else if (APPLY_TO_METHODS && !node.value.id && (node.value.type === "FunctionExpression" || node.value.type === "ArrowFunctionExpression")) {
461 if (IGNORE_CONSTRUCTORS && node.key.type === "Identifier" && isConstructor(node.key.name)) {
462 return;
463 }
464 if (AVOID_QUOTES && isStringLiteral(node.key)) {
465 return;
466 }
467
468 // {[x]: function(){}} should be written as {[x]() {}}
469 if (node.value.type === "FunctionExpression" ||
470 node.value.type === "ArrowFunctionExpression" &&
471 node.value.body.type === "BlockStatement" &&
472 AVOID_EXPLICIT_RETURN_ARROWS &&
473 !arrowsWithLexicalIdentifiers.has(node.value)
474 ) {
475 context.report({
476 node,
477 messageId: "expectedMethodShorthand",
478 fix: fixer => makeFunctionShorthand(fixer, node)
479 });
480 }
481 } else if (node.value.type === "Identifier" && node.key.name === node.value.name && APPLY_TO_PROPS) {
482
483 // {x: x} should be written as {x}
484 context.report({
485 node,
486 messageId: "expectedPropertyShorthand",
487 fix(fixer) {
488 return fixer.replaceText(node, node.value.name);
489 }
490 });
491 } else if (node.value.type === "Identifier" && node.key.type === "Literal" && node.key.value === node.value.name && APPLY_TO_PROPS) {
492 if (AVOID_QUOTES) {
493 return;
494 }
495
496 // {"x": x} should be written as {x}
497 context.report({
498 node,
499 messageId: "expectedPropertyShorthand",
500 fix(fixer) {
501 return fixer.replaceText(node, node.value.name);
502 }
503 });
504 }
505 }
506 };
507 }
508};