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
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."
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;
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();
122 //--------------------------------------------------------------------------
124 //--------------------------------------------------------------------------
126 const CTOR_PREFIX_REGEX = /[^_$0-9]/u;
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.
134 function isConstructor(name) {
135 const match = CTOR_PREFIX_REGEX.exec(name);
137 // Not a constructor if name has no characters apart from '_', '$' and digits e.g. '_', '$$', '_8'
142 const firstChar = name.charAt(match.index);
144 return firstChar === firstChar.toUpperCase();
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
154 function canHaveShorthand(property) {
155 return (property.kind !== "set" && property.kind !== "get" && property.type !== "SpreadElement" && property.type !== "SpreadProperty" && property.type !== "ExperimentalSpreadProperty");
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.
163 function isStringLiteral(node) {
164 return node.type === "Literal" && typeof node.value === "string";
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.
174 function isShorthand(property) {
176 // property.method is true when `{a(){}}`.
177 return (property.shorthand || property.method);
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.
187 function isRedundant(property) {
188 const value = property.value;
190 if (value.type === "FunctionExpression") {
191 return !value.id; // Only anonymous should be shorthand method.
193 if (value.type === "Identifier") {
194 return astUtils.getStaticPropertyName(property) === value.name;
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
207 function checkConsistency(node, checkRedundancy) {
209 // We are excluding getters/setters and spread properties as they are considered neither longform nor shorthand.
210 const properties = node.properties.filter(canHaveShorthand);
212 // Do we still have properties left after filtering the getters and setters?
213 if (properties.length > 0) {
214 const shorthandProperties = properties.filter(isShorthand);
217 * If we do not have an equal number of longform properties as
218 * shorthand properties, we are using the annotations inconsistently
220 if (shorthandProperties.length !== properties.length) {
222 // We have at least 1 shorthand property
223 if (shorthandProperties.length > 0) {
224 context.report({ node, messageId: "unexpectedMix" });
225 } else if (checkRedundancy) {
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.
231 const canAlwaysUseShorthand = properties.every(isRedundant);
233 if (canAlwaysUseShorthand) {
234 context.report({ node, messageId: "expectedAllPropertiesShorthanded" });
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
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]);
257 // key: /* */ () => {}
258 if (sourceCode.commentsExistBetween(lastKeyToken, node.value)) {
262 if (node.value.async) {
263 keyPrefix += "async ";
265 if (node.value.generator) {
269 const fixRange = [firstKeyToken.range[0], node.range[1]];
270 const methodPrefix = keyPrefix + keyText;
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;
276 return fixer.replaceTextRange(
278 methodPrefix + sourceCode.text.slice(tokenBeforeParams.range[1], node.value.range[1])
282 const arrowToken = sourceCode.getTokenBefore(node.value.body, astUtils.isArrowToken);
283 const fnBody = sourceCode.text.slice(arrowToken.range[1], node.value.range[1]);
285 let shouldAddParensAroundParameters = false;
286 let tokenBeforeParams;
288 if (node.value.params.length === 0) {
289 tokenBeforeParams = sourceCode.getFirstToken(node.value, astUtils.isOpeningParenToken);
291 tokenBeforeParams = sourceCode.getTokenBefore(node.value.params[0]);
294 if (node.value.params.length === 1) {
295 const hasParen = astUtils.isOpeningParenToken(tokenBeforeParams);
296 const isTokenOutsideNode = tokenBeforeParams.range[0] < node.range[0];
298 shouldAddParensAroundParameters = !hasParen || isTokenOutsideNode;
301 const sliceStart = shouldAddParensAroundParameters
302 ? node.value.params[0].range[0]
303 : tokenBeforeParams.range[0];
304 const sliceEnd = sourceCode.getTokenBefore(arrowToken).range[1];
306 const oldParamText = sourceCode.text.slice(sliceStart, sliceEnd);
307 const newParamText = shouldAddParensAroundParameters ? `(${oldParamText})` : oldParamText;
309 return fixer.replaceTextRange(
311 methodPrefix + newParamText + fnBody
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
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";
328 if (node.value.async) {
329 functionHeader = `async ${functionHeader}`;
331 if (node.value.generator) {
332 functionHeader = `${functionHeader}*`;
335 return fixer.replaceTextRange([node.range[0], lastKeyToken.range[1]], `${keyText}: ${functionHeader}`);
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.
348 const lexicalScopeStack = [];
349 const arrowsWithLexicalIdentifiers = new WeakSet();
350 const argumentsIdentifiers = new WeakSet();
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.
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));
365 * Exits a function. This pops the current set of arrow functions off the lexical scope stack.
368 function exitFunction() {
369 lexicalScopeStack.shift();
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.
377 function reportLexicalIdentifier() {
378 lexicalScopeStack[0].forEach(arrowFunction => arrowsWithLexicalIdentifiers.add(arrowFunction));
381 //--------------------------------------------------------------------------
383 //--------------------------------------------------------------------------
386 Program: enterFunction,
387 FunctionDeclaration: enterFunction,
388 FunctionExpression: enterFunction,
389 "Program:exit": exitFunction,
390 "FunctionDeclaration:exit": exitFunction,
391 "FunctionExpression:exit": exitFunction,
393 ArrowFunctionExpression(node) {
394 lexicalScopeStack[0].add(node);
396 "ArrowFunctionExpression:exit"(node) {
397 lexicalScopeStack[0].delete(node);
400 ThisExpression: reportLexicalIdentifier,
401 Super: reportLexicalIdentifier,
403 if (node.meta.name === "new" && node.property.name === "target") {
404 reportLexicalIdentifier();
408 if (argumentsIdentifiers.has(node)) {
409 reportLexicalIdentifier();
413 ObjectExpression(node) {
414 if (APPLY_CONSISTENT) {
415 checkConsistency(node, false);
416 } else if (APPLY_CONSISTENT_AS_NEEDED) {
417 checkConsistency(node, true);
421 "Property:exit"(node) {
422 const isConciseProperty = node.method || node.shorthand;
424 // Ignore destructuring assignment
425 if (node.parent.type === "ObjectPattern") {
429 // getters and setters are ignored
430 if (node.kind === "get" || node.kind === "set") {
434 // only computed methods can fail the following checks
435 if (node.computed && node.value.type !== "FunctionExpression" && node.value.type !== "ArrowFunctionExpression") {
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";
445 // { x() {} } should be written as { x: function() {} }
449 fix: fixer => makeFunctionLongform(fixer, node)
451 } else if (APPLY_NEVER) {
453 // { x } should be written as { x: x }
456 messageId: "expectedPropertyLongform",
457 fix: fixer => fixer.insertTextAfter(node.key, `: ${node.key.name}`)
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)) {
464 if (AVOID_QUOTES && isStringLiteral(node.key)) {
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)
477 messageId: "expectedMethodShorthand",
478 fix: fixer => makeFunctionShorthand(fixer, node)
481 } else if (node.value.type === "Identifier" && node.key.name === node.value.name && APPLY_TO_PROPS) {
483 // {x: x} should be written as {x}
486 messageId: "expectedPropertyShorthand",
488 return fixer.replaceText(node, node.value.name);
491 } else if (node.value.type === "Identifier" && node.key.type === "Literal" && node.key.value === node.value.name && APPLY_TO_PROPS) {
496 // {"x": x} should be written as {x}
499 messageId: "expectedPropertyShorthand",
501 return fixer.replaceText(node, node.value.name);