Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a684ae1

Browse files
JulesBlmljharb
authored andcommittedFeb 9, 2023
[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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 28 additions & 1 deletion
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

Lines changed: 41 additions & 0 deletions
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

Lines changed: 53 additions & 0 deletions
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+
};

‎tests/lib/rules/display-name.js

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,129 @@ ruleTester.run('display-name', rule, {
755755
},
756756
},
757757
},
758+
{
759+
code: `
760+
import React from 'react';
761+
762+
const Hello = React.createContext();
763+
Hello.displayName = "HelloContext"
764+
`,
765+
options: [{ checkContextObjects: true }],
766+
},
767+
{
768+
code: `
769+
import { createContext } from 'react';
770+
771+
const Hello = createContext();
772+
Hello.displayName = "HelloContext"
773+
`,
774+
options: [{ checkContextObjects: true }],
775+
},
776+
{
777+
code: `
778+
import { createContext } from 'react';
779+
780+
const Hello = createContext();
781+
782+
const obj = {};
783+
obj.displayName = "False positive";
784+
785+
Hello.displayName = "HelloContext"
786+
`,
787+
options: [{ checkContextObjects: true }],
788+
},
789+
{
790+
code: `
791+
import * as React from 'react';
792+
793+
const Hello = React.createContext();
794+
795+
const obj = {};
796+
obj.displayName = "False positive";
797+
798+
Hello.displayName = "HelloContext";
799+
`,
800+
options: [{ checkContextObjects: true }],
801+
},
802+
{
803+
code: `
804+
const obj = {};
805+
obj.displayName = "False positive";
806+
`,
807+
options: [{ checkContextObjects: true }],
808+
},
809+
// React.createContext should be accepted in React versions in the following range:
810+
// >= 16.13.0
811+
{
812+
code: `
813+
import { createContext } from 'react';
814+
815+
const Hello = createContext();
816+
`,
817+
settings: {
818+
react: {
819+
version: '16.2.0',
820+
},
821+
},
822+
options: [{ checkContextObjects: true }],
823+
},
824+
{
825+
code: `
826+
import { createContext } from 'react';
827+
828+
const Hello = createContext();
829+
Hello.displayName = "HelloContext";
830+
`,
831+
settings: {
832+
react: {
833+
version: '>16.3.0',
834+
},
835+
},
836+
options: [{ checkContextObjects: true }],
837+
},
838+
{
839+
code: `
840+
import { createContext } from 'react';
841+
842+
let Hello;
843+
Hello = createContext();
844+
Hello.displayName = "HelloContext";
845+
`,
846+
options: [{ checkContextObjects: true }],
847+
},
848+
{
849+
code: `
850+
import { createContext } from 'react';
851+
852+
const Hello = createContext();
853+
`,
854+
settings: {
855+
react: {
856+
version: '>16.3.0',
857+
},
858+
},
859+
options: [{ checkContextObjects: false }],
860+
},
861+
{
862+
code: `
863+
import { createContext } from 'react';
864+
865+
var Hello;
866+
Hello = createContext();
867+
Hello.displayName = "HelloContext";
868+
`,
869+
options: [{ checkContextObjects: true }],
870+
},
871+
{
872+
code: `
873+
import { createContext } from 'react';
874+
875+
var Hello;
876+
Hello = React.createContext();
877+
Hello.displayName = "HelloContext";
878+
`,
879+
options: [{ checkContextObjects: true }],
880+
},
758881
]),
759882

760883
invalid: parsers.all([
@@ -1180,5 +1303,77 @@ ruleTester.run('display-name', rule, {
11801303
},
11811304
],
11821305
},
1306+
{
1307+
code: `
1308+
import React from 'react';
1309+
1310+
const Hello = React.createContext();
1311+
`,
1312+
errors: [
1313+
{
1314+
messageId: 'noContextDisplayName',
1315+
line: 4,
1316+
},
1317+
],
1318+
options: [{ checkContextObjects: true }],
1319+
},
1320+
{
1321+
code: `
1322+
import * as React from 'react';
1323+
1324+
const Hello = React.createContext();
1325+
`,
1326+
errors: [
1327+
{
1328+
messageId: 'noContextDisplayName',
1329+
line: 4,
1330+
},
1331+
],
1332+
options: [{ checkContextObjects: true }],
1333+
},
1334+
{
1335+
code: `
1336+
import { createContext } from 'react';
1337+
1338+
const Hello = createContext();
1339+
`,
1340+
errors: [
1341+
{
1342+
messageId: 'noContextDisplayName',
1343+
line: 4,
1344+
},
1345+
],
1346+
options: [{ checkContextObjects: true }],
1347+
},
1348+
{
1349+
code: `
1350+
import { createContext } from 'react';
1351+
1352+
var Hello;
1353+
Hello = createContext();
1354+
`,
1355+
errors: [
1356+
{
1357+
messageId: 'noContextDisplayName',
1358+
line: 5,
1359+
},
1360+
],
1361+
options: [{ checkContextObjects: true }],
1362+
},
1363+
{
1364+
code: `
1365+
import { createContext } from 'react';
1366+
1367+
var Hello;
1368+
Hello = React.createContext();
1369+
`,
1370+
errors: [
1371+
{
1372+
messageId: 'noContextDisplayName',
1373+
line: 5,
1374+
},
1375+
],
1376+
options: [{ checkContextObjects: true }],
1377+
},
11831378
]),
11841379
});

0 commit comments

Comments
 (0)
Please sign in to comment.