Skip to content

Commit 9e66622

Browse files
committed
refactor: rework jsx attribute resolution
1 parent 35ab0cf commit 9e66622

19 files changed

+69
-315
lines changed

packages/plugins/eslint-plugin-react-dom/src/rules/no-unsafe-iframe-sandbox.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import type { unit } from "@eslint-react/eff";
1+
import * as ER from "@eslint-react/core";
22
import type { RuleContext, RuleFeature } from "@eslint-react/kit";
3-
43
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
54
import type { CamelCase } from "string-ts";
6-
import { createJsxElementResolver, createRule, resolveAttribute } from "../utils";
5+
6+
import { createJsxElementResolver, createRule } from "../utils";
77

88
export const RULE_NAME = "no-unsafe-iframe-sandbox";
99

@@ -15,7 +15,7 @@ const unsafeSandboxValues = [
1515
["allow-scripts", "allow-same-origin"],
1616
] as const;
1717

18-
function isSafeSandbox(value: string | unit): value is string {
18+
function isSafeSandbox(value: unknown): value is string {
1919
if (typeof value !== "string") return false;
2020
return !unsafeSandboxValues.some((values) => {
2121
return values.every((v) => value.includes(v));
@@ -43,13 +43,17 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
4343
const resolver = createJsxElementResolver(context);
4444
return {
4545
JSXElement(node) {
46-
const { attributes, domElementType } = resolver.resolve(node);
46+
const { domElementType } = resolver.resolve(node);
4747
if (domElementType !== "iframe") return;
48-
const sandboxAttribute = resolveAttribute(context, attributes, node, "sandbox");
49-
if (!isSafeSandbox(sandboxAttribute.attributeValueString)) {
48+
const getAttribute = ER.getAttribute(context, node.openingElement.attributes, context.sourceCode.getScope(node));
49+
const sandboxAttribute = getAttribute("sandbox");
50+
if (sandboxAttribute == null) return;
51+
const sandboxValue = ER.resolveAttributeValue(context, sandboxAttribute);
52+
const sandboxValueStatic = sandboxValue.toStatic("sandbox");
53+
if (!isSafeSandbox(sandboxValueStatic)) {
5054
context.report({
5155
messageId: "noUnsafeIframeSandbox",
52-
node: sandboxAttribute.attributeValue?.node ?? sandboxAttribute.attribute ?? node,
56+
node: sandboxValue.node ?? sandboxAttribute,
5357
});
5458
}
5559
},

packages/plugins/eslint-plugin-react-dom/src/utils/create-jsx-element-resolver.ts

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,66 @@ import type { TSESTree } from "@typescript-eslint/types";
44
import * as ER from "@eslint-react/core";
55
import { getSettingsFromContext } from "@eslint-react/shared";
66

7+
/**
8+
* Creates a resolver for JSX elements that determines both the JSX element type
9+
* and the underlying DOM element type.
10+
*
11+
* This resolver handles:
12+
* 1. Regular HTML elements (div, span, etc.)
13+
* 2. Polymorphic components (components that can render as different elements via a prop)
14+
*
15+
* @param context - The ESLint rule context
16+
* @returns An object with a resolve method to determine element types
17+
*/
718
export function createJsxElementResolver(context: RuleContext) {
8-
const { components, polymorphicPropName: polyPropName } = getSettingsFromContext(context);
19+
const { polymorphicPropName } = getSettingsFromContext(context);
20+
921
return {
22+
/**
23+
* Resolves the JSX element to determine its type and the underlying DOM element type
24+
*
25+
* @param node - The JSX element node to resolve
26+
* @returns An object containing the JSX element type and DOM element type
27+
*/
1028
resolve(node: TSESTree.JSXElement) {
11-
const name = ER.getElementType(context, node);
12-
const component = components
13-
.findLast((c) => c.name === name || c.re.test(name));
29+
// Get the element name/type (e.g., 'div', 'Button', etc.)
30+
const elementName = ER.getElementType(context, node);
31+
32+
// // Find if there's a matching component defined in settings
33+
// const matchingComponent = components
34+
// .findLast((component) => component.name === elementName || component.re.test(elementName));
35+
36+
// Create the base result with element types
1437
const result = {
15-
domElementType: component?.as ?? name,
16-
jsxElementType: name,
38+
domElementType: elementName,
39+
jsxElementType: elementName,
1740
};
18-
if (name === name.toLowerCase() || component != null || polyPropName == null) {
41+
42+
// Early return if any of these conditions are met:
43+
// 1. It's a native HTML element (lowercase name)
44+
// 2. No polymorphic prop name is configured
45+
if (elementName === elementName.toLowerCase() || polymorphicPropName == null) {
1946
return result;
2047
}
21-
const initialScope = context.sourceCode.getScope(node);
22-
const polyPropAttr = ER.getAttribute(context, node.openingElement.attributes, initialScope)(polyPropName);
23-
if (polyPropAttr != null) {
24-
const polyPropValue = ER.resolveAttributeValue(context, polyPropAttr);
25-
const staticValue = polyPropValue.toStatic(polyPropName);
48+
49+
// Look for the polymorphic prop (e.g., 'as', 'component') in the element's attributes
50+
const getAttribute = ER.getAttribute(context, node.openingElement.attributes, context.sourceCode.getScope(node));
51+
const polymorphicProp = getAttribute(polymorphicPropName);
52+
53+
// If the polymorphic prop exists, try to determine its static value
54+
if (polymorphicProp != null) {
55+
const polymorphicPropValue = ER.resolveAttributeValue(context, polymorphicProp);
56+
const staticValue = polymorphicPropValue.toStatic(polymorphicPropName);
57+
58+
// If we have a string value, use it as the DOM element type
2659
if (typeof staticValue === "string") {
2760
return {
2861
...result,
2962
domElementType: staticValue,
3063
};
3164
}
3265
}
66+
3367
return result;
3468
},
3569
} as const;

packages/plugins/eslint-plugin-react-x/src/rules/no-children-prop.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ export default createRule<[], MessageID>({
3131
export function create(context: RuleContext<MessageID, []>): RuleListener {
3232
return {
3333
JSXElement(node) {
34-
const attributes = node.openingElement.attributes;
35-
const childrenProp = ER.getAttribute(context, "children", attributes, context.sourceCode.getScope(node));
34+
const getAttribute = ER.getAttribute(
35+
context,
36+
node.openingElement.attributes,
37+
context.sourceCode.getScope(node),
38+
);
39+
const childrenProp = getAttribute("children");
3640
if (childrenProp != null) {
3741
context.report({
3842
messageId: "noChildrenProp",

packages/plugins/eslint-plugin/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,8 @@ export default defineConfig([
167167

168168
Contributions are welcome!
169169

170-
Please follow our [contributing guidelines](https://github.com/Rel1cx/eslint-react/tree/2.0.0/.github/CONTRIBUTING.md).
170+
Please follow our [contributing guidelines](https://github.com/Rel1cx/eslint-react/tree/refactor/jsx-attribute-resolution/.github/CONTRIBUTING.md).
171171

172172
## License
173173

174-
This project is licensed under the MIT License - see the [LICENSE](https://github.com/Rel1cx/eslint-react/tree/2.0.0/LICENSE) file for details.
174+
This project is licensed under the MIT License - see the [LICENSE](https://github.com/Rel1cx/eslint-react/tree/refactor/jsx-attribute-resolution/LICENSE) file for details.

packages/shared/docs/README.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,16 @@
66

77
## Interfaces
88

9-
- [CustomComponentNormalized](interfaces/CustomComponentNormalized.md)
10-
- [CustomComponentPropNormalized](interfaces/CustomComponentPropNormalized.md)
119
- [ESLintReactSettingsNormalized](interfaces/ESLintReactSettingsNormalized.md)
1210

1311
## Type Aliases
1412

15-
- [CustomComponent](type-aliases/CustomComponent.md)
16-
- [CustomComponentProp](type-aliases/CustomComponentProp.md)
1713
- [CustomHooks](type-aliases/CustomHooks.md)
1814
- [ESLintReactSettings](type-aliases/ESLintReactSettings.md)
1915
- [ESLintSettings](type-aliases/ESLintSettings.md)
2016

2117
## Variables
2218

23-
- [CustomComponentPropSchema](variables/CustomComponentPropSchema.md)
24-
- [CustomComponentSchema](variables/CustomComponentSchema.md)
2519
- [CustomHooksSchema](variables/CustomHooksSchema.md)
2620
- [DEFAULT\_ESLINT\_REACT\_SETTINGS](variables/DEFAULT_ESLINT_REACT_SETTINGS.md)
2721
- [DEFAULT\_ESLINT\_SETTINGS](variables/DEFAULT_ESLINT_SETTINGS.md)

packages/shared/docs/functions/coerceSettings.md

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,6 @@ The settings object to coerce
2020

2121
## Returns
2222

23-
### additionalComponents?
24-
25-
> `optional` **additionalComponents**: `object`[]
26-
27-
User-defined components configuration
28-
Informs ESLint React how to treat these components during validation
29-
30-
#### Example
31-
32-
```ts
33-
[{ name: "Link", as: "a", attributes: [{ name: "to", as: "href" }] }]
34-
```
35-
3623
### additionalHooks?
3724

3825
> `optional` **additionalHooks**: `object`

packages/shared/docs/functions/decodeSettings.md

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,6 @@ The settings object to decode
2020

2121
## Returns
2222

23-
### additionalComponents?
24-
25-
> `optional` **additionalComponents**: `object`[]
26-
27-
User-defined components configuration
28-
Informs ESLint React how to treat these components during validation
29-
30-
#### Example
31-
32-
```ts
33-
[{ name: "Link", as: "a", attributes: [{ name: "to", as: "href" }] }]
34-
```
35-
3623
### additionalHooks?
3724

3825
> `optional` **additionalHooks**: `object`

packages/shared/docs/functions/isESLintReactSettings.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
# Function: isESLintReactSettings()
88

9-
> **isESLintReactSettings**(`settings`): `settings is { additionalComponents?: { as?: string; attributes?: { as?: string; defaultValue?: string; name: string }[]; name: string }[]; additionalHooks?: { use?: string[]; useActionState?: string[]; useCallback?: string[]; useContext?: string[]; useDebugValue?: string[]; useDeferredValue?: string[]; useEffect?: string[]; useFormStatus?: string[]; useId?: string[]; useImperativeHandle?: string[]; useInsertionEffect?: string[]; useLayoutEffect?: string[]; useMemo?: string[]; useOptimistic?: string[]; useReducer?: string[]; useRef?: string[]; useState?: string[]; useSyncExternalStore?: string[]; useTransition?: string[] }; importSource?: string; polymorphicPropName?: string; version?: string }`
9+
> **isESLintReactSettings**(`settings`): `settings is { additionalHooks?: { use?: string[]; useActionState?: string[]; useCallback?: string[]; useContext?: string[]; useDebugValue?: string[]; useDeferredValue?: string[]; useEffect?: string[]; useFormStatus?: string[]; useId?: string[]; useImperativeHandle?: string[]; useInsertionEffect?: string[]; useLayoutEffect?: string[]; useMemo?: string[]; useOptimistic?: string[]; useReducer?: string[]; useRef?: string[]; useState?: string[]; useSyncExternalStore?: string[]; useTransition?: string[] }; importSource?: string; polymorphicPropName?: string; version?: string }`
1010
1111
Checks if the provided settings conform to ESLintReactSettings schema
1212

@@ -20,4 +20,4 @@ The settings object to validate
2020

2121
## Returns
2222

23-
`settings is { additionalComponents?: { as?: string; attributes?: { as?: string; defaultValue?: string; name: string }[]; name: string }[]; additionalHooks?: { use?: string[]; useActionState?: string[]; useCallback?: string[]; useContext?: string[]; useDebugValue?: string[]; useDeferredValue?: string[]; useEffect?: string[]; useFormStatus?: string[]; useId?: string[]; useImperativeHandle?: string[]; useInsertionEffect?: string[]; useLayoutEffect?: string[]; useMemo?: string[]; useOptimistic?: string[]; useReducer?: string[]; useRef?: string[]; useState?: string[]; useSyncExternalStore?: string[]; useTransition?: string[] }; importSource?: string; polymorphicPropName?: string; version?: string }`
23+
`settings is { additionalHooks?: { use?: string[]; useActionState?: string[]; useCallback?: string[]; useContext?: string[]; useDebugValue?: string[]; useDeferredValue?: string[]; useEffect?: string[]; useFormStatus?: string[]; useId?: string[]; useImperativeHandle?: string[]; useInsertionEffect?: string[]; useLayoutEffect?: string[]; useMemo?: string[]; useOptimistic?: string[]; useReducer?: string[]; useRef?: string[]; useState?: string[]; useSyncExternalStore?: string[]; useTransition?: string[] }; importSource?: string; polymorphicPropName?: string; version?: string }`

packages/shared/docs/functions/normalizeSettings.md

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,6 @@ Transforms component definitions and resolves version information
1515

1616
### \_\_namedParameters
1717

18-
#### additionalComponents?
19-
20-
`object`[] = `[]`
21-
22-
User-defined components configuration
23-
Informs ESLint React how to treat these components during validation
24-
25-
**Example**
26-
27-
```ts
28-
[{ name: "Link", as: "a", attributes: [{ name: "to", as: "href" }] }]
29-
```
30-
3118
#### additionalHooks?
3219

3320
\{ `use?`: `string`[]; `useActionState?`: `string`[]; `useCallback?`: `string`[]; `useContext?`: `string`[]; `useDebugValue?`: `string`[]; `useDeferredValue?`: `string`[]; `useEffect?`: `string`[]; `useFormStatus?`: `string`[]; `useId?`: `string`[]; `useImperativeHandle?`: `string`[]; `useInsertionEffect?`: `string`[]; `useLayoutEffect?`: `string`[]; `useMemo?`: `string`[]; `useOptimistic?`: `string`[]; `useReducer?`: `string`[]; `useRef?`: `string`[]; `useState?`: `string`[]; `useSyncExternalStore?`: `string`[]; `useTransition?`: `string`[]; \} = `{}`
@@ -251,10 +238,6 @@ React version to use
251238

252239
> `optional` **useTransition**: `string`[]
253240
254-
### components
255-
256-
> `readonly` **components**: `object`[]
257-
258241
### importSource
259242

260243
> **importSource**: `string`

packages/shared/docs/interfaces/CustomComponentNormalized.md

Lines changed: 0 additions & 47 deletions
This file was deleted.

0 commit comments

Comments
 (0)