dot-notation.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. /**
  2. * @fileoverview Rule to warn about using dot notation instead of square bracket notation when possible.
  3. * @author Josh Perez
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. const keywords = require("./utils/keywords");
  11. //------------------------------------------------------------------------------
  12. // Rule Definition
  13. //------------------------------------------------------------------------------
  14. const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/u;
  15. // `null` literal must be handled separately.
  16. const literalTypesToCheck = new Set(["string", "boolean"]);
  17. /** @type {import('../shared/types').Rule} */
  18. module.exports = {
  19. meta: {
  20. type: "suggestion",
  21. defaultOptions: [{
  22. allowKeywords: true,
  23. allowPattern: ""
  24. }],
  25. docs: {
  26. description: "Enforce dot notation whenever possible",
  27. recommended: false,
  28. url: "https://eslint.org/docs/latest/rules/dot-notation"
  29. },
  30. schema: [
  31. {
  32. type: "object",
  33. properties: {
  34. allowKeywords: {
  35. type: "boolean"
  36. },
  37. allowPattern: {
  38. type: "string"
  39. }
  40. },
  41. additionalProperties: false
  42. }
  43. ],
  44. fixable: "code",
  45. messages: {
  46. useDot: "[{{key}}] is better written in dot notation.",
  47. useBrackets: ".{{key}} is a syntax error."
  48. }
  49. },
  50. create(context) {
  51. const [options] = context.options;
  52. const allowKeywords = options.allowKeywords;
  53. const sourceCode = context.sourceCode;
  54. let allowPattern;
  55. if (options.allowPattern) {
  56. allowPattern = new RegExp(options.allowPattern, "u");
  57. }
  58. /**
  59. * Check if the property is valid dot notation
  60. * @param {ASTNode} node The dot notation node
  61. * @param {string} value Value which is to be checked
  62. * @returns {void}
  63. */
  64. function checkComputedProperty(node, value) {
  65. if (
  66. validIdentifier.test(value) &&
  67. (allowKeywords || !keywords.includes(String(value))) &&
  68. !(allowPattern && allowPattern.test(value))
  69. ) {
  70. const formattedValue = node.property.type === "Literal" ? JSON.stringify(value) : `\`${value}\``;
  71. context.report({
  72. node: node.property,
  73. messageId: "useDot",
  74. data: {
  75. key: formattedValue
  76. },
  77. *fix(fixer) {
  78. const leftBracket = sourceCode.getTokenAfter(node.object, astUtils.isOpeningBracketToken);
  79. const rightBracket = sourceCode.getLastToken(node);
  80. const nextToken = sourceCode.getTokenAfter(node);
  81. // Don't perform any fixes if there are comments inside the brackets.
  82. if (sourceCode.commentsExistBetween(leftBracket, rightBracket)) {
  83. return;
  84. }
  85. // Replace the brackets by an identifier.
  86. if (!node.optional) {
  87. yield fixer.insertTextBefore(
  88. leftBracket,
  89. astUtils.isDecimalInteger(node.object) ? " ." : "."
  90. );
  91. }
  92. yield fixer.replaceTextRange(
  93. [leftBracket.range[0], rightBracket.range[1]],
  94. value
  95. );
  96. // Insert a space after the property if it will be connected to the next token.
  97. if (
  98. nextToken &&
  99. rightBracket.range[1] === nextToken.range[0] &&
  100. !astUtils.canTokensBeAdjacent(String(value), nextToken)
  101. ) {
  102. yield fixer.insertTextAfter(node, " ");
  103. }
  104. }
  105. });
  106. }
  107. }
  108. return {
  109. MemberExpression(node) {
  110. if (
  111. node.computed &&
  112. node.property.type === "Literal" &&
  113. (literalTypesToCheck.has(typeof node.property.value) || astUtils.isNullLiteral(node.property))
  114. ) {
  115. checkComputedProperty(node, node.property.value);
  116. }
  117. if (
  118. node.computed &&
  119. astUtils.isStaticTemplateLiteral(node.property)
  120. ) {
  121. checkComputedProperty(node, node.property.quasis[0].value.cooked);
  122. }
  123. if (
  124. !allowKeywords &&
  125. !node.computed &&
  126. node.property.type === "Identifier" &&
  127. keywords.includes(String(node.property.name))
  128. ) {
  129. context.report({
  130. node: node.property,
  131. messageId: "useBrackets",
  132. data: {
  133. key: node.property.name
  134. },
  135. *fix(fixer) {
  136. const dotToken = sourceCode.getTokenBefore(node.property);
  137. // A statement that starts with `let[` is parsed as a destructuring variable declaration, not a MemberExpression.
  138. if (node.object.type === "Identifier" && node.object.name === "let" && !node.optional) {
  139. return;
  140. }
  141. // Don't perform any fixes if there are comments between the dot and the property name.
  142. if (sourceCode.commentsExistBetween(dotToken, node.property)) {
  143. return;
  144. }
  145. // Replace the identifier to brackets.
  146. if (!node.optional) {
  147. yield fixer.remove(dotToken);
  148. }
  149. yield fixer.replaceText(node.property, `["${node.property.name}"]`);
  150. }
  151. });
  152. }
  153. }
  154. };
  155. }
  156. };