Open History to see the last commit
1import { defineRule, type ESTree } from "@oxlint/plugins";
2
3const MANUAL_MEMO_HOOKS = new Set(["useCallback", "useMemo"]);
4// "use no memo" and "use no forget" skip a component the compiler would bail on,
5// which hides the bailout from the coverage checks as much as "use memo" does.
6const MEMO_DIRECTIVES = new Set(["use memo", "use no memo", "use no forget"]);
7
8const readAllowMemoHooks = (options: ReadonlyArray<unknown>): boolean => {
9 const [first] = options;
10 return (
11 typeof first === "object" &&
12 first !== null &&
13 "allowMemoHooks" in first &&
14 first.allowMemoHooks === true
15 );
16};
17
18function manualMemoHookName(callee: ESTree.Node): string | null {
19 if (callee.type === "Identifier") {
20 return MANUAL_MEMO_HOOKS.has(callee.name) ? callee.name : null;
21 }
22 if (
23 callee.type === "MemberExpression" &&
24 !callee.computed &&
25 callee.object.type === "Identifier" &&
26 callee.object.name === "React" &&
27 callee.property.type === "Identifier" &&
28 MANUAL_MEMO_HOOKS.has(callee.property.name)
29 ) {
30 return callee.property.name;
31 }
32 return null;
33}
34
35export default defineRule({
36 meta: {
37 type: "suggestion",
38 docs: {
39 description:
40 "Disallow useCallback, useMemo and memo directives in files the React Compiler must compile; it memoizes them.",
41 },
42 schema: [
43 {
44 type: "object",
45 properties: {
46 allowMemoHooks: {
47 type: "boolean",
48 description:
49 "Still permit useMemo and useCallback while a codebase ratchets toward full compiler coverage; directives stay banned.",
50 },
51 },
52 additionalProperties: false,
53 },
54 ],
55 defaultOptions: [{ allowMemoHooks: false }],
56 },
57 create(context) {
58 const allowMemoHooks = readAllowMemoHooks(context.options);
59 return {
60 // A "use memo" helper only exists to force a cache scope the compiler withheld,
61 // which hides the ordering or structure problem that withheld it.
62 ExpressionStatement(node) {
63 if (typeof node.directive !== "string" || !MEMO_DIRECTIVES.has(node.directive)) return;
64 context.report({
65 node,
66 message:
67 node.directive === "use memo"
68 ? 'Drop "use memo": restructure so the compiler caches this on its own, e.g. move plain uses of a value before the hook call that splits its scope, or inline the helper.'
69 : `Drop "${node.directive}": fix the code the compiler cannot prove safe instead of skipping it.`,
70 });
71 },
72 CallExpression(node) {
73 if (allowMemoHooks) return;
74 const name = manualMemoHookName(node.callee);
75 if (name === null) return;
76 context.report({
77 node: node.callee,
78 message: `Drop ${name}: the React Compiler memoizes this file. Write the plain value or function, and use useStableCallback only where a handler must keep one identity across renders.`,
79 });
80 },
81 };
82 },
83});