no-keyword-prefix.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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. /* istanbul ignore next: Can't find a case to cover this line */
  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 righthand 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 && 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 (
  127. Boolean(keyword)
  128. && parent.local
  129. && parent.local.name === name
  130. ) {
  131. report(node, keyword);
  132. }
  133. // Report anything that is invalid that isn't a CallExpression
  134. } else if (
  135. Boolean(keyword)
  136. && !ALLOWED_PARENT_TYPES.has(effectiveParent.type)
  137. ) {
  138. report(node, keyword);
  139. }
  140. },
  141. };
  142. };
  143. const schema = [
  144. {
  145. type: 'object',
  146. additionalProperties: false,
  147. properties: {
  148. disallowedPrefixes: {
  149. type: 'array',
  150. items: [
  151. {
  152. type: 'string',
  153. },
  154. ],
  155. minItems: 0,
  156. uniqueItems: true,
  157. },
  158. checkProperties: {
  159. type: 'boolean',
  160. },
  161. onlyCamelCase: {
  162. type: 'boolean',
  163. },
  164. },
  165. },
  166. ];
  167. /** @type {import('eslint').Rule.RuleModule} */
  168. module.exports = {
  169. create,
  170. meta: {
  171. type: 'suggestion',
  172. docs: {
  173. description: 'Disallow identifiers starting with `new` or `class`.',
  174. },
  175. schema,
  176. messages,
  177. },
  178. };