id-length.js 7.5 KB

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