diff --git a/graph/test/typescript/cases/100-destructure-from-type-literal/src/globals.d.ts b/graph/test/typescript/cases/100-destructure-from-type-literal/src/globals.d.ts new file mode 100644 index 000000000..dd5843cc1 --- /dev/null +++ b/graph/test/typescript/cases/100-destructure-from-type-literal/src/globals.d.ts @@ -0,0 +1,11 @@ +// The case's own standard library under `noLib`. +interface Object {} +interface Boolean {} +interface Number {} +interface String {} +interface Array { length: number; } +interface Function {} +interface CallableFunction extends Function {} +interface NewableFunction extends Function {} +interface IArguments {} +interface RegExp {} diff --git a/graph/test/typescript/cases/100-destructure-from-type-literal/src/tsconfig.json b/graph/test/typescript/cases/100-destructure-from-type-literal/src/tsconfig.json new file mode 100644 index 000000000..dcbfb4f8d --- /dev/null +++ b/graph/test/typescript/cases/100-destructure-from-type-literal/src/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022" } } diff --git a/graph/test/typescript/cases/100-destructure-from-type-literal/src/use.ts b/graph/test/typescript/cases/100-destructure-from-type-literal/src/use.ts new file mode 100644 index 000000000..1012b8536 --- /dev/null +++ b/graph/test/typescript/cases/100-destructure-from-type-literal/src/use.ts @@ -0,0 +1,47 @@ +// A name destructured out of a value whose declared type is an object TYPE LITERAL +// (inline, or through an alias) takes that member's type, as `opts.port.execute()` does. +export interface Port { execute(x: number): number } +export interface Clock { now(): number } + +export function inlineVar(opts: { port: Port; n: number }) { + const { port } = opts; + return port.execute(1); // -> Port.execute +} +export function memberAccess(opts: { port: Port }) { + return opts.port.execute(2); // -> Port.execute (already resolved: the reference) +} +type Opts = { port: Port; clock: Clock }; +export function aliasParam({ port, clock }: Opts) { + clock.now(); // -> Clock.now + return port.execute(3); // -> Port.execute +} +export function inlineParam({ port }: { port: Port }) { + return port.execute(4); // -> Port.execute +} +export function aliasVar(o: Opts) { + const { clock } = o; + return clock.now(); // -> Clock.now +} +export function nestedShape(opts: { deps: { port: Port } }) { + const { deps } = opts; + const { port } = deps; + return port.execute(5); // -> Port.execute +} +export function shapeMethod(opts: { run(x: number): number }) { + const { run } = opts; + return run(6); // -> the type literal's run +} + +// controls: no declared type, and a name the shape does not have +export function untyped(opts: any) { + const { port } = opts; + return port.execute(7); // stays unresolved +} +export function absent(opts: { clock: Clock }) { + const { port } = opts as { clock: Clock; port?: never }; + return port; // no call +} +export function otherShape(opts: { port: { execute(x: number): number } }) { + const { port } = opts; + return port.execute(8); // -> its own literal's execute, NOT Port.execute +} diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.edges b/graph/test/typescript/expected/100-destructure-from-type-literal.edges new file mode 100644 index 000000000..6b4bede19 --- /dev/null +++ b/graph/test/typescript/expected/100-destructure-from-type-literal.edges @@ -0,0 +1,10 @@ +ambiguous_unknown METHOD_CALL use#untyped(any) @L38 -> - +known_edge FUNCTION_CALL use#shapeMethod({ run(x: number): number }) @L32 -> use#run(number) +known_edge METHOD_CALL use#aliasParam(Opts) @L15 -> Clock#now() +known_edge METHOD_CALL use#aliasParam(Opts) @L16 -> Port#execute(number) +known_edge METHOD_CALL use#aliasVar(Opts) @L23 -> Clock#now() +known_edge METHOD_CALL use#inlineParam({ port: Port }) @L19 -> Port#execute(number) +known_edge METHOD_CALL use#inlineVar({ port: Port; n: number }) @L8 -> Port#execute(number) +known_edge METHOD_CALL use#memberAccess({ port: Port }) @L11 -> Port#execute(number) +known_edge METHOD_CALL use#nestedShape({ deps: { port: Port } }) @L28 -> Port#execute(number) +known_edge METHOD_CALL use#otherShape({ port: { execute(x: number): number } }) @L46 -> use#execute(number) diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.entries b/graph/test/typescript/expected/100-destructure-from-type-literal.entries new file mode 100644 index 000000000..17fbd316e --- /dev/null +++ b/graph/test/typescript/expected/100-destructure-from-type-literal.entries @@ -0,0 +1,13 @@ +── entry_point (12) ── + exported_from_entry_module use#absent use.ts:40 + exported_from_entry_module use#aliasParam use.ts:14 + exported_from_entry_module use#aliasVar use.ts:21 + exported_from_entry_module use#inlineParam use.ts:18 + exported_from_entry_module use#inlineVar use.ts:6 + exported_from_entry_module use#memberAccess use.ts:10 + exported_from_entry_module use#nestedShape use.ts:25 + exported_from_entry_module use#otherShape use.ts:44 + exported_from_entry_module use#shapeMethod use.ts:30 + exported_from_entry_module use#untyped use.ts:36 + unimported_module globals# globals.d.ts:1 + unimported_module use# use.ts:1 diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.fields b/graph/test/typescript/expected/100-destructure-from-type-literal.fields new file mode 100644 index 000000000..33a761ca6 --- /dev/null +++ b/graph/test/typescript/expected/100-destructure-from-type-literal.fields @@ -0,0 +1 @@ +known_edge read use#memberAccess({ port: Port }) -> { port: Port }#port diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.fields-oracle b/graph/test/typescript/expected/100-destructure-from-type-literal.fields-oracle new file mode 100644 index 000000000..503afa2c0 --- /dev/null +++ b/graph/test/typescript/expected/100-destructure-from-type-literal.fields-oracle @@ -0,0 +1,9 @@ +100-destructure-from-type-literal [fields] + precision 0.0000 (0 correct, 1 wrong) + recall 0.0000 (0 of 1 the compiler resolved) + sites 1 resolved 1 (100.0%) + tiers known_edge=1 + access read=1 + not scored: 0 rows whose target is not a client declaration + WRONG use#memberAccess({ port: Port }) READ { port: Port }#port + MISSING use#memberAccess({ port: Port }) READ use#port diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.oracle b/graph/test/typescript/expected/100-destructure-from-type-literal.oracle new file mode 100644 index 000000000..dbaf45eaa --- /dev/null +++ b/graph/test/typescript/expected/100-destructure-from-type-literal.oracle @@ -0,0 +1 @@ +oracle=9 engine=9 agree=9 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.type-use b/graph/test/typescript/expected/100-destructure-from-type-literal.type-use new file mode 100644 index 000000000..07d5aaba0 --- /dev/null +++ b/graph/test/typescript/expected/100-destructure-from-type-literal.type-use @@ -0,0 +1,9 @@ +known_edge FIELD_TYPE 1 Opts [TYPE] -> Clock +known_edge FIELD_TYPE 1 Opts [TYPE] -> Port +known_edge FIELD_TYPE 1 use [EXPRESSION] -> Clock +known_edge FIELD_TYPE 1 use [METHOD_PARAM] -> Clock +known_edge FIELD_TYPE 1 use [METHOD_PARAM] -> Port +known_edge FIELD_TYPE 2 use [METHOD_PARAM] -> Port +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Opts +known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function +known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.types-oracle b/graph/test/typescript/expected/100-destructure-from-type-literal.types-oracle new file mode 100644 index 000000000..feb77f3a8 --- /dev/null +++ b/graph/test/typescript/expected/100-destructure-from-type-literal.types-oracle @@ -0,0 +1,7 @@ +100-destructure-from-type-literal [types] + precision 1.0000 (7 correct, 0 wrong) + recall 1.0000 (7 of 7 the compiler resolved) + sites 12 resolved 12 (100.0%) + tiers known_edge=12 + contexts FIELD_TYPE=8 METHOD_PARAM=2 SUPER_TYPE=2 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/75-jsx-wrapper-guards.edges b/graph/test/typescript/expected/75-jsx-wrapper-guards.edges index 78ebdfe74..562520af9 100644 --- a/graph/test/typescript/expected/75-jsx-wrapper-guards.edges +++ b/graph/test/typescript/expected/75-jsx-wrapper-guards.edges @@ -7,14 +7,9 @@ ambient_terminal FUNCTION_CALL app#() @L22 -> parts#dynamic(() =Fc ambient_terminal FUNCTION_CALL app#() @L29 -> parts#memo(Fc) ambient_terminal FUNCTION_CALL app#() @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#(?,?) @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#() @L15 -> - ambiguous_unknown METHOD_CALL app#() @L16 -> - ambiguous_unknown METHOD_CALL app#() @L18 -> - @@ -30,7 +25,7 @@ callback_registered FUNCTION_CALL app#() @L30 -> app#ObservedImpl() callback_registered METHOD_CALL app#() @L15 -> app#(?) callback_registered METHOD_CALL app#() @L16 -> app#(?) callback_registered METHOD_CALL app#() @L18 -> app#(?) -callback_registered METHOD_CALL app#ArrowRenameTag({ icon: Fc }[]) @L57 -> app#(?,?) +callback_registered METHOD_CALL app#ArrowRenameTag({ icon: Fc }[]) @L57 -> app#(?) intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L15 -> - intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L16 -> - intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L18 -> - @@ -41,10 +36,13 @@ known_edge FUNCTION_CALL app#() @L25 -> app#factoryArgBody() known_edge FUNCTION_CALL app#() @L25 -> parts#makeThing(() =) known_edge FUNCTION_CALL app#() @L26 -> parts#useMemoLike(() =,unknown[]) known_edge FUNCTION_CALL app#MemoInner() @L29 -> app#memoBody() +known_edge FUNCTION_CALL app#RenameCall({ run: () =) @L67 -> app#() +known_edge FUNCTION_CALL app#ShorthandCall({ Button: () =) @L64 -> app#() 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#() known_edge FUNCTION_CALL parts#useMemoLike(() =,unknown[]) @L27 -> parts#() +known_edge JSX_COMPONENT_CALL app#(?) @L57 -> parts#(T) known_edge JSX_COMPONENT_CALL app#Page() @L35 -> heavy#NamedHeavy() known_edge JSX_COMPONENT_CALL app#Page() @L36 -> heavy#NamedHeavy() known_edge JSX_COMPONENT_CALL app#Page() @L37 -> heavy#NamedHeavy() @@ -55,4 +53,6 @@ known_edge JSX_COMPONENT_CALL app#Page() @L41 -> parts#(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#(T) +known_edge JSX_COMPONENT_CALL app#RenameTag({ icon: Fc }) @L54 -> parts#(T) +known_edge JSX_COMPONENT_CALL app#ShorthandTag({ Button: Fc }) @L51 -> parts#(T) known_edge JSX_COMPONENT_CALL app#UsesButton() @L72 -> ui/Button#Button({ label: string }) diff --git a/graph/test/typescript/expected/75-jsx-wrapper-guards.known-missing b/graph/test/typescript/expected/75-jsx-wrapper-guards.known-missing index 4b3ae9b66..cb34e31cb 100644 --- a/graph/test/typescript/expected/75-jsx-wrapper-guards.known-missing +++ b/graph/test/typescript/expected/75-jsx-wrapper-guards.known-missing @@ -2,15 +2,6 @@ # 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#(?) -> parts#(T) -app#RenameTag({ icon: Fc }) -> parts#(T) -app#ShorthandTag({ Button: Fc }) -> parts#(T) -app#RenameCall({ run: () =) -> app#() -app#ShorthandCall({ Button: () =) -> app#() # `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 diff --git a/graph/test/typescript/expected/75-jsx-wrapper-guards.oracle b/graph/test/typescript/expected/75-jsx-wrapper-guards.oracle index ff8858f30..94c50e44c 100644 --- a/graph/test/typescript/expected/75-jsx-wrapper-guards.oracle +++ b/graph/test/typescript/expected/75-jsx-wrapper-guards.oracle @@ -1,10 +1,5 @@ -oracle=21 engine=19 agree=15 missing=6 (known 6, NEW 0) extra=4 - known app#(?) -> parts#(T) +oracle=21 engine=24 agree=20 missing=1 (known 1, NEW 0) extra=4 known app#Page() -> app#MadeInner() - known app#RenameCall({ run: () =) -> app#() - known app#RenameTag({ icon: Fc }) -> parts#(T) - known app#ShorthandCall({ Button: () =) -> app#() - known app#ShorthandTag({ Button: Fc }) -> parts#(T) extra app#Page() -> app#MemoInner() extra app#Page() -> app#ObservedImpl() extra app#Page() -> heavy#Heavy() diff --git a/graph/test/typescript/tools/normalize_edges.py b/graph/test/typescript/tools/normalize_edges.py index 9807d7574..ad8513cbd 100644 --- a/graph/test/typescript/tools/normalize_edges.py +++ b/graph/test/typescript/tools/normalize_edges.py @@ -198,7 +198,15 @@ def enclosing_owner(type_hash): for r in rows(f'{ir}/all-typescript-methods.csv'): h = r['tsMethodUniqueHash'] ps = [] - for p in sorted(params.get(h, []), key=lambda x: int(x.get('position') or 0)): + # A destructured parameter has a row for the pattern and one per name it binds, all at the pattern's + # position (bindingSourceKind PROPERTY/INDEX): one parameter, labelled once, as the compiler prints it. + by_pos = {} + for p in params.get(h, []): + pos = int(p.get('position') or 0) + bound = (p.get('bindingSourceKind') or 'NONE') != 'NONE' + if pos not in by_pos or (by_pos[pos][0] and not bound): + by_pos[pos] = (bound, p) + for _, p in sorted((pos, bp[1]) for pos, bp in by_pos.items()): if p.get('isReceiverParameter') == 'true': continue t = simple(p.get('parameterTypeName') or p.get('parameterBaseType')) diff --git a/graph/typescript/engine/resolution/reference-types.dl b/graph/typescript/engine/resolution/reference-types.dl index 04b5345bb..831f1f27a 100644 --- a/graph/typescript/engine/resolution/reference-types.dl +++ b/graph/typescript/engine/resolution/reference-types.dl @@ -494,6 +494,28 @@ param_binding_method(bh, m2) :- param_binding(_, _, _, "PROPERTY", src, _, bh), scope_sibling(t, sib), declared_method(sib, src, _, m2). +// (c) the pattern is typed by an OBJECT TYPE LITERAL — `({ port }: { port: Port })`, or +// `({ port }: Opts)` with `type Opts = { port: Port }`. A type literal has no declaration +// to give param_type; it is a SHAPE (param_shape_target), and a shape is its own scope +// (scope_sibling(s, s)), so the field and method lookups above read off it unchanged. +binding_pattern_shape(bh, sh) :- param_binding(_, _, pos, "PROPERTY", _, m, bh), + param_decl(_, _, pos, _, m, ph), + param_shape_target(ph, sh). +param_type(bh, prov, ft) :- param_binding(_, _, _, "PROPERTY", src, _, bh), + binding_pattern_shape(bh, sh), + scope_sibling(sh, sib), + declared_field(sib, src, _, f), + field_type(f, prov, ft). +param_shape_target(bh, fs) :- param_binding(_, _, _, "PROPERTY", src, _, bh), + binding_pattern_shape(bh, sh), + scope_sibling(sh, sib), + declared_field(sib, src, _, f), + field_shape(f, fs). +param_binding_method(bh, m2) :- param_binding(_, _, _, "PROPERTY", src, _, bh), + binding_pattern_shape(bh, sh), + scope_sibling(sh, sib), + declared_method(sib, src, _, m2). + // ── a DESTRUCTURED VARIABLE takes the type of the member it names ─────────── // `const { helper, nested } = ctx` — the mirror of the parameter case, and the more @@ -527,6 +549,27 @@ var_binding_method(v, m) :- var_binding("client", _, "PROPERTY", src, _, v), scope_sibling(t, sib), declared_method(sib, src, _, m). +// (c) the initializer has a SHAPE, not a type — `const { port } = opts` with +// `opts: { port: Port }`. `opts.port.execute()` already resolved through the shape +// (expr_shape off a shape qualifier); the destructured name beside it did not. +var_binding_shape(v, sh) :- var_binding("client", _, "PROPERTY", _, e, v), + e != "", + expr_shape(e, sh). +var_type(v, prov, ft) :- var_binding("client", _, "PROPERTY", src, _, v), + var_binding_shape(v, sh), + scope_sibling(sh, sib), + declared_field(sib, src, _, f), + field_type(f, prov, ft). +var_shape(v, fs) :- var_binding("client", _, "PROPERTY", src, _, v), + var_binding_shape(v, sh), + scope_sibling(sh, sib), + declared_field(sib, src, _, f), + field_shape(f, fs). +var_binding_method(v, m) :- var_binding("client", _, "PROPERTY", src, _, v), + var_binding_shape(v, sh), + scope_sibling(sh, sib), + declared_method(sib, src, _, m). + // ── an INDEX-DESTRUCTURED VARIABLE takes the ELEMENT type of its source ───── // `const [first] = ops` and `const [a, b] = makePair()`. The parser has emitted these diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index a703e6015..ae3f25e50 100644 --- a/graph/typescript/souffle/decls_all.dl +++ b/graph/typescript/souffle/decls_all.dl @@ -464,6 +464,7 @@ .decl owner_kind_is_module_level(c0:symbol) .decl owner_name_has_overload_signature(c0:symbol,c1:symbol) .decl param_call_signature(c0:symbol,c1:symbol) +.decl binding_pattern_shape(c0:symbol,c1:symbol) .decl binding_pattern_type(c0:symbol,c1:symbol,c2:symbol) .decl param_binding_method(c0:symbol,c1:symbol) .decl param_binding(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol,c6:symbol) @@ -641,6 +642,7 @@ .decl library_receiver(c0:symbol,c1:symbol) .decl unresolved_receiver(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl var_binding_method(c0:symbol,c1:symbol) +.decl var_binding_shape(c0:symbol,c1:symbol) .decl var_binding_target(c0:symbol,c1:symbol,c2:symbol) .decl var_binding(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol) .decl var_is_destructured(c0:symbol)