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 | const argumentIsCallExpression = node.arguments && node.arguments[0] && node.arguments[0].type === 'CallExpression';
|
---|
80 |
|
---|
81 | return node.type === 'CallExpression' && argumentIsCallExpression && utils.isPragmaComponentWrapper(node);
|
---|
82 | }
|
---|
83 |
|
---|
84 | /**
|
---|
85 | * Reports missing display name for a given component
|
---|
86 | * @param {Object} component The component to process
|
---|
87 | */
|
---|
88 | function reportMissingDisplayName(component) {
|
---|
89 | if (
|
---|
90 | testReactVersion(context, '^0.14.10 || ^15.7.0 || >= 16.12.0')
|
---|
91 | && isNestedMemo(component.node)
|
---|
92 | ) {
|
---|
93 | return;
|
---|
94 | }
|
---|
95 |
|
---|
96 | report(context, messages.noDisplayName, 'noDisplayName', {
|
---|
97 | node: component.node,
|
---|
98 | });
|
---|
99 | }
|
---|
100 |
|
---|
101 | /**
|
---|
102 | * Reports missing display name for a given context object
|
---|
103 | * @param {Object} contextObj The context object to process
|
---|
104 | */
|
---|
105 | function reportMissingContextDisplayName(contextObj) {
|
---|
106 | report(context, messages.noContextDisplayName, 'noContextDisplayName', {
|
---|
107 | node: contextObj.node,
|
---|
108 | });
|
---|
109 | }
|
---|
110 |
|
---|
111 | /**
|
---|
112 | * Checks if the component have a name set by the transpiler
|
---|
113 | * @param {ASTNode} node The AST node being checked.
|
---|
114 | * @returns {Boolean} True if component has a name, false if not.
|
---|
115 | */
|
---|
116 | function hasTranspilerName(node) {
|
---|
117 | const namedObjectAssignment = (
|
---|
118 | node.type === 'ObjectExpression'
|
---|
119 | && node.parent
|
---|
120 | && node.parent.parent
|
---|
121 | && node.parent.parent.type === 'AssignmentExpression'
|
---|
122 | && (
|
---|
123 | !node.parent.parent.left.object
|
---|
124 | || node.parent.parent.left.object.name !== 'module'
|
---|
125 | || node.parent.parent.left.property.name !== 'exports'
|
---|
126 | )
|
---|
127 | );
|
---|
128 | const namedObjectDeclaration = (
|
---|
129 | node.type === 'ObjectExpression'
|
---|
130 | && node.parent
|
---|
131 | && node.parent.parent
|
---|
132 | && node.parent.parent.type === 'VariableDeclarator'
|
---|
133 | );
|
---|
134 | const namedClass = (
|
---|
135 | (node.type === 'ClassDeclaration' || node.type === 'ClassExpression')
|
---|
136 | && node.id
|
---|
137 | && !!node.id.name
|
---|
138 | );
|
---|
139 |
|
---|
140 | const namedFunctionDeclaration = (
|
---|
141 | (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression')
|
---|
142 | && node.id
|
---|
143 | && !!node.id.name
|
---|
144 | );
|
---|
145 |
|
---|
146 | const namedFunctionExpression = (
|
---|
147 | astUtil.isFunctionLikeExpression(node)
|
---|
148 | && node.parent
|
---|
149 | && (node.parent.type === 'VariableDeclarator' || node.parent.type === 'Property' || node.parent.method === true)
|
---|
150 | && (!node.parent.parent || !componentUtil.isES5Component(node.parent.parent, context))
|
---|
151 | );
|
---|
152 |
|
---|
153 | if (
|
---|
154 | namedObjectAssignment || namedObjectDeclaration
|
---|
155 | || namedClass
|
---|
156 | || namedFunctionDeclaration || namedFunctionExpression
|
---|
157 | ) {
|
---|
158 | return true;
|
---|
159 | }
|
---|
160 | return false;
|
---|
161 | }
|
---|
162 |
|
---|
163 | // --------------------------------------------------------------------------
|
---|
164 | // Public
|
---|
165 | // --------------------------------------------------------------------------
|
---|
166 |
|
---|
167 | return {
|
---|
168 | ExpressionStatement(node) {
|
---|
169 | if (checkContextObjects && isCreateContext(node)) {
|
---|
170 | contextObjects.set(node.expression.left.name, { node, hasDisplayName: false });
|
---|
171 | }
|
---|
172 | },
|
---|
173 | VariableDeclarator(node) {
|
---|
174 | if (checkContextObjects && isCreateContext(node)) {
|
---|
175 | contextObjects.set(node.id.name, { node, hasDisplayName: false });
|
---|
176 | }
|
---|
177 | },
|
---|
178 | 'ClassProperty, PropertyDefinition'(node) {
|
---|
179 | if (!propsUtil.isDisplayNameDeclaration(node)) {
|
---|
180 | return;
|
---|
181 | }
|
---|
182 | markDisplayNameAsDeclared(node);
|
---|
183 | },
|
---|
184 |
|
---|
185 | MemberExpression(node) {
|
---|
186 | if (!propsUtil.isDisplayNameDeclaration(node.property)) {
|
---|
187 | return;
|
---|
188 | }
|
---|
189 | if (
|
---|
190 | checkContextObjects
|
---|
191 | && node.object
|
---|
192 | && node.object.name
|
---|
193 | && contextObjects.has(node.object.name)
|
---|
194 | ) {
|
---|
195 | contextObjects.get(node.object.name).hasDisplayName = true;
|
---|
196 | }
|
---|
197 | const component = utils.getRelatedComponent(node);
|
---|
198 | if (!component) {
|
---|
199 | return;
|
---|
200 | }
|
---|
201 | markDisplayNameAsDeclared(component.node.type === 'TSAsExpression' ? component.node.expression : component.node);
|
---|
202 | },
|
---|
203 |
|
---|
204 | 'FunctionExpression, FunctionDeclaration, ArrowFunctionExpression'(node) {
|
---|
205 | if (ignoreTranspilerName || !hasTranspilerName(node)) {
|
---|
206 | return;
|
---|
207 | }
|
---|
208 | if (components.get(node)) {
|
---|
209 | markDisplayNameAsDeclared(node);
|
---|
210 | }
|
---|
211 | },
|
---|
212 |
|
---|
213 | MethodDefinition(node) {
|
---|
214 | if (!propsUtil.isDisplayNameDeclaration(node.key)) {
|
---|
215 | return;
|
---|
216 | }
|
---|
217 | markDisplayNameAsDeclared(node);
|
---|
218 | },
|
---|
219 |
|
---|
220 | 'ClassExpression, ClassDeclaration'(node) {
|
---|
221 | if (ignoreTranspilerName || !hasTranspilerName(node)) {
|
---|
222 | return;
|
---|
223 | }
|
---|
224 | markDisplayNameAsDeclared(node);
|
---|
225 | },
|
---|
226 |
|
---|
227 | ObjectExpression(node) {
|
---|
228 | if (!componentUtil.isES5Component(node, context)) {
|
---|
229 | return;
|
---|
230 | }
|
---|
231 | if (ignoreTranspilerName || !hasTranspilerName(node)) {
|
---|
232 | // Search for the displayName declaration
|
---|
233 | node.properties.forEach((property) => {
|
---|
234 | if (!property.key || !propsUtil.isDisplayNameDeclaration(property.key)) {
|
---|
235 | return;
|
---|
236 | }
|
---|
237 | markDisplayNameAsDeclared(node);
|
---|
238 | });
|
---|
239 | return;
|
---|
240 | }
|
---|
241 | markDisplayNameAsDeclared(node);
|
---|
242 | },
|
---|
243 |
|
---|
244 | CallExpression(node) {
|
---|
245 | if (!utils.isPragmaComponentWrapper(node)) {
|
---|
246 | return;
|
---|
247 | }
|
---|
248 |
|
---|
249 | if (node.arguments.length > 0 && astUtil.isFunctionLikeExpression(node.arguments[0])) {
|
---|
250 | // Skip over React.forwardRef declarations that are embedded within
|
---|
251 | // a React.memo i.e. React.memo(React.forwardRef(/* ... */))
|
---|
252 | // This means that we raise a single error for the call to React.memo
|
---|
253 | // instead of one for React.memo and one for React.forwardRef
|
---|
254 | const isWrappedInAnotherPragma = utils.getPragmaComponentWrapper(node);
|
---|
255 | if (
|
---|
256 | !isWrappedInAnotherPragma
|
---|
257 | && (ignoreTranspilerName || !hasTranspilerName(node.arguments[0]))
|
---|
258 | ) {
|
---|
259 | return;
|
---|
260 | }
|
---|
261 |
|
---|
262 | if (components.get(node)) {
|
---|
263 | markDisplayNameAsDeclared(node);
|
---|
264 | }
|
---|
265 | }
|
---|
266 | },
|
---|
267 |
|
---|
268 | 'Program:exit'() {
|
---|
269 | const list = components.list();
|
---|
270 | // Report missing display name for all components
|
---|
271 | values(list).filter((component) => !component.hasDisplayName).forEach((component) => {
|
---|
272 | reportMissingDisplayName(component);
|
---|
273 | });
|
---|
274 | if (checkContextObjects) {
|
---|
275 | // Report missing display name for all context objects
|
---|
276 | forEach(
|
---|
277 | filter(contextObjects.values(), (v) => !v.hasDisplayName),
|
---|
278 | (contextObj) => reportMissingContextDisplayName(contextObj)
|
---|
279 | );
|
---|
280 | }
|
---|
281 | },
|
---|
282 | };
|
---|
283 | }),
|
---|
284 | };
|
---|