Skip to content

Commit a684ae1

Browse files
JulesBlmljharb
authored andcommitted
[New] display-name: add checkContextObjects option
1 parent abb4871 commit a684ae1

File tree

5 files changed

+321
-1
lines changed

5 files changed

+321
-1
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
55

66
## Unreleased
77

8+
### Added
9+
* [`display-name`]: add `checkContextObjects` option ([#3529][] @JulesBlm)
10+
811
### Fixed
912
* [`no-array-index-key`]: consider flatMap ([#3530][] @k-yle)
1013

1114
[#3530]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3530
15+
[#3529]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3529
1216

1317
## [7.32.2] - 2023.01.28
1418

docs/rules/display-name.md

+28-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const Hello = React.memo(function Hello({ a }) {
4545

4646
```js
4747
...
48-
"react/display-name": [<enabled>, { "ignoreTranspilerName": <boolean> }]
48+
"react/display-name": [<enabled>, { "ignoreTranspilerName": <boolean>, "checkContextObjects": <boolean> }]
4949
...
5050
```
5151

@@ -128,6 +128,33 @@ function HelloComponent() {
128128
module.exports = HelloComponent();
129129
```
130130

131+
### checkContextObjects (default: `false`)
132+
133+
`displayName` allows you to [name your context](https://reactjs.org/docs/context.html#contextdisplayname) object. This name is used in the React dev tools for the context's `Provider` and `Consumer`.
134+
When `true` this rule will warn on context objects without a `displayName`.
135+
136+
Examples of **incorrect** code for this rule:
137+
138+
```jsx
139+
const Hello = React.createContext();
140+
```
141+
142+
```jsx
143+
const Hello = createContext();
144+
```
145+
146+
Examples of **correct** code for this rule:
147+
148+
```jsx
149+
const Hello = React.createContext();
150+
Hello.displayName = "HelloContext";
151+
```
152+
153+
```jsx
154+
const Hello = createContext();
155+
Hello.displayName = "HelloContext";
156+
```
157+
131158
## About component detection
132159

133160
For this rule to work we need to detect React components, this could be very hard since components could be declared in a lot of ways.

lib/rules/display-name.js

+41
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
const values = require('object.values');
99

1010
const Components = require('../util/Components');
11+
const isCreateContext = require('../util/isCreateContext');
1112
const astUtil = require('../util/ast');
1213
const componentUtil = require('../util/componentUtil');
1314
const docsUrl = require('../util/docsUrl');
@@ -21,6 +22,7 @@ const report = require('../util/report');
2122

2223
const messages = {
2324
noDisplayName: 'Component definition is missing display name',
25+
noContextDisplayName: 'Context definition is missing display name',
2426
};
2527

2628
module.exports = {
@@ -40,6 +42,9 @@ module.exports = {
4042
ignoreTranspilerName: {
4143
type: 'boolean',
4244
},
45+
checkContextObjects: {
46+
type: 'boolean',
47+
},
4348
},
4449
additionalProperties: false,
4550
}],
@@ -48,6 +53,9 @@ module.exports = {
4853
create: Components.detect((context, components, utils) => {
4954
const config = context.options[0] || {};
5055
const ignoreTranspilerName = config.ignoreTranspilerName || false;
56+
const checkContextObjects = (config.checkContextObjects || false) && testReactVersion(context, '>= 16.3.0');
57+
58+
const contextObjects = new Map();
5159

5260
/**
5361
* Mark a prop type as declared
@@ -87,6 +95,16 @@ module.exports = {
8795
});
8896
}
8997

98+
/**
99+
* Reports missing display name for a given context object
100+
* @param {Object} contextObj The context object to process
101+
*/
102+
function reportMissingContextDisplayName(contextObj) {
103+
report(context, messages.noContextDisplayName, 'noContextDisplayName', {
104+
node: contextObj.node,
105+
});
106+
}
107+
90108
/**
91109
* Checks if the component have a name set by the transpiler
92110
* @param {ASTNode} node The AST node being checked.
@@ -144,6 +162,16 @@ module.exports = {
144162
// --------------------------------------------------------------------------
145163

146164
return {
165+
ExpressionStatement(node) {
166+
if (checkContextObjects && isCreateContext(node)) {
167+
contextObjects.set(node.expression.left.name, { node, hasDisplayName: false });
168+
}
169+
},
170+
VariableDeclarator(node) {
171+
if (checkContextObjects && isCreateContext(node)) {
172+
contextObjects.set(node.id.name, { node, hasDisplayName: false });
173+
}
174+
},
147175
'ClassProperty, PropertyDefinition'(node) {
148176
if (!propsUtil.isDisplayNameDeclaration(node)) {
149177
return;
@@ -155,6 +183,14 @@ module.exports = {
155183
if (!propsUtil.isDisplayNameDeclaration(node.property)) {
156184
return;
157185
}
186+
if (
187+
checkContextObjects
188+
&& node.object
189+
&& node.object.name
190+
&& contextObjects.has(node.object.name)
191+
) {
192+
contextObjects.get(node.object.name).hasDisplayName = true;
193+
}
158194
const component = utils.getRelatedComponent(node);
159195
if (!component) {
160196
return;
@@ -258,6 +294,11 @@ module.exports = {
258294
values(list).filter((component) => !component.hasDisplayName).forEach((component) => {
259295
reportMissingDisplayName(component);
260296
});
297+
if (checkContextObjects) {
298+
// Report missing display name for all context objects
299+
const contextsList = Array.from(contextObjects.values()).filter((v) => !v.hasDisplayName);
300+
contextsList.forEach((contextObj) => reportMissingContextDisplayName(contextObj));
301+
}
261302
},
262303
};
263304
}),

lib/util/isCreateContext.js

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use strict';
2+
3+
/**
4+
* Checks if the node is a React.createContext call
5+
* @param {ASTNode} node - The AST node being checked.
6+
* @returns {Boolean} - True if node is a React.createContext call, false if not.
7+
*/
8+
module.exports = function isCreateContext(node) {
9+
if (
10+
node.init
11+
&& node.init.type === 'CallExpression'
12+
&& node.init.callee
13+
&& node.init.callee.name === 'createContext'
14+
) {
15+
return true;
16+
}
17+
18+
if (
19+
node.init
20+
&& node.init.callee
21+
&& node.init.callee.type === 'MemberExpression'
22+
&& node.init.callee.property
23+
&& node.init.callee.property.name === 'createContext'
24+
) {
25+
return true;
26+
}
27+
28+
if (
29+
node.expression
30+
&& node.expression.type === 'AssignmentExpression'
31+
&& node.expression.operator === '='
32+
&& node.expression.right.type === 'CallExpression'
33+
&& node.expression.right.callee
34+
&& node.expression.right.callee.name === 'createContext'
35+
) {
36+
return true;
37+
}
38+
39+
if (
40+
node.expression
41+
&& node.expression.type === 'AssignmentExpression'
42+
&& node.expression.operator === '='
43+
&& node.expression.right.type === 'CallExpression'
44+
&& node.expression.right.callee
45+
&& node.expression.right.callee.type === 'MemberExpression'
46+
&& node.expression.right.callee.property
47+
&& node.expression.right.callee.property.name === 'createContext'
48+
) {
49+
return true;
50+
}
51+
52+
return false;
53+
};

0 commit comments

Comments
 (0)