diff --git a/graph/test/typescript/expected/74-jsx-component-forms.edges b/graph/test/typescript/expected/74-jsx-component-forms.edges index 8a9a7700b..1215ed2b5 100644 --- a/graph/test/typescript/expected/74-jsx-component-forms.edges +++ b/graph/test/typescript/expected/74-jsx-component-forms.edges @@ -10,6 +10,7 @@ callback_registered FUNCTION_CALL app#() @L4 -> app#() callback_registered FUNCTION_CALL parts#() @L33 -> parts#UserCard({ id: string }) callback_registered FUNCTION_CALL parts#() @L41 -> parts#FieldImpl({ label: string },unknown) callback_registered FUNCTION_CALL parts#() @L73 -> parts#buildDefault() +callback_registered JSX_COMPONENT_CALL app#App() @L13 -> Panel#render() intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L4 -> - known_edge FUNCTION_CALL app#useFactory() @L23 -> parts#()@L22 known_edge FUNCTION_CALL parts#FieldImpl({ label: string },unknown) @L39 -> parts#trackClick(string) diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges b/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges index 332f06303..15c58497d 100644 --- a/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges @@ -14,6 +14,7 @@ ambiguous_unknown FUNCTION_CALL parts#render() @L35 -> - ambiguous_unknown FUNCTION_CALL reg#() @L18 -> - ambiguous_unknown JSX_COMPONENT_CALL app#() @L20 -> - callback_registered FUNCTION_CALL parts#() @L40 -> parts#(?) +callback_registered JSX_COMPONENT_CALL app#() @L? -> app#onPing() known_edge FUNCTION_CALL app#onPing() @L12 -> app#helper() known_edge FUNCTION_CALL button#() @L10 -> button#leafButton() known_edge FUNCTION_CALL card#render() @L12 -> card#leafCard() diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges index 40aed3c84..92c54f4d1 100644 --- a/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges @@ -14,6 +14,7 @@ boundary_lib FUNCTION_CALL parts#() @L42 -> vue#defineNuxtComponent boundary_lib FUNCTION_CALL parts#() @L49 -> vue#makeStore(ComponentOptions) boundary_lib FUNCTION_CALL reg#() @L18 -> vue#defineComponent(ComponentOptions) callback_registered FUNCTION_CALL parts#() @L40 -> parts#(?) +callback_registered JSX_COMPONENT_CALL app#() @L? -> app#onPing() known_edge FUNCTION_CALL app#onPing() @L12 -> app#helper() known_edge FUNCTION_CALL button#() @L10 -> button#leafButton() known_edge FUNCTION_CALL card#render() @L12 -> card#leafCard() diff --git a/graph/typescript/engine/call-edge-generation/call_chain.dl b/graph/typescript/engine/call-edge-generation/call_chain.dl index 565187993..890a08ad5 100644 --- a/graph/typescript/engine/call-edge-generation/call_chain.dl +++ b/graph/typescript/engine/call-edge-generation/call_chain.dl @@ -134,6 +134,14 @@ call_chain_edge(ce, caller, "-", m, "client", "callback_registered", kind) :- caller != m, invocation_site(ce, kind). +// …and a function written inside a JSX `{…}` (resolution/value-flow.dl, jsx_handed_value). The `{…}` is +// the site: an intrinsic tag has no call site of its own, and the expression positions the edge. +call_chain_edge(r, caller, "-", m, "client", "callback_registered", "JSX_COMPONENT_CALL") :- + jsx_handed_value(r, m), + method_prov(m, "client"), + jsx_root_caller(r, caller), + caller != m. + // ── call_runs_edge(FromMethod, ToMethod, Prov) ────────────────────────────── // The chain a consumer should WALK: overload signatures re-pointed to the body that // actually executes (resolution/overload-sets.dl). Kept separate from diff --git a/graph/typescript/engine/call-edge-generation/calls.dl b/graph/typescript/engine/call-edge-generation/calls.dl index f90275c34..b4b87c8f0 100644 --- a/graph/typescript/engine/call-edge-generation/calls.dl +++ b/graph/typescript/engine/call-edge-generation/calls.dl @@ -151,5 +151,42 @@ library_value(e, spec) :- expr_kind("client", k, _, e), library_value(e, spec) :- expr_referenced("client", "VARIABLE", v, e), var_initializer("client", _, i, v), !var_reassigned(v), library_value(i, spec). call_passes_project_instance(e) :- expr_child("client", e, "ARGUMENT", _, a), expr_type(a, "client", _). +// …and the runtime's own objects: `JSON.parse(s)`, `Promise.resolve(v).then(f)`, `Object.keys(o).forEach(g)`, +// `document.createElement('a')`, `console.error(e)`. With no standard library staged the name resolves to nothing +// in the run (AMBIENT_GLOBAL), so every such call was an untyped `x.parse()` to the consumers, and a project method +// that happens to be named parse, resolve, all, keys, error or getMetadata was given every one of them as a by-name +// caller — and every test around them. The receiver is the platform's, never the project's: a program that declares +// the name itself binds it as its own variable, not as an ambient global, and stays out. Only the objects whose +// members are called through the name; `window`, `globalThis` and `self` are left out, since a program may hang its +// own functions on them. +library_value(e, n) :- expr_referenced("client", "AMBIENT_GLOBAL", _, e), expr_name("client", n, e), + builtin_global_object(n). +builtin_global_object("JSON"). +builtin_global_object("Math"). +builtin_global_object("Object"). +builtin_global_object("Reflect"). +builtin_global_object("Promise"). +builtin_global_object("Array"). +builtin_global_object("Number"). +builtin_global_object("String"). +builtin_global_object("Boolean"). +builtin_global_object("BigInt"). +builtin_global_object("Symbol"). +builtin_global_object("Date"). +builtin_global_object("RegExp"). +builtin_global_object("Error"). +builtin_global_object("Intl"). +builtin_global_object("Atomics"). +builtin_global_object("ArrayBuffer"). +builtin_global_object("console"). +builtin_global_object("process"). +builtin_global_object("Buffer"). +builtin_global_object("document"). +builtin_global_object("navigator"). +builtin_global_object("localStorage"). +builtin_global_object("sessionStorage"). +builtin_global_object("performance"). +builtin_global_object("crypto"). +builtin_global_object("URL"). library_receiver(ce, spec) :- call_site("client", _, _, rk, recv, ce, _), receiver_kind_is_value(rk), !call_resolved(ce), library_value(recv, spec). diff --git a/graph/typescript/engine/resolution/contextual-params.dl b/graph/typescript/engine/resolution/contextual-params.dl index c9f675e15..99b05b5a1 100644 --- a/graph/typescript/engine/resolution/contextual-params.dl +++ b/graph/typescript/engine/resolution/contextual-params.dl @@ -278,6 +278,20 @@ builtin_elem_ref(parent, x) :- ref_is_readonly_operator(parent), builtin_elem_ref(child, x). builtin_elem_ref(ref, x) :- ref_via_alias(ref, rhs), builtin_elem_ref(rhs, x). +// A tuple's elements are its members: `[Module, ...Module[]]`. +builtin_elem_ref(ref, child) :- type_ref(rp, "TUPLE", _, _, _, _, _, ref), + type_ref_parent(rp, ref, _, child). +// A TYPE VARIABLE constrained to a list holds that list's elements: `(ms: Ms)`, +// `(...modules: Ms)`, then `modules.map((m) => m.init())`. The +// constraint is what every element is at least, as for a `` receiver. Both routes +// ref_type_target takes: the exact link to the declaration, and the name in scope where it is absent. +builtin_elem_ref(ref, x) :- type_ref_type_param(rp, tp, ref), + type_param_constraint(rp, cref, tp), + builtin_elem_ref(cref, x). +builtin_elem_ref(ref, x) :- type_ref(_, "TYPE_VARIABLE", _, tn, _, owner, _, ref), + !type_ref_type_param(_, _, ref), + type_var_constraint_in_scope(owner, tn, cref), + builtin_elem_ref(cref, x). builtin_elem_ref(ref, a) :- ref_via_alias(ref, rhs), builtin_elem_ref(rhs, x), type_ref(_, "TYPE_VARIABLE", _, n, _, _, _, x), @@ -339,6 +353,13 @@ lambda_param_ref(lp, c) :- call_site("client", _, "then", _, recv, ce, _), !param_type_ref(_, _, lp). lambda_param_type(lp, prov, t) :- lambda_param_ref(lp, c), ref_type_target(c, prov, t). +// …and its SHAPE, where the element is an object type written as a type alias or a literal rather +// than a class or an interface: `type Plugin = { init(): void }`, then `plugins.map((p) => p.init())`. +// The element has no type declaration to dispatch on, only members, so the type clause above gave +// the parameter nothing and every call on it was unresolved; an annotated parameter of the same type +// resolved, through param_shape_target. The element reference is the same one, read as a shape. +lambda_param_shape(lp, sh) :- lambda_param_ref(lp, c), + ref_shape_target(c, sh). // The parameter's reference is its declared reference, as an annotation's would be. expr_decl_ref(e, c) :- expr_referenced("client", "PARAMETER", p, e), lambda_param_ref(p, c). diff --git a/graph/typescript/engine/resolution/type-resolution.dl b/graph/typescript/engine/resolution/type-resolution.dl index 2c8e5ef20..0d3cff9f9 100644 --- a/graph/typescript/engine/resolution/type-resolution.dl +++ b/graph/typescript/engine/resolution/type-resolution.dl @@ -587,6 +587,19 @@ ref_element_target(ref, prov, t) :- type_ref(rp, k, _, _, _, _, _, ref), ref_kind_is_array(k), type_ref_parent(rp, ref, _, child), ref_type_target(child, prov, t). +// …and of a TYPE VARIABLE constrained to an array or a tuple: `(ms: Ms)`, +// `(...modules: Ms)`, then `modules.map((m) => m.init())`. The +// constraint is what every element is at least, exactly as it is for a `` receiver +// (ref_type_target above), so the callback's parameter is typed by the constraint's element. Without it +// the element of a generic list had no type, and every call on it was unresolved. Same two routes as +// ref_type_target: the exact link to the declaration, and the name in scope where the link is absent. +ref_element_target(ref, prov, t) :- type_ref_type_param(rp, tp, ref), + type_param_constraint(rp, cref, tp), + ref_element_target(cref, prov, t). +ref_element_target(ref, prov, t) :- type_ref(_, "TYPE_VARIABLE", _, tn, _, owner, _, ref), + !type_ref_type_param(_, _, ref), + type_var_constraint_in_scope(owner, tn, cref), + ref_element_target(cref, prov, t). // ── ref_element_shape(Ref, Shape) — an element that is ITSELF a function type ── // `fns: readonly ((data: T) => boolean)[]` then `fns.every((fn) => fn(1))`. The element diff --git a/graph/typescript/engine/resolution/value-flow.dl b/graph/typescript/engine/resolution/value-flow.dl index 75e9d1296..284c6e881 100644 --- a/graph/typescript/engine/resolution/value-flow.dl +++ b/graph/typescript/engine/resolution/value-flow.dl @@ -294,6 +294,69 @@ proxy_trap_name("preventExtensions"). proxy_trap_name("apply"). proxy_trap_name("construct"). +// …AND A FUNCTION WRITTEN AS A JSX ATTRIBUTE OR CHILD. ``, +// ` + + ) + } +} + +export function mountBoard(): unknown { + return +} + +// a function component handing a module function and a render callback to elements +export function Toolbar(): unknown { + return ( + + ) +} + +export function List(props: { renderItem: (i: number) => string }): unknown { + return
    {props.renderItem.length}
+} + +// CONTROL: a class with a render method that is never written as a tag is not a component: building it runs no render +export class Report { + render(): number { return measure(1) } +} +export function makeReport(): Report { + return new Report() +} + +// CONTROL: an element whose attributes are data hands nothing over +export function Badge(): unknown { + return +} diff --git a/tests/cases/typescript/jsx-handed-functions/src/jsx.d.ts b/tests/cases/typescript/jsx-handed-functions/src/jsx.d.ts new file mode 100644 index 000000000..e5c8fd2d3 --- /dev/null +++ b/tests/cases/typescript/jsx-handed-functions/src/jsx.d.ts @@ -0,0 +1,13 @@ +// the project supplies its own JSX namespace and component base, so the case needs no framework installed +declare global { + namespace JSX { + type Element = unknown + interface ElementClass { render(): unknown } + interface ElementAttributesProperty { props: unknown } + interface IntrinsicElements { [name: string]: unknown } + } +} +export class Component

{ + props: P + constructor(props: P) { this.props = props } +} diff --git a/tests/cases/typescript/jsx-handed-functions/src/work.ts b/tests/cases/typescript/jsx-handed-functions/src/work.ts new file mode 100644 index 000000000..46ad4ace7 --- /dev/null +++ b/tests/cases/typescript/jsx-handed-functions/src/work.ts @@ -0,0 +1,7 @@ +export function hitTest(x: number): number { return x * 2 } +export function persist(id: string): string { return id.trim() } +export function startClock(): number { return Date.now() } +export function stopClock(): number { return 0 } +export function onSave(): string { return "saved" } +export function drawRow(i: number): string { return String(i) } +export function measure(n: number): number { return n + 1 } diff --git a/tests/cases/typescript/jsx-handed-functions/tsconfig.json b/tests/cases/typescript/jsx-handed-functions/tsconfig.json new file mode 100644 index 000000000..f75ffe5b1 --- /dev/null +++ b/tests/cases/typescript/jsx-handed-functions/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "jsx": "preserve", "strict": true } }