camelcase.js 14 KB

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