1/** 2 * @fileoverview Rule to require function names to match the name of the variable or property to which they are assigned. 3 * @author Annie Zhang, Pavel Strashkin 4 */ 5 6"use strict"; 7 8//-------------------------------------------------------------------------- 9// Requirements 10//-------------------------------------------------------------------------- 11 12const astUtils = require("./utils/ast-utils"); 13const esutils = require("esutils"); 14 15//-------------------------------------------------------------------------- 16// Helpers 17//-------------------------------------------------------------------------- 18 19/** 20 * Determines if a pattern is `module.exports` or `module["exports"]` 21 * @param {ASTNode} pattern The left side of the AssignmentExpression 22 * @returns {boolean} True if the pattern is `module.exports` or `module["exports"]` 23 */ 24function isModuleExports(pattern) { 25 if (pattern.type === "MemberExpression" && pattern.object.type === "Identifier" && pattern.object.name === "module") { 26 27 // module.exports 28 if (pattern.property.type === "Identifier" && pattern.property.name === "exports") { 29 return true; 30 } 31 32 // module["exports"] 33 if (pattern.property.type === "Literal" && pattern.property.value === "exports") { 34 return true; 35 } 36 } 37 return false; 38} 39 40/** 41 * Determines if a string name is a valid identifier 42 * @param {string} name The string to be checked 43 * @param {int} ecmaVersion The ECMAScript version if specified in the parserOptions config 44 * @returns {boolean} True if the string is a valid identifier 45 */ 46function isIdentifier(name, ecmaVersion) { 47 if (ecmaVersion >= 6) { 48 return esutils.keyword.isIdentifierES6(name); 49 } 50 return esutils.keyword.isIdentifierES5(name); 51} 52 53//------------------------------------------------------------------------------ 54// Rule Definition 55//------------------------------------------------------------------------------ 56 57const alwaysOrNever = { enum: ["always", "never"] }; 58const optionsObject = { 59 type: "object", 60 properties: { 61 considerPropertyDescriptor: { 62 type: "boolean" 63 }, 64 includeCommonJSModuleExports: { 65 type: "boolean" 66 } 67 }, 68 additionalProperties: false 69}; 70 71module.exports = { 72 meta: { 73 type: "suggestion", 74 75 docs: { 76 description: "require function names to match the name of the variable or property to which they are assigned", 77 category: "Stylistic Issues", 78 recommended: false, 79 url: "https://eslint.org/docs/rules/func-name-matching" 80 }, 81 82 schema: { 83 anyOf: [{ 84 type: "array", 85 additionalItems: false, 86 items: [alwaysOrNever, optionsObject] 87 }, { 88 type: "array", 89 additionalItems: false, 90 items: [optionsObject] 91 }] 92 }, 93 94 messages: { 95 matchProperty: "Function name `{{funcName}}` should match property name `{{name}}`.", 96 matchVariable: "Function name `{{funcName}}` should match variable name `{{name}}`.", 97 notMatchProperty: "Function name `{{funcName}}` should not match property name `{{name}}`.", 98 notMatchVariable: "Function name `{{funcName}}` should not match variable name `{{name}}`." 99 } 100 }, 101 102 create(context) { 103 const options = (typeof context.options[0] === "object" ? context.options[0] : context.options[1]) || {}; 104 const nameMatches = typeof context.options[0] === "string" ? context.options[0] : "always"; 105 const considerPropertyDescriptor = options.considerPropertyDescriptor; 106 const includeModuleExports = options.includeCommonJSModuleExports; 107 const ecmaVersion = context.parserOptions && context.parserOptions.ecmaVersion ? context.parserOptions.ecmaVersion : 5; 108 109 /** 110 * Check whether node is a certain CallExpression. 111 * @param {string} objName object name 112 * @param {string} funcName function name 113 * @param {ASTNode} node The node to check 114 * @returns {boolean} `true` if node matches CallExpression 115 */ 116 function isPropertyCall(objName, funcName, node) { 117 if (!node) { 118 return false; 119 } 120 return node.type === "CallExpression" && astUtils.isSpecificMemberAccess(node.callee, objName, funcName); 121 } 122 123 /** 124 * Compares identifiers based on the nameMatches option 125 * @param {string} x the first identifier 126 * @param {string} y the second identifier 127 * @returns {boolean} whether the two identifiers should warn. 128 */ 129 function shouldWarn(x, y) { 130 return (nameMatches === "always" && x !== y) || (nameMatches === "never" && x === y); 131 } 132 133 /** 134 * Reports 135 * @param {ASTNode} node The node to report 136 * @param {string} name The variable or property name 137 * @param {string} funcName The function name 138 * @param {boolean} isProp True if the reported node is a property assignment 139 * @returns {void} 140 */ 141 function report(node, name, funcName, isProp) { 142 let messageId; 143 144 if (nameMatches === "always" && isProp) { 145 messageId = "matchProperty"; 146 } else if (nameMatches === "always") { 147 messageId = "matchVariable"; 148 } else if (isProp) { 149 messageId = "notMatchProperty"; 150 } else { 151 messageId = "notMatchVariable"; 152 } 153 context.report({ 154 node, 155 messageId, 156 data: { 157 name, 158 funcName 159 } 160 }); 161 } 162 163 /** 164 * Determines whether a given node is a string literal 165 * @param {ASTNode} node The node to check 166 * @returns {boolean} `true` if the node is a string literal 167 */ 168 function isStringLiteral(node) { 169 return node.type === "Literal" && typeof node.value === "string"; 170 } 171 172 //-------------------------------------------------------------------------- 173 // Public 174 //-------------------------------------------------------------------------- 175 176 return { 177 VariableDeclarator(node) { 178 if (!node.init || node.init.type !== "FunctionExpression" || node.id.type !== "Identifier") { 179 return; 180 } 181 if (node.init.id && shouldWarn(node.id.name, node.init.id.name)) { 182 report(node, node.id.name, node.init.id.name, false); 183 } 184 }, 185 186 AssignmentExpression(node) { 187 if ( 188 node.right.type !== "FunctionExpression" || 189 (node.left.computed && node.left.property.type !== "Literal") || 190 (!includeModuleExports && isModuleExports(node.left)) || 191 (node.left.type !== "Identifier" && node.left.type !== "MemberExpression") 192 ) { 193 return; 194 } 195 196 const isProp = node.left.type === "MemberExpression"; 197 const name = isProp ? astUtils.getStaticPropertyName(node.left) : node.left.name; 198 199 if (node.right.id && isIdentifier(name) && shouldWarn(name, node.right.id.name)) { 200 report(node, name, node.right.id.name, isProp); 201 } 202 }, 203 204 Property(node) { 205 if (node.value.type !== "FunctionExpression" || !node.value.id || node.computed && !isStringLiteral(node.key)) { 206 return; 207 } 208 209 if (node.key.type === "Identifier") { 210 const functionName = node.value.id.name; 211 let propertyName = node.key.name; 212 213 if (considerPropertyDescriptor && propertyName === "value") { 214 if (isPropertyCall("Object", "defineProperty", node.parent.parent) || isPropertyCall("Reflect", "defineProperty", node.parent.parent)) { 215 const property = node.parent.parent.arguments[1]; 216 217 if (isStringLiteral(property) && shouldWarn(property.value, functionName)) { 218 report(node, property.value, functionName, true); 219 } 220 } else if (isPropertyCall("Object", "defineProperties", node.parent.parent.parent.parent)) { 221 propertyName = node.parent.parent.key.name; 222 if (!node.parent.parent.computed && shouldWarn(propertyName, functionName)) { 223 report(node, propertyName, functionName, true); 224 } 225 } else if (isPropertyCall("Object", "create", node.parent.parent.parent.parent)) { 226 propertyName = node.parent.parent.key.name; 227 if (!node.parent.parent.computed && shouldWarn(propertyName, functionName)) { 228 report(node, propertyName, functionName, true); 229 } 230 } else if (shouldWarn(propertyName, functionName)) { 231 report(node, propertyName, functionName, true); 232 } 233 } else if (shouldWarn(propertyName, functionName)) { 234 report(node, propertyName, functionName, true); 235 } 236 return; 237 } 238 239 if ( 240 isStringLiteral(node.key) && 241 isIdentifier(node.key.value, ecmaVersion) && 242 shouldWarn(node.key.value, node.value.id.name) 243 ) { 244 report(node, node.key.value, node.value.id.name, true); 245 } 246 } 247 }; 248 } 249}; 250