id-match.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. /**
  2. * @fileoverview Rule to flag non-matching identifiers
  3. * @author Matthieu Larcher
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Rule Definition
  12. //------------------------------------------------------------------------------
  13. /** @type {import('../types').Rule.RuleModule} */
  14. module.exports = {
  15. meta: {
  16. type: "suggestion",
  17. defaultOptions: [
  18. "^.+$",
  19. {
  20. classFields: false,
  21. ignoreDestructuring: false,
  22. onlyDeclarations: false,
  23. properties: false,
  24. },
  25. ],
  26. docs: {
  27. description:
  28. "Require identifiers to match a specified regular expression",
  29. recommended: false,
  30. frozen: true,
  31. url: "https://eslint.org/docs/latest/rules/id-match",
  32. },
  33. schema: [
  34. {
  35. type: "string",
  36. },
  37. {
  38. type: "object",
  39. properties: {
  40. properties: {
  41. type: "boolean",
  42. },
  43. classFields: {
  44. type: "boolean",
  45. },
  46. onlyDeclarations: {
  47. type: "boolean",
  48. },
  49. ignoreDestructuring: {
  50. type: "boolean",
  51. },
  52. },
  53. additionalProperties: false,
  54. },
  55. ],
  56. messages: {
  57. notMatch:
  58. "Identifier '{{name}}' does not match the pattern '{{pattern}}'.",
  59. notMatchPrivate:
  60. "Identifier '#{{name}}' does not match the pattern '{{pattern}}'.",
  61. },
  62. },
  63. create(context) {
  64. //--------------------------------------------------------------------------
  65. // Options
  66. //--------------------------------------------------------------------------
  67. const [
  68. pattern,
  69. {
  70. classFields: checkClassFields,
  71. ignoreDestructuring,
  72. onlyDeclarations,
  73. properties: checkProperties,
  74. },
  75. ] = context.options;
  76. const regexp = new RegExp(pattern, "u");
  77. const sourceCode = context.sourceCode;
  78. let globalScope;
  79. //--------------------------------------------------------------------------
  80. // Helpers
  81. //--------------------------------------------------------------------------
  82. // contains reported nodes to avoid reporting twice on destructuring with shorthand notation
  83. const reportedNodes = new Set();
  84. const ALLOWED_PARENT_TYPES = new Set([
  85. "CallExpression",
  86. "NewExpression",
  87. ]);
  88. const DECLARATION_TYPES = new Set([
  89. "FunctionDeclaration",
  90. "VariableDeclarator",
  91. ]);
  92. const IMPORT_TYPES = new Set([
  93. "ImportSpecifier",
  94. "ImportNamespaceSpecifier",
  95. "ImportDefaultSpecifier",
  96. ]);
  97. /**
  98. * Checks whether the given node represents a reference to a global variable that is not declared in the source code.
  99. * These identifiers will be allowed, as it is assumed that user has no control over the names of external global variables.
  100. * @param {ASTNode} node `Identifier` node to check.
  101. * @returns {boolean} `true` if the node is a reference to a global variable.
  102. */
  103. function isReferenceToGlobalVariable(node) {
  104. const variable = globalScope.set.get(node.name);
  105. return (
  106. variable &&
  107. variable.defs.length === 0 &&
  108. variable.references.some(ref => ref.identifier === node)
  109. );
  110. }
  111. /**
  112. * Checks if a string matches the provided pattern
  113. * @param {string} name The string to check.
  114. * @returns {boolean} if the string is a match
  115. * @private
  116. */
  117. function isInvalid(name) {
  118. return !regexp.test(name);
  119. }
  120. /**
  121. * Checks if a parent of a node is an ObjectPattern.
  122. * @param {ASTNode} node The node to check.
  123. * @returns {boolean} if the node is inside an ObjectPattern
  124. * @private
  125. */
  126. function isInsideObjectPattern(node) {
  127. let { parent } = node;
  128. while (parent) {
  129. if (parent.type === "ObjectPattern") {
  130. return true;
  131. }
  132. parent = parent.parent;
  133. }
  134. return false;
  135. }
  136. /**
  137. * Verifies if we should report an error or not based on the effective
  138. * parent node and the identifier name.
  139. * @param {ASTNode} effectiveParent The effective parent node of the node to be reported
  140. * @param {string} name The identifier name of the identifier node
  141. * @returns {boolean} whether an error should be reported or not
  142. */
  143. function shouldReport(effectiveParent, name) {
  144. return (
  145. (!onlyDeclarations ||
  146. DECLARATION_TYPES.has(effectiveParent.type)) &&
  147. !ALLOWED_PARENT_TYPES.has(effectiveParent.type) &&
  148. isInvalid(name)
  149. );
  150. }
  151. /**
  152. * Reports an AST node as a rule violation.
  153. * @param {ASTNode} node The node to report.
  154. * @returns {void}
  155. * @private
  156. */
  157. function report(node) {
  158. /*
  159. * We used the range instead of the node because it's possible
  160. * for the same identifier to be represented by two different
  161. * nodes, with the most clear example being shorthand properties:
  162. * { foo }
  163. * In this case, "foo" is represented by one node for the name
  164. * and one for the value. The only way to know they are the same
  165. * is to look at the range.
  166. */
  167. if (!reportedNodes.has(node.range.toString())) {
  168. const messageId =
  169. node.type === "PrivateIdentifier"
  170. ? "notMatchPrivate"
  171. : "notMatch";
  172. context.report({
  173. node,
  174. messageId,
  175. data: {
  176. name: node.name,
  177. pattern,
  178. },
  179. });
  180. reportedNodes.add(node.range.toString());
  181. }
  182. }
  183. return {
  184. Program(node) {
  185. globalScope = sourceCode.getScope(node);
  186. },
  187. Identifier(node) {
  188. const name = node.name,
  189. parent = node.parent,
  190. effectiveParent =
  191. parent.type === "MemberExpression"
  192. ? parent.parent
  193. : parent;
  194. if (
  195. isReferenceToGlobalVariable(node) ||
  196. astUtils.isImportAttributeKey(node)
  197. ) {
  198. return;
  199. }
  200. if (parent.type === "MemberExpression") {
  201. if (!checkProperties) {
  202. return;
  203. }
  204. // Always check object names
  205. if (
  206. parent.object.type === "Identifier" &&
  207. parent.object.name === name
  208. ) {
  209. if (isInvalid(name)) {
  210. report(node);
  211. }
  212. // Report AssignmentExpressions left side's assigned variable id
  213. } else if (
  214. effectiveParent.type === "AssignmentExpression" &&
  215. effectiveParent.left.type === "MemberExpression" &&
  216. effectiveParent.left.property.name === node.name
  217. ) {
  218. if (isInvalid(name)) {
  219. report(node);
  220. }
  221. // Report AssignmentExpressions only if they are the left side of the assignment
  222. } else if (
  223. effectiveParent.type === "AssignmentExpression" &&
  224. effectiveParent.right.type !== "MemberExpression"
  225. ) {
  226. if (isInvalid(name)) {
  227. report(node);
  228. }
  229. }
  230. // For https://github.com/eslint/eslint/issues/15123
  231. } else if (
  232. parent.type === "Property" &&
  233. parent.parent.type === "ObjectExpression" &&
  234. parent.key === node &&
  235. !parent.computed
  236. ) {
  237. if (checkProperties && isInvalid(name)) {
  238. report(node);
  239. }
  240. /*
  241. * Properties have their own rules, and
  242. * AssignmentPattern nodes can be treated like Properties:
  243. * e.g.: const { no_camelcased = false } = bar;
  244. */
  245. } else if (
  246. parent.type === "Property" ||
  247. parent.type === "AssignmentPattern"
  248. ) {
  249. if (
  250. parent.parent &&
  251. parent.parent.type === "ObjectPattern"
  252. ) {
  253. if (
  254. !ignoreDestructuring &&
  255. parent.shorthand &&
  256. parent.value.left &&
  257. isInvalid(name)
  258. ) {
  259. report(node);
  260. }
  261. const assignmentKeyEqualsValue =
  262. parent.key.name === parent.value.name;
  263. // prevent checking righthand side of destructured object
  264. if (!assignmentKeyEqualsValue && parent.key === node) {
  265. return;
  266. }
  267. const valueIsInvalid =
  268. parent.value.name && isInvalid(name);
  269. // ignore destructuring if the option is set, unless a new identifier is created
  270. if (
  271. valueIsInvalid &&
  272. !(assignmentKeyEqualsValue && ignoreDestructuring)
  273. ) {
  274. report(node);
  275. }
  276. }
  277. // never check properties or always ignore destructuring
  278. if (
  279. (!checkProperties && !parent.computed) ||
  280. (ignoreDestructuring && isInsideObjectPattern(node))
  281. ) {
  282. return;
  283. }
  284. // don't check right hand side of AssignmentExpression to prevent duplicate warnings
  285. if (
  286. parent.right !== node &&
  287. shouldReport(effectiveParent, name)
  288. ) {
  289. report(node);
  290. }
  291. // Check if it's an import specifier
  292. } else if (IMPORT_TYPES.has(parent.type)) {
  293. // Report only if the local imported identifier is invalid
  294. if (
  295. parent.local &&
  296. parent.local.name === node.name &&
  297. isInvalid(name)
  298. ) {
  299. report(node);
  300. }
  301. } else if (parent.type === "PropertyDefinition") {
  302. if (checkClassFields && isInvalid(name)) {
  303. report(node);
  304. }
  305. // Report anything that is invalid that isn't a CallExpression
  306. } else if (shouldReport(effectiveParent, name)) {
  307. report(node);
  308. }
  309. },
  310. PrivateIdentifier(node) {
  311. const isClassField = node.parent.type === "PropertyDefinition";
  312. if (isClassField && !checkClassFields) {
  313. return;
  314. }
  315. if (isInvalid(node.name)) {
  316. report(node);
  317. }
  318. },
  319. };
  320. },
  321. };