2 * @fileoverview Rule to enforce concise object methods and properties.
3 * @author Jamund Ferguson
12 properties: "properties",
13 consistent: "consistent",
14 consistentAsNeeded: "consistent-as-needed"
17 //------------------------------------------------------------------------------
19 //------------------------------------------------------------------------------
20 const astUtils = require("./utils/ast-utils");
22 //------------------------------------------------------------------------------
24 //------------------------------------------------------------------------------
30 description: "require or disallow method and property shorthand syntax for object literals",
31 category: "ECMAScript 6",
33 url: "https://eslint.org/docs/rules/object-shorthand"
44 enum: ["always", "methods", "properties", "never", "consistent", "consistent-as-needed"]
54 enum: ["always", "methods", "properties"]
63 additionalProperties: false
73 enum: ["always", "methods"]
84 avoidExplicitReturnArrows: {
88 additionalProperties: false
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;
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();
112 //--------------------------------------------------------------------------
114 //--------------------------------------------------------------------------
116 const CTOR_PREFIX_REGEX = /[^_$0-9]/u;
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.
124 function isConstructor(name) {
125 const match = CTOR_PREFIX_REGEX.exec(name);
127 // Not a constructor if name has no characters apart from '_', '$' and digits e.g. '_', '$$', '_8'
132 const firstChar = name.charAt(match.index);
134 return firstChar === firstChar.toUpperCase();
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
144 function canHaveShorthand(property) {
145 return (property.kind !== "set" && property.kind !== "get" && property.type !== "SpreadElement" && property.type !== "SpreadProperty" && property.type !== "ExperimentalSpreadProperty");
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.
153 function isStringLiteral(node) {
154 return node.type === "Literal" && typeof node.value === "string";
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.
164 function isShorthand(property) {
166 // property.method is true when `{a(){}}`.
167 return (property.shorthand || property.method);
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.
177 function isRedundant(property) {
178 const value = property.value;
180 if (value.type === "FunctionExpression") {
181 return !value.id; // Only anonymous should be shorthand method.
183 if (value.type === "Identifier") {
184 return astUtils.getStaticPropertyName(property) === value.name;
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
197 function checkConsistency(node, checkRedundancy) {
199 // We are excluding getters/setters and spread properties as they are considered neither longform nor shorthand.
200 const properties = node.properties.filter(canHaveShorthand);
202 // Do we still have properties left after filtering the getters and setters?
203 if (properties.length > 0) {
204 const shorthandProperties = properties.filter(isShorthand);
207 * If we do not have an equal number of longform properties as
208 * shorthand properties, we are using the annotations inconsistently
210 if (shorthandProperties.length !== properties.length) {
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) {
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.
221 const canAlwaysUseShorthand = properties.every(isRedundant);
223 if (canAlwaysUseShorthand) {
224 context.report({ node, message: "Expected shorthand for all properties." });
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
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]);
247 // key: /* */ () => {}
248 if (sourceCode.commentsExistBetween(lastKeyToken, node.value)) {
252 if (node.value.async) {
253 keyPrefix += "async ";
255 if (node.value.generator) {
259 const fixRange = [firstKeyToken.range[0], node.range[1]];
260 const methodPrefix = keyPrefix + keyText;
262 if (node.value.type === "FunctionExpression") {
263 const functionToken = sourceCode.getTokens(node.value).find(token => token.type === "Keyword" && token.value === "function");
264 const tokenBeforeParams = node.value.generator ? sourceCode.getTokenAfter(functionToken) : functionToken;
266 return fixer.replaceTextRange(
268 methodPrefix + sourceCode.text.slice(tokenBeforeParams.range[1], node.value.range[1])
272 const arrowToken = sourceCode.getTokenBefore(node.value.body, astUtils.isArrowToken);
273 const fnBody = sourceCode.text.slice(arrowToken.range[1], node.value.range[1]);
275 let shouldAddParensAroundParameters = false;
276 let tokenBeforeParams;
278 if (node.value.params.length === 0) {
279 tokenBeforeParams = sourceCode.getFirstToken(node.value, astUtils.isOpeningParenToken);
281 tokenBeforeParams = sourceCode.getTokenBefore(node.value.params[0]);
284 if (node.value.params.length === 1) {
285 const hasParen = astUtils.isOpeningParenToken(tokenBeforeParams);
286 const isTokenOutsideNode = tokenBeforeParams.range[0] < node.range[0];
288 shouldAddParensAroundParameters = !hasParen || isTokenOutsideNode;
291 const sliceStart = shouldAddParensAroundParameters
292 ? node.value.params[0].range[0]
293 : tokenBeforeParams.range[0];
294 const sliceEnd = sourceCode.getTokenBefore(arrowToken).range[1];
296 const oldParamText = sourceCode.text.slice(sliceStart, sliceEnd);
297 const newParamText = shouldAddParensAroundParameters ? `(${oldParamText})` : oldParamText;
299 return fixer.replaceTextRange(
301 methodPrefix + newParamText + fnBody
307 * Fixes a FunctionExpression node by making it into a longform property.
308 * @param {SourceCodeFixer} fixer The fixer object
309 * @param {ASTNode} node A `Property` node that has a `FunctionExpression` as its value
310 * @returns {Object} A fix for this node
312 function makeFunctionLongform(fixer, node) {
313 const firstKeyToken = node.computed ? sourceCode.getTokens(node).find(token => token.value === "[") : sourceCode.getFirstToken(node.key);
314 const lastKeyToken = node.computed ? sourceCode.getTokensBetween(node.key, node.value).find(token => token.value === "]") : sourceCode.getLastToken(node.key);
315 const keyText = sourceCode.text.slice(firstKeyToken.range[0], lastKeyToken.range[1]);
316 let functionHeader = "function";
318 if (node.value.async) {
319 functionHeader = `async ${functionHeader}`;
321 if (node.value.generator) {
322 functionHeader = `${functionHeader}*`;
325 return fixer.replaceTextRange([node.range[0], lastKeyToken.range[1]], `${keyText}: ${functionHeader}`);
329 * To determine whether a given arrow function has a lexical identifier (`this`, `arguments`, `super`, or `new.target`),
330 * create a stack of functions that define these identifiers (i.e. all functions except arrow functions) as the AST is
331 * traversed. Whenever a new function is encountered, create a new entry on the stack (corresponding to a different lexical
332 * scope of `this`), and whenever a function is exited, pop that entry off the stack. When an arrow function is entered,
333 * keep a reference to it on the current stack entry, and remove that reference when the arrow function is exited.
334 * When a lexical identifier is encountered, mark all the arrow functions on the current stack entry by adding them
335 * to an `arrowsWithLexicalIdentifiers` set. Any arrow function in that set will not be reported by this rule,
336 * because converting it into a method would change the value of one of the lexical identifiers.
338 const lexicalScopeStack = [];
339 const arrowsWithLexicalIdentifiers = new WeakSet();
340 const argumentsIdentifiers = new WeakSet();
343 * Enters a function. This creates a new lexical identifier scope, so a new Set of arrow functions is pushed onto the stack.
344 * Also, this marks all `arguments` identifiers so that they can be detected later.
347 function enterFunction() {
348 lexicalScopeStack.unshift(new Set());
349 context.getScope().variables.filter(variable => variable.name === "arguments").forEach(variable => {
350 variable.references.map(ref => ref.identifier).forEach(identifier => argumentsIdentifiers.add(identifier));
355 * Exits a function. This pops the current set of arrow functions off the lexical scope stack.
358 function exitFunction() {
359 lexicalScopeStack.shift();
363 * Marks the current function as having a lexical keyword. This implies that all arrow functions
364 * in the current lexical scope contain a reference to this lexical keyword.
367 function reportLexicalIdentifier() {
368 lexicalScopeStack[0].forEach(arrowFunction => arrowsWithLexicalIdentifiers.add(arrowFunction));
371 //--------------------------------------------------------------------------
373 //--------------------------------------------------------------------------
376 Program: enterFunction,
377 FunctionDeclaration: enterFunction,
378 FunctionExpression: enterFunction,
379 "Program:exit": exitFunction,
380 "FunctionDeclaration:exit": exitFunction,
381 "FunctionExpression:exit": exitFunction,
383 ArrowFunctionExpression(node) {
384 lexicalScopeStack[0].add(node);
386 "ArrowFunctionExpression:exit"(node) {
387 lexicalScopeStack[0].delete(node);
390 ThisExpression: reportLexicalIdentifier,
391 Super: reportLexicalIdentifier,
393 if (node.meta.name === "new" && node.property.name === "target") {
394 reportLexicalIdentifier();
398 if (argumentsIdentifiers.has(node)) {
399 reportLexicalIdentifier();
403 ObjectExpression(node) {
404 if (APPLY_CONSISTENT) {
405 checkConsistency(node, false);
406 } else if (APPLY_CONSISTENT_AS_NEEDED) {
407 checkConsistency(node, true);
411 "Property:exit"(node) {
412 const isConciseProperty = node.method || node.shorthand;
414 // Ignore destructuring assignment
415 if (node.parent.type === "ObjectPattern") {
419 // getters and setters are ignored
420 if (node.kind === "get" || node.kind === "set") {
424 // only computed methods can fail the following checks
425 if (node.computed && node.value.type !== "FunctionExpression" && node.value.type !== "ArrowFunctionExpression") {
429 //--------------------------------------------------------------
430 // Checks for property/method shorthand.
431 if (isConciseProperty) {
432 if (node.method && (APPLY_NEVER || AVOID_QUOTES && isStringLiteral(node.key))) {
433 const message = APPLY_NEVER ? "Expected longform method syntax." : "Expected longform method syntax for string literal keys.";
435 // { x() {} } should be written as { x: function() {} }
439 fix: fixer => makeFunctionLongform(fixer, node)
441 } else if (APPLY_NEVER) {
443 // { x } should be written as { x: x }
446 message: "Expected longform property syntax.",
447 fix: fixer => fixer.insertTextAfter(node.key, `: ${node.key.name}`)
450 } else if (APPLY_TO_METHODS && !node.value.id && (node.value.type === "FunctionExpression" || node.value.type === "ArrowFunctionExpression")) {
451 if (IGNORE_CONSTRUCTORS && node.key.type === "Identifier" && isConstructor(node.key.name)) {
454 if (AVOID_QUOTES && isStringLiteral(node.key)) {
458 // {[x]: function(){}} should be written as {[x]() {}}
459 if (node.value.type === "FunctionExpression" ||
460 node.value.type === "ArrowFunctionExpression" &&
461 node.value.body.type === "BlockStatement" &&
462 AVOID_EXPLICIT_RETURN_ARROWS &&
463 !arrowsWithLexicalIdentifiers.has(node.value)
467 message: "Expected method shorthand.",
468 fix: fixer => makeFunctionShorthand(fixer, node)
471 } else if (node.value.type === "Identifier" && node.key.name === node.value.name && APPLY_TO_PROPS) {
473 // {x: x} should be written as {x}
476 message: "Expected property shorthand.",
478 return fixer.replaceText(node, node.value.name);
481 } else if (node.value.type === "Identifier" && node.key.type === "Literal" && node.key.value === node.value.name && APPLY_TO_PROPS) {
486 // {"x": x} should be written as {x}
489 message: "Expected property shorthand.",
491 return fixer.replaceText(node, node.value.name);