Skip to content

Commit 73f7df0

Browse files
committed
Add extraction of function directives
1 parent 55f52c1 commit 73f7df0

11 files changed

Lines changed: 113 additions & 34 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/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: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,14 @@ export function useComponentCollector(
6464
const id = getFunctionComponentId(context, node);
6565
const name = id == null ? unit : AST.toStringFormat(id, getText);
6666
const initPath = AST.getFunctionInitPath(node);
67-
functionEntries.push({
67+
const directives = AST.getFunctionDirectives(node);
68+
const entry = {
6869
id: getFunctionComponentId(context, node),
6970
key,
7071
kind: "function",
7172
name,
7273
node,
74+
directives,
7375
displayName: unit,
7476
flag: getComponentFlagFromInitPath(initPath),
7577
hint,
@@ -79,7 +81,11 @@ export function useComponentCollector(
7981
isExportDefault,
8082
isExportDefaultDeclaration,
8183
rets: [],
82-
});
84+
} as const satisfies FunctionEntry;
85+
functionEntries.push(entry);
86+
if (entry.isComponentDefinition && directives.some((d) => d.value === "use memo" || d.value === "use no memo")) {
87+
components.set(entry.key, entry);
88+
}
8389
};
8490
const onFunctionExit = () => {
8591
return functionEntries.pop();

packages/core/src/component/component-semantic-node.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ export interface FunctionComponentSemanticNode extends SemanticNode {
7070
displayName:
7171
| unit
7272
| TSESTree.Expression;
73+
74+
/**
75+
* The directives used in the function (e.g., "use strict", "use client", etc.)
76+
*/
77+
directives: TSESTree.StringLiteral[];
7378
}
7479

7580
/**

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export function useHookCollector(context: RuleContext): useHookCollector.ReturnT
4646
kind: "function",
4747
name: AST.toStringFormat(id, getText),
4848
node,
49+
directives: [],
4950
flag: 0n,
5051
hint: 0n,
5152
hookCalls: [],

packages/core/src/hook/hook-semantic-node.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,9 @@ export interface HookSemanticNode extends SemanticNode {
1414
name: string;
1515
// The other hooks called by the hook
1616
hookCalls: TSESTree.CallExpression[];
17+
/**
18+
* The directives used in the function (e.g., "use strict", "use client", etc.)
19+
*/
20+
directives: TSESTree.StringLiteral[];
1721
}
1822
/* eslint-enable perfectionist/sort-interfaces */

packages/plugins/eslint-plugin-react-debug/src/rules/function-component.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,44 @@ import rule, { RULE_NAME } from "./function-component";
66

77
ruleTester.run(RULE_NAME, rule, {
88
invalid: [
9+
{
10+
code: tsx`
11+
function App() {
12+
"use memo";
13+
}
14+
`,
15+
errors: [{
16+
messageId: "functionComponent",
17+
data: {
18+
json: stringify({
19+
name: "App",
20+
displayName: "none",
21+
forwardRef: false,
22+
hookCalls: 0,
23+
memo: false,
24+
}),
25+
},
26+
}],
27+
},
28+
{
29+
code: tsx`
30+
function App() {
31+
"use no memo";
32+
}
33+
`,
34+
errors: [{
35+
messageId: "functionComponent",
36+
data: {
37+
json: stringify({
38+
name: "App",
39+
displayName: "none",
40+
forwardRef: false,
41+
hookCalls: 0,
42+
memo: false,
43+
}),
44+
},
45+
}],
46+
},
947
{
1048
code: tsx`
1149
function App() {

packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-iframe-sandbox.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,4 @@ function MyComponent() {
7272
- [`no-missing-button-type`](./dom-no-missing-button-type)\
7373
Enforces an explicit `type` attribute for `button` elements.
7474
- [`no-unsafe-iframe-sandbox`](./dom-no-unsafe-iframe-sandbox)\
75-
Enforces the `sandbox` attribute for `iframe` elements is not set to unsafe combinations.
75+
Enforces that the `sandbox` attribute for `iframe` elements is not set to unsafe combinations.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { AST_NODE_TYPES as T, type TSESTree } from "@typescript-eslint/types";
2+
3+
import { getUnderlyingExpression } from "./expression-base";
4+
import { isLiteral } from "./literal";
5+
import type { TSESTreeFunction } from "./types";
6+
7+
/**
8+
* Get all directive string literals from a function node
9+
* @param node The function AST node
10+
* @returns The array of directive string literals (e.g., "use memo", "use no memo")
11+
*/
12+
export function getFunctionDirectives(node: TSESTreeFunction): TSESTree.StringLiteral[] {
13+
const directives: TSESTree.StringLiteral[] = [];
14+
if (node.body.type !== T.BlockStatement) return directives;
15+
for (const stmt of node.body.body) {
16+
if (stmt.type !== T.ExpressionStatement) continue;
17+
const expr = getUnderlyingExpression(stmt.expression);
18+
if (!isLiteral(expr, "string")) continue;
19+
directives.push(expr);
20+
}
21+
return directives;
22+
}

0 commit comments

Comments
 (0)