Skip to content

Commit 5ab2c55

Browse files
authored
refactor: improve function component detection (#290)
1 parent c71d604 commit 5ab2c55

File tree

7 files changed

+46
-49
lines changed

7 files changed

+46
-49
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## v0.10.4-beta.1 (Sat Jan 6 2024)
2+
3+
### 🪄 Improvements
4+
5+
- Improve function component detection in rule `react/no-unstable-nested-components` and `debug/function-component`.
6+
17
## v0.10.4-beta.0 (Sat Jan 6 2024)
28

39
### 🪄 Improvements

packages/core/docs/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -379,13 +379,14 @@ ___
379379

380380
### hasNoneOrValidComponentName
381381

382-
**hasNoneOrValidComponentName**(`node`): `boolean`
382+
**hasNoneOrValidComponentName**(`node`, `context`): `boolean`
383383

384384
#### Parameters
385385

386386
| Name | Type |
387387
| :------ | :------ |
388388
| `node` | `TSESTreeFunction` |
389+
| `context` | `Readonly`\<`RuleContext`\<`string`, readonly `unknown`[]\>\> |
389390

390391
#### Returns
391392

@@ -628,7 +629,7 @@ ___
628629

629630
### isComponentName
630631

631-
**isComponentName**(`name`): name is string
632+
**isComponentName**(`name`): `boolean`
632633

633634
#### Parameters
634635

@@ -638,7 +639,7 @@ ___
638639

639640
#### Returns
640641

641-
name is string
642+
`boolean`
642643

643644
___
644645

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,7 @@ export function useComponentCollector(
118118
if (O.isNone(maybeCurrentFn)) return;
119119
const [currentFn, isKnown, hookCalls] = maybeCurrentFn.value;
120120
if (isKnown) return;
121-
const isComponent = F.constTrue()
122-
&& hasNoneOrValidComponentName(currentFn)
121+
const isComponent = hasNoneOrValidComponentName(currentFn, context)
123122
&& isJSXValue(node.argument, context, hint)
124123
&& hasValidHierarchy(currentFn, context, hint);
125124
if (!isComponent) return;
@@ -152,7 +151,7 @@ export function useComponentCollector(
152151
const [currentFn, _, hookCalls] = maybeCurrentFn.value;
153152
const { body } = currentFn;
154153
const isComponent = F.constTrue()
155-
&& hasNoneOrValidComponentName(currentFn)
154+
&& hasNoneOrValidComponentName(currentFn, context)
156155
&& isJSXValue(body, context, hint)
157156
&& hasValidHierarchy(currentFn, context, hint);
158157
if (!isComponent) return;
Lines changed: 15 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
1-
import { isOneOf, NodeType, type TSESTreeFunction } from "@eslint-react/ast";
1+
import { getFunctionIdentifier, NodeType, type TSESTreeFunction } from "@eslint-react/ast";
22
import { O } from "@eslint-react/tools";
33
import type { RuleContext } from "@eslint-react/types";
44
import type { TSESTree } from "@typescript-eslint/types";
55

6+
import { isReactHookCallWithNameLoose } from "../hook";
67
import { isForwardRefCall, isMemoCall } from "../react-api";
78

8-
function isMemoOrForwardRefCall(node: TSESTree.Node, context: RuleContext) {
9+
function isComponentWrapperCall(node: TSESTree.Node, context: RuleContext) {
910
if (node.type !== NodeType.CallExpression) return false;
1011

11-
return isMemoCall(node, context) || isForwardRefCall(node, context);
12+
return isMemoCall(node, context)
13+
|| isForwardRefCall(node, context)
14+
|| isReactHookCallWithNameLoose("useCallback")(node);
1215
}
1316

1417
export function getFunctionComponentIdentifier(
1518
node: TSESTreeFunction,
1619
context: RuleContext,
1720
): O.Option<TSESTree.Identifier | TSESTree.Identifier[]> {
18-
const { id, parent } = node;
19-
if (node.type === NodeType.FunctionDeclaration) return O.fromNullable(id);
20-
if (
21-
parent.type === NodeType.VariableDeclarator
22-
&& parent.id.type === NodeType.Identifier
23-
&& parent.parent.type === NodeType.VariableDeclaration
24-
) {
25-
return O.some(parent.id);
21+
const functionId = getFunctionIdentifier(node);
22+
23+
if (O.isSome(functionId)) {
24+
return functionId;
2625
}
2726

27+
const { parent } = node;
28+
2829
if (
2930
parent.type === NodeType.CallExpression
30-
&& isMemoOrForwardRefCall(parent, context)
31+
&& isComponentWrapperCall(parent, context)
3132
&& parent.parent.type === NodeType.VariableDeclarator
3233
&& parent.parent.id.type === NodeType.Identifier
3334
&& parent.parent.parent.type === NodeType.VariableDeclaration
@@ -37,36 +38,15 @@ export function getFunctionComponentIdentifier(
3738

3839
if (
3940
parent.type === NodeType.CallExpression
40-
&& isMemoOrForwardRefCall(parent, context)
41+
&& isComponentWrapperCall(parent, context)
4142
&& parent.parent.type === NodeType.CallExpression
42-
&& isMemoOrForwardRefCall(parent.parent, context)
43+
&& isComponentWrapperCall(parent.parent, context)
4344
&& parent.parent.parent.type === NodeType.VariableDeclarator
4445
&& parent.parent.parent.id.type === NodeType.Identifier
4546
&& parent.parent.parent.parent.type === NodeType.VariableDeclaration
4647
) {
4748
return O.some(parent.parent.parent.id);
4849
}
4950

50-
if (
51-
parent.type === NodeType.Property
52-
&& parent.key.type === NodeType.Identifier
53-
&& parent.parent.type === NodeType.ObjectExpression
54-
&& parent.parent.parent.type === NodeType.VariableDeclarator
55-
&& parent.parent.parent.id.type === NodeType.Identifier
56-
&& parent.parent.parent.parent.type === NodeType.VariableDeclaration
57-
) {
58-
return O.some([parent.parent.parent.id, parent.key]);
59-
}
60-
61-
if (
62-
isOneOf([NodeType.MethodDefinition, NodeType.PropertyDefinition])(parent)
63-
&& parent.key.type === NodeType.Identifier
64-
&& parent.parent.type === NodeType.ClassBody
65-
&& parent.parent.parent.type === NodeType.ClassDeclaration
66-
&& parent.parent.parent.id?.type === NodeType.Identifier
67-
) {
68-
return O.some([parent.parent.parent.id, parent.key]);
69-
}
70-
7151
return O.none();
7252
}
Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { getFunctionIdentifier, type TSESTreeFunction } from "@eslint-react/ast";
1+
import { type TSESTreeFunction } from "@eslint-react/ast";
22
import { F, O } from "@eslint-react/tools";
3+
import type { RuleContext } from "@eslint-react/types";
34
import type { TSESTree } from "@typescript-eslint/types";
45

6+
import { getFunctionComponentIdentifier } from "./component-identifier";
7+
58
export const RE_COMPONENT_NAME = /^[A-Z]/u;
69

710
export function getComponentNameFromIdentifier(node: TSESTree.Identifier | TSESTree.Identifier[]) {
@@ -10,16 +13,22 @@ export function getComponentNameFromIdentifier(node: TSESTree.Identifier | TSEST
1013
: node.name;
1114
}
1215

13-
export function isComponentName(name: string): name is string {
16+
export function isComponentName(name: string) {
1417
return !!name && RE_COMPONENT_NAME.test(name);
1518
}
1619

17-
export function hasNoneOrValidComponentName(node: TSESTreeFunction) {
20+
export function hasNoneOrValidComponentName(node: TSESTreeFunction, context: RuleContext) {
1821
return O.match(
19-
getFunctionIdentifier(node),
22+
getFunctionComponentIdentifier(node, context),
2023
{
2124
onNone: F.constTrue,
22-
onSome: id => isComponentName(id.name),
25+
onSome: id => {
26+
const name = Array.isArray(id)
27+
? id.at(-1)?.name
28+
: id.name;
29+
30+
return !!name && isComponentName(name);
31+
},
2332
},
2433
);
2534
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -829,7 +829,7 @@ ruleTester.run(RULE_NAME, rule, {
829829
{
830830
messageId: "FUNCTION_COMPONENT",
831831
data: {
832-
name: "anonymous",
832+
name: "SomeFooter",
833833
memo: false,
834834
forwardRef: false,
835835
hookCalls: 0,
@@ -1035,7 +1035,7 @@ ruleTester.run(RULE_NAME, rule, {
10351035
{
10361036
messageId: "FUNCTION_COMPONENT",
10371037
data: {
1038-
name: "anonymous",
1038+
name: "Header",
10391039
memo: false,
10401040
forwardRef: false,
10411041
hookCalls: 0,

packages/plugins/eslint-plugin-react/src/rules/no-unstable-nested-components.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ export default createRule<[], MessageID>({
7373
for (const { node: component, name: componentName } of functionComponents) {
7474
// Do not mark objects containing render methods
7575
if (unsafeIsDirectValueOfRenderProperty(component)) continue;
76-
const name = O.getOrElse(() => "unknown")(componentName);
76+
// Do not mark anonymous function components to reduce false positives
77+
if (O.isNone(componentName)) continue;
78+
const name = componentName.value;
7779
const isInsideProperty = component.parent.type === NodeType.Property;
7880
const isInsideJSXPropValue = isInsidePropValue(component);
7981
if (isInsideJSXPropValue) {

0 commit comments

Comments
 (0)