camelcase.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. /**
  2. * @fileoverview Rule to flag non-camelcased identifiers
  3. * @author Nicholas C. Zakas
  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. allow: [],
  20. ignoreDestructuring: false,
  21. ignoreGlobals: false,
  22. ignoreImports: false,
  23. properties: "always",
  24. },
  25. ],
  26. docs: {
  27. description: "Enforce camelcase naming convention",
  28. recommended: false,
  29. frozen: true,
  30. url: "https://eslint.org/docs/latest/rules/camelcase",
  31. },
  32. schema: [
  33. {
  34. type: "object",
  35. properties: {
  36. ignoreDestructuring: {
  37. type: "boolean",
  38. },
  39. ignoreImports: {
  40. type: "boolean",
  41. },
  42. ignoreGlobals: {
  43. type: "boolean",
  44. },
  45. properties: {
  46. enum: ["always", "never"],
  47. },
  48. allow: {
  49. type: "array",
  50. items: {
  51. type: "string",
  52. },
  53. minItems: 0,
  54. uniqueItems: true,
  55. },
  56. },
  57. additionalProperties: false,
  58. },
  59. ],
  60. messages: {
  61. notCamelCase: "Identifier '{{name}}' is not in camel case.",
  62. notCamelCasePrivate: "#{{name}} is not in camel case.",
  63. },
  64. },
  65. create(context) {
  66. const [
  67. {
  68. allow,
  69. ignoreDestructuring,
  70. ignoreGlobals,
  71. ignoreImports,
  72. properties,
  73. },
  74. ] = context.options;
  75. const sourceCode = context.sourceCode;
  76. //--------------------------------------------------------------------------
  77. // Helpers
  78. //--------------------------------------------------------------------------
  79. // contains reported nodes to avoid reporting twice on destructuring with shorthand notation
  80. const reported = new Set();
  81. /**
  82. * Checks if a string contains an underscore and isn't all upper-case
  83. * @param {string} name The string to check.
  84. * @returns {boolean} if the string is underscored
  85. * @private
  86. */
  87. function isUnderscored(name) {
  88. const nameBody = name.replace(/^_+|_+$/gu, "");
  89. // if there's an underscore, it might be A_CONSTANT, which is okay
  90. return (
  91. nameBody.includes("_") && nameBody !== nameBody.toUpperCase()
  92. );
  93. }
  94. /**
  95. * Checks if a string match the ignore list
  96. * @param {string} name The string to check.
  97. * @returns {boolean} if the string is ignored
  98. * @private
  99. */
  100. function isAllowed(name) {
  101. return allow.some(
  102. entry => name === entry || name.match(new RegExp(entry, "u")),
  103. );
  104. }
  105. /**
  106. * Checks if a given name is good or not.
  107. * @param {string} name The name to check.
  108. * @returns {boolean} `true` if the name is good.
  109. * @private
  110. */
  111. function isGoodName(name) {
  112. return !isUnderscored(name) || isAllowed(name);
  113. }
  114. /**
  115. * Checks if a given identifier reference or member expression is an assignment
  116. * target.
  117. * @param {ASTNode} node The node to check.
  118. * @returns {boolean} `true` if the node is an assignment target.
  119. */
  120. function isAssignmentTarget(node) {
  121. const parent = node.parent;
  122. switch (parent.type) {
  123. case "AssignmentExpression":
  124. case "AssignmentPattern":
  125. return parent.left === node;
  126. case "Property":
  127. return (
  128. parent.parent.type === "ObjectPattern" &&
  129. parent.value === node
  130. );
  131. case "ArrayPattern":
  132. case "RestElement":
  133. return true;
  134. default:
  135. return false;
  136. }
  137. }
  138. /**
  139. * Checks if a given binding identifier uses the original name as-is.
  140. * - If it's in object destructuring or object expression, the original name is its property name.
  141. * - If it's in import declaration, the original name is its exported name.
  142. * @param {ASTNode} node The `Identifier` node to check.
  143. * @returns {boolean} `true` if the identifier uses the original name as-is.
  144. */
  145. function equalsToOriginalName(node) {
  146. const localName = node.name;
  147. const valueNode =
  148. node.parent.type === "AssignmentPattern" ? node.parent : node;
  149. const parent = valueNode.parent;
  150. switch (parent.type) {
  151. case "Property":
  152. return (
  153. (parent.parent.type === "ObjectPattern" ||
  154. parent.parent.type === "ObjectExpression") &&
  155. parent.value === valueNode &&
  156. !parent.computed &&
  157. parent.key.type === "Identifier" &&
  158. parent.key.name === localName
  159. );
  160. case "ImportSpecifier":
  161. return (
  162. parent.local === node &&
  163. astUtils.getModuleExportName(parent.imported) ===
  164. localName
  165. );
  166. default:
  167. return false;
  168. }
  169. }
  170. /**
  171. * Reports an AST node as a rule violation.
  172. * @param {ASTNode} node The node to report.
  173. * @returns {void}
  174. * @private
  175. */
  176. function report(node) {
  177. if (reported.has(node.range[0])) {
  178. return;
  179. }
  180. reported.add(node.range[0]);
  181. // Report it.
  182. context.report({
  183. node,
  184. messageId:
  185. node.type === "PrivateIdentifier"
  186. ? "notCamelCasePrivate"
  187. : "notCamelCase",
  188. data: { name: node.name },
  189. });
  190. }
  191. /**
  192. * Reports an identifier reference or a binding identifier.
  193. * @param {ASTNode} node The `Identifier` node to report.
  194. * @returns {void}
  195. */
  196. function reportReferenceId(node) {
  197. /*
  198. * For backward compatibility, if it's in callings then ignore it.
  199. * Not sure why it is.
  200. */
  201. if (
  202. node.parent.type === "CallExpression" ||
  203. node.parent.type === "NewExpression"
  204. ) {
  205. return;
  206. }
  207. /*
  208. * For backward compatibility, if it's a default value of
  209. * destructuring/parameters then ignore it.
  210. * Not sure why it is.
  211. */
  212. if (
  213. node.parent.type === "AssignmentPattern" &&
  214. node.parent.right === node
  215. ) {
  216. return;
  217. }
  218. /*
  219. * The `ignoreDestructuring` flag skips the identifiers that uses
  220. * the property name as-is.
  221. */
  222. if (ignoreDestructuring && equalsToOriginalName(node)) {
  223. return;
  224. }
  225. /*
  226. * Import attribute keys are always ignored
  227. */
  228. if (astUtils.isImportAttributeKey(node)) {
  229. return;
  230. }
  231. report(node);
  232. }
  233. return {
  234. // Report camelcase of global variable references ------------------
  235. Program(node) {
  236. const scope = sourceCode.getScope(node);
  237. if (!ignoreGlobals) {
  238. // Defined globals in config files or directive comments.
  239. for (const variable of scope.variables) {
  240. if (
  241. variable.identifiers.length > 0 ||
  242. isGoodName(variable.name)
  243. ) {
  244. continue;
  245. }
  246. for (const reference of variable.references) {
  247. /*
  248. * For backward compatibility, this rule reports read-only
  249. * references as well.
  250. */
  251. reportReferenceId(reference.identifier);
  252. }
  253. }
  254. }
  255. // Undefined globals.
  256. for (const reference of scope.through) {
  257. const id = reference.identifier;
  258. if (
  259. isGoodName(id.name) ||
  260. astUtils.isImportAttributeKey(id)
  261. ) {
  262. continue;
  263. }
  264. /*
  265. * For backward compatibility, this rule reports read-only
  266. * references as well.
  267. */
  268. reportReferenceId(id);
  269. }
  270. },
  271. // Report camelcase of declared variables --------------------------
  272. [[
  273. "VariableDeclaration",
  274. "FunctionDeclaration",
  275. "FunctionExpression",
  276. "ArrowFunctionExpression",
  277. "ClassDeclaration",
  278. "ClassExpression",
  279. "CatchClause",
  280. ]](node) {
  281. for (const variable of sourceCode.getDeclaredVariables(node)) {
  282. if (isGoodName(variable.name)) {
  283. continue;
  284. }
  285. const id = variable.identifiers[0];
  286. // Report declaration.
  287. if (!(ignoreDestructuring && equalsToOriginalName(id))) {
  288. report(id);
  289. }
  290. /*
  291. * For backward compatibility, report references as well.
  292. * It looks unnecessary because declarations are reported.
  293. */
  294. for (const reference of variable.references) {
  295. if (reference.init) {
  296. continue; // Skip the write references of initializers.
  297. }
  298. reportReferenceId(reference.identifier);
  299. }
  300. }
  301. },
  302. // Report camelcase in properties ----------------------------------
  303. [[
  304. "ObjectExpression > Property[computed!=true] > Identifier.key",
  305. "MethodDefinition[computed!=true] > Identifier.key",
  306. "PropertyDefinition[computed!=true] > Identifier.key",
  307. "MethodDefinition > PrivateIdentifier.key",
  308. "PropertyDefinition > PrivateIdentifier.key",
  309. ]](node) {
  310. if (
  311. properties === "never" ||
  312. astUtils.isImportAttributeKey(node) ||
  313. isGoodName(node.name)
  314. ) {
  315. return;
  316. }
  317. report(node);
  318. },
  319. "MemberExpression[computed!=true] > Identifier.property"(node) {
  320. if (
  321. properties === "never" ||
  322. !isAssignmentTarget(node.parent) || // ← ignore read-only references.
  323. isGoodName(node.name)
  324. ) {
  325. return;
  326. }
  327. report(node);
  328. },
  329. // Report camelcase in import --------------------------------------
  330. ImportDeclaration(node) {
  331. for (const variable of sourceCode.getDeclaredVariables(node)) {
  332. if (isGoodName(variable.name)) {
  333. continue;
  334. }
  335. const id = variable.identifiers[0];
  336. // Report declaration.
  337. if (!(ignoreImports && equalsToOriginalName(id))) {
  338. report(id);
  339. }
  340. /*
  341. * For backward compatibility, report references as well.
  342. * It looks unnecessary because declarations are reported.
  343. */
  344. for (const reference of variable.references) {
  345. reportReferenceId(reference.identifier);
  346. }
  347. }
  348. },
  349. // Report camelcase in re-export -----------------------------------
  350. [[
  351. "ExportAllDeclaration > Identifier.exported",
  352. "ExportSpecifier > Identifier.exported",
  353. ]](node) {
  354. if (isGoodName(node.name)) {
  355. return;
  356. }
  357. report(node);
  358. },
  359. // Report camelcase in labels --------------------------------------
  360. [[
  361. "LabeledStatement > Identifier.label",
  362. /*
  363. * For backward compatibility, report references as well.
  364. * It looks unnecessary because declarations are reported.
  365. */
  366. "BreakStatement > Identifier.label",
  367. "ContinueStatement > Identifier.label",
  368. ]](node) {
  369. if (isGoodName(node.name)) {
  370. return;
  371. }
  372. report(node);
  373. },
  374. };
  375. },
  376. };