dot-notation.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  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_$][\w$]*$/u;
  15. // `null` literal must be handled separately.
  16. const literalTypesToCheck = new Set(["string", "boolean"]);
  17. /** @type {import('../types').Rule.RuleModule} */
  18. module.exports = {
  19. meta: {
  20. type: "suggestion",
  21. defaultOptions: [
  22. {
  23. allowKeywords: true,
  24. allowPattern: "",
  25. },
  26. ],
  27. docs: {
  28. description: "Enforce dot notation whenever possible",
  29. recommended: false,
  30. frozen: true,
  31. url: "https://eslint.org/docs/latest/rules/dot-notation",
  32. },
  33. schema: [
  34. {
  35. type: "object",
  36. properties: {
  37. allowKeywords: {
  38. type: "boolean",
  39. },
  40. allowPattern: {
  41. type: "string",
  42. },
  43. },
  44. additionalProperties: false,
  45. },
  46. ],
  47. fixable: "code",
  48. messages: {
  49. useDot: "[{{key}}] is better written in dot notation.",
  50. useBrackets: ".{{key}} is a syntax error.",
  51. },
  52. },
  53. create(context) {
  54. const [options] = context.options;
  55. const allowKeywords = options.allowKeywords;
  56. const sourceCode = context.sourceCode;
  57. let allowPattern;
  58. if (options.allowPattern) {
  59. allowPattern = new RegExp(options.allowPattern, "u");
  60. }
  61. /**
  62. * Check if the property is valid dot notation
  63. * @param {ASTNode} node The dot notation node
  64. * @param {string} value Value which is to be checked
  65. * @returns {void}
  66. */
  67. function checkComputedProperty(node, value) {
  68. if (
  69. validIdentifier.test(value) &&
  70. (allowKeywords || !keywords.includes(String(value))) &&
  71. !(allowPattern && allowPattern.test(value))
  72. ) {
  73. const formattedValue =
  74. node.property.type === "Literal"
  75. ? JSON.stringify(value)
  76. : `\`${value}\``;
  77. context.report({
  78. node: node.property,
  79. messageId: "useDot",
  80. data: {
  81. key: formattedValue,
  82. },
  83. *fix(fixer) {
  84. const leftBracket = sourceCode.getTokenAfter(
  85. node.object,
  86. astUtils.isOpeningBracketToken,
  87. );
  88. const rightBracket = sourceCode.getLastToken(node);
  89. const nextToken = sourceCode.getTokenAfter(node);
  90. // Don't perform any fixes if there are comments inside the brackets.
  91. if (
  92. sourceCode.commentsExistBetween(
  93. leftBracket,
  94. rightBracket,
  95. )
  96. ) {
  97. return;
  98. }
  99. // Replace the brackets by an identifier.
  100. if (!node.optional) {
  101. yield fixer.insertTextBefore(
  102. leftBracket,
  103. astUtils.isDecimalInteger(node.object)
  104. ? " ."
  105. : ".",
  106. );
  107. }
  108. yield fixer.replaceTextRange(
  109. [leftBracket.range[0], rightBracket.range[1]],
  110. value,
  111. );
  112. // Insert a space after the property if it will be connected to the next token.
  113. if (
  114. nextToken &&
  115. rightBracket.range[1] === nextToken.range[0] &&
  116. !astUtils.canTokensBeAdjacent(
  117. String(value),
  118. nextToken,
  119. )
  120. ) {
  121. yield fixer.insertTextAfter(node, " ");
  122. }
  123. },
  124. });
  125. }
  126. }
  127. return {
  128. MemberExpression(node) {
  129. if (
  130. node.computed &&
  131. node.property.type === "Literal" &&
  132. (literalTypesToCheck.has(typeof node.property.value) ||
  133. astUtils.isNullLiteral(node.property))
  134. ) {
  135. checkComputedProperty(node, node.property.value);
  136. }
  137. if (
  138. node.computed &&
  139. astUtils.isStaticTemplateLiteral(node.property)
  140. ) {
  141. checkComputedProperty(
  142. node,
  143. node.property.quasis[0].value.cooked,
  144. );
  145. }
  146. if (
  147. !allowKeywords &&
  148. !node.computed &&
  149. node.property.type === "Identifier" &&
  150. keywords.includes(String(node.property.name))
  151. ) {
  152. context.report({
  153. node: node.property,
  154. messageId: "useBrackets",
  155. data: {
  156. key: node.property.name,
  157. },
  158. *fix(fixer) {
  159. const dotToken = sourceCode.getTokenBefore(
  160. node.property,
  161. );
  162. // A statement that starts with `let[` is parsed as a destructuring variable declaration, not a MemberExpression.
  163. if (
  164. node.object.type === "Identifier" &&
  165. node.object.name === "let" &&
  166. !node.optional
  167. ) {
  168. return;
  169. }
  170. // Don't perform any fixes if there are comments between the dot and the property name.
  171. if (
  172. sourceCode.commentsExistBetween(
  173. dotToken,
  174. node.property,
  175. )
  176. ) {
  177. return;
  178. }
  179. // Replace the identifier to brackets.
  180. if (!node.optional) {
  181. yield fixer.remove(dotToken);
  182. }
  183. yield fixer.replaceText(
  184. node.property,
  185. `["${node.property.name}"]`,
  186. );
  187. },
  188. });
  189. }
  190. },
  191. };
  192. },
  193. };