From c070c29d823a5a12a70c4deec1c83714b2adb4cb Mon Sep 17 00:00:00 2001 From: Rel1cx Date: Wed, 12 Mar 2025 18:59:40 +0800 Subject: [PATCH] fix: fixed 'no-context-provider' replaces '' with '<>', closes #984 --- packages/core/docs/README.md | 4 +- ...Name.md => hasNoneOrLooseComponentName.md} | 6 +- .../docs/functions/isComponentNameLoose.md | 19 ++++ .../docs/variables/RE_COMPONENT_NAME_LOOSE.md | 9 ++ .../core/src/component/component-collector.ts | 6 +- packages/core/src/component/component-name.ts | 20 ++-- .../src/rules/context-name.ts | 4 +- .../src/rules/no-context-provider.spec.ts | 92 ++++++++++++++++--- .../src/rules/no-context-provider.ts | 30 +++--- .../src/rules/no-default-props.ts | 4 +- .../src/rules/no-prop-types.ts | 4 +- .../rules/prefer-destructuring-assignment.ts | 4 +- 12 files changed, 153 insertions(+), 49 deletions(-) rename packages/core/docs/functions/{hasNoneOrValidComponentName.md => hasNoneOrLooseComponentName.md} (57%) create mode 100644 packages/core/docs/functions/isComponentNameLoose.md create mode 100644 packages/core/docs/variables/RE_COMPONENT_NAME_LOOSE.md 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); }