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}; 509