jsx-handler-names.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. /**
  2. * @fileoverview Enforce event handler naming conventions in JSX
  3. * @author Jake Marsh
  4. */
  5. 'use strict';
  6. const minimatch = require('minimatch');
  7. const docsUrl = require('../util/docsUrl');
  8. const getText = require('../util/eslint').getText;
  9. const report = require('../util/report');
  10. // ------------------------------------------------------------------------------
  11. // Rule Definition
  12. // ------------------------------------------------------------------------------
  13. const messages = {
  14. badHandlerName: 'Handler function for {{propKey}} prop key must be a camelCase name beginning with \'{{handlerPrefix}}\' only',
  15. badPropKey: 'Prop key for {{propValue}} must begin with \'{{handlerPropPrefix}}\'',
  16. };
  17. function isPrefixDisabled(prefix) {
  18. return prefix === false;
  19. }
  20. function isInlineHandler(node) {
  21. return node.value.expression.type === 'ArrowFunctionExpression';
  22. }
  23. /** @type {import('eslint').Rule.RuleModule} */
  24. module.exports = {
  25. meta: {
  26. docs: {
  27. description: 'Enforce event handler naming conventions in JSX',
  28. category: 'Stylistic Issues',
  29. recommended: false,
  30. url: docsUrl('jsx-handler-names'),
  31. },
  32. messages,
  33. schema: [{
  34. anyOf: [
  35. {
  36. type: 'object',
  37. properties: {
  38. eventHandlerPrefix: { type: 'string' },
  39. eventHandlerPropPrefix: { type: 'string' },
  40. checkLocalVariables: { type: 'boolean' },
  41. checkInlineFunction: { type: 'boolean' },
  42. ignoreComponentNames: {
  43. type: 'array',
  44. uniqueItems: true,
  45. items: { type: 'string' },
  46. },
  47. },
  48. additionalProperties: false,
  49. }, {
  50. type: 'object',
  51. properties: {
  52. eventHandlerPrefix: { type: 'string' },
  53. eventHandlerPropPrefix: {
  54. type: 'boolean',
  55. enum: [false],
  56. },
  57. checkLocalVariables: { type: 'boolean' },
  58. checkInlineFunction: { type: 'boolean' },
  59. ignoreComponentNames: {
  60. type: 'array',
  61. uniqueItems: true,
  62. items: { type: 'string' },
  63. },
  64. },
  65. additionalProperties: false,
  66. }, {
  67. type: 'object',
  68. properties: {
  69. eventHandlerPrefix: {
  70. type: 'boolean',
  71. enum: [false],
  72. },
  73. eventHandlerPropPrefix: { type: 'string' },
  74. checkLocalVariables: { type: 'boolean' },
  75. checkInlineFunction: { type: 'boolean' },
  76. ignoreComponentNames: {
  77. type: 'array',
  78. uniqueItems: true,
  79. items: { type: 'string' },
  80. },
  81. },
  82. additionalProperties: false,
  83. }, {
  84. type: 'object',
  85. properties: {
  86. checkLocalVariables: { type: 'boolean' },
  87. },
  88. additionalProperties: false,
  89. }, {
  90. type: 'object',
  91. properties: {
  92. checkInlineFunction: { type: 'boolean' },
  93. },
  94. additionalProperties: false,
  95. },
  96. {
  97. type: 'object',
  98. properties: {
  99. ignoreComponentNames: {
  100. type: 'array',
  101. uniqueItems: true,
  102. items: { type: 'string' },
  103. },
  104. },
  105. },
  106. ],
  107. }],
  108. },
  109. create(context) {
  110. const configuration = context.options[0] || {};
  111. const eventHandlerPrefix = isPrefixDisabled(configuration.eventHandlerPrefix)
  112. ? null
  113. : configuration.eventHandlerPrefix || 'handle';
  114. const eventHandlerPropPrefix = isPrefixDisabled(configuration.eventHandlerPropPrefix)
  115. ? null
  116. : configuration.eventHandlerPropPrefix || 'on';
  117. const EVENT_HANDLER_REGEX = !eventHandlerPrefix
  118. ? null
  119. : new RegExp(`^((props\\.${eventHandlerPropPrefix || ''})|((.*\\.)?${eventHandlerPrefix}))[0-9]*[A-Z].*$`);
  120. const PROP_EVENT_HANDLER_REGEX = !eventHandlerPropPrefix
  121. ? null
  122. : new RegExp(`^(${eventHandlerPropPrefix}[A-Z].*|ref)$`);
  123. const checkLocal = !!configuration.checkLocalVariables;
  124. const checkInlineFunction = !!configuration.checkInlineFunction;
  125. const ignoreComponentNames = configuration.ignoreComponentNames || [];
  126. return {
  127. JSXAttribute(node) {
  128. const componentName = node.parent.name.name;
  129. const isComponentNameIgnored = ignoreComponentNames.some((ignoredComponentNamePattern) => minimatch(
  130. componentName,
  131. ignoredComponentNamePattern
  132. ));
  133. if (
  134. !node.value
  135. || !node.value.expression
  136. || (!checkInlineFunction && isInlineHandler(node))
  137. || (
  138. !checkLocal
  139. && (isInlineHandler(node)
  140. ? !node.value.expression.body.callee || !node.value.expression.body.callee.object
  141. : !node.value.expression.object
  142. )
  143. )
  144. || isComponentNameIgnored
  145. ) {
  146. return;
  147. }
  148. const propKey = typeof node.name === 'object' ? node.name.name : node.name;
  149. const expression = node.value.expression;
  150. const propValue = getText(
  151. context,
  152. checkInlineFunction && isInlineHandler(node) ? expression.body.callee : expression
  153. ).replace(/\s*/g, '').replace(/^this\.|.*::/, '');
  154. if (propKey === 'ref') {
  155. return;
  156. }
  157. const propIsEventHandler = PROP_EVENT_HANDLER_REGEX && PROP_EVENT_HANDLER_REGEX.test(propKey);
  158. const propFnIsNamedCorrectly = EVENT_HANDLER_REGEX && EVENT_HANDLER_REGEX.test(propValue);
  159. if (
  160. propIsEventHandler
  161. && propFnIsNamedCorrectly !== null
  162. && !propFnIsNamedCorrectly
  163. ) {
  164. report(context, messages.badHandlerName, 'badHandlerName', {
  165. node,
  166. data: {
  167. propKey,
  168. handlerPrefix: eventHandlerPrefix,
  169. },
  170. });
  171. } else if (
  172. propFnIsNamedCorrectly
  173. && propIsEventHandler !== null
  174. && !propIsEventHandler
  175. ) {
  176. report(context, messages.badPropKey, 'badPropKey', {
  177. node,
  178. data: {
  179. propValue,
  180. handlerPropPrefix: eventHandlerPropPrefix,
  181. },
  182. });
  183. }
  184. },
  185. };
  186. },
  187. };