Skip to content

Commit 0320f5e

Browse files
authored
Add extraction of function directives (#1440)
1 parent ae07539 commit 0320f5e

16 files changed

Lines changed: 170 additions & 61 deletions

File tree

apps/website/content/docs/rules/overview.mdx

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -262,26 +262,26 @@ full: true
262262
DOM rules target `React DOM`-specific concerns including security vulnerabilities, deprecated APIs, and DOM property usage.
263263
</Callout>
264264

265-
| Rule || 🌟 | Description | `react-dom` |
266-
| :--------------------------------------------------------------------------------------------- | :-----: | :-------: | :------------------------------------------------------------------------------------------ | :---------: |
267-
| [`no-dangerously-set-innerhtml`](dom-no-dangerously-set-innerhtml) | 1️⃣ 1️⃣ | | Disallows DOM elements from using `dangerouslySetInnerHTML` | |
268-
| [`no-dangerously-set-innerhtml-with-children`](dom-no-dangerously-set-innerhtml-with-children) | 2️⃣ 2️⃣ | | Disallows DOM elements from using `dangerouslySetInnerHTML` and `children` at the same time | |
269-
| [`no-find-dom-node`](dom-no-find-dom-node) | 2️⃣ 2️⃣ | | Disallows `findDOMNode` | |
270-
| [`no-flush-sync`](dom-no-flush-sync) | 2️⃣ 2️⃣ | | Disallows `flushSync` | |
271-
| [`no-hydrate`](dom-no-hydrate) | 2️⃣ 2️⃣ | `🔄` | Replaces usage of `ReactDOM.hydrate()` with `hydrateRoot()` | >=18.0.0 |
272-
| [`no-missing-button-type`](dom-no-missing-button-type) | 0️⃣ 1️⃣ | `🔧` | Enforces an explicit `type` attribute for `button` elements | |
273-
| [`no-missing-iframe-sandbox`](dom-no-missing-iframe-sandbox) | 0️⃣ 1️⃣ | `🔧` | Enforces an explicit `sandbox` attribute for `iframe` elements | |
274-
| [`no-namespace`](dom-no-namespace) | 2️⃣ 2️⃣ | | Enforces the absence of a `namespace` in React elements | |
275-
| [`no-render`](dom-no-render) | 2️⃣ 2️⃣ | `🔄` | Replaces usage of `ReactDOM.render()` with `createRoot(node).render()` | >=18.0.0 |
276-
| [`no-render-return-value`](dom-no-render-return-value) | 2️⃣ 2️⃣ | | Disallows the return value of `ReactDOM.render` | |
277-
| [`no-script-url`](dom-no-script-url) | 1️⃣ 1️⃣ | | Disallows `javascript:` URLs as attribute values | |
278-
| [`no-string-style-prop`](dom-no-string-style-prop) | 0️⃣ 0️⃣ | | Disallows the use of string style prop in JSX. Use an object instead | |
279-
| [`no-unknown-property`](dom-no-unknown-property) | 0️⃣ 0️⃣ | `🔧` `⚙️` | Disallows unknown `DOM` properties | |
280-
| [`no-unsafe-iframe-sandbox`](dom-no-unsafe-iframe-sandbox) | 1️⃣ 1️⃣ | | Enforces `sandbox` attribute for `iframe` elements is not set to unsafe combinations | |
281-
| [`no-unsafe-target-blank`](dom-no-unsafe-target-blank) | 0️⃣ 1️⃣ | `🔧` | Disallows `target="_blank"` without `rel="noreferrer noopener"` | |
282-
| [`no-use-form-state`](dom-no-use-form-state) | 2️⃣ 2️⃣ | `🔄` | Replaces usage of `useFormState` with `useActionState` | >=19.0.0 |
283-
| [`no-void-elements-with-children`](dom-no-void-elements-with-children) | 2️⃣ 2️⃣ | | Disallows `children` in void DOM elements | |
284-
| [`prefer-namespace-import`](dom-prefer-namespace-import) | 0️⃣ 0️⃣ | `🔧` | Enforces importing React DOM via a namespace import | |
265+
| Rule || 🌟 | Description | `react-dom` |
266+
| :--------------------------------------------------------------------------------------------- | :-----: | :-------: | :-------------------------------------------------------------------------------------------- | :---------: |
267+
| [`no-dangerously-set-innerhtml`](dom-no-dangerously-set-innerhtml) | 1️⃣ 1️⃣ | | Disallows DOM elements from using `dangerouslySetInnerHTML` | |
268+
| [`no-dangerously-set-innerhtml-with-children`](dom-no-dangerously-set-innerhtml-with-children) | 2️⃣ 2️⃣ | | Disallows DOM elements from using `dangerouslySetInnerHTML` and `children` at the same time | |
269+
| [`no-find-dom-node`](dom-no-find-dom-node) | 2️⃣ 2️⃣ | | Disallows `findDOMNode` | |
270+
| [`no-flush-sync`](dom-no-flush-sync) | 2️⃣ 2️⃣ | | Disallows `flushSync` | |
271+
| [`no-hydrate`](dom-no-hydrate) | 2️⃣ 2️⃣ | `🔄` | Replaces usage of `ReactDOM.hydrate()` with `hydrateRoot()` | >=18.0.0 |
272+
| [`no-missing-button-type`](dom-no-missing-button-type) | 0️⃣ 1️⃣ | `🔧` | Enforces an explicit `type` attribute for `button` elements | |
273+
| [`no-missing-iframe-sandbox`](dom-no-missing-iframe-sandbox) | 0️⃣ 1️⃣ | `🔧` | Enforces an explicit `sandbox` attribute for `iframe` elements | |
274+
| [`no-namespace`](dom-no-namespace) | 2️⃣ 2️⃣ | | Enforces the absence of a `namespace` in React elements | |
275+
| [`no-render`](dom-no-render) | 2️⃣ 2️⃣ | `🔄` | Replaces usage of `ReactDOM.render()` with `createRoot(node).render()` | >=18.0.0 |
276+
| [`no-render-return-value`](dom-no-render-return-value) | 2️⃣ 2️⃣ | | Disallows the return value of `ReactDOM.render` | |
277+
| [`no-script-url`](dom-no-script-url) | 1️⃣ 1️⃣ | | Disallows `javascript:` URLs as attribute values | |
278+
| [`no-string-style-prop`](dom-no-string-style-prop) | 0️⃣ 0️⃣ | | Disallows the use of string style prop in JSX. Use an object instead | |
279+
| [`no-unknown-property`](dom-no-unknown-property) | 0️⃣ 0️⃣ | `🔧` `⚙️` | Disallows unknown `DOM` properties | |
280+
| [`no-unsafe-iframe-sandbox`](dom-no-unsafe-iframe-sandbox) | 1️⃣ 1️⃣ | | Enforces that the `sandbox` attribute for `iframe` elements is not set to unsafe combinations | |
281+
| [`no-unsafe-target-blank`](dom-no-unsafe-target-blank) | 0️⃣ 1️⃣ | `🔧` | Disallows `target="_blank"` without `rel="noreferrer noopener"` | |
282+
| [`no-use-form-state`](dom-no-use-form-state) | 2️⃣ 2️⃣ | `🔄` | Replaces usage of `useFormState` with `useActionState` | >=19.0.0 |
283+
| [`no-void-elements-with-children`](dom-no-void-elements-with-children) | 2️⃣ 2️⃣ | | Disallows `children` in void DOM elements | |
284+
| [`prefer-namespace-import`](dom-prefer-namespace-import) | 0️⃣ 0️⃣ | `🔧` | Enforces importing React DOM via a namespace import | |
285285

286286
## Web API Rules
287287

packages/core/docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@
124124
| [getJsxConfigFromAnnotation](functions/getJsxConfigFromAnnotation.md) | Get JsxConfig from pragma comments (annotations) in the source code |
125125
| [getJsxConfigFromContext](functions/getJsxConfigFromContext.md) | Get JsxConfig from the rule context by reading compiler options |
126126
| [getJsxElementType](functions/getJsxElementType.md) | Extracts the element type name from a JSX element or fragment For JSX elements, returns the stringified name (e.g., "div", "Button", "React.Fragment") For JSX fragments, returns an empty string |
127-
| [hasNoneOrLooseComponentName](functions/hasNoneOrLooseComponentName.md) | Check if the function has no name or a loose component name |
128127
| [isClassComponent](functions/isClassComponent.md) | Check if a node is a React class component |
129128
| [isComponentDefinition](functions/isComponentDefinition.md) | Determine if a function node represents a valid React component definition |
130129
| [isComponentDidMountCallback](functions/isComponentDidMountCallback.md) | Check if the given node is a componentDidMount callback |
@@ -136,6 +135,7 @@
136135
| [isComponentWrapperCallbackLoose](functions/isComponentWrapperCallbackLoose.md) | Check if the node is a callback function passed to a component wrapper loosely |
137136
| [isComponentWrapperCallLoose](functions/isComponentWrapperCallLoose.md) | Check if the node is a call expression for a component wrapper loosely |
138137
| [isDeclaredInRenderPropLoose](functions/isDeclaredInRenderPropLoose.md) | Unsafe check whether given node is declared inside a render prop `_ = <Component renderRow={"node"} /> ` ^^^^^^ ` _ = <Component rows={ [{ render: "node" }] } /> ` ^^^^^^ `` |
138+
| [isFunctionWithLooseComponentName](functions/isFunctionWithLooseComponentName.md) | Check if a function has a loose component name |
139139
| [isHook](functions/isHook.md) | Determine if a function node is a React Hook based on its name. |
140140
| [isHookCall](functions/isHookCall.md) | Check if the given node is a React Hook call by its name. |
141141
| [isHookCallWithName](functions/isHookCallWithName.md) | Check if a node is a call to a specific React hook. Returns a function that accepts a hook name to check against. |

packages/core/docs/functions/hasNoneOrLooseComponentName.md

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[@eslint-react/core](../README.md) / isFunctionWithLooseComponentName
2+
3+
# Function: isFunctionWithLooseComponentName()
4+
5+
```ts
6+
function isFunctionWithLooseComponentName(
7+
context: RuleContext,
8+
fn: TSESTreeFunction,
9+
allowNone: boolean): boolean;
10+
```
11+
12+
Check if a function has a loose component name
13+
14+
## Parameters
15+
16+
| Parameter | Type | Default value | Description |
17+
| ------ | ------ | ------ | ------ |
18+
| `context` | `RuleContext` | `undefined` | The rule context |
19+
| `fn` | `TSESTreeFunction` | `undefined` | The function to check |
20+
| `allowNone` | `boolean` | `false` | Whether to allow no name |
21+
22+
## Returns
23+
24+
`boolean`
25+
26+
Whether the function has a loose component name

packages/core/docs/interfaces/FunctionComponentSemanticNode.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Represents a React Function Component
1212

1313
| Property | Type | Description | Overrides | Inherited from |
1414
| ------ | ------ | ------ | ------ | ------ |
15+
| <a id="directives"></a> `directives` | `StringLiteral`[] | The directives used in the function (e.g., "use strict", "use client", etc.) | - | - |
1516
| <a id="displayname"></a> `displayName` | `Expression` \| `undefined` | The display name of the component | - | - |
1617
| <a id="flag"></a> `flag` | `bigint` | Flags describing the component's characteristics | [`SemanticNode`](SemanticNode.md).[`flag`](SemanticNode.md#flag) | - |
1718
| <a id="hint"></a> `hint` | `bigint` | Hint for how the component was detected | [`SemanticNode`](SemanticNode.md).[`hint`](SemanticNode.md#hint) | - |

packages/core/docs/interfaces/HookSemanticNode.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88

99
## Properties
1010

11-
| Property | Type | Overrides | Inherited from |
12-
| ------ | ------ | ------ | ------ |
13-
| <a id="flag"></a> `flag` | `bigint` | - | [`SemanticNode`](SemanticNode.md).[`flag`](SemanticNode.md#flag) |
14-
| <a id="hint"></a> `hint` | `bigint` | - | [`SemanticNode`](SemanticNode.md).[`hint`](SemanticNode.md#hint) |
15-
| <a id="hookcalls"></a> `hookCalls` | `CallExpression`[] | - | - |
16-
| <a id="id"></a> `id` | \| `ArrayExpression` \| `ArrayPattern` \| `ArrowFunctionExpression` \| `AssignmentExpression` \| `AwaitExpression` \| `PrivateInExpression` \| `SymmetricBinaryExpression` \| `CallExpression` \| `ChainExpression` \| `ClassExpression` \| `ConditionalExpression` \| `FunctionExpression` \| `Identifier` \| `ImportExpression` \| `JSXElement` \| `JSXFragment` \| `BigIntLiteral` \| `BooleanLiteral` \| `NullLiteral` \| `NumberLiteral` \| `RegExpLiteral` \| `StringLiteral` \| `LogicalExpression` \| `MemberExpressionComputedName` \| `MemberExpressionNonComputedName` \| `MetaProperty` \| `NewExpression` \| `ObjectExpression` \| `ObjectPattern` \| `PrivateIdentifier` \| `SequenceExpression` \| `Super` \| `TaggedTemplateExpression` \| `TemplateLiteral` \| `ThisExpression` \| `TSAsExpression` \| `TSInstantiationExpression` \| `TSNonNullExpression` \| `TSSatisfiesExpression` \| `TSTypeAssertion` \| `UnaryExpressionBitwiseNot` \| `UnaryExpressionDelete` \| `UnaryExpressionMinus` \| `UnaryExpressionNot` \| `UnaryExpressionPlus` \| `UnaryExpressionTypeof` \| `UnaryExpressionVoid` \| `UpdateExpression` \| `YieldExpression` \| `undefined` | [`SemanticNode`](SemanticNode.md).[`id`](SemanticNode.md#id) | - |
17-
| <a id="key"></a> `key` | `string` | - | [`SemanticNode`](SemanticNode.md).[`key`](SemanticNode.md#key) |
18-
| <a id="kind"></a> `kind` | `string` | - | [`SemanticNode`](SemanticNode.md).[`kind`](SemanticNode.md#kind) |
19-
| <a id="name"></a> `name` | `string` | [`SemanticNode`](SemanticNode.md).[`name`](SemanticNode.md#name) | - |
20-
| <a id="node"></a> `node` | `TSESTreeFunction` | [`SemanticNode`](SemanticNode.md).[`node`](SemanticNode.md#node) | - |
11+
| Property | Type | Description | Overrides | Inherited from |
12+
| ------ | ------ | ------ | ------ | ------ |
13+
| <a id="directives"></a> `directives` | `StringLiteral`[] | The directives used in the function (e.g., "use strict", "use client", etc.) | - | - |
14+
| <a id="flag"></a> `flag` | `bigint` | - | - | [`SemanticNode`](SemanticNode.md).[`flag`](SemanticNode.md#flag) |
15+
| <a id="hint"></a> `hint` | `bigint` | - | - | [`SemanticNode`](SemanticNode.md).[`hint`](SemanticNode.md#hint) |
16+
| <a id="hookcalls"></a> `hookCalls` | `CallExpression`[] | - | - | - |
17+
| <a id="id"></a> `id` | \| `ArrayExpression` \| `ArrayPattern` \| `ArrowFunctionExpression` \| `AssignmentExpression` \| `AwaitExpression` \| `PrivateInExpression` \| `SymmetricBinaryExpression` \| `CallExpression` \| `ChainExpression` \| `ClassExpression` \| `ConditionalExpression` \| `FunctionExpression` \| `Identifier` \| `ImportExpression` \| `JSXElement` \| `JSXFragment` \| `BigIntLiteral` \| `BooleanLiteral` \| `NullLiteral` \| `NumberLiteral` \| `RegExpLiteral` \| `StringLiteral` \| `LogicalExpression` \| `MemberExpressionComputedName` \| `MemberExpressionNonComputedName` \| `MetaProperty` \| `NewExpression` \| `ObjectExpression` \| `ObjectPattern` \| `PrivateIdentifier` \| `SequenceExpression` \| `Super` \| `TaggedTemplateExpression` \| `TemplateLiteral` \| `ThisExpression` \| `TSAsExpression` \| `TSInstantiationExpression` \| `TSNonNullExpression` \| `TSSatisfiesExpression` \| `TSTypeAssertion` \| `UnaryExpressionBitwiseNot` \| `UnaryExpressionDelete` \| `UnaryExpressionMinus` \| `UnaryExpressionNot` \| `UnaryExpressionPlus` \| `UnaryExpressionTypeof` \| `UnaryExpressionVoid` \| `UpdateExpression` \| `YieldExpression` \| `undefined` | - | [`SemanticNode`](SemanticNode.md).[`id`](SemanticNode.md#id) | - |
18+
| <a id="key"></a> `key` | `string` | - | - | [`SemanticNode`](SemanticNode.md).[`key`](SemanticNode.md#key) |
19+
| <a id="kind"></a> `kind` | `string` | - | - | [`SemanticNode`](SemanticNode.md).[`kind`](SemanticNode.md#kind) |
20+
| <a id="name"></a> `name` | `string` | - | [`SemanticNode`](SemanticNode.md).[`name`](SemanticNode.md#name) | - |
21+
| <a id="node"></a> `node` | `TSESTreeFunction` | - | [`SemanticNode`](SemanticNode.md).[`node`](SemanticNode.md#node) | - |

packages/core/src/component/component-collector.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { isComponentDefinition } from "./component-definition";
1414
import { DEFAULT_COMPONENT_DETECTION_HINT } from "./component-detection-hint";
1515
import { getFunctionComponentId } from "./component-id";
1616
import { getComponentFlagFromInitPath } from "./component-init-path";
17+
import { isFunctionWithLooseComponentName } from "./component-name";
1718

1819
const idGen = new IdGenerator("function_component_");
1920

@@ -64,12 +65,14 @@ export function useComponentCollector(
6465
const id = getFunctionComponentId(context, node);
6566
const name = id == null ? unit : AST.toStringFormat(id, getText);
6667
const initPath = AST.getFunctionInitPath(node);
67-
functionEntries.push({
68+
const directives = AST.getFunctionDirectives(node);
69+
const entry = {
6870
id: getFunctionComponentId(context, node),
6971
key,
7072
kind: "function",
7173
name,
7274
node,
75+
directives,
7376
displayName: unit,
7477
flag: getComponentFlagFromInitPath(initPath),
7578
hint,
@@ -79,7 +82,12 @@ export function useComponentCollector(
7982
isExportDefault,
8083
isExportDefaultDeclaration,
8184
rets: [],
82-
});
85+
} as const satisfies FunctionEntry;
86+
functionEntries.push(entry);
87+
if (!entry.isComponentDefinition || !isFunctionWithLooseComponentName(context, node, false)) return;
88+
if (directives.some((d) => d.value === "use memo" || d.value === "use no memo")) {
89+
components.set(entry.key, entry);
90+
}
8391
};
8492
const onFunctionExit = () => {
8593
return functionEntries.pop();

0 commit comments

Comments
 (0)