camelcase.js 14 KB

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