id-length.js 6.9 KB

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