display-name.js 9.0 KB

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