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