Skip to content

Commit 53e2ccf

Browse files
refactor(oxlint-plugin): consolidate five copies of isFunctionLike
Five sites each defined isFunctionLike — the test for whether an AST node is one of the three function shapes (ArrowFunctionExpression, FunctionExpression, FunctionDeclaration): - rules/react-builtins/rules-of-hooks.ts (plain boolean, used local FUNCTION_LIKE_TYPES) - semantic/closure-captures.ts (plain boolean) - semantic/control-flow-graph.ts (plain boolean) - semantic/scope-analysis.ts (plain boolean) - rules/state-and-effects/utils/effect/react.ts (type-guard form with null/undefined handling — strictly more useful) Promote the type-guard form from effect/react.ts to utils/is-function-like.ts and have all five sites import from it. The five callsites all pass a non-null EsTreeNode, so the narrowed signature (EsTreeNode | null | undefined → type guard) is a strict generalisation. The four sites that previously kept their own FUNCTION_LIKE_TYPES import no longer need it (the predicate's internals use isNodeOfType directly). Behaviour-neutral; -22 lines net. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
1 parent 5be94e9 commit 53e2ccf

6 files changed

Lines changed: 31 additions & 25 deletions

File tree

packages/oxlint-plugin-react-doctor/src/plugin/rules/react-builtins/rules-of-hooks.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import type { EsTreeNodeOfType } from "../../utils/es-tree-node-of-type.js";
55
import { isNodeOfType } from "../../utils/is-node-of-type.js";
66
import { isReactComponentOrHookName } from "../../utils/is-react-component-or-hook-name.js";
77
import { isReactHookName } from "../../utils/is-react-hook-name.js";
8-
import { FUNCTION_LIKE_TYPES } from "../../constants/js.js";
98
import { REACT_HOC_NAMES } from "../../constants/react.js";
9+
import { isFunctionLike } from "../../utils/is-function-like.js";
1010
import type { Rule } from "../../utils/rule.js";
1111

1212
// Port of `oxc_linter::rules::react::rules_of_hooks`. Enforces React's
@@ -330,8 +330,6 @@ const inferDestructureSourceKey = (bindingIdentifier: EsTreeNode): string | null
330330
// component / custom hook scope.
331331
const isReactUseHook = (hookName: string): boolean => hookName === "use";
332332

333-
const isFunctionLike = (node: EsTreeNode): boolean => FUNCTION_LIKE_TYPES.has(node.type);
334-
335333
interface FunctionInfo {
336334
node: EsTreeNode;
337335
// The name we'd display in error messages. Best-effort: the

packages/oxlint-plugin-react-doctor/src/plugin/rules/state-and-effects/utils/effect/react.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Reference, Scope } from "eslint-scope";
22
import type { EsTreeNode } from "../../../../utils/es-tree-node.js";
33
import type { EsTreeNodeOfType } from "../../../../utils/es-tree-node-of-type.js";
44
import { isAstNode } from "../../../../utils/is-ast-node.js";
5+
import { isFunctionLike } from "../../../../utils/is-function-like.js";
56
import { isNodeOfType } from "../../../../utils/is-node-of-type.js";
67
import { getDownstreamRefs, getRef, getUpstreamRefs, isEventualCallTo } from "./ast.js";
78
import type { ProgramAnalysis } from "./get-program-analysis.js";
@@ -40,19 +41,6 @@ const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
4041
const startsWithUppercase = (name: string | undefined): boolean =>
4142
Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
4243

43-
const isFunctionLike = (
44-
node: EsTreeNode | null | undefined,
45-
): node is
46-
| EsTreeNodeOfType<"ArrowFunctionExpression">
47-
| EsTreeNodeOfType<"FunctionExpression">
48-
| EsTreeNodeOfType<"FunctionDeclaration"> =>
49-
Boolean(
50-
node &&
51-
(isNodeOfType(node, "ArrowFunctionExpression") ||
52-
isNodeOfType(node, "FunctionExpression") ||
53-
isNodeOfType(node, "FunctionDeclaration")),
54-
);
55-
5644
const isReactFunctionalComponent = (node: EsTreeNode | null | undefined): boolean => {
5745
if (!node) return false;
5846
if (isNodeOfType(node, "FunctionDeclaration")) {

packages/oxlint-plugin-react-doctor/src/plugin/semantic/closure-captures.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { FUNCTION_LIKE_TYPES } from "../constants/js.js";
21
import type { EsTreeNode } from "../utils/es-tree-node.js";
32
import type { ReferenceDescriptor, ScopeAnalysis } from "./scope-analysis.js";
43
import { isDescendantScope } from "./scope-analysis.js";
54
import { isAstNode } from "../utils/is-ast-node.js";
6-
7-
const isFunctionLike = (node: EsTreeNode): boolean => FUNCTION_LIKE_TYPES.has(node.type);
5+
import { isFunctionLike } from "../utils/is-function-like.js";
86

97
const TYPE_ONLY_CHILD_KEYS: ReadonlySet<string> = new Set([
108
"implements",

packages/oxlint-plugin-react-doctor/src/plugin/semantic/control-flow-graph.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { FUNCTION_LIKE_TYPES } from "../constants/js.js";
21
import type { EsTreeNode } from "../utils/es-tree-node.js";
32
import { isAstNode } from "../utils/is-ast-node.js";
3+
import { isFunctionLike } from "../utils/is-function-like.js";
44
import { isNodeOfType } from "../utils/is-node-of-type.js";
55

66
// Per-function CFG. Mirrors the subset of `oxc_cfg` we need to answer:
@@ -48,8 +48,6 @@ export interface ControlFlowAnalysis {
4848
}
4949

5050

51-
const isFunctionLike = (node: EsTreeNode): boolean => FUNCTION_LIKE_TYPES.has(node.type);
52-
5351
interface CfgBuilder {
5452
blocks: BasicBlock[];
5553
entry: BasicBlock;

packages/oxlint-plugin-react-doctor/src/plugin/semantic/scope-analysis.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { FUNCTION_LIKE_TYPES } from "../constants/js.js";
21
import type { EsTreeNode } from "../utils/es-tree-node.js";
32
import type { EsTreeNodeOfType } from "../utils/es-tree-node-of-type.js";
43
import { isAstNode } from "../utils/is-ast-node.js";
4+
import { isFunctionLike } from "../utils/is-function-like.js";
55
import { isNodeOfType } from "../utils/is-node-of-type.js";
66

77
// Scope analyzer — per-file walker building a scope tree, symbol
@@ -100,8 +100,6 @@ export interface ScopeAnalysis {
100100
readonly isGlobalReference: (identifier: EsTreeNode) => boolean;
101101
}
102102

103-
const isFunctionLike = (node: EsTreeNode): boolean => FUNCTION_LIKE_TYPES.has(node.type);
104-
105103
const isHoistedBindingKind = (kind: SymbolKind): boolean => kind === "var" || kind === "function";
106104

107105
// Returns the nearest enclosing function-or-program scope for hoisting
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { EsTreeNode } from "./es-tree-node.js";
2+
import type { EsTreeNodeOfType } from "./es-tree-node-of-type.js";
3+
import { isNodeOfType } from "./is-node-of-type.js";
4+
5+
/**
6+
* Type-guard for the three "function-like" ESTree node shapes:
7+
* `ArrowFunctionExpression`, `FunctionExpression`,
8+
* `FunctionDeclaration`. Accepts `null | undefined` so callers
9+
* walking parent chains don't need their own pre-check.
10+
*
11+
* Was duplicated across five sites as both a plain boolean check
12+
* (`FUNCTION_LIKE_TYPES.has(node.type)`) and as a type-guard. The
13+
* type-guard form covers both shapes without callers paying a cast.
14+
*/
15+
export const isFunctionLike = (
16+
node: EsTreeNode | null | undefined,
17+
): node is
18+
| EsTreeNodeOfType<"ArrowFunctionExpression">
19+
| EsTreeNodeOfType<"FunctionExpression">
20+
| EsTreeNodeOfType<"FunctionDeclaration"> =>
21+
Boolean(
22+
node &&
23+
(isNodeOfType(node, "ArrowFunctionExpression") ||
24+
isNodeOfType(node, "FunctionExpression") ||
25+
isNodeOfType(node, "FunctionDeclaration")),
26+
);

0 commit comments

Comments
 (0)