diff --git a/packages/core/docs/functions/useComponentCollector.md b/packages/core/docs/functions/useComponentCollector.md index e93f0d5ed9..be247ee0e5 100644 --- a/packages/core/docs/functions/useComponentCollector.md +++ b/packages/core/docs/functions/useComponentCollector.md @@ -8,24 +8,34 @@ > **useComponentCollector**(`context`, `hint`, `options`): `object` +Get a ctx and listeners for the rule to collect function components + ## Parameters ### context `Readonly`\<`RuleContext`\<`string`, readonly `unknown`[]\>\> +The ESLint rule context + ### hint `bigint` = `DEFAULT_COMPONENT_HINT` +The hint to use + ### options [`ComponentCollectorOptions`](../interfaces/ComponentCollectorOptions.md) = `{}` +The options to use + ## Returns `object` +The component collector + ### ctx > **ctx**: `object` diff --git a/packages/core/docs/functions/useComponentCollectorLegacy.md b/packages/core/docs/functions/useComponentCollectorLegacy.md index 090e7eb739..51ee60d4aa 100644 --- a/packages/core/docs/functions/useComponentCollectorLegacy.md +++ b/packages/core/docs/functions/useComponentCollectorLegacy.md @@ -8,10 +8,14 @@ > **useComponentCollectorLegacy**(): `object` +Get a ctx and listeners for the rule to collect class components + ## Returns `object` +The context and listeners for the rule + ### ctx > **ctx**: `object` diff --git a/packages/core/src/component/component-collector-legacy.ts b/packages/core/src/component/component-collector-legacy.ts index 28b42a5a2d..848b958f52 100644 --- a/packages/core/src/component/component-collector-legacy.ts +++ b/packages/core/src/component/component-collector-legacy.ts @@ -7,6 +7,10 @@ import { ERClassComponentFlag } from "./component-flag"; import type { ERClassComponent } from "./component-semantic-node"; import { isClassComponent, isPureComponent } from "./is"; +/** + * Get a ctx and listeners for the rule to collect class components + * @returns The context and listeners for the rule + */ export function useComponentCollectorLegacy() { const components = new Map(); diff --git a/packages/core/src/component/component-collector.ts b/packages/core/src/component/component-collector.ts index 1fca2f8866..4d22b837a7 100644 --- a/packages/core/src/component/component-collector.ts +++ b/packages/core/src/component/component-collector.ts @@ -65,6 +65,13 @@ export interface ComponentCollectorOptions { // dprint-ignore const displayNameAssignmentSelector = "AssignmentExpression[type][operator='='][left.type='MemberExpression'][left.property.name='displayName']"; +/** + * Get a ctx and listeners for the rule to collect function components + * @param context The ESLint rule context + * @param hint The hint to use + * @param options The options to use + * @returns The component collector + */ export function useComponentCollector( context: RuleContext, hint = DEFAULT_COMPONENT_HINT, diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts index 9fd8e6199b..2f068a12f6 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-dangerously-set-innerhtml.ts @@ -31,7 +31,7 @@ export default createRule<[], MessageID>({ return { JSXElement(node) { const attributes = node.openingElement.attributes; - const attribute = JSX.getAttributeNode( + const attribute = JSX.getAttribute( "dangerouslySetInnerHTML", context.sourceCode.getScope(node), attributes, diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts index 82f8fdcac7..79a6b8b2c8 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts @@ -34,7 +34,7 @@ export default createRule<[], MessageID>({ return { JSXElement(node) { const [elementNameOnJsx, elementNameOnDom] = getElementNameOnJsxAndDom( - node.openingElement, + node, context, polymorphicPropName, additionalComponents, @@ -46,16 +46,15 @@ export default createRule<[], MessageID>({ const customComponent = findCustomComponent(elementNameOnJsx, additionalComponents); const customComponentProp = findCustomComponentProp("type", customComponent?.attributes ?? []); const propNameOnJsx = customComponentProp?.name ?? "type"; - const attributeNode = JSX.getAttributeNode( + const attributeNode = JSX.getAttribute( propNameOnJsx, elementScope, node.openingElement.attributes, ); if (attributeNode != null) { const attributeScope = context.sourceCode.getScope(attributeNode); - const attributeStaticValue = JSX.getAttributeStaticValue(attributeNode, attributeScope); - const attributeStringValue = JSX.toResolvedAttributeValue(propNameOnJsx, attributeStaticValue); - if (typeof attributeStringValue !== "string") { + const attributeValue = JSX.getAttributeValue(propNameOnJsx, attributeNode, attributeScope); + if (attributeValue.kind === "some" && typeof attributeValue.value !== "string") { context.report({ messageId: "noMissingButtonType", node: attributeNode, diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-iframe-sandbox.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-iframe-sandbox.ts index 3ddb362e04..e70fed1281 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-iframe-sandbox.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-iframe-sandbox.ts @@ -1,4 +1,3 @@ -import type { _ } from "@eslint-react/eff"; import * as JSX from "@eslint-react/jsx"; import type { RuleFeature } from "@eslint-react/shared"; import { getSettingsFromContext } from "@eslint-react/shared"; @@ -33,7 +32,7 @@ const validTypes = [ "allow-top-navigation-to-custom-protocols", ] as const; -function hasValidSandBox(value: string | _) { +function hasValidSandBox(value: unknown) { return typeof value === "string" && value .split(" ") @@ -60,7 +59,7 @@ export default createRule<[], MessageID>({ return { JSXElement(node) { const [elementNameOnJsx, elementNameOnDom] = getElementNameOnJsxAndDom( - node.openingElement, + node, context, polymorphicPropName, additionalComponents, @@ -72,16 +71,15 @@ export default createRule<[], MessageID>({ const customComponent = findCustomComponent(elementNameOnJsx, additionalComponents); const customComponentProp = findCustomComponentProp("sandbox", customComponent?.attributes ?? []); const propNameOnJsx = customComponentProp?.name ?? "sandbox"; - const attributeNode = JSX.getAttributeNode( + const attributeNode = JSX.getAttribute( propNameOnJsx, elementScope, node.openingElement.attributes, ); if (attributeNode != null) { const attributeScope = context.sourceCode.getScope(attributeNode); - const attributeStaticValue = JSX.getAttributeStaticValue(attributeNode, attributeScope); - const attributeStringValue = JSX.toResolvedAttributeValue(propNameOnJsx, attributeStaticValue); - if (hasValidSandBox(attributeStringValue)) return; + const attributeValue = JSX.getAttributeValue(propNameOnJsx, attributeNode, attributeScope); + if (attributeValue.kind === "some" && hasValidSandBox(attributeValue.value)) return; context.report({ messageId: "noMissingIframeSandbox", node: attributeNode, diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-namespace.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-namespace.ts index 8ea3a67c6d..c736256e13 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-namespace.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-namespace.ts @@ -27,14 +27,14 @@ export default createRule<[], MessageID>({ name: RULE_NAME, create(context) { return { - JSXOpeningElement(node) { + JSXElement(node) { const name = JSX.getElementName(node); if (typeof name !== "string" || !name.includes(":")) { return; } context.report({ messageId: "noNamespace", - node, + node: node.openingElement.name, data: { name, }, diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts index 27a6f1bfe8..d27add56d2 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts @@ -1,7 +1,6 @@ import * as JSX from "@eslint-react/jsx"; import type { RuleFeature } from "@eslint-react/shared"; import { RE_JAVASCRIPT_PROTOCOL } from "@eslint-react/shared"; -import * as VAR from "@eslint-react/var"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import type { CamelCase } from "string-ts"; @@ -39,10 +38,9 @@ export default createRule<[], MessageID>({ return; } const attributeScope = context.sourceCode.getScope(node); - const attributeValue = JSX.getAttributeStaticValue(node, attributeScope); - const attributeValueResolved = VAR.toResolved(attributeValue).value; - if (typeof attributeValueResolved !== "string") return; - if (RE_JAVASCRIPT_PROTOCOL.test(attributeValueResolved)) { + const attributeValue = JSX.getAttributeValue(JSX.toString(node.name), node, attributeScope); + if (attributeValue.kind === "none" || typeof attributeValue.value !== "string") return; + if (RE_JAVASCRIPT_PROTOCOL.test(attributeValue.value)) { context.report({ messageId: "noScriptUrl", node: node.value, diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-iframe-sandbox.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-iframe-sandbox.ts index fb4c7098e5..c176d5773c 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-iframe-sandbox.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-iframe-sandbox.ts @@ -1,4 +1,3 @@ -import type { _ } from "@eslint-react/eff"; import * as JSX from "@eslint-react/jsx"; import type { RuleFeature } from "@eslint-react/shared"; import { getSettingsFromContext } from "@eslint-react/shared"; @@ -18,8 +17,8 @@ const unsafeSandboxValues = [ ["allow-scripts", "allow-same-origin"], ] as const; -function hasNoneOrSafeSandbox(value: string | _) { - if (value == null) return true; +function hasSafeSandbox(value: unknown) { + if (typeof value !== "string") return false; return !unsafeSandboxValues.some((values) => { return values.every((v) => value.includes(v)); }); @@ -45,7 +44,7 @@ export default createRule<[], MessageID>({ return { JSXElement(node) { const [elementNameOnJsx, elementNameOnDom] = getElementNameOnJsxAndDom( - node.openingElement, + node, context, polymorphicPropName, additionalComponents, @@ -57,23 +56,24 @@ export default createRule<[], MessageID>({ const customComponent = findCustomComponent(elementNameOnJsx, additionalComponents); const customComponentProp = findCustomComponentProp("sandbox", customComponent?.attributes ?? []); const propNameOnJsx = customComponentProp?.name ?? "sandbox"; - const attributeNode = JSX.getAttributeNode( + const attributeNode = JSX.getAttribute( propNameOnJsx, elementScope, node.openingElement.attributes, ); if (attributeNode != null) { const attributeScope = context.sourceCode.getScope(attributeNode); - const attributeStaticValue = JSX.getAttributeStaticValue(attributeNode, attributeScope); - const attributeStringValue = JSX.toResolvedAttributeValue(propNameOnJsx, attributeStaticValue); - if (hasNoneOrSafeSandbox(attributeStringValue)) return; - context.report({ - messageId: "noUnsafeIframeSandbox", - node: attributeNode, - }); - return; + const attributeValue = JSX.getAttributeValue(propNameOnJsx, attributeNode, attributeScope); + if (attributeValue.kind === "some" && !hasSafeSandbox(attributeValue.value)) { + context.report({ + messageId: "noUnsafeIframeSandbox", + node: attributeNode, + }); + return; + } } - if (!hasNoneOrSafeSandbox(customComponentProp?.defaultValue)) { + if (customComponentProp?.defaultValue == null) return; + if (!hasSafeSandbox(customComponentProp.defaultValue)) { context.report({ messageId: "noUnsafeIframeSandbox", node, diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-target-blank.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-target-blank.ts index 1499391e80..64b70578a7 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-target-blank.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-target-blank.ts @@ -1,4 +1,4 @@ -import type { _ } from "@eslint-react/eff"; +import { _ } from "@eslint-react/eff"; import * as JSX from "@eslint-react/jsx"; import type { RuleFeature } from "@eslint-react/shared"; import { getSettingsFromContext } from "@eslint-react/shared"; @@ -49,7 +49,7 @@ export default createRule<[], MessageID>({ return { JSXElement(node: TSESTree.JSXElement) { const [elementNameOnJsx, elementNameOnDom] = getElementNameOnJsxAndDom( - node.openingElement, + node, context, polymorphicPropName, additionalComponents, @@ -58,27 +58,30 @@ export default createRule<[], MessageID>({ const elementScope = context.sourceCode.getScope(node); const customComponent = findCustomComponent(elementNameOnJsx, additionalComponents); - const getAttributeValue = (name: string) => { + const getAttributeStringValue = (name: string) => { const customComponentProp = findCustomComponentProp(name, customComponent?.attributes ?? []); const propNameOnJsx = customComponentProp?.name ?? name; - const attributeNode = JSX.getAttributeNode( + const attributeNode = JSX.getAttribute( propNameOnJsx, elementScope, node.openingElement.attributes, ); if (attributeNode == null) return customComponentProp?.defaultValue; const attributeScope = context.sourceCode.getScope(attributeNode); - const attributeStaticValue = JSX.getAttributeStaticValue(attributeNode, attributeScope); - return JSX.toResolvedAttributeValue(propNameOnJsx, attributeStaticValue); + const attributeValue = JSX.getAttributeValue(propNameOnJsx, attributeNode, attributeScope); + if (attributeValue.kind === "some" && typeof attributeValue.value === "string") { + return attributeValue.value; + } + return _; }; - if (getAttributeValue("target") !== "_blank") { + if (getAttributeStringValue("target") !== "_blank") { return; } - if (!isExternalLinkLike(getAttributeValue("href"))) { + if (!isExternalLinkLike(getAttributeStringValue("href"))) { return; } - if (isSafeRel(getAttributeValue("rel"))) { + if (isSafeRel(getAttributeStringValue("rel"))) { return; } context.report({ diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-void-elements-with-children.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-void-elements-with-children.ts index 5231d80816..3ccab8127a 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-void-elements-with-children.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-void-elements-with-children.ts @@ -48,7 +48,7 @@ export default createRule<[], MessageID>({ create(context) { return { JSXElement(node) { - const elementName = JSX.getElementName(node.openingElement); + const elementName = JSX.getElementName(node); if (elementName.length === 0 || !voidElements.has(elementName)) { return; } diff --git a/packages/plugins/eslint-plugin-react-dom/src/utils/get-element-name-on-jsx-and-dom.ts b/packages/plugins/eslint-plugin-react-dom/src/utils/get-element-name-on-jsx-and-dom.ts index 60f68cbbf8..60c9c6e7a9 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/utils/get-element-name-on-jsx-and-dom.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/utils/get-element-name-on-jsx-and-dom.ts @@ -1,10 +1,9 @@ import * as JSX from "@eslint-react/jsx"; import type { CustomComponentNormalized, RuleContext } from "@eslint-react/shared"; -import * as VAR from "@eslint-react/var"; import type { TSESTree } from "@typescript-eslint/types"; export function getElementNameOnJsxAndDom( - node: TSESTree.JSXOpeningElement, + node: TSESTree.JSXElement, context: RuleContext, polymorphicPropName?: string, additionalComponents: CustomComponentNormalized[] = [], @@ -19,15 +18,15 @@ export function getElementNameOnJsxAndDom( if (polymorphicPropName == null) return [name, name]; // Get the component name using the `settings["react-x"].polymorphicPropName` setting const initialScope = context.sourceCode.getScope(node); - const attributeNode = JSX.getAttributeNode( + const attributeNode = JSX.getAttribute( polymorphicPropName, initialScope, - node.attributes, + node.openingElement.attributes, ); if (attributeNode == null) return [name, name]; - const polymorphicPropValue = VAR.toResolved(JSX.getAttributeStaticValue(attributeNode, initialScope)).value; - if (typeof polymorphicPropValue === "string") { - return [name, polymorphicPropValue]; + const polymorphicPropValue = JSX.getAttributeValue(polymorphicPropName, attributeNode, initialScope); + if (polymorphicPropValue.kind === "some" && typeof polymorphicPropValue.value === "string") { + return [name, polymorphicPropValue.value]; } return [name, name]; } diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/is-set-function-call.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/is-set-function-call.ts index 77ca7e36ec..b64680ae4b 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/is-set-function-call.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/is-set-function-call.ts @@ -25,7 +25,7 @@ export function isSetFunctionCall(context: RuleContext, settings: ESLintReactSet return false; } const indexScope = context.sourceCode.getScope(node); - const indexValue = VAR.toResolved({ + const indexValue = VAR.toStaticValue({ kind: "lazy", node: index, initialScope: indexScope, @@ -45,7 +45,7 @@ export function isSetFunctionCall(context: RuleContext, settings: ESLintReactSet } const property = node.callee.property; const propertyScope = context.sourceCode.getScope(node); - const propertyValue = VAR.toResolved({ + const propertyValue = VAR.toStaticValue({ kind: "lazy", node: property, initialScope: propertyScope, diff --git a/packages/plugins/eslint-plugin-react-naming-convention/src/rules/component-name.ts b/packages/plugins/eslint-plugin-react-naming-convention/src/rules/component-name.ts index 34ef8ecdcc..d89e3e9fd4 100644 --- a/packages/plugins/eslint-plugin-react-naming-convention/src/rules/component-name.ts +++ b/packages/plugins/eslint-plugin-react-naming-convention/src/rules/component-name.ts @@ -139,7 +139,7 @@ export default createRule({ ...collector.listeners, ...collectorLegacy.listeners, JSXOpeningElement(node) { - const name = JSX.getElementName(node); + const name = JSX.getElementName(node.parent); if (/^[a-z]/u.test(name)) { return; } diff --git a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-event-listener.ts b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-event-listener.ts index cffc425e03..7a23c48a8c 100644 --- a/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-event-listener.ts +++ b/packages/plugins/eslint-plugin-react-web-api/src/rules/no-leaked-event-listener.ts @@ -99,7 +99,7 @@ function getOptions(node: TSESTree.CallExpressionArgument, initialScope: Scope): break; } default: { - v = VAR.toResolved({ kind: "lazy", node: value, initialScope }).value; + v = VAR.toStaticValue({ kind: "lazy", node: value, initialScope }).value; break; } } diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-children-prop.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-children-prop.ts index cdc08eda84..fcba835a78 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-children-prop.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-children-prop.ts @@ -28,7 +28,7 @@ export default createRule<[], MessageID>({ create(context) { return { JSXElement(node) { - const attribute = JSX.getAttributeNode( + const attribute = JSX.getAttribute( "children", context.sourceCode.getScope(node), node.openingElement.attributes, 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 8070c8c26b..07fdaf0b7e 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 @@ -39,7 +39,7 @@ export default createRule<[], MessageID>({ } return { JSXElement(node) { - const elementName = JSX.getElementName(node.openingElement); + const elementName = JSX.getElementName(node); if (!elementName.endsWith(".Provider")) { return; } diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-implicit-key.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-implicit-key.ts index ec85f3d017..cdb343bf92 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-implicit-key.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-implicit-key.ts @@ -31,7 +31,7 @@ export default createRule<[], MessageID>({ return { JSXOpeningElement(node: TSESTree.JSXOpeningElement) { const initialScope = context.sourceCode.getScope(node); - const keyPropFound = JSX.getAttributeNode("key", initialScope, node.attributes); + const keyPropFound = JSX.getAttribute("key", initialScope, node.attributes); const keyPropOnElement = node.attributes .some((n) => n.type === T.JSXAttribute diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-leaked-conditional-rendering.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-leaked-conditional-rendering.ts index f7d84a73f6..a8af4890e3 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-leaked-conditional-rendering.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-leaked-conditional-rendering.ts @@ -246,7 +246,7 @@ export default createRule<[], MessageID>({ const initialScope = context.sourceCode.getScope(left); const isLeftNan = (left.type === T.Identifier && left.name === "NaN") || VAR - .toResolved({ kind: "lazy", node: left, initialScope }) + .toStaticValue({ kind: "lazy", node: left, initialScope }) .value === "NaN"; if (isLeftNan) { return { diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-nested-components.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-nested-components.ts index 5130464c8d..181d3eda1f 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-nested-components.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-nested-components.ts @@ -89,7 +89,7 @@ export default createRule<[], MessageID>({ } const isInsideProperty = component.parent.type === T.Property; const isInsideJSXPropValue = component.parent.type === T.JSXAttribute - || JSX.findParentAttributeNode(node, (n) => n.value?.type === T.JSXExpressionContainer) != null; + || JSX.findParentAttribute(node, (n) => n.value?.type === T.JSXExpressionContainer) != null; if (isInsideJSXPropValue) { if (!isDeclaredInRenderPropLoose(component)) { context.report({ diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts index 18084b7b1b..a5c6c7e68e 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts @@ -110,7 +110,7 @@ export default createRule({ const { allowExpressions = true } = option; return { JSXElement(node) { - if (JSX.getElementName(node.openingElement).split(".").at(-1) !== "Fragment") { + if (JSX.getElementName(node).split(".").at(-1) !== "Fragment") { return; } checkAndReport(node, context, allowExpressions); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/prefer-shorthand-fragment.ts b/packages/plugins/eslint-plugin-react-x/src/rules/prefer-shorthand-fragment.ts index 359bdb58a8..6d6df72db5 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/prefer-shorthand-fragment.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/prefer-shorthand-fragment.ts @@ -31,7 +31,7 @@ export default createRule<[], MessageID>({ create(context) { return { JSXElement(node: TSESTree.JSXElement) { - if (JSX.getElementName(node.openingElement).split(".").at(-1) !== "Fragment") { + if (JSX.getElementName(node).split(".").at(-1) !== "Fragment") { return; } const hasAttributes = node.openingElement.attributes.length > 0; diff --git a/packages/utilities/jsx/src/attribute-name.ts b/packages/utilities/jsx/src/attribute-name.ts new file mode 100644 index 0000000000..e2c977fcce --- /dev/null +++ b/packages/utilities/jsx/src/attribute-name.ts @@ -0,0 +1,12 @@ +import type { TSESTree } from "@typescript-eslint/utils"; + +import { toString } from "./to-string"; + +/** + * Get the stringified name of a JSX attribute + * @param node The JSX attribute node + * @returns The name of the attribute + */ +export function getAttributeName(node: TSESTree.JSXAttribute) { + return toString(node.name); +} diff --git a/packages/utilities/jsx/src/element-name.ts b/packages/utilities/jsx/src/element-name.ts new file mode 100644 index 0000000000..d1ae3300db --- /dev/null +++ b/packages/utilities/jsx/src/element-name.ts @@ -0,0 +1,16 @@ +import type { TSESTree } from "@typescript-eslint/types"; +import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; + +import { toString } from "./to-string"; + +/** + * Get the stringified name of a JSX element + * @param node The JSX element node + * @returns The name of the element + */ +export function getElementName(node: TSESTree.JSXElement | TSESTree.JSXFragment) { + if (node.type === T.JSXFragment) { + return ""; + } + return toString(node.openingElement.name); +} diff --git a/packages/utilities/jsx/src/find-parent-attribute-node.ts b/packages/utilities/jsx/src/find-parent-attribute.ts similarity index 68% rename from packages/utilities/jsx/src/find-parent-attribute-node.ts rename to packages/utilities/jsx/src/find-parent-attribute.ts index 5be844d768..b70b500f89 100644 --- a/packages/utilities/jsx/src/find-parent-attribute-node.ts +++ b/packages/utilities/jsx/src/find-parent-attribute.ts @@ -4,7 +4,13 @@ import { returnTrue } from "@eslint-react/eff"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; -export function findParentAttributeNode( +/** + * Find the parent JSX attribute node of a node + * @param node The node to find the parent attribute of + * @param test The test to apply to the parent attribute + * @returns The parent attribute node or undefined + */ +export function findParentAttribute( node: TSESTree.Node, test: (node: TSESTree.JSXAttribute) => boolean = returnTrue, ): TSESTree.JSXAttribute | _ { diff --git a/packages/utilities/jsx/src/get-attribute-value.ts b/packages/utilities/jsx/src/get-attribute-value.ts index bd0472d127..dfb3b06ddc 100644 --- a/packages/utilities/jsx/src/get-attribute-value.ts +++ b/packages/utilities/jsx/src/get-attribute-value.ts @@ -1,12 +1,21 @@ -import type * as VAR from "@eslint-react/var"; +import * as VAR from "@eslint-react/var"; import type { Scope } from "@typescript-eslint/scope-manager"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import type { TSESTree } from "@typescript-eslint/utils"; +import { match, P } from "ts-pattern"; -export function getAttributeStaticValue( +/** + * Get a StaticValue of the attribute value + * @param name The name of the attribute + * @param node The JSX attribute node + * @param initialScope The initial scope to use + * @returns The StaticValue of the attribute value + */ +export function getAttributeValue( + name: string, node: TSESTree.JSXAttribute | TSESTree.JSXSpreadAttribute, initialScope: Scope, -): VAR.StaticValue { +): Exclude { switch (node.type) { case T.JSXAttribute: if (node.value?.type === T.Literal) { @@ -18,19 +27,31 @@ export function getAttributeStaticValue( } as const; } if (node.value?.type === T.JSXExpressionContainer) { - return { + return VAR.toStaticValue({ kind: "lazy", node: node.value.expression, initialScope, - } as const; + }); } return { kind: "none", node, initialScope } as const; - case T.JSXSpreadAttribute: - return { + case T.JSXSpreadAttribute: { + const staticValue = VAR.toStaticValue({ kind: "lazy", node: node.argument, initialScope, - } as const; + }); + if (staticValue.kind === "none") { + return staticValue; + } + return match(staticValue.value) + .with({ [name]: P.select(P.any) }, (value) => ({ + kind: "some", + node: node.argument, + initialScope, + value, + } as const)) + .otherwise(() => ({ kind: "none", node, initialScope } as const)); + } default: return { kind: "none", node, initialScope } as const; } diff --git a/packages/utilities/jsx/src/get-attribute.ts b/packages/utilities/jsx/src/get-attribute.ts index bd88c41008..cd6a9f8cd0 100644 --- a/packages/utilities/jsx/src/get-attribute.ts +++ b/packages/utilities/jsx/src/get-attribute.ts @@ -4,16 +4,16 @@ import type { Scope } from "@typescript-eslint/scope-manager"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import type { TSESTree } from "@typescript-eslint/utils"; -export function getAttributeName(node: TSESTree.JSXAttribute) { - switch (node.name.type) { - case T.JSXIdentifier: - return node.name.name; - case T.JSXNamespacedName: - return `${node.name.namespace.name}:${node.name.name.name}`; - } -} +import { getAttributeName } from "./attribute-name"; -export function getAttributeNode( +/** + * Get the JSX attribute node with the given name + * @param name The name of the attribute + * @param initialScope The initial scope to use for variable resolution + * @param attributes The attributes to search + * @returns The JSX attribute node or undefined + */ +export function getAttribute( name: string, initialScope: Scope, attributes: (TSESTree.JSXAttribute | TSESTree.JSXSpreadAttribute)[], diff --git a/packages/utilities/jsx/src/get-element-name.ts b/packages/utilities/jsx/src/get-element-name.ts deleted file mode 100644 index 87b335e5c0..0000000000 --- a/packages/utilities/jsx/src/get-element-name.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { TSESTree } from "@typescript-eslint/types"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; - -function resolveJSXMemberExpressions(object: TSESTree.JSXTagNameExpression, property: TSESTree.JSXIdentifier): string { - if (object.type === T.JSXMemberExpression) { - return `${resolveJSXMemberExpressions(object.object, object.property)}.${property.name}`; - } - if (object.type === T.JSXNamespacedName) { - return `${object.namespace.name}:${object.name.name}.${property.name}`; - } - return `${object.name}.${property.name}`; -} - -/** - * Returns the tag name associated with a JSXOpeningElement. - * @param node The visited JSXOpeningElement node object. - * @returns The element's tag name. - */ -export function getElementName( - node: - | TSESTree.JSXOpeningElement - | TSESTree.JSXOpeningFragment, -) { - if (node.type === T.JSXOpeningFragment) { - return "<>"; - } - const { name } = node; - if (name.type === T.JSXMemberExpression) { - const { object, property } = name; - return resolveJSXMemberExpressions(object, property); - } - if (name.type === T.JSXNamespacedName) { - return `${name.namespace.name}:${name.name.name}`; - } - return name.name; -} diff --git a/packages/utilities/jsx/src/has-attribute.ts b/packages/utilities/jsx/src/has-attribute.ts index 6cd6bec75d..9b0c54842d 100644 --- a/packages/utilities/jsx/src/has-attribute.ts +++ b/packages/utilities/jsx/src/has-attribute.ts @@ -1,14 +1,14 @@ import type { Scope } from "@typescript-eslint/scope-manager"; import type { TSESTree } from "@typescript-eslint/types"; -import { getAttributeNode } from "./get-attribute"; +import { getAttribute } from "./get-attribute"; export function hasAttribute( name: string, initialScope: Scope, attributes: TSESTree.JSXOpeningElement["attributes"], ) { - return getAttributeNode(name, initialScope, attributes) != null; + return getAttribute(name, initialScope, attributes) != null; } export function hasAnyAttribute( diff --git a/packages/utilities/jsx/src/index.ts b/packages/utilities/jsx/src/index.ts index d037681937..788687dbb3 100644 --- a/packages/utilities/jsx/src/index.ts +++ b/packages/utilities/jsx/src/index.ts @@ -1,11 +1,12 @@ -export * from "./find-parent-attribute-node"; +export * from "./attribute-name"; +export * from "./element-name"; +export * from "./find-parent-attribute"; export * from "./get-attribute"; export * from "./get-attribute-value"; -export * from "./get-element-name"; export * from "./has-attribute"; export * from "./is-element"; export * from "./is-jsx-value"; export * from "./is-literal"; -export * from "./to-resolved-attribute-value"; +export * from "./to-string"; export * from "./unescape-string-literal-text"; export * from "./xhtml-entities"; diff --git a/packages/utilities/jsx/src/is-jsx-value.ts b/packages/utilities/jsx/src/is-jsx-value.ts index fcd9188699..1213c13168 100644 --- a/packages/utilities/jsx/src/is-jsx-value.ts +++ b/packages/utilities/jsx/src/is-jsx-value.ts @@ -41,7 +41,7 @@ export const DEFAULT_JSX_VALUE_HINT = 0n | JSXValueHint.SkipBooleanLiteral; /** - * Check if a node is a JSX value + * Heruistic decision to determine if a node is a JSX value * @param node The AST node to check * @param jsxCtx The requirements for the check * @param jsxCtx.getScope The function to get the scope of a node diff --git a/packages/utilities/jsx/src/to-resolved-attribute-value.ts b/packages/utilities/jsx/src/to-resolved-attribute-value.ts deleted file mode 100644 index f441cfccd7..0000000000 --- a/packages/utilities/jsx/src/to-resolved-attribute-value.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { _, identity } from "@eslint-react/eff"; -import * as VAR from "@eslint-react/var"; -import { match, P } from "ts-pattern"; - -export function toResolvedAttributeValue( - name: string, - value: VAR.StaticValue, -) { - return match(VAR.toResolved(value).value) - .with(P.string, identity) - .with({ [name]: P.select(P.string) }, identity) - .otherwise(() => _); -} diff --git a/packages/utilities/jsx/src/to-string.ts b/packages/utilities/jsx/src/to-string.ts new file mode 100644 index 0000000000..3cd1bf53a3 --- /dev/null +++ b/packages/utilities/jsx/src/to-string.ts @@ -0,0 +1,38 @@ +import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; +import type { TSESTree } from "@typescript-eslint/utils"; + +/** + * Get the stringified representation of a JSX node + * @param node The JSX node + * @returns The stringified representation + */ +export function toString( + node: + | TSESTree.JSXIdentifier + | TSESTree.JSXMemberExpression + | TSESTree.JSXNamespacedName + | TSESTree.JSXOpeningElement + | TSESTree.JSXClosingElement + | TSESTree.JSXOpeningFragment + | TSESTree.JSXClosingFragment + | TSESTree.JSXText, +): string { + switch (node.type) { + case T.JSXIdentifier: + return node.name; + case T.JSXNamespacedName: + return `${node.namespace.name}:${node.name.name}`; + case T.JSXMemberExpression: + return `${toString(node.object)}.${toString(node.property)}`; + case T.JSXText: + return node.value; + case T.JSXOpeningElement: + return `<${toString(node.name)}>`; + case T.JSXClosingElement: + return ``; + case T.JSXOpeningFragment: + return "<>"; + case T.JSXClosingFragment: + return ""; + } +} diff --git a/packages/utilities/var/src/index.ts b/packages/utilities/var/src/index.ts index 5161552375..b0acf71cdd 100644 --- a/packages/utilities/var/src/index.ts +++ b/packages/utilities/var/src/index.ts @@ -6,5 +6,5 @@ export * from "./get-variable-node"; export * from "./get-variables"; export * from "./is-initialized-from-source"; export * from "./is-node-value-equal"; -export * from "./static-value"; +export * from "./lazy-value"; export * from "./value-construction"; diff --git a/packages/utilities/var/src/is-node-value-equal.ts b/packages/utilities/var/src/is-node-value-equal.ts index 7f2a0d0159..66f5d76ec6 100644 --- a/packages/utilities/var/src/is-node-value-equal.ts +++ b/packages/utilities/var/src/is-node-value-equal.ts @@ -5,7 +5,7 @@ import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import { findVariable } from "./find-variable"; import { getVariableNode } from "./get-variable-node"; -import { toResolved } from "./static-value"; +import { toStaticValue } from "./lazy-value"; const thisBlockTypes = [ T.FunctionDeclaration, @@ -99,8 +99,8 @@ export function isNodeValueEqual( return aFunction === bFunction; } default: { - const aStatic = toResolved({ kind: "lazy", node: a, initialScope: aScope }); - const bStatic = toResolved({ kind: "lazy", node: b, initialScope: bScope }); + const aStatic = toStaticValue({ kind: "lazy", node: a, initialScope: aScope }); + const bStatic = toStaticValue({ kind: "lazy", node: b, initialScope: bScope }); return aStatic.kind !== "none" && bStatic.kind !== "none" && aStatic.value === bStatic.value; } } diff --git a/packages/utilities/var/src/static-value.ts b/packages/utilities/var/src/lazy-value.ts similarity index 72% rename from packages/utilities/var/src/static-value.ts rename to packages/utilities/var/src/lazy-value.ts index 0814c97f23..75d26bc1d4 100644 --- a/packages/utilities/var/src/static-value.ts +++ b/packages/utilities/var/src/lazy-value.ts @@ -5,7 +5,7 @@ import type { Scope } from "@typescript-eslint/scope-manager"; import type { TSESTree } from "@typescript-eslint/types"; import { getStaticValue } from "@typescript-eslint/utils/ast-utils"; -export type StaticValue = +export type LazyValue = | { // Not resolved yet kind: "lazy"; @@ -26,15 +26,15 @@ export type StaticValue = initialScope: Scope | _; }; -export function toResolved(sv: StaticValue) { - const { kind, node, initialScope } = sv; +export function toStaticValue(lazyValue: LazyValue) { + const { kind, node, initialScope } = lazyValue; if (kind !== "lazy") { - return sv; + return lazyValue; } - const resolvedValue = initialScope == null + const staticValue = initialScope == null ? getStaticValue(node) : getStaticValue(node, initialScope); - return resolvedValue == null + return staticValue == null ? { kind: "none", node, initialScope } as const - : { kind: "some", node, initialScope, value: resolvedValue.value } as const; + : { kind: "some", node, initialScope, value: staticValue.value } as const; }