no-keyword-prefix.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. 'use strict';
  2. const isShorthandPropertyAssignmentPatternLeft = require('./utils/is-shorthand-property-assignment-pattern-left.js');
  3. const MESSAGE_ID = 'noKeywordPrefix';
  4. const messages = {
  5. [MESSAGE_ID]: 'Do not prefix identifiers with keyword `{{keyword}}`.',
  6. };
  7. const prepareOptions = ({
  8. disallowedPrefixes,
  9. checkProperties = true,
  10. onlyCamelCase = true,
  11. } = {}) => ({
  12. disallowedPrefixes: (disallowedPrefixes || [
  13. 'new',
  14. 'class',
  15. ]),
  16. checkProperties,
  17. onlyCamelCase,
  18. });
  19. function findKeywordPrefix(name, options) {
  20. return options.disallowedPrefixes.find(keyword => {
  21. const suffix = options.onlyCamelCase ? '[A-Z]' : '.';
  22. const regex = new RegExp(`^${keyword}${suffix}`);
  23. return name.match(regex);
  24. });
  25. }
  26. function checkMemberExpression(report, node, options) {
  27. const {name, parent} = node;
  28. const keyword = findKeywordPrefix(name, options);
  29. const effectiveParent = parent.type === 'MemberExpression' ? parent.parent : parent;
  30. if (!options.checkProperties) {
  31. return;
  32. }
  33. if (parent.object.type === 'Identifier' && parent.object.name === name && Boolean(keyword)) {
  34. report(node, keyword);
  35. } else if (
  36. effectiveParent.type === 'AssignmentExpression'
  37. && Boolean(keyword)
  38. && (effectiveParent.right.type !== 'MemberExpression' || effectiveParent.left.type === 'MemberExpression')
  39. && effectiveParent.left.property.name === name
  40. ) {
  41. report(node, keyword);
  42. }
  43. }
  44. function checkObjectPattern(report, node, options) {
  45. const {name, parent} = node;
  46. const keyword = findKeywordPrefix(name, options);
  47. /* c8 ignore next 3 */
  48. if (parent.shorthand && parent.value.left && Boolean(keyword)) {
  49. report(node, keyword);
  50. }
  51. const assignmentKeyEqualsValue = parent.key.name === parent.value.name;
  52. if (Boolean(keyword) && parent.computed) {
  53. report(node, keyword);
  54. }
  55. // Prevent checking right hand side of destructured object
  56. if (parent.key === node && parent.value !== node) {
  57. return true;
  58. }
  59. const valueIsInvalid = parent.value.name && Boolean(keyword);
  60. // Ignore destructuring if the option is set, unless a new identifier is created
  61. if (valueIsInvalid && !assignmentKeyEqualsValue) {
  62. report(node, keyword);
  63. }
  64. return false;
  65. }
  66. // Core logic copied from:
  67. // https://github.com/eslint/eslint/blob/master/lib/rules/camelcase.js
  68. const create = context => {
  69. const options = prepareOptions(context.options[0]);
  70. // Contains reported nodes to avoid reporting twice on destructuring with shorthand notation
  71. const reported = [];
  72. const ALLOWED_PARENT_TYPES = new Set(['CallExpression', 'NewExpression']);
  73. function report(node, keyword) {
  74. if (!reported.includes(node)) {
  75. reported.push(node);
  76. context.report({
  77. node,
  78. messageId: MESSAGE_ID,
  79. data: {
  80. name: node.name,
  81. keyword,
  82. },
  83. });
  84. }
  85. }
  86. return {
  87. Identifier(node) {
  88. const {name, parent} = node;
  89. const keyword = findKeywordPrefix(name, options);
  90. const effectiveParent = parent.type === 'MemberExpression' ? parent.parent : parent;
  91. if (parent.type === 'MemberExpression') {
  92. checkMemberExpression(report, node, options);
  93. } else if (
  94. parent.type === 'Property'
  95. || parent.type === 'AssignmentPattern'
  96. ) {
  97. if (parent.parent.type === 'ObjectPattern') {
  98. const finished = checkObjectPattern(report, node, options);
  99. if (finished) {
  100. return;
  101. }
  102. }
  103. if (
  104. !options.checkProperties
  105. ) {
  106. return;
  107. }
  108. // Don't check right hand side of AssignmentExpression to prevent duplicate warnings
  109. if (
  110. Boolean(keyword)
  111. && !ALLOWED_PARENT_TYPES.has(effectiveParent.type)
  112. && !(parent.right === node)
  113. && !isShorthandPropertyAssignmentPatternLeft(node)
  114. ) {
  115. report(node, keyword);
  116. }
  117. // Check if it's an import specifier
  118. } else if (
  119. [
  120. 'ImportSpecifier',
  121. 'ImportNamespaceSpecifier',
  122. 'ImportDefaultSpecifier',
  123. ].includes(parent.type)
  124. ) {
  125. // Report only if the local imported identifier is invalid
  126. if (Boolean(keyword) && parent.local?.name === name) {
  127. report(node, keyword);
  128. }
  129. // Report anything that is invalid that isn't a CallExpression
  130. } else if (
  131. Boolean(keyword)
  132. && !ALLOWED_PARENT_TYPES.has(effectiveParent.type)
  133. ) {
  134. report(node, keyword);
  135. }
  136. },
  137. };
  138. };
  139. const schema = [
  140. {
  141. type: 'object',
  142. additionalProperties: false,
  143. properties: {
  144. disallowedPrefixes: {
  145. type: 'array',
  146. items: [
  147. {
  148. type: 'string',
  149. },
  150. ],
  151. minItems: 0,
  152. uniqueItems: true,
  153. },
  154. checkProperties: {
  155. type: 'boolean',
  156. },
  157. onlyCamelCase: {
  158. type: 'boolean',
  159. },
  160. },
  161. },
  162. ];
  163. /** @type {import('eslint').Rule.RuleModule} */
  164. module.exports = {
  165. create,
  166. meta: {
  167. type: 'suggestion',
  168. docs: {
  169. description: 'Disallow identifiers starting with `new` or `class`.',
  170. },
  171. schema,
  172. messages,
  173. },
  174. };