display-name.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. /**
  2. * @fileoverview Prevent missing displayName in a React component definition
  3. * @author Yannick Croissant
  4. */
  5. 'use strict';
  6. const values = require('object.values');
  7. const filter = require('es-iterator-helpers/Iterator.prototype.filter');
  8. const forEach = require('es-iterator-helpers/Iterator.prototype.forEach');
  9. const Components = require('../util/Components');
  10. const isCreateContext = require('../util/isCreateContext');
  11. const astUtil = require('../util/ast');
  12. const componentUtil = require('../util/componentUtil');
  13. const docsUrl = require('../util/docsUrl');
  14. const testReactVersion = require('../util/version').testReactVersion;
  15. const propsUtil = require('../util/props');
  16. const report = require('../util/report');
  17. // ------------------------------------------------------------------------------
  18. // Rule Definition
  19. // ------------------------------------------------------------------------------
  20. const messages = {
  21. noDisplayName: 'Component definition is missing display name',
  22. noContextDisplayName: 'Context definition is missing display name',
  23. };
  24. /** @type {import('eslint').Rule.RuleModule} */
  25. module.exports = {
  26. meta: {
  27. docs: {
  28. description: 'Disallow missing displayName in a React component definition',
  29. category: 'Best Practices',
  30. recommended: true,
  31. url: docsUrl('display-name'),
  32. },
  33. messages,
  34. schema: [{
  35. type: 'object',
  36. properties: {
  37. ignoreTranspilerName: {
  38. type: 'boolean',
  39. },
  40. checkContextObjects: {
  41. type: 'boolean',
  42. },
  43. },
  44. additionalProperties: false,
  45. }],
  46. },
  47. create: Components.detect((context, components, utils) => {
  48. const config = context.options[0] || {};
  49. const ignoreTranspilerName = config.ignoreTranspilerName || false;
  50. const checkContextObjects = (config.checkContextObjects || false) && testReactVersion(context, '>= 16.3.0');
  51. const contextObjects = new Map();
  52. /**
  53. * Mark a prop type as declared
  54. * @param {ASTNode} node The AST node being checked.
  55. */
  56. function markDisplayNameAsDeclared(node) {
  57. components.set(node, {
  58. hasDisplayName: true,
  59. });
  60. }
  61. /**
  62. * Checks if React.forwardRef is nested inside React.memo
  63. * @param {ASTNode} node The AST node being checked.
  64. * @returns {boolean} True if React.forwardRef is nested inside React.memo, false if not.
  65. */
  66. function isNestedMemo(node) {
  67. return astUtil.isCallExpression(node)
  68. && node.arguments
  69. && astUtil.isCallExpression(node.arguments[0])
  70. && utils.isPragmaComponentWrapper(node);
  71. }
  72. /**
  73. * Reports missing display name for a given component
  74. * @param {Object} component The component to process
  75. */
  76. function reportMissingDisplayName(component) {
  77. if (
  78. testReactVersion(context, '^0.14.10 || ^15.7.0 || >= 16.12.0')
  79. && isNestedMemo(component.node)
  80. ) {
  81. return;
  82. }
  83. report(context, messages.noDisplayName, 'noDisplayName', {
  84. node: component.node,
  85. });
  86. }
  87. /**
  88. * Reports missing display name for a given context object
  89. * @param {Object} contextObj The context object to process
  90. */
  91. function reportMissingContextDisplayName(contextObj) {
  92. report(context, messages.noContextDisplayName, 'noContextDisplayName', {
  93. node: contextObj.node,
  94. });
  95. }
  96. /**
  97. * Checks if the component have a name set by the transpiler
  98. * @param {ASTNode} node The AST node being checked.
  99. * @returns {boolean} True if component has a name, false if not.
  100. */
  101. function hasTranspilerName(node) {
  102. const namedObjectAssignment = (
  103. node.type === 'ObjectExpression'
  104. && node.parent
  105. && node.parent.parent
  106. && node.parent.parent.type === 'AssignmentExpression'
  107. && (
  108. !node.parent.parent.left.object
  109. || node.parent.parent.left.object.name !== 'module'
  110. || node.parent.parent.left.property.name !== 'exports'
  111. )
  112. );
  113. const namedObjectDeclaration = (
  114. node.type === 'ObjectExpression'
  115. && node.parent
  116. && node.parent.parent
  117. && node.parent.parent.type === 'VariableDeclarator'
  118. );
  119. const namedClass = (
  120. (node.type === 'ClassDeclaration' || node.type === 'ClassExpression')
  121. && node.id
  122. && !!node.id.name
  123. );
  124. const namedFunctionDeclaration = (
  125. (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression')
  126. && node.id
  127. && !!node.id.name
  128. );
  129. const namedFunctionExpression = (
  130. astUtil.isFunctionLikeExpression(node)
  131. && node.parent
  132. && (node.parent.type === 'VariableDeclarator' || node.parent.type === 'Property' || node.parent.method === true)
  133. && (!node.parent.parent || !componentUtil.isES5Component(node.parent.parent, context))
  134. );
  135. if (
  136. namedObjectAssignment || namedObjectDeclaration
  137. || namedClass
  138. || namedFunctionDeclaration || namedFunctionExpression
  139. ) {
  140. return true;
  141. }
  142. return false;
  143. }
  144. // --------------------------------------------------------------------------
  145. // Public
  146. // --------------------------------------------------------------------------
  147. return {
  148. ExpressionStatement(node) {
  149. if (checkContextObjects && isCreateContext(node)) {
  150. contextObjects.set(node.expression.left.name, { node, hasDisplayName: false });
  151. }
  152. },
  153. VariableDeclarator(node) {
  154. if (checkContextObjects && isCreateContext(node)) {
  155. contextObjects.set(node.id.name, { node, hasDisplayName: false });
  156. }
  157. },
  158. 'ClassProperty, PropertyDefinition'(node) {
  159. if (!propsUtil.isDisplayNameDeclaration(node)) {
  160. return;
  161. }
  162. markDisplayNameAsDeclared(node);
  163. },
  164. MemberExpression(node) {
  165. if (!propsUtil.isDisplayNameDeclaration(node.property)) {
  166. return;
  167. }
  168. if (
  169. checkContextObjects
  170. && node.object
  171. && node.object.name
  172. && contextObjects.has(node.object.name)
  173. ) {
  174. contextObjects.get(node.object.name).hasDisplayName = true;
  175. }
  176. const component = utils.getRelatedComponent(node);
  177. if (!component) {
  178. return;
  179. }
  180. markDisplayNameAsDeclared(astUtil.unwrapTSAsExpression(component.node));
  181. },
  182. 'FunctionExpression, FunctionDeclaration, ArrowFunctionExpression'(node) {
  183. if (ignoreTranspilerName || !hasTranspilerName(node)) {
  184. return;
  185. }
  186. if (components.get(node)) {
  187. markDisplayNameAsDeclared(node);
  188. }
  189. },
  190. MethodDefinition(node) {
  191. if (!propsUtil.isDisplayNameDeclaration(node.key)) {
  192. return;
  193. }
  194. markDisplayNameAsDeclared(node);
  195. },
  196. 'ClassExpression, ClassDeclaration'(node) {
  197. if (ignoreTranspilerName || !hasTranspilerName(node)) {
  198. return;
  199. }
  200. markDisplayNameAsDeclared(node);
  201. },
  202. ObjectExpression(node) {
  203. if (!componentUtil.isES5Component(node, context)) {
  204. return;
  205. }
  206. if (ignoreTranspilerName || !hasTranspilerName(node)) {
  207. // Search for the displayName declaration
  208. node.properties.forEach((property) => {
  209. if (!property.key || !propsUtil.isDisplayNameDeclaration(property.key)) {
  210. return;
  211. }
  212. markDisplayNameAsDeclared(node);
  213. });
  214. return;
  215. }
  216. markDisplayNameAsDeclared(node);
  217. },
  218. CallExpression(node) {
  219. if (!utils.isPragmaComponentWrapper(node)) {
  220. return;
  221. }
  222. if (node.arguments.length > 0 && astUtil.isFunctionLikeExpression(node.arguments[0])) {
  223. // Skip over React.forwardRef declarations that are embedded within
  224. // a React.memo i.e. React.memo(React.forwardRef(/* ... */))
  225. // This means that we raise a single error for the call to React.memo
  226. // instead of one for React.memo and one for React.forwardRef
  227. const isWrappedInAnotherPragma = utils.getPragmaComponentWrapper(node);
  228. if (
  229. !isWrappedInAnotherPragma
  230. && (ignoreTranspilerName || !hasTranspilerName(node.arguments[0]))
  231. ) {
  232. return;
  233. }
  234. if (components.get(node)) {
  235. markDisplayNameAsDeclared(node);
  236. }
  237. }
  238. },
  239. 'Program:exit'() {
  240. const list = components.list();
  241. // Report missing display name for all components
  242. values(list).filter((component) => !component.hasDisplayName).forEach((component) => {
  243. reportMissingDisplayName(component);
  244. });
  245. if (checkContextObjects) {
  246. // Report missing display name for all context objects
  247. forEach(
  248. filter(contextObjects.values(), (v) => !v.hasDisplayName),
  249. (contextObj) => reportMissingContextDisplayName(contextObj)
  250. );
  251. }
  252. },
  253. };
  254. }),
  255. };