Skip to content

Commit 32d920c

Browse files
authored
fix: fixed 'no-context-provider' replaces '<Provider>' with '<>', closes #984 (#985)
1 parent f3992f1 commit 32d920c

File tree

12 files changed

+153
-49
lines changed

12 files changed

+153
-49
lines changed

packages/core/docs/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,14 @@
4343
- [ERComponentHint](variables/ERComponentHint.md)
4444
- [ERPhaseRelevance](variables/ERPhaseRelevance.md)
4545
- [RE\_COMPONENT\_NAME](variables/RE_COMPONENT_NAME.md)
46+
- [RE\_COMPONENT\_NAME\_LOOSE](variables/RE_COMPONENT_NAME_LOOSE.md)
4647
- [RE\_HOOK\_NAME](variables/RE_HOOK_NAME.md)
4748

4849
## Functions
4950

5051
- [getComponentNameFromIdentifier](functions/getComponentNameFromIdentifier.md)
5152
- [getFunctionComponentIdentifier](functions/getFunctionComponentIdentifier.md)
52-
- [hasNoneOrValidComponentName](functions/hasNoneOrValidComponentName.md)
53+
- [hasNoneOrLooseComponentName](functions/hasNoneOrLooseComponentName.md)
5354
- [isAssignmentToThisState](functions/isAssignmentToThisState.md)
5455
- [isChildrenCount](functions/isChildrenCount.md)
5556
- [isChildrenCountCall](functions/isChildrenCountCall.md)
@@ -67,6 +68,7 @@
6768
- [isComponentDidCatch](functions/isComponentDidCatch.md)
6869
- [isComponentDidMount](functions/isComponentDidMount.md)
6970
- [isComponentName](functions/isComponentName.md)
71+
- [isComponentNameLoose](functions/isComponentNameLoose.md)
7072
- [isComponentWillUnmount](functions/isComponentWillUnmount.md)
7173
- [isCreateContext](functions/isCreateContext.md)
7274
- [isCreateContextCall](functions/isCreateContextCall.md)

packages/core/docs/functions/hasNoneOrValidComponentName.md renamed to packages/core/docs/functions/hasNoneOrLooseComponentName.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
***
44

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

7-
# Function: hasNoneOrValidComponentName()
7+
# Function: hasNoneOrLooseComponentName()
88

9-
> **hasNoneOrValidComponentName**(`context`, `node`): `boolean`
9+
> **hasNoneOrLooseComponentName**(`context`, `node`): `boolean`
1010
1111
## Parameters
1212

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[**@eslint-react/core**](../README.md)
2+
3+
***
4+
5+
[@eslint-react/core](../README.md) / isComponentNameLoose
6+
7+
# Function: isComponentNameLoose()
8+
9+
> **isComponentNameLoose**(`name`): `boolean`
10+
11+
## Parameters
12+
13+
### name
14+
15+
`string`
16+
17+
## Returns
18+
19+
`boolean`
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[**@eslint-react/core**](../README.md)
2+
3+
***
4+
5+
[@eslint-react/core](../README.md) / RE\_COMPONENT\_NAME\_LOOSE
6+
7+
# Variable: RE\_COMPONENT\_NAME\_LOOSE
8+
9+
> `const` **RE\_COMPONENT\_NAME\_LOOSE**: [`RegExp`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { ERComponentHint } from "./component-collector-hint";
1313
import { DEFAULT_COMPONENT_HINT } from "./component-collector-hint";
1414
import { ERComponentFlag } from "./component-flag";
1515
import { getFunctionComponentIdentifier } from "./component-id";
16-
import { getComponentNameFromIdentifier, hasNoneOrValidComponentName } from "./component-name";
16+
import { getComponentNameFromIdentifier, hasNoneOrLooseComponentName } from "./component-name";
1717
import type { ERFunctionComponent } from "./component-semantic-node";
1818
import { hasValidHierarchy } from "./hierarchy";
1919

@@ -101,7 +101,7 @@ export function useComponentCollector(
101101
const entry = getCurrentEntry();
102102
if (entry == null) return;
103103
const { body } = entry.node;
104-
const isComponent = hasNoneOrValidComponentName(context, entry.node)
104+
const isComponent = hasNoneOrLooseComponentName(context, entry.node)
105105
&& JSX.isJSXValue(body, jsxCtx, hint)
106106
&& hasValidHierarchy(context, entry.node, hint);
107107
if (!isComponent) return;
@@ -150,7 +150,7 @@ export function useComponentCollector(
150150
"ReturnStatement[type]"(node: TSESTree.ReturnStatement) {
151151
const entry = getCurrentEntry();
152152
if (entry == null) return;
153-
const isComponent = hasNoneOrValidComponentName(context, entry.node)
153+
const isComponent = hasNoneOrLooseComponentName(context, entry.node)
154154
&& JSX.isJSXValue(node.argument, jsxCtx, hint)
155155
&& hasValidHierarchy(context, entry.node, hint);
156156
if (!isComponent) return;

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@ import type { TSESTree } from "@typescript-eslint/types";
55

66
import { getFunctionComponentIdentifier } from "./component-id";
77

8-
export const RE_COMPONENT_NAME = /^_?[A-Z]/u;
8+
export const RE_COMPONENT_NAME = /^[A-Z]/u;
9+
10+
export const RE_COMPONENT_NAME_LOOSE = /^_?[A-Z]/u;
11+
12+
export function isComponentName(name: string) {
13+
return RE_COMPONENT_NAME.test(name);
14+
}
15+
16+
export function isComponentNameLoose(name: string) {
17+
return RE_COMPONENT_NAME_LOOSE.test(name);
18+
}
919

1020
export function getComponentNameFromIdentifier(node: TSESTree.Identifier | TSESTree.Identifier[] | _) {
1121
if (node == null) return _;
@@ -14,15 +24,11 @@ export function getComponentNameFromIdentifier(node: TSESTree.Identifier | TSEST
1424
: node.name;
1525
}
1626

17-
export function isComponentName(name: string) {
18-
return RE_COMPONENT_NAME.test(name);
19-
}
20-
21-
export function hasNoneOrValidComponentName(context: RuleContext, node: AST.TSESTreeFunction) {
27+
export function hasNoneOrLooseComponentName(context: RuleContext, node: AST.TSESTreeFunction) {
2228
const id = getFunctionComponentIdentifier(context, node);
2329
if (id == null) return true;
2430
const name = Array.isArray(id)
2531
? id.at(-1)?.name
2632
: id.name;
27-
return name != null && isComponentName(name);
33+
return name != null && isComponentNameLoose(name);
2834
}

packages/plugins/eslint-plugin-react-naming-convention/src/rules/context-name.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getInstanceId, isCreateContextCall } from "@eslint-react/core";
1+
import { getInstanceId, isComponentName, isCreateContextCall } from "@eslint-react/core";
22
import { _, identity } from "@eslint-react/eff";
33
import type { RuleFeature } from "@eslint-react/shared";
44
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
@@ -37,7 +37,7 @@ export default createRule<[], MessageID>({
3737
.with({ type: T.Identifier, name: P.select() }, identity)
3838
.with({ type: T.MemberExpression, property: { name: P.select(P.string) } }, identity)
3939
.otherwise(() => _);
40-
if (name != null && /^[A-Z]/u.test(name) && name.endsWith("Context")) return;
40+
if (name != null && isComponentName(name) && name.endsWith("Context")) return;
4141
context.report({
4242
messageId: "invalid",
4343
node: id,

packages/plugins/eslint-plugin-react-x/src/rules/no-context-provider.spec.ts

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,24 @@ import rule, { RULE_NAME } from "./no-context-provider";
55

66
ruleTester.run(RULE_NAME, rule, {
77
invalid: [
8+
{
9+
code: tsx`<Provider />`,
10+
errors: [
11+
{
12+
messageId: "noContextProvider",
13+
},
14+
],
15+
settings: {
16+
"react-x": {
17+
version: "19.0.0",
18+
},
19+
},
20+
},
821
{
922
code: tsx`<Context.Provider />`,
1023
errors: [
1124
{
1225
messageId: "noContextProvider",
13-
data: {
14-
contextName: "Context",
15-
},
1626
},
1727
],
1828
output: tsx`<Context />`,
@@ -27,9 +37,6 @@ ruleTester.run(RULE_NAME, rule, {
2737
errors: [
2838
{
2939
messageId: "noContextProvider",
30-
data: {
31-
contextName: "ThemeContext",
32-
},
3340
},
3441
],
3542
output: tsx`<ThemeContext><App /></ThemeContext>`,
@@ -44,9 +51,6 @@ ruleTester.run(RULE_NAME, rule, {
4451
errors: [
4552
{
4653
messageId: "noContextProvider",
47-
data: {
48-
contextName: "Context",
49-
},
5054
},
5155
],
5256
output: tsx`<Context>{children}</Context>`,
@@ -61,9 +65,6 @@ ruleTester.run(RULE_NAME, rule, {
6165
errors: [
6266
{
6367
messageId: "noContextProvider",
64-
data: {
65-
contextName: "Foo.Bar",
66-
},
6768
},
6869
],
6970
output: tsx`<Foo.Bar>{children}</Foo.Bar>`,
@@ -73,6 +74,33 @@ ruleTester.run(RULE_NAME, rule, {
7374
},
7475
},
7576
},
77+
{
78+
code: tsx`<foo.Bar.Provider>{children}</foo.Bar.Provider>`,
79+
errors: [
80+
{
81+
messageId: "noContextProvider",
82+
},
83+
],
84+
output: tsx`<foo.Bar>{children}</foo.Bar>`,
85+
settings: {
86+
"react-x": {
87+
version: "19.0.0",
88+
},
89+
},
90+
},
91+
{
92+
code: tsx`<foo.bar.Provider>{children}</foo.bar.Provider>`,
93+
errors: [
94+
{
95+
messageId: "noContextProvider",
96+
},
97+
],
98+
settings: {
99+
"react-x": {
100+
version: "19.0.0",
101+
},
102+
},
103+
},
76104
],
77105
valid: [
78106
{
@@ -83,5 +111,45 @@ ruleTester.run(RULE_NAME, rule, {
83111
},
84112
},
85113
},
114+
{
115+
code: tsx`<Context />`,
116+
settings: {
117+
"react-x": {
118+
version: "19.0.0",
119+
},
120+
},
121+
},
122+
{
123+
code: tsx`<ThemeContext.Provider><App /></ThemeContext.Provider>`,
124+
settings: {
125+
"react-x": {
126+
version: "18.0.0",
127+
},
128+
},
129+
},
130+
{
131+
code: tsx`<ThemeContext.Provider>{children}</ThemeContext.Provider>`,
132+
settings: {
133+
"react-x": {
134+
version: "18.0.0",
135+
},
136+
},
137+
},
138+
{
139+
code: tsx`<ThemeContext><App /></ThemeContext>`,
140+
settings: {
141+
"react-x": {
142+
version: "19.0.0",
143+
},
144+
},
145+
},
146+
{
147+
code: tsx`<ThemeContext>{children}</ThemeContext>`,
148+
settings: {
149+
"react-x": {
150+
version: "19.0.0",
151+
},
152+
},
153+
},
86154
],
87155
});

packages/plugins/eslint-plugin-react-x/src/rules/no-context-provider.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isComponentNameLoose } from "@eslint-react/core";
12
import * as JSX from "@eslint-react/jsx";
23
import type { RuleFeature } from "@eslint-react/shared";
34
import { getSettingsFromContext } from "@eslint-react/shared";
@@ -24,38 +25,37 @@ export default createRule<[], MessageID>({
2425
},
2526
fixable: "code",
2627
messages: {
27-
noContextProvider:
28-
"In React 19, you can render '<{{contextName}}>' as a provider instead of '<{{contextName}}.Provider>'.",
28+
noContextProvider: "In React 19, you can render '<Context>' as a provider instead of '<Context.Provider>'.",
2929
},
3030
schema: [],
3131
},
3232
name: RULE_NAME,
3333
create(context) {
34-
if (!context.sourceCode.text.includes(".Provider")) return {};
34+
if (!context.sourceCode.text.includes("Provider")) return {};
3535
const { version } = getSettingsFromContext(context);
36-
if (compare(version, "19.0.0", "<")) {
37-
return {};
38-
}
36+
if (compare(version, "19.0.0", "<")) return {};
3937
return {
4038
JSXElement(node) {
41-
const [name, ...rest] = JSX.getElementType(node).split(".").reverse();
42-
if (name !== "Provider") return;
43-
const contextName = rest.reverse().join(".");
39+
const fullName = JSX.getElementType(node);
40+
const parts = fullName.split(".");
41+
const selfName = parts.pop();
42+
const contextFullName = parts.join(".");
43+
const contextSelfName = parts.pop();
44+
if (selfName !== "Provider") return;
4445
context.report({
4546
messageId: "noContextProvider",
4647
node,
47-
data: {
48-
contextName,
49-
},
5048
fix(fixer) {
49+
if (contextSelfName == null) return null;
50+
if (!isComponentNameLoose(contextSelfName)) return null;
5151
const openingElement = node.openingElement;
5252
const closingElement = node.closingElement;
5353
if (closingElement == null) {
54-
return fixer.replaceText(openingElement.name, contextName);
54+
return fixer.replaceText(openingElement.name, contextFullName);
5555
}
5656
return [
57-
fixer.replaceText(openingElement.name, contextName),
58-
fixer.replaceText(closingElement.name, contextName),
57+
fixer.replaceText(openingElement.name, contextFullName),
58+
fixer.replaceText(closingElement.name, contextFullName),
5959
];
6060
},
6161
});

packages/plugins/eslint-plugin-react-x/src/rules/no-default-props.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as AST from "@eslint-react/ast";
2-
import { isClassComponent, isComponentName } from "@eslint-react/core";
2+
import { isClassComponent, isComponentNameLoose } from "@eslint-react/core";
33
import type { RuleFeature } from "@eslint-react/shared";
44
import * as VAR from "@eslint-react/var";
55
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
@@ -44,7 +44,7 @@ export default createRule<[], MessageID>({
4444
if (property.type !== T.Identifier || property.name !== "defaultProps") {
4545
return;
4646
}
47-
if (!isComponentName(object.name)) {
47+
if (!isComponentNameLoose(object.name)) {
4848
return;
4949
}
5050
const variable = VAR.findVariable(object.name, context.sourceCode.getScope(node));

0 commit comments

Comments
 (0)