Skip to content

Commit a3f0fa3

Browse files
Merge pull request #1612 from AxiomCodeAI/loop/ja-is1590
javascript: JSX tags bound to memo/forwardRef wrappers or lazy() loaders render the wrapped component
2 parents f71faef + 6b7ce08 commit a3f0fa3

10 files changed

Lines changed: 205 additions & 4 deletions

File tree

‎graph/javascript/engine/call-edge-generation/jsx.dl‎

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@
2323
// `onToggle(todo)` inside `TodoItem({ onToggle })` resolve to handleToggle.
2424
// Brace children are the `children` prop (a render prop is a function).
2525
//
26-
// Not modelled: a component reached only through a library wrapper
27-
// (`memo(C)`, `connect(…)(C)`, `forwardRef(fn)`) whose library is not staged — the
28-
// tag's value is the wrapper's return, which is unknown, and the element stays an
29-
// ambiguous_unknown row.
26+
// A tag bound to a component WRAPPER (`memo(C)`, `forwardRef(fn)`, `observer(C)`,
27+
// `withRouter(C)`) or a LOADER (`lazy(() => import("./x"))`) renders what the wrapper
28+
// was handed or what the loader settles to — see "wrapped and lazy components" below.
29+
// Not modelled: `connect(…)(C)` and other curried wrappers.
3030
// ============================================================================
3131

3232
// ── the element and what its tag holds ──────────────────────────────────────
@@ -35,6 +35,68 @@ jsx_tag_value(je, k, i) :- jsx_element_tag(je, tag), expr_value(tag, k, i).
3535
// A CommonJS module value renders through its default export, as a call of it does.
3636
jsx_tag_value(je, k, i) :- jsx_element_tag(je, tag), expr_value(tag, "module", mod),
3737
module_export_value(mod, "default", k, i), k != "module".
38+
// A tag that holds a wrapper or loader call renders what that call renders.
39+
jsx_tag_value(je, k, i) :- jsx_element_tag(je, tag), jsx_holds(tag, w), jsx_call_renders(w, k, i).
40+
41+
// ── wrapped and lazy components ─────────────────────────────────────────────
42+
// `const Card = memo(Inner)` binds Card to what memo RETURNED, and memo's library is
43+
// usually not staged, so the tag has no value and the element was an ambiguous_unknown
44+
// row. What renders is argument 0. JSX only, and NOT an expr_value of the call: at an
45+
// ordinary call site `wrap(f)` is a factory whose argument says nothing about what
46+
// runs, and valuing the call would turn every `Card()` blind spot into a claimed
47+
// no_target. The wrappers are named by the libraries that define them — `memo` and
48+
// `forwardRef` (React, Preact), `observer` (MobX) — and by the higher-order-component
49+
// convention, a `with` prefix and a capital. Read off the callee name, so
50+
// `React.memo(…)` counts as `memo(…)`.
51+
jsx_wrapper_name("memo").
52+
jsx_wrapper_name("forwardRef").
53+
jsx_wrapper_name("observer").
54+
jsx_component_wrapper(w) :- expr_kind(_, "CALL", _, w), call_site("client", _, n, _, _, _, w, _, _), jsx_wrapper_name(n).
55+
jsx_component_wrapper(w) :- expr_kind(_, "CALL", _, w), call_site("client", _, n, _, _, _, w, _, _), match("with[A-Z].*", n).
56+
// A LOADER — `lazy(() => import("./Settings"))`, Next.js `dynamic(…)`, `loadable(…)`:
57+
// argument 0 returns a module or a promise of one, and what renders settles out of it.
58+
jsx_loader_name("lazy").
59+
jsx_loader_name("dynamic").
60+
jsx_loader_name("loadable").
61+
jsx_component_loader(w) :- expr_kind(_, "CALL", _, w), call_site("client", _, n, _, _, _, w, _, _), jsx_loader_name(n).
62+
63+
// jsx_holds(Expr, Call) — the wrapper or loader call an expression holds, followed the
64+
// ways a component reaches a tag: the call itself, a const initialised with it, a named
65+
// or default import of an export that holds it, and a re-export of one.
66+
jsx_holds(w, w) :- jsx_component_wrapper(w).
67+
jsx_holds(w, w) :- jsx_component_loader(w).
68+
jsx_holds(e, w) :- expr_binding(_, v, e), jsx_var_holds(v, w).
69+
jsx_var_holds(v, w) :- var_init(_, _, e, v), !var_binding_form(_, "OBJECT_PATTERN", v), !var_binding_form(_, "ARRAY_PATTERN", v),
70+
jsx_holds(e, w).
71+
jsx_var_holds(v, w) :- var_import(_, imp, v), jsx_import_holds(imp, w).
72+
jsx_import_holds(imp, w) :- import_decl(_, _, _, "DEFAULT", _, _, _, _, imp), import_module(imp, mod),
73+
jsx_export_holds(mod, "default", w).
74+
jsx_import_holds(imp, w) :- import_decl(_, _, _, bf, n, _, _, _, imp), import_binding_is_named(bf), n != "",
75+
import_module(imp, mod), jsx_export_holds(mod, n, w).
76+
jsx_export_holds(mod, n, w) :- export_decl(_, n, _, _, _, "VARIABLE", v, _, mod, x), n != "", v != "",
77+
!export_overwritten(_, x), jsx_var_holds(v, w).
78+
// `export default memo(Inner)` / `export default Card` — the source expression.
79+
jsx_export_holds(mod, n, w) :- export_decl(_, n, _, _, _, _, _, se, mod, x), n != "", se != "",
80+
!expr_kind(_, "ASSIGNMENT", _, se), !export_overwritten(_, x), jsx_holds(se, w).
81+
jsx_export_holds(mod, en, w) :- export_decl(_, en, ln, form, _, _, _, _, mod, x), form != "EXPORT_ALL", ln != "",
82+
export_reexport(_, _, imp, x), import_module(imp, src), jsx_export_holds(src, ln, w).
83+
84+
// jsx_call_renders(Call, K, I) — the value a wrapper or loader call renders.
85+
// A wrapper: its argument 0, or what argument 0 renders when it is itself a wrapped
86+
// component — `memo(forwardRef(function Inner() {…}))`, `memo(Card)`.
87+
jsx_call_renders(w, k, i) :- jsx_component_wrapper(w), call_arg(w, 0, a), expr_value(a, k, i), k != "ambient".
88+
jsx_call_renders(w, k, i) :- jsx_component_wrapper(w), call_arg(w, 0, a), jsx_holds(a, w2), jsx_call_renders(w2, k, i).
89+
// A loader: what argument 0 returns — the module, a promise of it, or through
90+
// `.then(cb)` what cb returns (promises.dl carries the module into cb's parameter).
91+
jsx_loader_settles(w, k, i) :- jsx_component_loader(w), call_arg(w, 0, a), expr_value(a, "func", f), return_value(f, k, i).
92+
// …the module itself: its default export renders.
93+
jsx_call_renders(w, k, i) :- jsx_loader_settles(w, "module", mod), module_export_value(mod, "default", k, i), k != "module".
94+
// …re-wrapped for `lazy`, `{ default: m.Named }`: the object's `default` property.
95+
jsx_call_renders(w, k, i) :- jsx_loader_settles(w, "obj", o), prop_value("obj", o, "default", k, i).
96+
// …or a component projected out of it, `.then((m) => m.Named)` (the `dynamic` form).
97+
jsx_call_renders(w, k, i) :- jsx_loader_settles(w, k, i), jsx_component_kind(k).
98+
jsx_component_kind("func").
99+
jsx_component_kind("ctor").
38100

39101
// ── jsx_renders(Element, Component) — the callables the element runs ───────
40102
// A function component (a constructor FUNCTION is callable_value's "ctor" half).

‎graph/javascript/souffle/decls_all.dl‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@
4949
.decl jsx_caller(c0:symbol, c1:symbol)
5050
.decl jsx_target_count(c0:symbol, c1:number)
5151
.decl jsx_class(c0:symbol, c1:symbol)
52+
.decl jsx_wrapper_name(c0:symbol)
53+
.decl jsx_component_wrapper(c0:symbol)
54+
.decl jsx_loader_name(c0:symbol)
55+
.decl jsx_component_loader(c0:symbol)
56+
.decl jsx_holds(c0:symbol, c1:symbol)
57+
.decl jsx_var_holds(c0:symbol, c1:symbol)
58+
.decl jsx_import_holds(c0:symbol, c1:symbol)
59+
.decl jsx_export_holds(c0:symbol, c1:symbol, c2:symbol)
60+
.decl jsx_call_renders(c0:symbol, c1:symbol, c2:symbol)
61+
.decl jsx_loader_settles(c0:symbol, c1:symbol, c2:symbol)
62+
.decl jsx_component_kind(c0:symbol)
5263

5364
// ── call-edge-generation/calls.dl ──
5465
.decl method_prov(c0:symbol, c1:symbol)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React, { memo, lazy } from "react";
2+
import { observer } from "mobx-react";
3+
import { makeThing } from "thing-kit";
4+
import DefMemo from "./defmemo";
5+
import { MemoFwd, PlainInner, ObservedInner, Plain } from "./parts";
6+
import { Forwarded } from "./reexport";
7+
8+
// A tag bound to a wrapper renders the wrapper's argument.
9+
const Card = memo(PlainInner);
10+
const Observed = observer(ObservedInner);
11+
const Dotted = React.memo(function DottedInner() { return <u />; });
12+
// A tag bound to a loader renders what the loader settles to.
13+
const LazyBlock = lazy(() => import("./heavy").then((m) => { return { default: m.ObjNamed }; }));
14+
const LazyConcise = lazy(() => import("./heavy").then((m) => ({ default: m.ObjNamed })));
15+
const LazyDefault = lazy(() => import("./heavy"));
16+
17+
// CONTROL: an unstaged factory that is not a wrapper — argument 0 is not what renders.
18+
export function thingBody() { return 1; }
19+
const Thing = makeThing(() => thingBody());
20+
21+
export function App() {
22+
return (
23+
<>
24+
<Card />
25+
<DefMemo />
26+
<MemoFwd />
27+
<Forwarded />
28+
<Observed />
29+
<Dotted />
30+
<LazyBlock />
31+
<LazyConcise />
32+
<LazyDefault />
33+
<Thing />
34+
</>
35+
);
36+
}
37+
38+
// CONTROL: a plain tag and a plain call keep resolving; a wrapper value called as a
39+
// function is not claimed.
40+
export function Controls() {
41+
PlainInner();
42+
Card();
43+
return <Plain />;
44+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { memo } from "react";
2+
import { DefaultMemoInner } from "./parts";
3+
4+
export default memo(DefaultMemoInner);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export function ObjNamed() { return <div />; }
2+
export default function HeavyDefault() { return <section />; }
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { memo, forwardRef } from "react";
2+
3+
export function PlainInner() { return <i />; }
4+
export function DefaultMemoInner() { return <p />; }
5+
export function ObservedInner() { return <em />; }
6+
export const MemoFwd = memo(forwardRef(function MemoFwdInner(p, ref) { return <div />; }));
7+
export function Plain() { return <b />; }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { MemoFwd as Forwarded } from "./parts";
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import_cause app.jsx:1:1 react not_staged
2+
import_cause app.jsx:1:17 react not_staged
3+
import_cause app.jsx:1:23 react not_staged
4+
import_cause app.jsx:2:10 mobx-react not_staged
5+
import_cause app.jsx:3:10 thing-kit not_staged
6+
import_cause defmemo.jsx:1:10 react not_staged
7+
import_cause parts.jsx:1:10 react not_staged
8+
import_cause parts.jsx:1:16 react not_staged
9+
package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> -
10+
unresolved app.jsx:10:18 FUNCTION_CALL observer callee_untyped
11+
unresolved app.jsx:11:16 METHOD_CALL memo receiver_untyped
12+
unresolved app.jsx:13:19 FUNCTION_CALL lazy callee_untyped
13+
unresolved app.jsx:13:30 DYNAMIC_IMPORT_CALL dynamic
14+
unresolved app.jsx:13:30 METHOD_CALL then member_absent
15+
unresolved app.jsx:14:21 FUNCTION_CALL lazy callee_untyped
16+
unresolved app.jsx:14:32 DYNAMIC_IMPORT_CALL dynamic
17+
unresolved app.jsx:14:32 METHOD_CALL then member_absent
18+
unresolved app.jsx:15:21 FUNCTION_CALL lazy callee_untyped
19+
unresolved app.jsx:15:32 DYNAMIC_IMPORT_CALL dynamic
20+
unresolved app.jsx:19:15 FUNCTION_CALL makeThing callee_untyped
21+
unresolved app.jsx:42:3 FUNCTION_CALL Card callee_untyped
22+
unresolved app.jsx:9:14 FUNCTION_CALL memo callee_untyped
23+
unresolved defmemo.jsx:4:16 FUNCTION_CALL memo callee_untyped
24+
unresolved parts.jsx:6:24 FUNCTION_CALL memo callee_untyped
25+
unresolved parts.jsx:6:29 FUNCTION_CALL forwardRef callee_untyped
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
app.jsx:10:18 FUNCTION_CALL observer -> ambiguous_unknown -
2+
app.jsx:10:18 FUNCTION_CALL observer -> callback_registered parts.jsx:5:1 ObservedInner
3+
app.jsx:11:16 METHOD_CALL React.memo -> ambiguous_unknown -
4+
app.jsx:11:16 METHOD_CALL React.memo -> callback_registered app.jsx:11:27 DottedInner
5+
app.jsx:13:19 FUNCTION_CALL lazy -> ambiguous_unknown -
6+
app.jsx:13:19 FUNCTION_CALL lazy -> callback_registered app.jsx:13:24 <arrow>
7+
app.jsx:13:30 DYNAMIC_IMPORT_CALL import -> dynamic_terminal -
8+
app.jsx:13:30 METHOD_CALL import("./heavy").then -> ambiguous_unknown -
9+
app.jsx:13:30 METHOD_CALL import("./heavy").then -> callback_registered app.jsx:13:53 <arrow>
10+
app.jsx:14:21 FUNCTION_CALL lazy -> ambiguous_unknown -
11+
app.jsx:14:21 FUNCTION_CALL lazy -> callback_registered app.jsx:14:26 <arrow>
12+
app.jsx:14:32 DYNAMIC_IMPORT_CALL import -> dynamic_terminal -
13+
app.jsx:14:32 METHOD_CALL import("./heavy").then -> ambiguous_unknown -
14+
app.jsx:14:32 METHOD_CALL import("./heavy").then -> callback_registered app.jsx:14:55 <arrow>
15+
app.jsx:15:21 FUNCTION_CALL lazy -> ambiguous_unknown -
16+
app.jsx:15:21 FUNCTION_CALL lazy -> callback_registered app.jsx:15:26 <arrow>
17+
app.jsx:15:32 DYNAMIC_IMPORT_CALL import -> dynamic_terminal -
18+
app.jsx:19:15 FUNCTION_CALL makeThing -> ambiguous_unknown -
19+
app.jsx:19:15 FUNCTION_CALL makeThing -> callback_registered app.jsx:19:25 <arrow>
20+
app.jsx:19:31 FUNCTION_CALL thingBody -> known_edge app.jsx:18:1 thingBody
21+
app.jsx:24:7 JSX_ELEMENT <Card /> -> known_edge parts.jsx:3:1 PlainInner
22+
app.jsx:25:7 JSX_ELEMENT <DefMemo /> -> known_edge parts.jsx:4:1 DefaultMemoInner
23+
app.jsx:26:7 JSX_ELEMENT <MemoFwd /> -> known_edge parts.jsx:6:40 MemoFwdInner
24+
app.jsx:27:7 JSX_ELEMENT <Forwarded /> -> known_edge parts.jsx:6:40 MemoFwdInner
25+
app.jsx:28:7 JSX_ELEMENT <Observed /> -> known_edge parts.jsx:5:1 ObservedInner
26+
app.jsx:29:7 JSX_ELEMENT <Dotted /> -> known_edge app.jsx:11:27 DottedInner
27+
app.jsx:30:7 JSX_ELEMENT <LazyBlock /> -> known_edge heavy.jsx:1:1 ObjNamed
28+
app.jsx:31:7 JSX_ELEMENT <LazyConcise /> -> known_edge heavy.jsx:1:1 ObjNamed
29+
app.jsx:32:7 JSX_ELEMENT <LazyDefault /> -> known_edge heavy.jsx:2:1 HeavyDefault
30+
app.jsx:33:7 JSX_ELEMENT <Thing /> -> ambiguous_unknown -
31+
app.jsx:41:3 FUNCTION_CALL PlainInner -> known_edge parts.jsx:3:1 PlainInner
32+
app.jsx:42:3 FUNCTION_CALL Card -> ambiguous_unknown -
33+
app.jsx:43:10 JSX_ELEMENT <Plain /> -> known_edge parts.jsx:7:1 Plain
34+
app.jsx:9:14 FUNCTION_CALL memo -> ambiguous_unknown -
35+
app.jsx:9:14 FUNCTION_CALL memo -> callback_registered parts.jsx:3:1 PlainInner
36+
defmemo.jsx:4:16 FUNCTION_CALL memo -> ambiguous_unknown -
37+
defmemo.jsx:4:16 FUNCTION_CALL memo -> callback_registered parts.jsx:4:1 DefaultMemoInner
38+
parts.jsx:6:24 FUNCTION_CALL memo -> ambiguous_unknown -
39+
parts.jsx:6:29 FUNCTION_CALL forwardRef -> ambiguous_unknown -
40+
parts.jsx:6:29 FUNCTION_CALL forwardRef -> callback_registered parts.jsx:6:40 MemoFwdInner
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
app.jsx:13:30 METHOD_CALL then LIB_MISSED
2+
app.jsx:14:32 METHOD_CALL then LIB_MISSED
3+
app.jsx:19:31 FUNCTION_CALL thingBody EXACT app.jsx:18:1
4+
app.jsx:41:3 FUNCTION_CALL PlainInner EXACT parts.jsx:3:1
5+
# defects: 0

0 commit comments

Comments
 (0)