id-length.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. /**
  2. * @fileoverview Rule that warns when identifier names are shorter or longer
  3. * than the values provided in configuration.
  4. * @author Burak Yigit Kaya aka BYK
  5. */
  6. "use strict";
  7. //------------------------------------------------------------------------------
  8. // Requirements
  9. //------------------------------------------------------------------------------
  10. const { getGraphemeCount } = require("../shared/string-utils");
  11. const {
  12. getModuleExportName,
  13. isImportAttributeKey,
  14. } = require("./utils/ast-utils");
  15. //------------------------------------------------------------------------------
  16. // Rule Definition
  17. //------------------------------------------------------------------------------
  18. /** @type {import('../types').Rule.RuleModule} */
  19. module.exports = {
  20. meta: {
  21. type: "suggestion",
  22. defaultOptions: [
  23. {
  24. exceptionPatterns: [],
  25. exceptions: [],
  26. min: 2,
  27. properties: "always",
  28. },
  29. ],
  30. docs: {
  31. description: "Enforce minimum and maximum identifier lengths",
  32. recommended: false,
  33. frozen: true,
  34. url: "https://eslint.org/docs/latest/rules/id-length",
  35. },
  36. schema: [
  37. {
  38. type: "object",
  39. properties: {
  40. min: {
  41. type: "integer",
  42. },
  43. max: {
  44. type: "integer",
  45. },
  46. exceptions: {
  47. type: "array",
  48. uniqueItems: true,
  49. items: {
  50. type: "string",
  51. },
  52. },
  53. exceptionPatterns: {
  54. type: "array",
  55. uniqueItems: true,
  56. items: {
  57. type: "string",
  58. },
  59. },
  60. properties: {
  61. enum: ["always", "never"],
  62. },
  63. },
  64. additionalProperties: false,
  65. },
  66. ],
  67. messages: {
  68. tooShort: "Identifier name '{{name}}' is too short (< {{min}}).",
  69. tooShortPrivate:
  70. "Identifier name '#{{name}}' is too short (< {{min}}).",
  71. tooLong: "Identifier name '{{name}}' is too long (> {{max}}).",
  72. tooLongPrivate:
  73. "Identifier name #'{{name}}' is too long (> {{max}}).",
  74. },
  75. },
  76. create(context) {
  77. const [options] = context.options;
  78. const { max: maxLength = Infinity, min: minLength } = options;
  79. const properties = options.properties !== "never";
  80. const exceptions = new Set(options.exceptions);
  81. const exceptionPatterns = options.exceptionPatterns.map(
  82. pattern => new RegExp(pattern, "u"),
  83. );
  84. const reportedNodes = new Set();
  85. /**
  86. * Checks if a string matches the provided exception patterns
  87. * @param {string} name The string to check.
  88. * @returns {boolean} if the string is a match
  89. * @private
  90. */
  91. function matchesExceptionPattern(name) {
  92. return exceptionPatterns.some(pattern => pattern.test(name));
  93. }
  94. const SUPPORTED_EXPRESSIONS = {
  95. MemberExpression:
  96. properties &&
  97. function (parent) {
  98. return (
  99. !parent.computed &&
  100. // regular property assignment
  101. ((parent.parent.left === parent &&
  102. parent.parent.type === "AssignmentExpression") ||
  103. // or the last identifier in an ObjectPattern destructuring
  104. (parent.parent.type === "Property" &&
  105. parent.parent.value === parent &&
  106. parent.parent.parent.type === "ObjectPattern" &&
  107. parent.parent.parent.parent.left ===
  108. parent.parent.parent))
  109. );
  110. },
  111. AssignmentPattern(parent, node) {
  112. return parent.left === node;
  113. },
  114. VariableDeclarator(parent, node) {
  115. return parent.id === node;
  116. },
  117. Property(parent, node) {
  118. if (parent.parent.type === "ObjectPattern") {
  119. const isKeyAndValueSame =
  120. parent.value.name === parent.key.name;
  121. return (
  122. (!isKeyAndValueSame && parent.value === node) ||
  123. (isKeyAndValueSame && parent.key === node && properties)
  124. );
  125. }
  126. return (
  127. properties &&
  128. !isImportAttributeKey(node) &&
  129. !parent.computed &&
  130. parent.key.name === node.name
  131. );
  132. },
  133. ImportSpecifier(parent, node) {
  134. return (
  135. parent.local === node &&
  136. getModuleExportName(parent.imported) !==
  137. getModuleExportName(parent.local)
  138. );
  139. },
  140. ImportDefaultSpecifier: true,
  141. ImportNamespaceSpecifier: true,
  142. RestElement: true,
  143. FunctionExpression: true,
  144. ArrowFunctionExpression: true,
  145. ClassDeclaration: true,
  146. FunctionDeclaration: true,
  147. MethodDefinition: true,
  148. PropertyDefinition: true,
  149. CatchClause: true,
  150. ArrayPattern: true,
  151. };
  152. return {
  153. [["Identifier", "PrivateIdentifier"]](node) {
  154. const name = node.name;
  155. const parent = node.parent;
  156. const nameLength = getGraphemeCount(name);
  157. const isShort = nameLength < minLength;
  158. const isLong = nameLength > maxLength;
  159. if (
  160. !(isShort || isLong) ||
  161. exceptions.has(name) ||
  162. matchesExceptionPattern(name)
  163. ) {
  164. return; // Nothing to report
  165. }
  166. const isValidExpression = SUPPORTED_EXPRESSIONS[parent.type];
  167. /*
  168. * We used the range instead of the node because it's possible
  169. * for the same identifier to be represented by two different
  170. * nodes, with the most clear example being shorthand properties:
  171. * { foo }
  172. * In this case, "foo" is represented by one node for the name
  173. * and one for the value. The only way to know they are the same
  174. * is to look at the range.
  175. */
  176. if (
  177. isValidExpression &&
  178. !reportedNodes.has(node.range.toString()) &&
  179. (isValidExpression === true ||
  180. isValidExpression(parent, node))
  181. ) {
  182. reportedNodes.add(node.range.toString());
  183. let messageId = isShort ? "tooShort" : "tooLong";
  184. if (node.type === "PrivateIdentifier") {
  185. messageId += "Private";
  186. }
  187. context.report({
  188. node,
  189. messageId,
  190. data: { name, min: minLength, max: maxLength },
  191. });
  192. }
  193. },
  194. };
  195. },
  196. };