178 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			178 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * @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 }
 | |
|                     });
 | |
|                 }
 | |
|             }
 | |
|         };
 | |
|     }
 | |
| };
 |