/** * @fileoverview Rule to flag dangling underscores in variable declarations. * @author Matt DuVall */ "use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { type: "suggestion", docs: { description: "Disallow dangling underscores in identifiers", recommended: false, url: "https://eslint.org/docs/rules/no-underscore-dangle" }, schema: [ { type: "object", properties: { allow: { type: "array", items: { type: "string" } }, allowAfterThis: { type: "boolean", default: false }, allowAfterSuper: { type: "boolean", default: false }, allowAfterThisConstructor: { type: "boolean", default: false }, enforceInMethodNames: { type: "boolean", default: false }, allowFunctionParams: { type: "boolean", default: true }, enforceInClassFields: { type: "boolean", default: false } }, additionalProperties: false } ], messages: { unexpectedUnderscore: "Unexpected dangling '_' in '{{identifier}}'." } }, create(context) { const options = context.options[0] || {}; const ALLOWED_VARIABLES = options.allow ? options.allow : []; const allowAfterThis = typeof options.allowAfterThis !== "undefined" ? options.allowAfterThis : false; const allowAfterSuper = typeof options.allowAfterSuper !== "undefined" ? options.allowAfterSuper : false; const allowAfterThisConstructor = typeof options.allowAfterThisConstructor !== "undefined" ? options.allowAfterThisConstructor : false; const enforceInMethodNames = typeof options.enforceInMethodNames !== "undefined" ? options.enforceInMethodNames : false; const enforceInClassFields = typeof options.enforceInClassFields !== "undefined" ? options.enforceInClassFields : false; const allowFunctionParams = typeof options.allowFunctionParams !== "undefined" ? options.allowFunctionParams : true; //------------------------------------------------------------------------- // Helpers //------------------------------------------------------------------------- /** * Check if identifier is present inside the allowed option * @param {string} identifier name of the node * @returns {boolean} true if its is present * @private */ function isAllowed(identifier) { return ALLOWED_VARIABLES.includes(identifier); } /** * Check if identifier has a dangling underscore * @param {string} identifier name of the node * @returns {boolean} true if its is present * @private */ function hasDanglingUnderscore(identifier) { const len = identifier.length; return identifier !== "_" && (identifier[0] === "_" || identifier[len - 1] === "_"); } /** * Check if identifier is a special case member expression * @param {string} identifier name of the node * @returns {boolean} true if its is a special case * @private */ function isSpecialCaseIdentifierForMemberExpression(identifier) { return identifier === "__proto__"; } /** * Check if identifier is a special case variable expression * @param {string} identifier name of the node * @returns {boolean} true if its is a special case * @private */ function isSpecialCaseIdentifierInVariableExpression(identifier) { // Checks for the underscore library usage here return identifier === "_"; } /** * Check if a node is a member reference of this.constructor * @param {ASTNode} node node to evaluate * @returns {boolean} true if it is a reference on this.constructor * @private */ function isThisConstructorReference(node) { return node.object.type === "MemberExpression" && node.object.property.name === "constructor" && node.object.object.type === "ThisExpression"; } /** * Check if function parameter has a dangling underscore. * @param {ASTNode} node function node to evaluate * @returns {void} * @private */ function checkForDanglingUnderscoreInFunctionParameters(node) { if (!allowFunctionParams) { node.params.forEach(param => { const { type } = param; let nodeToCheck; if (type === "RestElement") { nodeToCheck = param.argument; } else if (type === "AssignmentPattern") { nodeToCheck = param.left; } else { nodeToCheck = param; } if (nodeToCheck.type === "Identifier") { const identifier = nodeToCheck.name; if (hasDanglingUnderscore(identifier) && !isAllowed(identifier)) { context.report({ node: param, messageId: "unexpectedUnderscore", data: { identifier } }); } } }); } } /** * Check if function has a dangling underscore * @param {ASTNode} node node to evaluate * @returns {void} * @private */ function checkForDanglingUnderscoreInFunction(node) { if (node.type === "FunctionDeclaration" && node.id) { const identifier = node.id.name; if (typeof identifier !== "undefined" && hasDanglingUnderscore(identifier) && !isAllowed(identifier)) { context.report({ node, messageId: "unexpectedUnderscore", data: { identifier } }); } } checkForDanglingUnderscoreInFunctionParameters(node); } /** * Check if variable expression has a dangling underscore * @param {ASTNode} node node to evaluate * @returns {void} * @private */ function checkForDanglingUnderscoreInVariableExpression(node) { const identifier = node.id.name; if (typeof identifier !== "undefined" && hasDanglingUnderscore(identifier) && !isSpecialCaseIdentifierInVariableExpression(identifier) && !isAllowed(identifier)) { context.report({ node, messageId: "unexpectedUnderscore", data: { identifier } }); } } /** * Check if member expression has a dangling underscore * @param {ASTNode} node node to evaluate * @returns {void} * @private */ function checkForDanglingUnderscoreInMemberExpression(node) { const identifier = node.property.name, isMemberOfThis = node.object.type === "ThisExpression", isMemberOfSuper = node.object.type === "Super", isMemberOfThisConstructor = isThisConstructorReference(node); if (typeof identifier !== "undefined" && hasDanglingUnderscore(identifier) && !(isMemberOfThis && allowAfterThis) && !(isMemberOfSuper && allowAfterSuper) && !(isMemberOfThisConstructor && allowAfterThisConstructor) && !isSpecialCaseIdentifierForMemberExpression(identifier) && !isAllowed(identifier)) { context.report({ node, messageId: "unexpectedUnderscore", data: { identifier } }); } } /** * Check if method declaration or method property has a dangling underscore * @param {ASTNode} node node to evaluate * @returns {void} * @private */ function checkForDanglingUnderscoreInMethod(node) { const identifier = node.key.name; const isMethod = node.type === "MethodDefinition" || node.type === "Property" && node.method; if (typeof identifier !== "undefined" && enforceInMethodNames && isMethod && hasDanglingUnderscore(identifier) && !isAllowed(identifier)) { context.report({ node, messageId: "unexpectedUnderscore", data: { identifier: node.key.type === "PrivateIdentifier" ? `#${identifier}` : identifier } }); } } /** * Check if a class field has a dangling underscore * @param {ASTNode} node node to evaluate * @returns {void} * @private */ function checkForDanglingUnderscoreInClassField(node) { const identifier = node.key.name; if (typeof identifier !== "undefined" && hasDanglingUnderscore(identifier) && enforceInClassFields && !isAllowed(identifier)) { context.report({ node, messageId: "unexpectedUnderscore", data: { identifier: node.key.type === "PrivateIdentifier" ? `#${identifier}` : identifier } }); } } //-------------------------------------------------------------------------- // Public API //-------------------------------------------------------------------------- return { FunctionDeclaration: checkForDanglingUnderscoreInFunction, VariableDeclarator: checkForDanglingUnderscoreInVariableExpression, MemberExpression: checkForDanglingUnderscoreInMemberExpression, MethodDefinition: checkForDanglingUnderscoreInMethod, PropertyDefinition: checkForDanglingUnderscoreInClassField, Property: checkForDanglingUnderscoreInMethod, FunctionExpression: checkForDanglingUnderscoreInFunction, ArrowFunctionExpression: checkForDanglingUnderscoreInFunction }; } };