Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions graph/test/java/expected/61-var-local-type.known-missing
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Accepted gap, measured by the bytecode oracle in CI: a `var` local whose type comes from something
# other than its initializer expression is left a declared unknown. Before #1401 each of these was a
# wrong `external:var.<m>` edge; now the receiver is honestly untyped. Three shapes:
# - the element variable of an enhanced for loop (`for (var w : widgets)`): its type is the
# iterable's element type;
# - a try-with-resources variable (`try (var w = open())` where open() is not typed client-side);
# - `var` lambda parameters (`(var w) -> w.paint()`): the type comes from the target functional
# interface.
# Follow-up: type `var` in these three shapes. It cannot rot: when one starts resolving, this line
# reports NOW-FIXED and the case fails until it is removed.
probe.VarLocals#fromEnhancedFor -> probe.Widget#paint()
probe.VarLocals#fromResource -> probe.Widget#close()
probe.VarLocals#lambdaParams -> probe.Widget#paint()
4 changes: 4 additions & 0 deletions graph/test/java/expected/61-var-local-type.oracle
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
oracle=20 engine=17 agree=17 missing=3 (known 3, NEW 0) extra=0
known-missing probe.VarLocals#fromEnhancedFor -> probe.Widget#paint()
known-missing probe.VarLocals#fromResource -> probe.Widget#close()
known-missing probe.VarLocals#lambdaParams -> probe.Widget#paint()
2 changes: 1 addition & 1 deletion graph/test/java/expected/oracle-agreement.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cases compared 45 agreeing 45
cases compared 46 agreeing 46
CONSTRUCTOR-RULE disagreements: 0 cases, 0 rows
OTHER disagreements: 0
rows naming no method: 0
Expand Down
73 changes: 73 additions & 0 deletions graph/test/typescript/cases/75-jsx-wrapper-guards/src/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { dynamic, lazy, makeThing, memo, observer, useMemoLike, type Fc } from "./parts";
import { Button } from "./ui/Button";

export function factoryArgBody(): number {
return 3;
}
export function memoBody(): number {
return 4;
}
function ObservedImpl(): string {
return "observed";
}

// A loader that projects a NAMED export: the component is NamedHeavy, never Heavy.
const LazyNamed = lazy(() => import("./heavy").then((m) => ({ default: m.NamedHeavy })));
const DynNamed = dynamic(() => import("./heavy").then((m) => m.NamedHeavy));
// …and the same projections written with a block body, where the returned value is a row.
const LazyBlock = lazy(() => import("./heavy").then((m) => { return { default: m.NamedHeavy }; }));
const DynAwait = dynamic(async () => { const m = await import("./heavy"); return m.NamedHeavy; });
// CONTROL: a loader returning the module itself renders its default export.
const LazyDefault = lazy(() => import("./heavy"));
const DynDefault = dynamic(() => import("./heavy"));

// NOT wrappers: argument 0 is not what the product renders.
const Thing = makeThing(() => factoryArgBody());
const Made = useMemoLike(() => function MadeInner(): string { return "made"; }, []);

// CONTROL: real wrappers still reach their argument.
const Memoed = memo(function MemoInner(): string { return String(memoBody()); });
const Observed = observer(ObservedImpl);

export function Page(): string {
return (
<main>
<LazyNamed />
<DynNamed />
<LazyBlock />
<DynAwait />
<LazyDefault />
<DynDefault />
<Thing />
<Made />
<Memoed />
<Observed />
</main>
);
}

// A destructured prop SHADOWS the imported Button: none of these render ui/Button.
export function ShorthandTag({ Button }: { Button: Fc<{}> }): string {
return <Button />;
}
export function RenameTag({ icon: Button }: { icon: Fc<{}> }): string {
return <Button />;
}
export function ArrowRenameTag(xs: { icon: Fc<{}> }[]): string[] {
return xs.map(({ icon: Button }) => <Button />);
}
export function PlainParamTag(Button: Fc<{}>): string {
return <Button />;
}
// …and the same for ordinary calls.
export function ShorthandCall({ Button }: { Button: () => string }): string {
return Button();
}
export function RenameCall({ run: Button }: { run: () => string }): string {
return Button();
}

// CONTROL: with nothing shadowing it, the tag is the imported component.
export function UsesButton(): string {
return <Button label="ok" />;
}
16 changes: 16 additions & 0 deletions graph/test/typescript/cases/75-jsx-wrapper-guards/src/heavy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// A module with a default AND a named component. A loader that projects the named one
// out of the module (`.then(m => m.NamedHeavy)`) must not land on the default.
export function heavyWork(): number {
return 1;
}
export function namedWork(): number {
return 2;
}

export default function Heavy(): string {
return String(heavyWork());
}

export function NamedHeavy(): string {
return String(namedWork());
}
28 changes: 28 additions & 0 deletions graph/test/typescript/cases/75-jsx-wrapper-guards/src/parts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Wrappers, loaders and ordinary factories, declared with the signatures that matter.
// No React dependency.
declare global {
namespace JSX {
type Element = string;
interface ElementAttributesProperty {
props: unknown;
}
interface IntrinsicElements {
[name: string]: unknown;
}
}
}

export type Fc<P> = (props: P) => string;
export declare function memo<P>(component: Fc<P>): Fc<P>;
export declare function observer<P>(component: Fc<P>): Fc<P>;
export declare function lazy(load: () => Promise<{ default: Fc<any> }>): Fc<any>;
export declare function dynamic(load: () => Promise<Fc<any> | { default: Fc<any> }>): Fc<any>;

// NOT component wrappers: what they return is not argument 0.
export function makeThing(f: () => number): Fc<{}> {
f();
return () => "thing";
}
export function useMemoLike<T>(f: () => T, deps: unknown[]): T {
return f();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function Button(p: { label: string }): string {
return p.label;
}
46 changes: 46 additions & 0 deletions graph/test/typescript/expected/75-jsx-wrapper-guards.edges
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
ambient_terminal FUNCTION_CALL app#<module-init>() @L15 -> parts#lazy(() ={ default: Fc })
ambient_terminal FUNCTION_CALL app#<module-init>() @L16 -> parts#dynamic(() =Fc | { default: Fc })
ambient_terminal FUNCTION_CALL app#<module-init>() @L18 -> parts#lazy(() ={ default: Fc })
ambient_terminal FUNCTION_CALL app#<module-init>() @L19 -> parts#dynamic(() =Fc | { default: Fc })
ambient_terminal FUNCTION_CALL app#<module-init>() @L21 -> parts#lazy(() ={ default: Fc })
ambient_terminal FUNCTION_CALL app#<module-init>() @L22 -> parts#dynamic(() =Fc | { default: Fc })
ambient_terminal FUNCTION_CALL app#<module-init>() @L29 -> parts#memo(Fc)
ambient_terminal FUNCTION_CALL app#<module-init>() @L30 -> parts#observer(Fc)
ambiguous_unknown FUNCTION_CALL app#MemoInner() @L29 -> -
ambiguous_unknown FUNCTION_CALL app#RenameCall({ run: () =,{ run: () =) @L67 -> -
ambiguous_unknown FUNCTION_CALL app#ShorthandCall({ Button: () =,{ Button: () =) @L64 -> -
ambiguous_unknown FUNCTION_CALL heavy#Heavy() @L11 -> -
ambiguous_unknown FUNCTION_CALL heavy#NamedHeavy() @L15 -> -
ambiguous_unknown JSX_COMPONENT_CALL app#<arrow@57>(?,?) @L57 -> -
ambiguous_unknown JSX_COMPONENT_CALL app#Page() @L42 -> -
ambiguous_unknown JSX_COMPONENT_CALL app#RenameTag({ icon: Fc },{ icon: Fc }) @L54 -> -
ambiguous_unknown JSX_COMPONENT_CALL app#ShorthandTag({ Button: Fc },{ Button: Fc }) @L51 -> -
ambiguous_unknown METHOD_CALL app#<arrow@15>() @L15 -> -
ambiguous_unknown METHOD_CALL app#<arrow@16>() @L16 -> -
ambiguous_unknown METHOD_CALL app#<arrow@18>() @L18 -> -
ambiguous_unknown METHOD_CALL app#ArrowRenameTag({ icon: Fc }[]) @L57 -> -
intrinsic_terminal DYNAMIC_IMPORT_CALL app#<arrow@15>() @L15 -> -
intrinsic_terminal DYNAMIC_IMPORT_CALL app#<arrow@16>() @L16 -> -
intrinsic_terminal DYNAMIC_IMPORT_CALL app#<arrow@18>() @L18 -> -
intrinsic_terminal DYNAMIC_IMPORT_CALL app#<arrow@19>() @L19 -> -
intrinsic_terminal DYNAMIC_IMPORT_CALL app#<arrow@21>() @L21 -> -
intrinsic_terminal DYNAMIC_IMPORT_CALL app#<arrow@22>() @L22 -> -
known_edge FUNCTION_CALL app#<arrow@25>() @L25 -> app#factoryArgBody()
known_edge FUNCTION_CALL app#<module-init>() @L25 -> parts#makeThing(() =)
known_edge FUNCTION_CALL app#<module-init>() @L26 -> parts#useMemoLike(() =,unknown[])
known_edge FUNCTION_CALL app#MemoInner() @L29 -> app#memoBody()
known_edge FUNCTION_CALL heavy#Heavy() @L11 -> heavy#heavyWork()
known_edge FUNCTION_CALL heavy#NamedHeavy() @L15 -> heavy#namedWork()
known_edge FUNCTION_CALL parts#makeThing(() =) @L23 -> parts#<arrow@22>()
known_edge FUNCTION_CALL parts#useMemoLike(() =,unknown[]) @L27 -> parts#<arrow@26>()
known_edge JSX_COMPONENT_CALL app#Page() @L35 -> parts#<arrow@15>(T)
known_edge JSX_COMPONENT_CALL app#Page() @L36 -> parts#<arrow@15>(T)
known_edge JSX_COMPONENT_CALL app#Page() @L37 -> heavy#NamedHeavy()
known_edge JSX_COMPONENT_CALL app#Page() @L38 -> heavy#NamedHeavy()
known_edge JSX_COMPONENT_CALL app#Page() @L39 -> heavy#Heavy()
known_edge JSX_COMPONENT_CALL app#Page() @L40 -> heavy#Heavy()
known_edge JSX_COMPONENT_CALL app#Page() @L41 -> parts#<arrow@15>(T)
known_edge JSX_COMPONENT_CALL app#Page() @L43 -> app#MemoInner()
known_edge JSX_COMPONENT_CALL app#Page() @L44 -> app#ObservedImpl()
known_edge JSX_COMPONENT_CALL app#PlainParamTag(Fc) @L60 -> parts#<arrow@15>(T)
known_edge JSX_COMPONENT_CALL app#UsesButton() @L72 -> ui/Button#Button({ label: string })
12 changes: 12 additions & 0 deletions graph/test/typescript/expected/75-jsx-wrapper-guards.entries
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
── entry_point (11) ──
exported_from_entry_module app#ArrowRenameTag app.tsx:56
exported_from_entry_module app#Page app.tsx:32
exported_from_entry_module app#PlainParamTag app.tsx:59
exported_from_entry_module app#RenameCall app.tsx:66
exported_from_entry_module app#RenameTag app.tsx:53
exported_from_entry_module app#ShorthandCall app.tsx:63
exported_from_entry_module app#ShorthandTag app.tsx:50
exported_from_entry_module app#UsesButton app.tsx:71
exported_from_entry_module app#factoryArgBody app.tsx:4
exported_from_entry_module app#memoBody app.tsx:7
unimported_module app#<module> app.tsx:1
15 changes: 15 additions & 0 deletions graph/test/typescript/expected/75-jsx-wrapper-guards.envelope
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
value parts#<function-type>@15:21 -> app#MemoInner
value parts#<function-type>@15:21 -> app#ObservedImpl
value parts#<function-type>@15:21 -> parts#<arrow>
value parts#<function-type>@18:36 -> app#<arrow>@15
value parts#<function-type>@18:36 -> app#<arrow>@18
value parts#<function-type>@18:36 -> app#<arrow>@21
value parts#<function-type>@19:39 -> app#<arrow>@16
value parts#<function-type>@19:39 -> app#<arrow>@19
value parts#<function-type>@19:39 -> app#<arrow>@22
value parts#<function-type>@22:30 -> app#<arrow>@25
value parts#<function-type>@26:35 -> app#<arrow>@26
value_call app#Page -> app#MadeInner
value_call app#Page -> parts#<arrow>
value_call parts#makeThing -> app#<arrow>@25
value_call parts#useMemoLike -> app#<arrow>@26
4 changes: 4 additions & 0 deletions graph/test/typescript/expected/75-jsx-wrapper-guards.fields
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ambiguous_unknown read app#<arrow@18>(?) -> -
ambiguous_unknown read app#<arrow@19>() -> -
ambiguous_unknown read app#<module-init>() -> -
known_edge read ui/Button#Button({ label: string }) -> { label: string }#label
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
75-jsx-wrapper-guards [fields]
precision 0.0000 (0 correct, 1 wrong)
recall 0.0000 (0 of 1 the compiler resolved)
sites 5 resolved 1 (20.0%)
tiers ambiguous_unknown=4 known_edge=1
access read=5
not scored: 4 rows whose target is not a client declaration
WRONG ui/Button#Button({ label: string }) READ { label: string }#label
MISSING ui/Button#Button({ label: string }) READ ui/Button#label
18 changes: 18 additions & 0 deletions graph/test/typescript/expected/75-jsx-wrapper-guards.known-missing
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Accepted gaps for 75-jsx-wrapper-guards — each line is an edge the TypeScript compiler
# resolves and this engine does not. A NEW missing edge fails the suite;
# a line here that STARTS working also fails, so the debt cannot rot.
#
# A name bound by a DESTRUCTURED parameter carries no declared type the engine reads, so
# the call through it stays unresolved. The compiler names the prop's type signature.
# Before this case these sites resolved to the same-named IMPORT (a wrong edge); unresolved
# is the honest answer until destructured parameters are typed.
app#<arrow@57>(?) -> parts#<arrow@15>(T)
app#RenameTag({ icon: Fc }) -> parts#<arrow@15>(T)
app#ShorthandTag({ Button: Fc }) -> parts#<arrow@15>(T)
app#RenameCall({ run: () =) -> app#<arrow@66>()
app#ShorthandCall({ Button: () =) -> app#<arrow@63>()
# `useMemoLike(() => function MadeInner…)` returns what its argument RETURNS, through a
# generic T. Argument 0 is not a component wrapper, so the tag no longer lands on the
# argument arrow; following the generic return to MadeInner is value flow the engine
# does not do here.
app#Page() -> app#MadeInner()
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Accepted client->client gaps for 75-jsx-wrapper-guards (library pass) — each line is an edge the TypeScript compiler
# resolves and this engine does not. A NEW missing edge fails the suite;
# a line here that STARTS working also fails, so the debt cannot rot.
#
# A name bound by a DESTRUCTURED parameter carries no declared type the engine reads, so
# the call through it stays unresolved. The compiler names the prop's type signature.
# Before this case these sites resolved to the same-named IMPORT (a wrong edge); unresolved
# is the honest answer until destructured parameters are typed.
app#<arrow@57>(?) -> parts#<arrow@15>(T)
app#RenameTag({ icon: Fc }) -> parts#<arrow@15>(T)
app#ShorthandTag({ Button: Fc }) -> parts#<arrow@15>(T)
app#RenameCall({ run: () =) -> app#<arrow@66>()
app#ShorthandCall({ Button: () =) -> app#<arrow@63>()
# `useMemoLike(() => function MadeInner…)` returns what its argument RETURNS, through a
# generic T. Argument 0 is not a component wrapper, so the tag no longer lands on the
# argument arrow; following the generic return to MadeInner is value flow the engine
# does not do here.
app#Page() -> app#MadeInner()
11 changes: 11 additions & 0 deletions graph/test/typescript/expected/75-jsx-wrapper-guards.oracle
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
oracle=21 engine=19 agree=15 missing=6 (known 6, NEW 0) extra=4
known app#<arrow@57>(?) -> parts#<arrow@15>(T)
known app#Page() -> app#MadeInner()
known app#RenameCall({ run: () =) -> app#<arrow@66>()
known app#RenameTag({ icon: Fc }) -> parts#<arrow@15>(T)
known app#ShorthandCall({ Button: () =) -> app#<arrow@63>()
known app#ShorthandTag({ Button: Fc }) -> parts#<arrow@15>(T)
extra app#Page() -> app#MemoInner()
extra app#Page() -> app#ObservedImpl()
extra app#Page() -> heavy#Heavy()
extra app#Page() -> heavy#NamedHeavy()
9 changes: 9 additions & 0 deletions graph/test/typescript/expected/75-jsx-wrapper-guards.type-use
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ambiguous_unknown METHOD_RETURN 1 parts [METHOD_PARAM] -> -
known_edge FIELD_TYPE 1 app [METHOD_PARAM] -> Fc
known_edge FIELD_TYPE 2 app [METHOD_PARAM] -> Fc
known_edge FIELD_TYPE 3 parts [METHOD_PARAM] -> Fc
known_edge FIELD_TYPE 4 parts [METHOD_PARAM] -> Fc
known_edge METHOD_PARAM 0 app [METHOD_PARAM] -> Fc
known_edge METHOD_PARAM 0 parts [METHOD_PARAM] -> Fc
known_edge METHOD_RETURN 0 parts [METHOD] -> Fc
known_edge TYPE_ELEMENT 3 parts [METHOD_PARAM] -> Fc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
75-jsx-wrapper-guards [types]
precision 1.0000 (2 correct, 0 wrong)
recall 1.0000 (2 of 2 the compiler resolved)
sites 16 resolved 14 (87.5%)
tiers ambiguous_unknown=2 known_edge=14
contexts FIELD_TYPE=5 METHOD_PARAM=3 METHOD_RETURN=7 TYPE_ELEMENT=1
not scored: 2 rows whose target is not a client declaration
4 changes: 2 additions & 2 deletions graph/typescript/PARSER-DEFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ day the defect is fixed.

**Status: fixed.** A component element emits a JSX_ELEMENT / JSX_SELF_CLOSING row and a
JSX_COMPONENT_CALL site; `callee-resolution.dl` rule (13) resolves it, and also follows a
wrapper's argument 0 (`memo`, `forwardRef`) and a `lazy()` loader's default export. Pinned
by cases 10 and 74. The measurement below is kept as the record of the defect.
wrapper's argument 0 (`memo`, `forwardRef`, `observer`, `withX`) and what a `lazy` /
`dynamic` loader settles to. Pinned by cases 10, 74 and 75. The measurement below is kept as the record of the defect.

**Severity: blocking for any React / Preact / Solid codebase.**

Expand Down
9 changes: 6 additions & 3 deletions graph/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,12 @@ demonstrable false positive.
Stated because a graph you cannot trust the boundaries of is not useful.

* **A JSX member tag is only as good as its receiver.** `<Badge/>` is a
`JSX_COMPONENT_CALL` resolved by tag name, including through `memo()` / `forwardRef()`
wrappers and `lazy(() => import(...))`; `<ui.Card/>` is a `METHOD_CALL` and resolves
only where `ui.Card(...)` would. `<div>` produces no site.
`JSX_COMPONENT_CALL` resolved by tag name, including through the component wrappers
(`memo`, `forwardRef`, `observer`, a `withX` HOC) and a `lazy` / `dynamic` loader;
`<ui.Card/>` is a `METHOD_CALL` and resolves only where `ui.Card(...)` would. `<div>`
produces no site. A tag bound by a destructured prop (`({ icon: Icon }) => <Icon/>`)
is the prop, not a same-named import, and stays unresolved; so does a loader whose
`.then` projects a named export through a concise arrow body.
* **Generic inference through a callback** — `map<U>(f: (t: T) => U): U[]` — is not
substituted. `U` is bound by the argument's return type, which is inference rather
than substitution, and guessing it would fabricate. Those sites stay unresolved and
Expand Down
Loading
Loading