object-shorthand.js 18 KB

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