Skip to content

Commit 35ab0cf

Browse files
committed
wip
1 parent 48b7c6d commit 35ab0cf

23 files changed

+654
-751
lines changed

packages/core/docs/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
## Type Aliases
2424

25+
- [AttributeValue](type-aliases/AttributeValue.md)
2526
- [Component](type-aliases/Component.md)
2627
- [ComponentDetectionHint](type-aliases/ComponentDetectionHint.md)
2728
- [ComponentEffectPhaseKind](type-aliases/ComponentEffectPhaseKind.md)
@@ -113,7 +114,6 @@
113114
- [findParentAttribute](functions/findParentAttribute.md)
114115
- [getAttribute](functions/getAttribute.md)
115116
- [getAttributeName](functions/getAttributeName.md)
116-
- [getAttributeValue](functions/getAttributeValue.md)
117117
- [getComponentFlagFromInitPath](functions/getComponentFlagFromInitPath.md)
118118
- [getComponentNameFromId](functions/getComponentNameFromId.md)
119119
- [getElementType](functions/getElementType.md)
@@ -151,6 +151,7 @@
151151
- [isRenderPropLoose](functions/isRenderPropLoose.md)
152152
- [isThisSetState](functions/isThisSetState.md)
153153
- [isUseEffectCallLoose](functions/isUseEffectCallLoose.md)
154+
- [resolveAttributeValue](functions/resolveAttributeValue.md)
154155
- [stringifyJsx](functions/stringifyJsx.md)
155156
- [useComponentCollector](functions/useComponentCollector.md)
156157
- [useComponentCollectorLegacy](functions/useComponentCollectorLegacy.md)

packages/core/docs/functions/getAttribute.md

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,32 @@
66

77
# Function: getAttribute()
88

9-
> **getAttribute**(`context`, `name`, `attributes`, `initialScope?`): `undefined` \| `JSXAttribute` \| `JSXSpreadAttribute`
10-
11-
Searches for a specific JSX attribute by name in a list of attributes
12-
Returns the last matching attribute (rightmost in JSX)
9+
> **getAttribute**(`context`, `attributes`, `initialScope?`): (`name`) => `undefined` \| `TSESTreeJSXAttributeLike`
1310
1411
## Parameters
1512

1613
### context
1714

1815
`RuleContext`
1916

20-
ESLint rule context
21-
22-
### name
23-
24-
`string`
25-
26-
The name of the attribute to find
27-
2817
### attributes
2918

30-
(`JSXAttribute` \| `JSXSpreadAttribute`)[]
31-
32-
Array of JSX attributes to search through
19+
`TSESTreeJSXAttributeLike`[]
3320

3421
### initialScope?
3522

3623
`Scope`
3724

38-
Optional scope for resolving variables
39-
4025
## Returns
4126

42-
`undefined` \| `JSXAttribute` \| `JSXSpreadAttribute`
27+
> (`name`): `undefined` \| `TSESTreeJSXAttributeLike`
28+
29+
### Parameters
30+
31+
#### name
32+
33+
`string`
34+
35+
### Returns
4336

44-
The found attribute or undefined
37+
`undefined` \| `TSESTreeJSXAttributeLike`

packages/core/docs/functions/getAttributeValue.md

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[**@eslint-react/core**](../README.md)
2+
3+
***
4+
5+
[@eslint-react/core](../README.md) / resolveAttributeValue
6+
7+
# Function: resolveAttributeValue()
8+
9+
> **resolveAttributeValue**(`context`, `attribute`): \{ `kind`: `"boolean"`; `node?`: `undefined`; `toStatic`: `true`; \} \| \{ `kind`: `"literal"`; `node`: `BigIntLiteral` \| `BooleanLiteral` \| `NullLiteral` \| `NumberLiteral` \| `RegExpLiteral` \| `StringLiteral`; `toStatic`: `null` \| `string` \| `number` \| `bigint` \| `boolean` \| [`RegExp`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp); \} \| \{ `kind`: `"expression"`; `node`: `JSXEmptyExpression` \| `Expression`; `toStatic`: `unknown`; \} \| \{ `kind`: `"element"`; `node`: `JSXElement`; `toStatic`: `undefined`; \} \| \{ `kind`: `"spreadChild"`; `node`: `JSXEmptyExpression` \| `Expression`; `toStatic`: `undefined`; \} \| \{ `kind`: `"spreadProps"`; `node`: `Expression`; `toStatic`: `unknown`; \}
10+
11+
## Parameters
12+
13+
### context
14+
15+
`RuleContext`
16+
17+
### attribute
18+
19+
`TSESTreeJSXAttributeLike`
20+
21+
## Returns
22+
23+
\{ `kind`: `"boolean"`; `node?`: `undefined`; `toStatic`: `true`; \} \| \{ `kind`: `"literal"`; `node`: `BigIntLiteral` \| `BooleanLiteral` \| `NullLiteral` \| `NumberLiteral` \| `RegExpLiteral` \| `StringLiteral`; `toStatic`: `null` \| `string` \| `number` \| `bigint` \| `boolean` \| [`RegExp`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp); \} \| \{ `kind`: `"expression"`; `node`: `JSXEmptyExpression` \| `Expression`; `toStatic`: `unknown`; \} \| \{ `kind`: `"element"`; `node`: `JSXElement`; `toStatic`: `undefined`; \} \| \{ `kind`: `"spreadChild"`; `node`: `JSXEmptyExpression` \| `Expression`; `toStatic`: `undefined`; \} \| \{ `kind`: `"spreadProps"`; `node`: `Expression`; `toStatic`: `unknown`; \}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[**@eslint-react/core**](../README.md)
2+
3+
***
4+
5+
[@eslint-react/core](../README.md) / AttributeValue
6+
7+
# Type Alias: AttributeValue
8+
9+
> **AttributeValue** = \{ `kind`: `"boolean"`; `toStatic`: `true`; \} \| \{ `kind`: `"element"`; `node`: `TSESTree.JSXElement`; `toStatic`: `unknown`; \} \| \{ `kind`: `"literal"`; `node`: `TSESTree.Literal`; `toStatic`: `null` \| `string` \| `number` \| `bigint` \| `boolean` \| [`RegExp`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp); \} \| \{ `kind`: `"expression"`; `node`: `TSESTree.JSXExpressionContainer`\[`"expression"`\]; `toStatic`: `unknown`; \} \| \{ `kind`: `"spreadProps"`; `node`: `TSESTree.JSXSpreadAttribute`\[`"argument"`\]; `toStatic`: `unknown`; \} \| \{ `kind`: `"spreadChild"`; `node`: `TSESTree.JSXSpreadChild`\[`"expression"`\]; `toStatic`: `unknown`; \}
10+
11+
Represents possible JSX attribute value types that can be resolved
Lines changed: 85 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,98 @@
1+
import type * as AST from "@eslint-react/ast";
2+
import { unit } from "@eslint-react/eff";
3+
import { identity } from "@eslint-react/eff";
14
import type { RuleContext } from "@eslint-react/kit";
2-
import * as VAR from "@eslint-react/var";
5+
import type { TSESTree } from "@typescript-eslint/types";
36
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
4-
import type { TSESTree } from "@typescript-eslint/utils";
7+
import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
58
import { match, P } from "ts-pattern";
69

710
/**
8-
* Extracts the value of a JSX attribute by name
9-
* @param context - ESLint rule context
10-
* @param node - JSX attribute or spread attribute node
11-
* @param name - Name of the attribute to extract
12-
* @returns The extracted attribute value in a structured format
11+
* Represents possible JSX attribute value types that can be resolved
1312
*/
14-
export function getAttributeValue(
15-
context: RuleContext,
16-
node: TSESTree.JSXAttribute | TSESTree.JSXSpreadAttribute,
17-
name: string,
18-
): Exclude<VAR.LazyValue, { kind: "lazy" }> {
19-
// Get the initial scope from the node's context
20-
const initialScope = context.sourceCode.getScope(node);
21-
switch (node.type) {
22-
case T.JSXAttribute:
23-
// Case 1: Literal value (e.g., className="container")
24-
if (node.value?.type === T.Literal) {
13+
export type AttributeValue =
14+
| { kind: "boolean"; toStatic(): true } // Boolean attributes (e.g., disabled)
15+
// | { kind: "default"; toStatic(): unit | string } // Default attribute values
16+
| { kind: "element"; node: TSESTree.JSXElement; toStatic(): unknown } // JSX element as value (e.g., <Component element=<JSXElement /> />)
17+
| { kind: "literal"; node: TSESTree.Literal; toStatic(): TSESTree.Literal["value"] } // Literal values
18+
| { kind: "expression"; node: TSESTree.JSXExpressionContainer["expression"]; toStatic(): unknown } // Expression attributes (e.g., {value}, {...props})
19+
| { kind: "spreadProps"; node: TSESTree.JSXSpreadAttribute["argument"]; toStatic(name?: string): unknown } // Spread props (e.g., {...props})
20+
| { kind: "spreadChild"; node: TSESTree.JSXSpreadChild["expression"]; toStatic(): unknown }; // Spread children (e.g., {...["Hello", " ", "spread", " ", "children"]})
21+
22+
export function resolveAttributeValue(context: RuleContext, attribute: AST.TSESTreeJSXAttributeLike) {
23+
const initialScope = context.sourceCode.getScope(attribute);
24+
function handleJsxAttribute(node: TSESTree.JSXAttribute) {
25+
// Case 1: Boolean attribute with no value (e.g., disabled)
26+
if (node.value == null) {
27+
return {
28+
kind: "boolean",
29+
toStatic() {
30+
return true;
31+
},
32+
} as const satisfies AttributeValue;
33+
}
34+
switch (node.value.type) {
35+
// Case 2: Literal value (e.g., className="container")
36+
case T.Literal: {
37+
const staticValue = node.value.value;
2538
return {
26-
kind: "some",
39+
kind: "literal",
2740
node: node.value,
28-
initialScope,
29-
value: node.value.value,
30-
} as const;
41+
toStatic() {
42+
return staticValue;
43+
},
44+
} as const satisfies AttributeValue;
3145
}
32-
// Case 2: Expression container (e.g., className={variable})
33-
if (node.value?.type === T.JSXExpressionContainer) {
34-
return VAR.toStaticValue({
35-
kind: "lazy",
36-
node: node.value.expression,
37-
initialScope,
38-
});
39-
}
40-
// Case 3: Boolean attribute with no value (e.g., disabled)
41-
return { kind: "none", node, initialScope } as const;
42-
case T.JSXSpreadAttribute: {
43-
// For spread attributes (e.g., {...props}), try to extract static value
44-
const staticValue = VAR.toStaticValue({
45-
kind: "lazy",
46-
node: node.argument,
47-
initialScope,
48-
});
49-
// If can't extract static value, return none
50-
if (staticValue.kind === "none") {
51-
return staticValue;
46+
// Case 3: Expression container (e.g., className={variable})
47+
case T.JSXExpressionContainer: {
48+
const expr = node.value.expression;
49+
return {
50+
kind: "expression",
51+
node: expr,
52+
toStatic() {
53+
return getStaticValue(expr, initialScope)?.value;
54+
},
55+
} as const satisfies AttributeValue;
5256
}
53-
// If spread object contains the named property, extract its value
54-
return match(staticValue.value)
55-
.with({ [name]: P.select(P.any) }, (value) => ({
56-
kind: "some",
57-
node: node.argument,
58-
initialScope,
59-
value,
60-
} as const))
61-
.otherwise(() => ({ kind: "none", node, initialScope } as const));
57+
// Case 4: JSX Element as value (e.g., element=<JSXElement />)
58+
case T.JSXElement:
59+
return {
60+
kind: "element",
61+
node: node.value,
62+
toStatic() {
63+
return unit;
64+
},
65+
} as const satisfies AttributeValue;
66+
// Case 5: JSX spread children (e.g., <div>{...["Hello", " ", "spread", " ", "children"]}</div>)
67+
case T.JSXSpreadChild:
68+
return {
69+
kind: "spreadChild",
70+
node: node.value.expression,
71+
toStatic() {
72+
return unit;
73+
},
74+
} as const satisfies AttributeValue;
6275
}
63-
default:
64-
// Fallback case for unknown node types
65-
return { kind: "none", node, initialScope } as const;
76+
}
77+
78+
function handleJsxSpreadAttribute(node: TSESTree.JSXSpreadAttribute) {
79+
// For spread attributes (e.g., {...props}), try to extract static value
80+
return {
81+
kind: "spreadProps",
82+
node: node.argument,
83+
toStatic(name?: string) {
84+
if (name == null) return unit;
85+
// If spread object contains the named property, extract its value
86+
return match(getStaticValue(node.argument, initialScope)?.value)
87+
.with({ [name]: P.select(P.any) }, identity)
88+
.otherwise(() => unit);
89+
},
90+
} as const satisfies AttributeValue;
91+
}
92+
switch (attribute.type) {
93+
case T.JSXAttribute:
94+
return handleJsxAttribute(attribute);
95+
case T.JSXSpreadAttribute:
96+
return handleJsxSpreadAttribute(attribute);
6697
}
6798
}
Lines changed: 25 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,35 @@
1-
import type { unit } from "@eslint-react/eff";
1+
import type * as AST from "@eslint-react/ast";
22
import type { RuleContext } from "@eslint-react/kit";
33
import * as VAR from "@eslint-react/var";
44
import type { Scope } from "@typescript-eslint/scope-manager";
5-
import type { TSESTree } from "@typescript-eslint/utils";
65

76
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
87
import { getAttributeName } from "./jsx-attribute-name";
98

10-
/**
11-
* Searches for a specific JSX attribute by name in a list of attributes
12-
* Returns the last matching attribute (rightmost in JSX)
13-
*
14-
* @param context - ESLint rule context
15-
* @param name - The name of the attribute to find
16-
* @param attributes - Array of JSX attributes to search through
17-
* @param initialScope - Optional scope for resolving variables
18-
* @returns The found attribute or undefined
19-
*/
20-
export function getAttribute(
21-
context: RuleContext,
22-
name: string,
23-
attributes: (TSESTree.JSXAttribute | TSESTree.JSXSpreadAttribute)[],
24-
initialScope?: Scope,
25-
): TSESTree.JSXAttribute | TSESTree.JSXSpreadAttribute | unit {
26-
return attributes.findLast((attr) => {
27-
// Case 1: Direct JSX attribute (e.g., className="value")
28-
if (attr.type === T.JSXAttribute) {
29-
return getAttributeName(context, attr) === name;
30-
}
31-
32-
// For spread attributes, we need a scope to resolve variables
33-
if (initialScope == null) return false;
34-
35-
switch (attr.argument.type) {
36-
// Case 2: Spread from variable (e.g., {...props})
37-
case T.Identifier: {
38-
const variable = VAR.findVariable(attr.argument.name, initialScope);
39-
const variableNode = VAR.getVariableDefinitionNode(variable, 0);
40-
if (variableNode?.type === T.ObjectExpression) {
41-
return VAR.findProperty(name, variableNode.properties, initialScope) != null;
9+
export function getAttribute(context: RuleContext, attributes: AST.TSESTreeJSXAttributeLike[], initialScope?: Scope) {
10+
return (name: string) => {
11+
return attributes.findLast((attr) => {
12+
// Case 1: Direct JSX attribute (e.g., className="value")
13+
if (attr.type === T.JSXAttribute) {
14+
return getAttributeName(context, attr) === name;
15+
}
16+
// For spread attributes, we need a scope to resolve variables
17+
if (initialScope == null) return false;
18+
switch (attr.argument.type) {
19+
// Case 2: Spread from variable (e.g., {...props})
20+
case T.Identifier: {
21+
const variable = VAR.findVariable(attr.argument.name, initialScope);
22+
const variableNode = VAR.getVariableDefinitionNode(variable, 0);
23+
if (variableNode?.type === T.ObjectExpression) {
24+
return VAR.findProperty(name, variableNode.properties, initialScope) != null;
25+
}
26+
return false;
4227
}
43-
return false;
28+
// Case 3: Spread from object literal (e.g., {{...{prop: value}}})
29+
case T.ObjectExpression:
30+
return VAR.findProperty(name, attr.argument.properties, initialScope) != null;
4431
}
45-
// Case 3: Spread from object literal (e.g., {{...{prop: value}}})
46-
case T.ObjectExpression:
47-
return VAR.findProperty(name, attr.argument.properties, initialScope) != null;
48-
}
49-
return false;
50-
});
32+
return false;
33+
});
34+
};
5135
}

packages/core/src/jsx/jsx-has.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function hasAttribute(
1818
attributes: TSESTree.JSXOpeningElement["attributes"],
1919
initialScope?: Scope,
2020
) {
21-
return getAttribute(context, name, attributes, initialScope) != null;
21+
return getAttribute(context, attributes, initialScope)(name) != null;
2222
}
2323

2424
/**

0 commit comments

Comments
 (0)