display-name.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. /**
  2. * @fileoverview Prevent missing displayName in a React component definition
  3. * @author Yannick Croissant
  4. */
  5. 'use strict';
  6. const has = require('has');
  7. const Components = require('../util/Components');
  8. const docsUrl = require('../util/docsUrl');
  9. // ------------------------------------------------------------------------------
  10. // Rule Definition
  11. // ------------------------------------------------------------------------------
  12. module.exports = {
  13. meta: {
  14. docs: {
  15. description: 'Prevent missing displayName in a React component definition',
  16. category: 'Best Practices',
  17. recommended: true,
  18. url: docsUrl('display-name')
  19. },
  20. schema: [{
  21. type: 'object',
  22. properties: {
  23. ignoreTranspilerName: {
  24. type: 'boolean'
  25. }
  26. },
  27. additionalProperties: false
  28. }]
  29. },
  30. create: Components.detect((context, components, utils) => {
  31. const config = context.options[0] || {};
  32. const ignoreTranspilerName = config.ignoreTranspilerName || false;
  33. const MISSING_MESSAGE = 'Component definition is missing display name';
  34. /**
  35. * Checks if we are declaring a display name
  36. * @param {ASTNode} node The AST node being checked.
  37. * @returns {Boolean} True if we are declaring a display name, false if not.
  38. */
  39. function isDisplayNameDeclaration(node) {
  40. switch (node.type) {
  41. case 'ClassProperty':
  42. return node.key && node.key.name === 'displayName';
  43. case 'Identifier':
  44. return node.name === 'displayName';
  45. case 'Literal':
  46. return node.value === 'displayName';
  47. default:
  48. return false;
  49. }
  50. }
  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. * Reports missing display name for a given component
  62. * @param {Object} component The component to process
  63. */
  64. function reportMissingDisplayName(component) {
  65. context.report({
  66. node: component.node,
  67. message: MISSING_MESSAGE,
  68. data: {
  69. component: component.name
  70. }
  71. });
  72. }
  73. /**
  74. * Checks if the component have a name set by the transpiler
  75. * @param {ASTNode} node The AST node being checked.
  76. * @returns {Boolean} True if component has a name, false if not.
  77. */
  78. function hasTranspilerName(node) {
  79. const namedObjectAssignment = (
  80. node.type === 'ObjectExpression' &&
  81. node.parent &&
  82. node.parent.parent &&
  83. node.parent.parent.type === 'AssignmentExpression' &&
  84. (
  85. !node.parent.parent.left.object ||
  86. node.parent.parent.left.object.name !== 'module' ||
  87. node.parent.parent.left.property.name !== 'exports'
  88. )
  89. );
  90. const namedObjectDeclaration = (
  91. node.type === 'ObjectExpression' &&
  92. node.parent &&
  93. node.parent.parent &&
  94. node.parent.parent.type === 'VariableDeclarator'
  95. );
  96. const namedClass = (
  97. (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') &&
  98. node.id &&
  99. node.id.name
  100. );
  101. const namedFunctionDeclaration = (
  102. (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') &&
  103. node.id &&
  104. node.id.name
  105. );
  106. const namedFunctionExpression = (
  107. (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') &&
  108. node.parent &&
  109. (node.parent.type === 'VariableDeclarator' || node.parent.method === true) &&
  110. (!node.parent.parent || !utils.isES5Component(node.parent.parent))
  111. );
  112. if (
  113. namedObjectAssignment || namedObjectDeclaration ||
  114. namedClass ||
  115. namedFunctionDeclaration || namedFunctionExpression
  116. ) {
  117. return true;
  118. }
  119. return false;
  120. }
  121. // --------------------------------------------------------------------------
  122. // Public
  123. // --------------------------------------------------------------------------
  124. return {
  125. ClassProperty: function(node) {
  126. if (!isDisplayNameDeclaration(node)) {
  127. return;
  128. }
  129. markDisplayNameAsDeclared(node);
  130. },
  131. MemberExpression: function(node) {
  132. if (!isDisplayNameDeclaration(node.property)) {
  133. return;
  134. }
  135. const component = utils.getRelatedComponent(node);
  136. if (!component) {
  137. return;
  138. }
  139. markDisplayNameAsDeclared(component.node);
  140. },
  141. FunctionExpression: function(node) {
  142. if (ignoreTranspilerName || !hasTranspilerName(node)) {
  143. return;
  144. }
  145. markDisplayNameAsDeclared(node);
  146. },
  147. FunctionDeclaration: function(node) {
  148. if (ignoreTranspilerName || !hasTranspilerName(node)) {
  149. return;
  150. }
  151. markDisplayNameAsDeclared(node);
  152. },
  153. ArrowFunctionExpression: function(node) {
  154. if (ignoreTranspilerName || !hasTranspilerName(node)) {
  155. return;
  156. }
  157. markDisplayNameAsDeclared(node);
  158. },
  159. MethodDefinition: function(node) {
  160. if (!isDisplayNameDeclaration(node.key)) {
  161. return;
  162. }
  163. markDisplayNameAsDeclared(node);
  164. },
  165. ClassExpression: function(node) {
  166. if (ignoreTranspilerName || !hasTranspilerName(node)) {
  167. return;
  168. }
  169. markDisplayNameAsDeclared(node);
  170. },
  171. ClassDeclaration: function(node) {
  172. if (ignoreTranspilerName || !hasTranspilerName(node)) {
  173. return;
  174. }
  175. markDisplayNameAsDeclared(node);
  176. },
  177. ObjectExpression: function(node) {
  178. if (ignoreTranspilerName || !hasTranspilerName(node)) {
  179. // Search for the displayName declaration
  180. node.properties.forEach(property => {
  181. if (!property.key || !isDisplayNameDeclaration(property.key)) {
  182. return;
  183. }
  184. markDisplayNameAsDeclared(node);
  185. });
  186. return;
  187. }
  188. markDisplayNameAsDeclared(node);
  189. },
  190. 'Program:exit': function() {
  191. const list = components.list();
  192. // Report missing display name for all components
  193. for (const component in list) {
  194. if (!has(list, component) || list[component].hasDisplayName) {
  195. continue;
  196. }
  197. reportMissingDisplayName(list[component]);
  198. }
  199. }
  200. };
  201. })
  202. };