UNPKG

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