| /** | |
|  * @fileoverview Rule that warns when identifier names are shorter or longer | |
|  * than the values provided in configuration. | |
|  * @author Burak Yigit Kaya aka BYK | |
|  */ | |
| 
 | |
| "use strict"; | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Requirements | |
| //------------------------------------------------------------------------------ | |
|  | |
| const { getGraphemeCount } = require("../shared/string-utils"); | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** @type {import('../shared/types').Rule} */ | |
| module.exports = { | |
|     meta: { | |
|         type: "suggestion", | |
| 
 | |
|         docs: { | |
|             description: "Enforce minimum and maximum identifier lengths", | |
|             recommended: false, | |
|             url: "https://eslint.org/docs/latest/rules/id-length" | |
|         }, | |
| 
 | |
|         schema: [ | |
|             { | |
|                 type: "object", | |
|                 properties: { | |
|                     min: { | |
|                         type: "integer", | |
|                         default: 2 | |
|                     }, | |
|                     max: { | |
|                         type: "integer" | |
|                     }, | |
|                     exceptions: { | |
|                         type: "array", | |
|                         uniqueItems: true, | |
|                         items: { | |
|                             type: "string" | |
|                         } | |
|                     }, | |
|                     exceptionPatterns: { | |
|                         type: "array", | |
|                         uniqueItems: true, | |
|                         items: { | |
|                             type: "string" | |
|                         } | |
|                     }, | |
|                     properties: { | |
|                         enum: ["always", "never"] | |
|                     } | |
|                 }, | |
|                 additionalProperties: false | |
|             } | |
|         ], | |
|         messages: { | |
|             tooShort: "Identifier name '{{name}}' is too short (< {{min}}).", | |
|             tooShortPrivate: "Identifier name '#{{name}}' is too short (< {{min}}).", | |
|             tooLong: "Identifier name '{{name}}' is too long (> {{max}}).", | |
|             tooLongPrivate: "Identifier name #'{{name}}' is too long (> {{max}})." | |
|         } | |
|     }, | |
| 
 | |
|     create(context) { | |
|         const options = context.options[0] || {}; | |
|         const minLength = typeof options.min !== "undefined" ? options.min : 2; | |
|         const maxLength = typeof options.max !== "undefined" ? options.max : Infinity; | |
|         const properties = options.properties !== "never"; | |
|         const exceptions = new Set(options.exceptions); | |
|         const exceptionPatterns = (options.exceptionPatterns || []).map(pattern => new RegExp(pattern, "u")); | |
|         const reportedNodes = new Set(); | |
| 
 | |
|         /** | |
|          * Checks if a string matches the provided exception patterns | |
|          * @param {string} name The string to check. | |
|          * @returns {boolean} if the string is a match | |
|          * @private | |
|          */ | |
|         function matchesExceptionPattern(name) { | |
|             return exceptionPatterns.some(pattern => pattern.test(name)); | |
|         } | |
| 
 | |
|         const SUPPORTED_EXPRESSIONS = { | |
|             MemberExpression: properties && function(parent) { | |
|                 return !parent.computed && ( | |
| 
 | |
|                     // regular property assignment | |
|                     (parent.parent.left === parent && parent.parent.type === "AssignmentExpression" || | |
| 
 | |
|                     // or the last identifier in an ObjectPattern destructuring | |
|                     parent.parent.type === "Property" && parent.parent.value === parent && | |
|                     parent.parent.parent.type === "ObjectPattern" && parent.parent.parent.parent.left === parent.parent.parent) | |
|                 ); | |
|             }, | |
|             AssignmentPattern(parent, node) { | |
|                 return parent.left === node; | |
|             }, | |
|             VariableDeclarator(parent, node) { | |
|                 return parent.id === node; | |
|             }, | |
|             Property(parent, node) { | |
| 
 | |
|                 if (parent.parent.type === "ObjectPattern") { | |
|                     const isKeyAndValueSame = parent.value.name === parent.key.name; | |
| 
 | |
|                     return ( | |
|                         !isKeyAndValueSame && parent.value === node || | |
|                         isKeyAndValueSame && parent.key === node && properties | |
|                     ); | |
|                 } | |
|                 return properties && !parent.computed && parent.key.name === node.name; | |
|             }, | |
|             ImportDefaultSpecifier: true, | |
|             RestElement: true, | |
|             FunctionExpression: true, | |
|             ArrowFunctionExpression: true, | |
|             ClassDeclaration: true, | |
|             FunctionDeclaration: true, | |
|             MethodDefinition: true, | |
|             PropertyDefinition: true, | |
|             CatchClause: true, | |
|             ArrayPattern: true | |
|         }; | |
| 
 | |
|         return { | |
|             [[ | |
|                 "Identifier", | |
|                 "PrivateIdentifier" | |
|             ]](node) { | |
|                 const name = node.name; | |
|                 const parent = node.parent; | |
| 
 | |
|                 const nameLength = getGraphemeCount(name); | |
| 
 | |
|                 const isShort = nameLength < minLength; | |
|                 const isLong = nameLength > maxLength; | |
| 
 | |
|                 if (!(isShort || isLong) || exceptions.has(name) || matchesExceptionPattern(name)) { | |
|                     return; // Nothing to report | |
|                 } | |
| 
 | |
|                 const isValidExpression = SUPPORTED_EXPRESSIONS[parent.type]; | |
| 
 | |
|                 /* | |
|                  * We used the range instead of the node because it's possible | |
|                  * for the same identifier to be represented by two different | |
|                  * nodes, with the most clear example being shorthand properties: | |
|                  * { foo } | |
|                  * In this case, "foo" is represented by one node for the name | |
|                  * and one for the value. The only way to know they are the same | |
|                  * is to look at the range. | |
|                  */ | |
|                 if (isValidExpression && !reportedNodes.has(node.range.toString()) && (isValidExpression === true || isValidExpression(parent, node))) { | |
|                     reportedNodes.add(node.range.toString()); | |
| 
 | |
|                     let messageId = isShort ? "tooShort" : "tooLong"; | |
| 
 | |
|                     if (node.type === "PrivateIdentifier") { | |
|                         messageId += "Private"; | |
|                     } | |
| 
 | |
|                     context.report({ | |
|                         node, | |
|                         messageId, | |
|                         data: { name, min: minLength, max: maxLength } | |
|                     }); | |
|                 } | |
|             } | |
|         }; | |
|     } | |
| };
 |