/** * @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"); const { getModuleExportName, isImportAttributeKey, } = require("./utils/ast-utils"); //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../types').Rule.RuleModule} */ module.exports = { meta: { type: "suggestion", defaultOptions: [ { exceptionPatterns: [], exceptions: [], min: 2, properties: "always", }, ], docs: { description: "Enforce minimum and maximum identifier lengths", recommended: false, frozen: true, url: "https://eslint.org/docs/latest/rules/id-length", }, schema: [ { type: "object", properties: { min: { type: "integer", }, 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; const { max: maxLength = Infinity, min: minLength } = options; 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 && !isImportAttributeKey(node) && !parent.computed && parent.key.name === node.name ); }, ImportSpecifier(parent, node) { return ( parent.local === node && getModuleExportName(parent.imported) !== getModuleExportName(parent.local) ); }, ImportDefaultSpecifier: true, ImportNamespaceSpecifier: 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 }, }); } }, }; }, };