Skip to content

Commit a76d213

Browse files
authored
feat: add 'no-missing-context-display-name' rule (#941)
1 parent 680c1e3 commit a76d213

22 files changed

+303
-80
lines changed

apps/website/content/docs/rules/meta.json

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"no-implicit-key",
3131
"no-leaked-conditional-rendering",
3232
"no-missing-component-display-name",
33+
"no-missing-context-display-name",
3334
"no-missing-key",
3435
"no-nested-components",
3536
"no-prop-types",

apps/website/content/docs/rules/overview.md

+54-53
Large diffs are not rendered by default.

packages/core/docs/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
## Variables
4141

42-
- [COMPONENT\_DISPLAY\_NAME\_ASSIGNMENT\_SELECTOR](variables/COMPONENT_DISPLAY_NAME_ASSIGNMENT_SELECTOR.md)
42+
- [COMPONENT\_DISPLAY\_NAME\_ASSIGNMENT\_SELECTOR](variables/DISPLAY_NAME_ASSIGNMENT_SELECTOR.md)
4343
- [DEFAULT\_COMPONENT\_HINT](variables/DEFAULT_COMPONENT_HINT.md)
4444
- [ERComponentFlag](variables/ERComponentFlag.md)
4545
- [ERComponentHint](variables/ERComponentHint.md)
@@ -70,7 +70,7 @@
7070
- [isCloneElementCall](functions/isCloneElementCall.md)
7171
- [isComponentDidCatch](functions/isComponentDidCatch.md)
7272
- [isComponentDidMount](functions/isComponentDidMount.md)
73-
- [isComponentDisplayNameAssignment](functions/isComponentDisplayNameAssignment.md)
73+
- [isDisplayNameAssignment](functions/isDisplayNameAssignment.md)
7474
- [isComponentName](functions/isComponentName.md)
7575
- [isComponentWillUnmount](functions/isComponentWillUnmount.md)
7676
- [isCreateContext](functions/isCreateContext.md)

packages/core/docs/functions/isComponentDisplayNameAssignment.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
***
44

5-
[@eslint-react/core](../README.md) / isComponentDisplayNameAssignment
5+
[@eslint-react/core](../README.md) / isDisplayNameAssignment
66

7-
# Function: isComponentDisplayNameAssignment()
7+
# Function: isDisplayNameAssignment()
88

9-
> **isComponentDisplayNameAssignment**(`node`): `node is AssignmentExpression`
9+
> **isDisplayNameAssignment**(`node`): `node is AssignmentExpression`
1010
1111
Check if the node is a component display name assignment expression
1212

packages/core/src/component/component-collector.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import type { ESLintUtils } from "@typescript-eslint/utils";
99

1010
import { isChildrenOfCreateElement } from "../element";
1111
import { isReactHookCall } from "../hook";
12+
import { DISPLAY_NAME_ASSIGNMENT_SELECTOR } from "../utils";
1213
import { DEFAULT_COMPONENT_HINT, ERComponentHint } from "./component-collector-hint";
13-
import { COMPONENT_DISPLAY_NAME_ASSIGNMENT_SELECTOR } from "./component-display-name";
1414
import { ERComponentFlag } from "./component-flag";
1515
import { getFunctionComponentIdentifier } from "./component-id";
1616
import { isFunctionOfRenderMethod } from "./component-lifecycle";
@@ -106,7 +106,7 @@ export function useComponentCollector(
106106
},
107107
...collectDisplayName
108108
? {
109-
[COMPONENT_DISPLAY_NAME_ASSIGNMENT_SELECTOR](node: TSESTree.AssignmentExpression) {
109+
[DISPLAY_NAME_ASSIGNMENT_SELECTOR](node: TSESTree.AssignmentExpression) {
110110
const { left, right } = node;
111111
if (left.type !== T.MemberExpression) return;
112112
const componentName = left.object.type === T.Identifier

packages/core/src/component/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export * from "./component-collector";
22
export * from "./component-collector-hint";
33
export * from "./component-collector-legacy";
4-
export * from "./component-display-name";
54
export * from "./component-flag";
65
export * from "./component-id";
76
export type * from "./component-kind";

packages/core/src/utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from "./is-display-name-assignment";
12
export * from "./is-from-react";
23
export * from "./is-initialized-from-react";
34
export * from "./is-react-api";

packages/core/src/component/component-display-name.ts renamed to packages/core/src/utils/is-display-name-assignment.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { AST_NODE_TYPES as T, type TSESTree } from "@typescript-eslint/types";
44
/**
55
* The ESQuery selector for a component display name assignment expression
66
*/
7-
export const COMPONENT_DISPLAY_NAME_ASSIGNMENT_SELECTOR = [
7+
export const DISPLAY_NAME_ASSIGNMENT_SELECTOR = [
88
"AssignmentExpression",
99
"[type]",
1010
"[operator='=']",
@@ -17,7 +17,7 @@ export const COMPONENT_DISPLAY_NAME_ASSIGNMENT_SELECTOR = [
1717
* @param node The AST node
1818
* @returns `true` if the node is a component display name assignment
1919
*/
20-
export function isComponentDisplayNameAssignment(node: TSESTree.Node | _): node is TSESTree.AssignmentExpression {
20+
export function isDisplayNameAssignment(node: TSESTree.Node | _): node is TSESTree.AssignmentExpression {
2121
if (node == null) return false;
2222
return node.type === T.AssignmentExpression
2323
&& node.operator === "="

packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-interval.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { AST_NODE_TYPES as T } from "@typescript-eslint/utils";
88
import { isMatching, P } from "ts-pattern";
99

1010
import type { TimerEntry } from "../models";
11-
import { createRule, getPhaseKindOfFunction, isInstanceIDEqual } from "../utils";
11+
import { createRule, getPhaseKindOfFunction, isInstanceIdEqual } from "../utils";
1212

1313
// #region Rule Metadata
1414

@@ -81,7 +81,7 @@ export default createRule<[], MessageID>({
8181
const sEntries: TimerEntry[] = [];
8282
const cEntries: TimerEntry[] = [];
8383
function isInverseEntry(a: TimerEntry, b: TimerEntry) {
84-
return isInstanceIDEqual(a.timerId, b.timerId, context);
84+
return isInstanceIdEqual(a.timerId, b.timerId, context);
8585
}
8686
return {
8787
[":function"](node: AST.TSESTreeFunction) {

packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-resize-observer.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { TSESTree } from "@typescript-eslint/utils";
88
import { AST_NODE_TYPES as T } from "@typescript-eslint/utils";
99
import { isMatching, match, P } from "ts-pattern";
1010

11-
import { createRule, getInstanceID, getPhaseKindOfFunction, isInstanceIDEqual } from "../utils";
11+
import { createRule, getPhaseKindOfFunction, isInstanceIdEqual } from "../utils";
1212
import type { ObserverEntry, ObserverMethod } from "./../models";
1313

1414
// #region Rule Metadata
@@ -185,7 +185,7 @@ export default createRule<[], MessageID>({
185185
if (!isNewResizeObserver(node)) {
186186
return;
187187
}
188-
const id = getInstanceID(node);
188+
const id = VAR.getVariableId(node);
189189
if (id == null) {
190190
context.report({
191191
messageId: "unexpectedFloatingInstance",
@@ -202,11 +202,11 @@ export default createRule<[], MessageID>({
202202
},
203203
["Program:exit"]() {
204204
for (const { id, node, phaseNode } of observers) {
205-
if (dEntries.some((e) => isInstanceIDEqual(e.observer, id, context))) {
205+
if (dEntries.some((e) => isInstanceIdEqual(e.observer, id, context))) {
206206
continue;
207207
}
208-
const oentries = oEntries.filter((e) => isInstanceIDEqual(e.observer, id, context));
209-
const uentries = uEntries.filter((e) => isInstanceIDEqual(e.observer, id, context));
208+
const oentries = oEntries.filter((e) => isInstanceIdEqual(e.observer, id, context));
209+
const uentries = uEntries.filter((e) => isInstanceIdEqual(e.observer, id, context));
210210
const isDynamic = (node: TSESTree.Node | _) => node?.type === T.CallExpression || AST.isConditional(node);
211211
const isPhaseNode = (node: TSESTree.Node | _) => node === phaseNode;
212212
const hasDynamicallyAdded = oentries
@@ -216,7 +216,7 @@ export default createRule<[], MessageID>({
216216
continue;
217217
}
218218
for (const oEntry of oentries) {
219-
if (uentries.some((uEntry) => isInstanceIDEqual(uEntry.element, oEntry.element, context))) {
219+
if (uentries.some((uEntry) => isInstanceIdEqual(uEntry.element, oEntry.element, context))) {
220220
continue;
221221
}
222222
context.report({ messageId: "expectedDisconnectOrUnobserveInCleanup", node: oEntry.node });

packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-timeout.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { AST_NODE_TYPES as T } from "@typescript-eslint/utils";
88
import { isMatching, P } from "ts-pattern";
99

1010
import type { TimerEntry } from "../models";
11-
import { createRule, getPhaseKindOfFunction, isInstanceIDEqual } from "../utils";
11+
import { createRule, getPhaseKindOfFunction, isInstanceIdEqual } from "../utils";
1212

1313
// #region Rule Metadata
1414

@@ -80,7 +80,7 @@ export default createRule<[], MessageID>({
8080
const sEntries: TimerEntry[] = [];
8181
const rEntries: TimerEntry[] = [];
8282
function isInverseEntry(a: TimerEntry, b: TimerEntry) {
83-
return isInstanceIDEqual(a.timerId, b.timerId, context);
83+
return isInstanceIdEqual(a.timerId, b.timerId, context);
8484
}
8585
return {
8686
[":function"](node: AST.TSESTreeFunction) {
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
export * from "./create-rule";
2-
export * from "./get-instance-id";
32
export * from "./get-phase-kind-of-function";
4-
export * from "./is-instance-id-equal";
3+
export * from "./is-instance-Id-equal";

packages/plugins/eslint-plugin-react-web-api/src/utils/is-instance-id-equal.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import * as AST from "@eslint-react/ast";
21
import type { RuleContext } from "@eslint-react/shared";
32
import * as VAR from "@eslint-react/var";
43
import type { TSESTree } from "@typescript-eslint/types";
54

6-
export function isInstanceIDEqual(a: TSESTree.Node, b: TSESTree.Node, context: RuleContext) {
7-
return AST.isNodeEqual(a, b) || VAR.isNodeValueEqual(a, b, [
5+
export function isInstanceIdEqual(a: TSESTree.Node, b: TSESTree.Node, context: RuleContext) {
6+
return VAR.isVariableIdEqual(a, b, [
87
context.sourceCode.getScope(a),
98
context.sourceCode.getScope(b),
109
]);

packages/plugins/eslint-plugin-react-x/src/plugin.ts

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import noForwardRef from "./rules/no-forward-ref";
2727
import noImplicitKey from "./rules/no-implicit-key";
2828
import noLeakedConditionalRendering from "./rules/no-leaked-conditional-rendering";
2929
import noMissingComponentDisplayName from "./rules/no-missing-component-display-name";
30+
import noMissingContextDisplayName from "./rules/no-missing-context-display-name";
3031
import noMissingKey from "./rules/no-missing-key";
3132
import noNestedComponents from "./rules/no-nested-components";
3233
import noPropTypes from "./rules/no-prop-types";
@@ -85,6 +86,7 @@ export const plugin = {
8586
"no-implicit-key": noImplicitKey,
8687
"no-leaked-conditional-rendering": noLeakedConditionalRendering,
8788
"no-missing-component-display-name": noMissingComponentDisplayName,
89+
"no-missing-context-display-name": noMissingContextDisplayName,
8890
"no-missing-key": noMissingKey,
8991
"no-nested-components": noNestedComponents,
9092
"no-prop-types": noPropTypes,

packages/plugins/eslint-plugin-react-x/src/rules/no-missing-component-display-name.md

+7
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,10 @@ export default function Button() {
9898

9999
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-component-display-name.ts)
100100
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-component-display-name.spec.ts)
101+
102+
---
103+
104+
## See Also
105+
106+
- [`no-missing-context-display-name`](./no-missing-context-display-name)\
107+
Enforces that all contexts have a `displayName` which React can use as its `displayName` in devtools.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
title: no-missing-context-display-name
3+
---
4+
5+
**Full Name in `eslint-plugin-react-x`**
6+
7+
```plain copy
8+
react-x/no-missing-context-display-name
9+
```
10+
11+
**Full Name in `@eslint-react/eslint-plugin`**
12+
13+
```plain copy
14+
@eslint-react/no-missing-context-display-name
15+
```
16+
17+
**Features**
18+
19+
`🔍`
20+
21+
## What it does
22+
23+
Enforces that all contexts have a `displayName` which React can use as its `displayName` in devtools.
24+
25+
## Examples
26+
27+
### Failing
28+
29+
```tsx
30+
import React from "react";
31+
32+
const MyContext = React.createContext();
33+
```
34+
35+
### Passing
36+
37+
```tsx
38+
import React from "react";
39+
40+
const MyContext = React.createContext();
41+
MyContext.displayName = "MyContext";
42+
```
43+
44+
## Implementation
45+
46+
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.ts)
47+
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.spec.ts)
48+
49+
---
50+
51+
## See Also
52+
53+
- [`no-missing-component-display-name`](./no-missing-component-display-name)\
54+
Enforces that all components have a `displayName` which React can use as its `displayName` in devtools.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { allFunctions, ruleTester } from "../../../../../test";
2+
import rule, { RULE_NAME } from "./no-missing-context-display-name";
3+
4+
ruleTester.run(RULE_NAME, rule, {
5+
invalid: [
6+
{
7+
code: /* tsx */ `createContext();`,
8+
errors: [{ messageId: "noMissingContextDisplayName" }],
9+
},
10+
{
11+
code: /* tsx */ `const ctx = createContext();`,
12+
errors: [{ messageId: "noMissingContextDisplayName" }],
13+
},
14+
{
15+
code: /* tsx */ `
16+
const ctx1 = createContext();
17+
const ctx2 = createContext();
18+
ctx1.displayName = "ctx";
19+
`,
20+
errors: [{ messageId: "noMissingContextDisplayName" }],
21+
},
22+
{
23+
code: /* tsx */ `
24+
const ctx = createContext();
25+
ctx.displayname = "ctx";
26+
`,
27+
errors: [{ messageId: "noMissingContextDisplayName" }],
28+
},
29+
{
30+
code: /* tsx */ `
31+
createContext();
32+
ctx.displayName = "ctx";
33+
`,
34+
errors: [{ messageId: "noMissingContextDisplayName" }],
35+
},
36+
],
37+
valid: [
38+
...allFunctions,
39+
/* tsx */ `const ctx = createContext(); ctx.displayName = "ctx";`,
40+
/* tsx */ `
41+
const ctx = createContext();
42+
const displayName = "ctx";
43+
ctx.displayName = displayName;
44+
`,
45+
/* tsx */ `
46+
const ctx1 = createContext();
47+
const ctx2 = createContext();
48+
ctx1.displayName = "ctx1";
49+
ctx2.displayName = "ctx2";
50+
`,
51+
/* tsx */ `
52+
const ctx1 = createContext();
53+
const ctx2 = createContext();
54+
const displayName = "ctx";
55+
ctx1.displayName = displayName;
56+
ctx2.displayName = displayName;
57+
`,
58+
/* tsx */ `
59+
const ctx1 = createContext();
60+
const ctx2 = createContext();
61+
{
62+
const displayName = "ctx";
63+
ctx1.displayName = displayName;
64+
ctx2.displayName = displayName;
65+
}
66+
`,
67+
],
68+
});

0 commit comments

Comments
 (0)