diff --git a/packages/core/docs/README.md b/packages/core/docs/README.md
index b3d1cba2c4..cc2df912a5 100644
--- a/packages/core/docs/README.md
+++ b/packages/core/docs/README.md
@@ -43,13 +43,14 @@
- [ERComponentHint](variables/ERComponentHint.md)
- [ERPhaseRelevance](variables/ERPhaseRelevance.md)
- [RE\_COMPONENT\_NAME](variables/RE_COMPONENT_NAME.md)
+- [RE\_COMPONENT\_NAME\_LOOSE](variables/RE_COMPONENT_NAME_LOOSE.md)
- [RE\_HOOK\_NAME](variables/RE_HOOK_NAME.md)
## Functions
- [getComponentNameFromIdentifier](functions/getComponentNameFromIdentifier.md)
- [getFunctionComponentIdentifier](functions/getFunctionComponentIdentifier.md)
-- [hasNoneOrValidComponentName](functions/hasNoneOrValidComponentName.md)
+- [hasNoneOrLooseComponentName](functions/hasNoneOrLooseComponentName.md)
- [isAssignmentToThisState](functions/isAssignmentToThisState.md)
- [isChildrenCount](functions/isChildrenCount.md)
- [isChildrenCountCall](functions/isChildrenCountCall.md)
@@ -67,6 +68,7 @@
- [isComponentDidCatch](functions/isComponentDidCatch.md)
- [isComponentDidMount](functions/isComponentDidMount.md)
- [isComponentName](functions/isComponentName.md)
+- [isComponentNameLoose](functions/isComponentNameLoose.md)
- [isComponentWillUnmount](functions/isComponentWillUnmount.md)
- [isCreateContext](functions/isCreateContext.md)
- [isCreateContextCall](functions/isCreateContextCall.md)
diff --git a/packages/core/docs/functions/hasNoneOrValidComponentName.md b/packages/core/docs/functions/hasNoneOrLooseComponentName.md
similarity index 57%
rename from packages/core/docs/functions/hasNoneOrValidComponentName.md
rename to packages/core/docs/functions/hasNoneOrLooseComponentName.md
index 04274244c2..41107488af 100644
--- a/packages/core/docs/functions/hasNoneOrValidComponentName.md
+++ b/packages/core/docs/functions/hasNoneOrLooseComponentName.md
@@ -2,11 +2,11 @@
***
-[@eslint-react/core](../README.md) / hasNoneOrValidComponentName
+[@eslint-react/core](../README.md) / hasNoneOrLooseComponentName
-# Function: hasNoneOrValidComponentName()
+# Function: hasNoneOrLooseComponentName()
-> **hasNoneOrValidComponentName**(`context`, `node`): `boolean`
+> **hasNoneOrLooseComponentName**(`context`, `node`): `boolean`
## Parameters
diff --git a/packages/core/docs/functions/isComponentNameLoose.md b/packages/core/docs/functions/isComponentNameLoose.md
new file mode 100644
index 0000000000..240d17f939
--- /dev/null
+++ b/packages/core/docs/functions/isComponentNameLoose.md
@@ -0,0 +1,19 @@
+[**@eslint-react/core**](../README.md)
+
+***
+
+[@eslint-react/core](../README.md) / isComponentNameLoose
+
+# Function: isComponentNameLoose()
+
+> **isComponentNameLoose**(`name`): `boolean`
+
+## Parameters
+
+### name
+
+`string`
+
+## Returns
+
+`boolean`
diff --git a/packages/core/docs/variables/RE_COMPONENT_NAME_LOOSE.md b/packages/core/docs/variables/RE_COMPONENT_NAME_LOOSE.md
new file mode 100644
index 0000000000..a2b2630b83
--- /dev/null
+++ b/packages/core/docs/variables/RE_COMPONENT_NAME_LOOSE.md
@@ -0,0 +1,9 @@
+[**@eslint-react/core**](../README.md)
+
+***
+
+[@eslint-react/core](../README.md) / RE\_COMPONENT\_NAME\_LOOSE
+
+# Variable: RE\_COMPONENT\_NAME\_LOOSE
+
+> `const` **RE\_COMPONENT\_NAME\_LOOSE**: [`RegExp`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp)
diff --git a/packages/core/src/component/component-collector.ts b/packages/core/src/component/component-collector.ts
index f7cbd1cb25..998a09c5bd 100644
--- a/packages/core/src/component/component-collector.ts
+++ b/packages/core/src/component/component-collector.ts
@@ -13,7 +13,7 @@ import type { ERComponentHint } from "./component-collector-hint";
import { DEFAULT_COMPONENT_HINT } from "./component-collector-hint";
import { ERComponentFlag } from "./component-flag";
import { getFunctionComponentIdentifier } from "./component-id";
-import { getComponentNameFromIdentifier, hasNoneOrValidComponentName } from "./component-name";
+import { getComponentNameFromIdentifier, hasNoneOrLooseComponentName } from "./component-name";
import type { ERFunctionComponent } from "./component-semantic-node";
import { hasValidHierarchy } from "./hierarchy";
@@ -101,7 +101,7 @@ export function useComponentCollector(
const entry = getCurrentEntry();
if (entry == null) return;
const { body } = entry.node;
- const isComponent = hasNoneOrValidComponentName(context, entry.node)
+ const isComponent = hasNoneOrLooseComponentName(context, entry.node)
&& JSX.isJSXValue(body, jsxCtx, hint)
&& hasValidHierarchy(context, entry.node, hint);
if (!isComponent) return;
@@ -150,7 +150,7 @@ export function useComponentCollector(
"ReturnStatement[type]"(node: TSESTree.ReturnStatement) {
const entry = getCurrentEntry();
if (entry == null) return;
- const isComponent = hasNoneOrValidComponentName(context, entry.node)
+ const isComponent = hasNoneOrLooseComponentName(context, entry.node)
&& JSX.isJSXValue(node.argument, jsxCtx, hint)
&& hasValidHierarchy(context, entry.node, hint);
if (!isComponent) return;
diff --git a/packages/core/src/component/component-name.ts b/packages/core/src/component/component-name.ts
index 865eab2e72..d9ce2d609d 100644
--- a/packages/core/src/component/component-name.ts
+++ b/packages/core/src/component/component-name.ts
@@ -5,7 +5,17 @@ import type { TSESTree } from "@typescript-eslint/types";
import { getFunctionComponentIdentifier } from "./component-id";
-export const RE_COMPONENT_NAME = /^_?[A-Z]/u;
+export const RE_COMPONENT_NAME = /^[A-Z]/u;
+
+export const RE_COMPONENT_NAME_LOOSE = /^_?[A-Z]/u;
+
+export function isComponentName(name: string) {
+ return RE_COMPONENT_NAME.test(name);
+}
+
+export function isComponentNameLoose(name: string) {
+ return RE_COMPONENT_NAME_LOOSE.test(name);
+}
export function getComponentNameFromIdentifier(node: TSESTree.Identifier | TSESTree.Identifier[] | _) {
if (node == null) return _;
@@ -14,15 +24,11 @@ export function getComponentNameFromIdentifier(node: TSESTree.Identifier | TSEST
: node.name;
}
-export function isComponentName(name: string) {
- return RE_COMPONENT_NAME.test(name);
-}
-
-export function hasNoneOrValidComponentName(context: RuleContext, node: AST.TSESTreeFunction) {
+export function hasNoneOrLooseComponentName(context: RuleContext, node: AST.TSESTreeFunction) {
const id = getFunctionComponentIdentifier(context, node);
if (id == null) return true;
const name = Array.isArray(id)
? id.at(-1)?.name
: id.name;
- return name != null && isComponentName(name);
+ return name != null && isComponentNameLoose(name);
}
diff --git a/packages/plugins/eslint-plugin-react-naming-convention/src/rules/context-name.ts b/packages/plugins/eslint-plugin-react-naming-convention/src/rules/context-name.ts
index f4aee3d914..5e0b66aa28 100644
--- a/packages/plugins/eslint-plugin-react-naming-convention/src/rules/context-name.ts
+++ b/packages/plugins/eslint-plugin-react-naming-convention/src/rules/context-name.ts
@@ -1,4 +1,4 @@
-import { getInstanceId, isCreateContextCall } from "@eslint-react/core";
+import { getInstanceId, isComponentName, isCreateContextCall } from "@eslint-react/core";
import { _, identity } from "@eslint-react/eff";
import type { RuleFeature } from "@eslint-react/shared";
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
@@ -37,7 +37,7 @@ export default createRule<[], MessageID>({
.with({ type: T.Identifier, name: P.select() }, identity)
.with({ type: T.MemberExpression, property: { name: P.select(P.string) } }, identity)
.otherwise(() => _);
- if (name != null && /^[A-Z]/u.test(name) && name.endsWith("Context")) return;
+ if (name != null && isComponentName(name) && name.endsWith("Context")) return;
context.report({
messageId: "invalid",
node: id,
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-context-provider.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-context-provider.spec.ts
index 1aa4c1d75e..0845ab8cc8 100644
--- a/packages/plugins/eslint-plugin-react-x/src/rules/no-context-provider.spec.ts
+++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-context-provider.spec.ts
@@ -5,14 +5,24 @@ import rule, { RULE_NAME } from "./no-context-provider";
ruleTester.run(RULE_NAME, rule, {
invalid: [
+ {
+ code: tsx``,
+ errors: [
+ {
+ messageId: "noContextProvider",
+ },
+ ],
+ settings: {
+ "react-x": {
+ version: "19.0.0",
+ },
+ },
+ },
{
code: tsx``,
errors: [
{
messageId: "noContextProvider",
- data: {
- contextName: "Context",
- },
},
],
output: tsx``,
@@ -27,9 +37,6 @@ ruleTester.run(RULE_NAME, rule, {
errors: [
{
messageId: "noContextProvider",
- data: {
- contextName: "ThemeContext",
- },
},
],
output: tsx``,
@@ -44,9 +51,6 @@ ruleTester.run(RULE_NAME, rule, {
errors: [
{
messageId: "noContextProvider",
- data: {
- contextName: "Context",
- },
},
],
output: tsx`{children}`,
@@ -61,9 +65,6 @@ ruleTester.run(RULE_NAME, rule, {
errors: [
{
messageId: "noContextProvider",
- data: {
- contextName: "Foo.Bar",
- },
},
],
output: tsx`{children}`,
@@ -73,6 +74,33 @@ ruleTester.run(RULE_NAME, rule, {
},
},
},
+ {
+ code: tsx`{children}`,
+ errors: [
+ {
+ messageId: "noContextProvider",
+ },
+ ],
+ output: tsx`{children}`,
+ settings: {
+ "react-x": {
+ version: "19.0.0",
+ },
+ },
+ },
+ {
+ code: tsx`{children}`,
+ errors: [
+ {
+ messageId: "noContextProvider",
+ },
+ ],
+ settings: {
+ "react-x": {
+ version: "19.0.0",
+ },
+ },
+ },
],
valid: [
{
@@ -83,5 +111,45 @@ ruleTester.run(RULE_NAME, rule, {
},
},
},
+ {
+ code: tsx``,
+ settings: {
+ "react-x": {
+ version: "19.0.0",
+ },
+ },
+ },
+ {
+ code: tsx``,
+ settings: {
+ "react-x": {
+ version: "18.0.0",
+ },
+ },
+ },
+ {
+ code: tsx`{children}`,
+ settings: {
+ "react-x": {
+ version: "18.0.0",
+ },
+ },
+ },
+ {
+ code: tsx``,
+ settings: {
+ "react-x": {
+ version: "19.0.0",
+ },
+ },
+ },
+ {
+ code: tsx`{children}`,
+ settings: {
+ "react-x": {
+ version: "19.0.0",
+ },
+ },
+ },
],
});
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-context-provider.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-context-provider.ts
index b1f9ca77f5..57ebcc87c7 100644
--- a/packages/plugins/eslint-plugin-react-x/src/rules/no-context-provider.ts
+++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-context-provider.ts
@@ -1,3 +1,4 @@
+import { isComponentNameLoose } from "@eslint-react/core";
import * as JSX from "@eslint-react/jsx";
import type { RuleFeature } from "@eslint-react/shared";
import { getSettingsFromContext } from "@eslint-react/shared";
@@ -24,38 +25,37 @@ export default createRule<[], MessageID>({
},
fixable: "code",
messages: {
- noContextProvider:
- "In React 19, you can render '<{{contextName}}>' as a provider instead of '<{{contextName}}.Provider>'.",
+ noContextProvider: "In React 19, you can render '' as a provider instead of ''.",
},
schema: [],
},
name: RULE_NAME,
create(context) {
- if (!context.sourceCode.text.includes(".Provider")) return {};
+ if (!context.sourceCode.text.includes("Provider")) return {};
const { version } = getSettingsFromContext(context);
- if (compare(version, "19.0.0", "<")) {
- return {};
- }
+ if (compare(version, "19.0.0", "<")) return {};
return {
JSXElement(node) {
- const [name, ...rest] = JSX.getElementType(node).split(".").reverse();
- if (name !== "Provider") return;
- const contextName = rest.reverse().join(".");
+ const fullName = JSX.getElementType(node);
+ const parts = fullName.split(".");
+ const selfName = parts.pop();
+ const contextFullName = parts.join(".");
+ const contextSelfName = parts.pop();
+ if (selfName !== "Provider") return;
context.report({
messageId: "noContextProvider",
node,
- data: {
- contextName,
- },
fix(fixer) {
+ if (contextSelfName == null) return null;
+ if (!isComponentNameLoose(contextSelfName)) return null;
const openingElement = node.openingElement;
const closingElement = node.closingElement;
if (closingElement == null) {
- return fixer.replaceText(openingElement.name, contextName);
+ return fixer.replaceText(openingElement.name, contextFullName);
}
return [
- fixer.replaceText(openingElement.name, contextName),
- fixer.replaceText(closingElement.name, contextName),
+ fixer.replaceText(openingElement.name, contextFullName),
+ fixer.replaceText(closingElement.name, contextFullName),
];
},
});
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-default-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-default-props.ts
index 3762168de5..f29bd385ad 100644
--- a/packages/plugins/eslint-plugin-react-x/src/rules/no-default-props.ts
+++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-default-props.ts
@@ -1,5 +1,5 @@
import * as AST from "@eslint-react/ast";
-import { isClassComponent, isComponentName } from "@eslint-react/core";
+import { isClassComponent, isComponentNameLoose } from "@eslint-react/core";
import type { RuleFeature } from "@eslint-react/shared";
import * as VAR from "@eslint-react/var";
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
@@ -44,7 +44,7 @@ export default createRule<[], MessageID>({
if (property.type !== T.Identifier || property.name !== "defaultProps") {
return;
}
- if (!isComponentName(object.name)) {
+ if (!isComponentNameLoose(object.name)) {
return;
}
const variable = VAR.findVariable(object.name, context.sourceCode.getScope(node));
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-prop-types.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-prop-types.ts
index 731a9b6285..55572c39b9 100644
--- a/packages/plugins/eslint-plugin-react-x/src/rules/no-prop-types.ts
+++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-prop-types.ts
@@ -1,5 +1,5 @@
import * as AST from "@eslint-react/ast";
-import { isClassComponent, isComponentName } from "@eslint-react/core";
+import { isClassComponent, isComponentNameLoose } from "@eslint-react/core";
import type { RuleFeature } from "@eslint-react/shared";
import * as VAR from "@eslint-react/var";
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
@@ -44,7 +44,7 @@ export default createRule<[], MessageID>({
if (property.type !== T.Identifier || property.name !== "propTypes") {
return;
}
- if (!isComponentName(object.name)) {
+ if (!isComponentNameLoose(object.name)) {
return;
}
const variable = VAR.findVariable(object.name, context.sourceCode.getScope(node));
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/prefer-destructuring-assignment.ts b/packages/plugins/eslint-plugin-react-x/src/rules/prefer-destructuring-assignment.ts
index df75bf6f43..85446619e0 100644
--- a/packages/plugins/eslint-plugin-react-x/src/rules/prefer-destructuring-assignment.ts
+++ b/packages/plugins/eslint-plugin-react-x/src/rules/prefer-destructuring-assignment.ts
@@ -1,5 +1,5 @@
import * as AST from "@eslint-react/ast";
-import { isComponentName, useComponentCollector } from "@eslint-react/core";
+import { isComponentNameLoose, useComponentCollector } from "@eslint-react/core";
import type { RuleFeature } from "@eslint-react/shared";
import type { Scope } from "@typescript-eslint/scope-manager";
import type { TSESTree } from "@typescript-eslint/types";
@@ -61,7 +61,7 @@ export default createRule<[], MessageID>({
}
const id = AST.getFunctionIdentifier(block);
return id != null
- && isComponentName(id.name)
+ && isComponentNameLoose(id.name)
&& components.some((component) => component.node === block);
}