diff --git a/graph/bundle/SCHEMA.md b/graph/bundle/SCHEMA.md index 2c17b391..0a5204d4 100644 --- a/graph/bundle/SCHEMA.md +++ b/graph/bundle/SCHEMA.md @@ -442,7 +442,7 @@ One row per place a call is written (or, for a synthesised edge, the construct t - **typescript** — end_line / end_column come from the expression row; the call-site row itself records only the start. - **javascript** — caller_id is the parser's enclosing method, or the module initializer for top-level code. end_line / end_column come from the expression row. `require()` is a module edge, not a call site. - **typescript** — PROPERTY_READ and PROPERTY_WRITE rows are accessor invocations with no written call: the site is the property-access expression that runs the getter or setter, positioned from the expressions table, and callee_name is NULL because nothing was written; the accessor's name is on the callee's methods row. Filter them out with kind NOT IN (…) when counting calls. -- **python** — PROPERTY_READ, CONTEXT_MANAGER, ITERATION_PROTOCOL, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls. +- **python** — PROPERTY_READ, PROPERTY_WRITE, CONTEXT_MANAGER, ITERATION_PROTOCOL, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls. - **python** — The id is an EXPRESSION hash for a written call; a DECORATOR hash (PY_DECORATOR_…) for DECORATOR_APPLICATION and DECORATOR_* sites, positioned at the decorator line; and the class's TYPE hash for METACLASS_CREATION, positioned at the class declaration. ### `call_edges` @@ -467,6 +467,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h | `new` | csharp | An object creation. The target is the constructed type's constructor, and it is never dispatched. | | `ctor_delegate` | csharp | `: this(...)` or `: base(...)`. No name is written, so the target is structural. | | `primary_ctor_base` | csharp | SYNTHESISED. A primary constructor's base invocation, written in the heritage clause: `class D(int a) : B(a)`. There is no call syntax anywhere in the body. FromExpr is the heritage type reference. | +| `implicit_base_ctor` | csharp | SYNTHESISED. The base class's parameterless constructor that a constructor with no `: base(...)` / `: this(...)` runs before its body, or that the implicit constructor of a class declaring none runs at its `new`. FromExpr is the heritage type reference, or the `new` site. | | `delegate` | csharp | A call through a delegate value: `handler(x)` or `handler.Invoke(x)`. | | `operator` | csharp | A user-defined operator invoked by operator syntax. | | `conversion` | csharp | A user-defined conversion. An implicit one has no syntax at the call site. | @@ -531,6 +532,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h | `DECORATOR_*` | python | Applying an unparenthesised decorator; the suffix is the parser's decorator kind: BARE, ATTRIBUTE, SUBSCRIPT, EXPRESSION (and CALL/ATTRIBUTE_CALL when the factory expression is not itself a call site). The site is the decorator hash. | | `METACLASS_CREATION` | python | A class statement invokes its metaclass's `__new__` / `__init__` at import time, whether the metaclass is written on the statement (`class X(metaclass=M)`) or inherited from a base, and the nearest base's `__init_subclass__`. No written call; the site is the class's type hash. | | `PROPERTY_READ` | python | Reading `obj.attr` where `attr` is a `@property` runs the getter; reading `Cls.attr` where the METACLASS defines `attr` as a property runs that getter. No written call; the site is the attribute-access expression. | +| `PROPERTY_WRITE` | python | Assigning `obj.attr = v` where `attr` is a `@property` with a setter runs the setter; `del obj.attr` runs its deleter. No written call; the site is the attribute-access expression. | | `CONTEXT_MANAGER` | python | `with expr:` runs `__enter__` / `__exit__` (or the async pair). No written call; the site is the context-manager expression. | | `ITERATION_PROTOCOL` | python | `for x in expr:` (and comprehensions) runs `__iter__` / `__next__` (or the async pair). No written call; the site is the iterated expression. | | `SUBSCRIPT_PROTOCOL` | python | `x[k]` runs `__getitem__` (and `x[k] = v` / `del x[k]` the setter and deleter) of the receiver's class. No written call; the site is the subscript expression. Its own kind so it is never counted as a written call. | diff --git a/graph/bundle/schema.ts b/graph/bundle/schema.ts index 2e9af322..40ff4da4 100644 --- a/graph/bundle/schema.ts +++ b/graph/bundle/schema.ts @@ -358,6 +358,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'call_edges', column: 'kind', value: 'new', languages: C, meaning: 'An object creation. The target is the constructed type\'s constructor, and it is never dispatched.' }, { table: 'call_edges', column: 'kind', value: 'ctor_delegate', languages: C, meaning: '`: this(...)` or `: base(...)`. No name is written, so the target is structural.' }, { table: 'call_edges', column: 'kind', value: 'primary_ctor_base', languages: C, meaning: 'SYNTHESISED. A primary constructor\'s base invocation, written in the heritage clause: `class D(int a) : B(a)`. There is no call syntax anywhere in the body. FromExpr is the heritage type reference.' }, + { table: 'call_edges', column: 'kind', value: 'implicit_base_ctor', languages: C, meaning: 'SYNTHESISED. The base class\'s parameterless constructor that a constructor with no `: base(...)` / `: this(...)` runs before its body, or that the implicit constructor of a class declaring none runs at its `new`. FromExpr is the heritage type reference, or the `new` site.' }, { table: 'call_edges', column: 'kind', value: 'delegate', languages: C, meaning: 'A call through a delegate value: `handler(x)` or `handler.Invoke(x)`.' }, { table: 'call_edges', column: 'kind', value: 'operator', languages: C, meaning: 'A user-defined operator invoked by operator syntax.' }, { table: 'call_edges', column: 'kind', value: 'conversion', languages: C, meaning: 'A user-defined conversion. An implicit one has no syntax at the call site.' }, @@ -667,6 +668,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'call_edges', column: 'kind', value: 'DECORATOR_*', languages: P, meaning: 'Applying an unparenthesised decorator; the suffix is the parser\'s decorator kind: BARE, ATTRIBUTE, SUBSCRIPT, EXPRESSION (and CALL/ATTRIBUTE_CALL when the factory expression is not itself a call site). The site is the decorator hash.' }, { table: 'call_edges', column: 'kind', value: 'METACLASS_CREATION', languages: P, meaning: 'A class statement invokes its metaclass\'s `__new__` / `__init__` at import time, whether the metaclass is written on the statement (`class X(metaclass=M)`) or inherited from a base, and the nearest base\'s `__init_subclass__`. No written call; the site is the class\'s type hash.' }, { table: 'call_edges', column: 'kind', value: 'PROPERTY_READ', languages: P, meaning: 'Reading `obj.attr` where `attr` is a `@property` runs the getter; reading `Cls.attr` where the METACLASS defines `attr` as a property runs that getter. No written call; the site is the attribute-access expression.' }, + { table: 'call_edges', column: 'kind', value: 'PROPERTY_WRITE', languages: P, meaning: 'Assigning `obj.attr = v` where `attr` is a `@property` with a setter runs the setter; `del obj.attr` runs its deleter. No written call; the site is the attribute-access expression.' }, { table: 'call_edges', column: 'kind', value: 'CONTEXT_MANAGER', languages: P, meaning: '`with expr:` runs `__enter__` / `__exit__` (or the async pair). No written call; the site is the context-manager expression.' }, { table: 'call_edges', column: 'kind', value: 'ITERATION_PROTOCOL', languages: P, meaning: '`for x in expr:` (and comprehensions) runs `__iter__` / `__next__` (or the async pair). No written call; the site is the iterated expression.' }, { table: 'call_edges', column: 'kind', value: 'SUBSCRIPT_PROTOCOL', languages: P, meaning: '`x[k]` runs `__getitem__` (and `x[k] = v` / `del x[k]` the setter and deleter) of the receiver\'s class. No written call; the site is the subscript expression. Its own kind so it is never counted as a written call.' }, @@ -748,7 +750,7 @@ export const NOTES: readonly NoteSpec[] = [ { language: 'typescript', table: 'overrides', note: 'EMPTY — this table is Java-shaped. The TypeScript dispatch envelope is in dispatch_candidates, with basis `nominal` or `structural`.' }, { language: 'typescript', table: 'type_instantiated', note: 'Every row has how = `new`. Not restricted to client provenance: a type the library constructs is still a type that exists at run time, and dropping it would narrow the envelope unsoundly.' }, { language: 'typescript', table: 'call_sites', note: 'PROPERTY_READ and PROPERTY_WRITE rows are accessor invocations with no written call: the site is the property-access expression that runs the getter or setter, positioned from the expressions table, and callee_name is NULL because nothing was written; the accessor\'s name is on the callee\'s methods row. Filter them out with kind NOT IN (…) when counting calls.' }, - { language: 'python', table: 'call_sites', note: 'PROPERTY_READ, CONTEXT_MANAGER, ITERATION_PROTOCOL, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls.' }, + { language: 'python', table: 'call_sites', note: 'PROPERTY_READ, PROPERTY_WRITE, CONTEXT_MANAGER, ITERATION_PROTOCOL, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls.' }, { language: 'python', table: 'call_sites', note: 'The id is an EXPRESSION hash for a written call; a DECORATOR hash (PY_DECORATOR_…) for DECORATOR_APPLICATION and DECORATOR_* sites, positioned at the decorator line; and the class\'s TYPE hash for METACLASS_CREATION, positioned at the class declaration.' }, { language: 'python', table: 'call_edges', note: 'A `boundary_lib` edge may point at a builtin (callee_provenance builtin, callee_label `builtin:NAME`) or at an unstaged import path (callee_provenance external) — neither has a methods row.' }, { language: 'java', table: 'call_edges', note: 'A `boundary_lib` edge with callee_provenance external names a method of an ancestor type no staged IR declares (callee_label `external:.`, no methods row). A site whose receiver is declared as such a type is multi_inferred even with one client override: the platform method itself, and the platform\'s own subclasses, are the other possible targets. Stage the library to replace the label with the real method.' }, diff --git a/graph/csharp/engine/call-edge-generation/call_chain.dl b/graph/csharp/engine/call-edge-generation/call_chain.dl index 69647979..c0e3de06 100644 --- a/graph/csharp/engine/call-edge-generation/call_chain.dl +++ b/graph/csharp/engine/call-edge-generation/call_chain.dl @@ -286,6 +286,52 @@ call_chain_edge(tr, caller, "-", ctor, "lib", "boundary_lib", "primary_ctor_base method_decl("lib", _, _, _, ctor), type_ctor("client", gk, caller). +// ── SYNTHESISED EDGES: THE IMPLICIT `base()` ──────────────────────────────── +// A constructor that writes neither `: base(..)` nor `: this(..)` runs its base +// class's parameterless constructor before its own body, and a class that declares +// no constructor at all gets one that does the same. Neither is written anywhere, so +// without these edges a base constructor that every derived class runs has no caller. +// +// implicit_base_ctor(TypeGroup, Ctor): the constructor `base()` runs for a type -- +// its base class's constructor that takes no argument, or, where the base declares +// no constructor either, the one the base's own implicit constructor runs. +implicit_base_ctor(gk, ctor) :- + type_extends("client", gk, bgk), + type_ctor("client", bgk, ctor), ctor_accepts_argc("client", ctor, "0"). +implicit_base_ctor(gk, ctor) :- + type_extends("client", gk, bgk), + !type_ctor("client", bgk, _), type_group("client", _, bgk), + implicit_base_ctor(bgk, ctor). + +// A constructor that hands off with `: this(..)` or `: base(..)`, or a primary +// constructor whose heritage passes arguments: the base constructor it runs is the +// one written there, already an edge. +ctor_has_initializer(m) :- call_is_base_ctor("client", e), expr_ultimate_method("client", e, m). +ctor_has_initializer(m) :- call_is_this_ctor("client", e), expr_ultimate_method("client", e, m). +ctor_has_initializer(m) :- + heritage_has_ctor_args("client", t, _), type_group("client", t, gk), type_ctor("client", gk, m). + +// The FromExpr is the base class's entry in the heritage clause, as for a primary +// constructor's base invocation: the closest thing to where the call is written. +implicit_base_site(gk, tr) :- + type_extends("client", gk, bgk), + heritage_resolves("client", gk, pos, bgk), + heritage_of_entity("client", gk, pos, _, _, tr), tr != "". + +// (1) A declared constructor with no initializer. +call_chain_edge(tr, m, "-", ctor, "client", "known_edge", "implicit_base_ctor") :- + type_ctor("client", gk, m), method_decl("client", _, _, _, m), + !ctor_has_initializer(m), + implicit_base_ctor(gk, ctor), + implicit_base_site(gk, tr). + +// (2) `new T()` where T declares no constructor: the implicit one runs T's base's. +// The edge sits on the `new` site itself, beside its known_implicit_ctor row. +call_chain_edge(e, caller, "-", ctor, "client", "known_edge", "implicit_base_ctor") :- + call_ctor_implicit("client", e, gk), + implicit_base_ctor(gk, ctor), + call_from_expr("client", e, caller). + // ── ACCESSOR EDGES: A PROPERTY READ IS A CALL ─────────────────────────────── // `x.Name` invokes get_Name, `a[i]` an indexer accessor, `e += h` an add accessor. // These have no cs_call_site row -- the parser cannot know whether `x.Name` is a diff --git a/graph/csharp/engine/containment/type-nesting.dl b/graph/csharp/engine/containment/type-nesting.dl index e584db41..8ac8b94d 100644 --- a/graph/csharp/engine/containment/type-nesting.dl +++ b/graph/csharp/engine/containment/type-nesting.dl @@ -149,3 +149,27 @@ module_in_scope(prov, mod, outer) :- // The GLOBAL namespace ("") is always in scope. module_in_scope(prov, mod, "") :- module_decl(prov, _, _, _, mod). + +// module_ns_chain(Prov, ModuleHash, Namespace) -- the namespaces a name written in this +// file is read from: the file's own namespaces and every dotted prefix of them, as +// text. A prefix that holds no type of its own (`Acme` over `Acme.Shop`) still +// qualifies a name written below it, so this is not ns_encloses, which needs one. +module_ns_chain(prov, mod, ns) :- module_namespace_own(prov, mod, ns), ns != "". +module_ns_chain(prov, mod, p) :- module_namespace_own(prov, mod, inner), ns_prefix(prov, inner, p). + +// ── A USING WRITTEN INSIDE A NAMESPACE ───────────────────────────────────── +// `namespace Shop.Tests; using Results;` imports Shop.Results. C# reads the name in +// a using directive from the namespace the directive sits in, innermost outward, +// exactly as it reads any other name there. Taken as written it names a namespace +// that does not exist, and every type the file names through it reads as a library +// type. +// +// The IR does not record whether a using sits inside the namespace or above it. +// The language decides it instead: a using whose name is no namespace at all only +// compiles inside a namespace that has it as a child. So the relative reading is +// taken only when the name as written names nothing, and only for a child that +// exists -- a file whose namespaces have no such child gains nothing. +module_in_scope(prov, mod, full) :- + using_namespace(prov, mod, n), !ns_exists(prov, n), + module_ns_chain(prov, mod, e), full = cat(e, cat(".", n)), + ns_exists(prov, full). diff --git a/graph/csharp/engine/expression-resolution/expr-type.dl b/graph/csharp/engine/expression-resolution/expr-type.dl index 5c962379..fe5e35e7 100644 --- a/graph/csharp/engine/expression-resolution/expr-type.dl +++ b/graph/csharp/engine/expression-resolution/expr-type.dl @@ -459,6 +459,25 @@ expr_type(prov, e, gk) :- expr_type(prov, q, qgk), member_lookup(prov, qgk, n, "field", f), field_type(prov, f, gk). +// A STATIC member read through its TYPE NAME: `Context.Current.Factory.Create()`. +// The qualifier `Context` names a type, not a value, so it has no expr_type and the +// two clauses above cannot start from it: the getter of `Current` is an edge +// (properties.dl reads the type name), and these give its RESULT a type, so that +// `.Factory` and every call after it resolve. ref_names_type already refuses a name +// that also binds a value, which is the language's member-over-type rule. +expr_type(prov, e, gk) :- + expr_kind(prov, e, "MEMBER_ACCESS"), + expr_qualifier_child(prov, e, q), expr_member_name_child(prov, e, nameExpr), + expr_written_name(prov, nameExpr, n), + ref_names_type(prov, q, qgk), + member_lookup(prov, qgk, n, "property", p), property_type(prov, p, gk). +expr_type(prov, e, gk) :- + expr_kind(prov, e, "MEMBER_ACCESS"), + expr_qualifier_child(prov, e, q), expr_member_name_child(prov, e, nameExpr), + expr_written_name(prov, nameExpr, n), + ref_names_type(prov, q, qgk), + member_lookup(prov, qgk, n, "field", f), field_type(prov, f, gk). + // ── A GENERIC ARGUMENT, SUBSTITUTED FOR THE PARAMETER IT FILLS ────────────── // `IWrap w; w.Value.Flag` -- `Value` is declared `T`, and without binding // T to Settings the access resolves and the type of the RESULT is the type parameter, diff --git a/graph/csharp/engine/projections/expressions.dl b/graph/csharp/engine/projections/expressions.dl index 46e28c76..8a82e1ba 100644 --- a/graph/csharp/engine/projections/expressions.dl +++ b/graph/csharp/engine/projections/expressions.dl @@ -117,6 +117,15 @@ expr_written_name("lib", e, n) :- lib_cs_expression(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, n, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e), n != "". +// ── expr_type_argc(Prov, ExprHash, TypeArgumentCount) ────────────────────── +// The type arguments written on a name: `Cache` is the name `Cache` at arity 1. +// A generic name used as a receiver or qualifier is resolved at this arity, since +// `Cache` and `Cache` are two types. +expr_type_argc("client", e, tac) :- + cs_expression(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, tac, _, _, _, _, _, _, _, _, _, _, _, _, e). +expr_type_argc("lib", e, tac) :- + lib_cs_expression(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, tac, _, _, _, _, _, _, _, _, _, _, _, _, e). + // ── expr_literal(Prov, ExprHash, LiteralKind, LiteralValue) ───────────────── expr_literal("client", e, lk, lv) :- cs_expression(_, _, _, _, _, _, _, _, _, _, lk, lv, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e), diff --git a/graph/csharp/engine/projections/variables.dl b/graph/csharp/engine/projections/variables.dl index 27be6bf5..841c1f86 100644 --- a/graph/csharp/engine/projections/variables.dl +++ b/graph/csharp/engine/projections/variables.dl @@ -61,6 +61,10 @@ var_initializer("lib", v, e) :- // The initializer IS a value of the variable's type. var_value_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "LOCAL", _, v). var_value_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "CONST", _, v). +// A `using` resource -- `using (var w = new Writer())` and `using var w = ...;` -- is +// the disposable itself, so its initializer is a value of its type exactly as a +// local's is. Without it every call on such a resource has an untyped receiver. +var_value_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "USING", _, v). // The initializer is a COLLECTION and the variable takes its element type. var_iterated_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "FOREACH", _, v). diff --git a/graph/csharp/engine/resolution/extensions.dl b/graph/csharp/engine/resolution/extensions.dl index 23516414..5a09820c 100644 --- a/graph/csharp/engine/resolution/extensions.dl +++ b/graph/csharp/engine/resolution/extensions.dl @@ -118,6 +118,25 @@ call_recv_type_name(prov, e, tn) :- call_recv_type_name(prov, e, tn) :- call_receiver_expr(prov, e, r), ref_denotes(prov, r, "LAMBDA_PARAMETER", p), param_type_name(prov, p, tn, _). +// AN IMPLICITLY TYPED LAMBDA PARAMETER takes its type from the delegate it is +// converted to (lambda-parameters.dl): `Configure(s => s.AddRules())` with +// `Configure(Action c)` makes `s` an IServiceCollection. Where +// that type is in source, param_type already says so; where it is unstaged, the +// NAME is all there is, and without it an extension written for +// `this IServiceCollection` cannot match the lambda that calls it. +call_recv_type_name(prov, e, tn) :- + call_receiver_expr(prov, e, r), + ref_denotes(prov, r, "LAMBDA_PARAMETER", p), + lambda_param_arg_ref(prov, p, ar), type_ref_name(prov, ar, tn), tn != "". +// AN IMPLICITLY TYPED LOCAL made by `new T(..)` holds a T, resolved or not: +// `var services = new ServiceCollection(); services.AddRules();`. The name is the +// one written at the creation (external-types.dl labels the boundary from the same +// fact); without it the extension rungs have nothing to compare. +call_recv_type_name(prov, e, tn) :- + call_receiver_expr(prov, e, r), + ref_denotes(prov, r, "LOCAL_VARIABLE", v), var_is_implicit(prov, v), + var_value_initializer(prov, v, init), expr_kind(prov, init, "OBJECT_CREATION"), + call_callee_name(prov, init, tn), tn != "". // A receiver that is itself a call: the callee's declared return type name. The ROOT // reference's name where there is one (`List`, not `List`), which is how every // other receiver is named; the written text only where no reference was recorded. diff --git a/graph/csharp/engine/resolution/local-binding.dl b/graph/csharp/engine/resolution/local-binding.dl index f2ec9440..e7bc391f 100644 --- a/graph/csharp/engine/resolution/local-binding.dl +++ b/graph/csharp/engine/resolution/local-binding.dl @@ -220,7 +220,8 @@ ref_unbound(prov, e, n) :- !ref_names_type(prov, e, _). // A reference that names a TYPE rather than a value: the receiver of a static call. -// Resolved through type_name_resolves in the file's scope. +// Resolved through type_name_resolves in the file's scope, at the arity written on +// it: `Cache.Get()` names Cache`1, not a non-generic Cache. // // NOT WHERE THE NAME ALSO BINDS TO A VALUE. C# allows a member and a type to share a // name (the language's own "Color Color" rule) and prefers the MEMBER in an @@ -235,10 +236,10 @@ ref_binds_value(prov, e) :- ref_denotes(prov, e, _, _). ref_binds_value(prov, e) :- local_binds(prov, e, _). ref_names_type(prov, e, gk) :- - expr_written_name(prov, e, n), + expr_written_name(prov, e, n), expr_type_argc(prov, e, ar), expr_ultimate_module(prov, e, mod), !ref_binds_value(prov, e), - type_name_resolves(prov, mod, n, "0", gk). + type_name_resolves(prov, mod, n, ar, gk). // ── IMPLICIT `this` ───────────────────────────────────────────────────────── // An unqualified instance member reference has an implicit `this` receiver whose diff --git a/graph/csharp/engine/resolution/type-resolution.dl b/graph/csharp/engine/resolution/type-resolution.dl index fb90ddc4..f4d1b803 100644 --- a/graph/csharp/engine/resolution/type-resolution.dl +++ b/graph/csharp/engine/resolution/type-resolution.dl @@ -89,6 +89,26 @@ type_name_cand(prov, mod, qn, ar, gk, 1) :- type_decl(prov, _, qn, ar, _, t), type_group(prov, t, gk), !type_is_top_level(prov, gk). +// ── A QUALIFIED NAME READ FROM ITS NAMESPACE ──────────────────────────────── +// `Reflection.Info.Create(..)` written inside namespace Shop names +// Shop.Reflection.Info: the first segment of a qualified name is a +// simple name, looked up in the enclosing namespaces like any other. Matched only +// in full, such a receiver stays untyped and every call through it unresolved. +// +// Keyed from the DECLARATION side, so nothing is enumerated per written name: each +// type's qualified name is split at each dot into the namespace before it and the +// dotted tail after it, and the tail is a candidate in every file whose namespace +// chain holds that prefix. A tail with no dot is a simple name, which ranks 2-5 +// already answer. +type_qn_tail(prov, qn, p, s) :- + type_decl(prov, _, qn, _, _, _), + i = range(1, strlen(qn)), substr(qn, i, 1) = ".", + p = substr(qn, 0, i), s = substr(qn, i + 1, strlen(qn) - i - 1), + contains(".", s). +type_name_cand(prov, mod, s, ar, gk, 1) :- + type_qn_tail(prov, qn, p, s), module_ns_chain(prov, mod, p), + type_decl(prov, _, qn, ar, _, t), type_group(prov, t, gk). + // ── A CLIENT FILE NAMING A STAGED LIBRARY TYPE ────────────────────────────── // Every clause above is SINGLE-PROVENANCE: it joins a module and a declaration // under one `prov`, so a client module and a library namespace are never brought @@ -211,10 +231,10 @@ type_ref_demand(prov, mod, n, ar) :- // a type name and produced `external:s.Describe` -- an external target named after a // local variable, which is a wrong answer rather than a missing one. The !ref_denotes // term is what makes the guard "unbound by anyone". -type_ref_demand(prov, mod, n, "0") :- +type_ref_demand(prov, mod, n, ar) :- call_receiver_expr(prov, e, r), expr_ref_unknown(prov, r), - expr_written_name(prov, r, n), + expr_written_name(prov, r, n), expr_type_argc(prov, r, ar), !ref_denotes(prov, r, _, _), call_module(prov, e, mod). diff --git a/graph/csharp/souffle/decls_all.dl b/graph/csharp/souffle/decls_all.dl index b1faa46b..89a6ff36 100644 --- a/graph/csharp/souffle/decls_all.dl +++ b/graph/csharp/souffle/decls_all.dl @@ -65,9 +65,9 @@ .decl call_callee_name(c0:symbol,c1:symbol,c2:symbol) .decl call_caller_unknown(c0:symbol,c1:symbol) .decl call_chain_edge(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol,c6:symbol) -.decl call_class_kind(c0:symbol) .decl call_chain_summary(c0:symbol,c1:number) .decl call_class(c0:symbol,c1:symbol,c2:symbol) +.decl call_class_kind(c0:symbol) .decl call_context(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl call_ctor_implicit(c0:symbol,c1:symbol,c2:symbol) .decl call_ctor_target(c0:symbol,c1:symbol,c2:symbol) @@ -265,6 +265,7 @@ .decl ctl_route_c(c0:symbol,c1:symbol,c2:symbol) .decl ctl_route_tok(c0:symbol,c1:symbol,c2:symbol) .decl ctor_accepts_argc(c0:symbol,c1:symbol,c2:symbol) +.decl ctor_has_initializer(c0:symbol) .decl ctor_implicit_type(c0:symbol,c1:symbol) .decl decl_return_param(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl delegate_arg_expr(c0:symbol,c1:symbol) @@ -359,6 +360,7 @@ .decl expr_target_any(c0:symbol,c1:symbol,c2:symbol) .decl expr_target_type(c0:symbol,c1:symbol,c2:symbol) .decl expr_type(c0:symbol,c1:symbol,c2:symbol) +.decl expr_type_argc(c0:symbol,c1:symbol,c2:symbol) .decl expr_type_owner(c0:symbol,c1:symbol,c2:symbol) .decl expr_type_via_ref(c0:symbol,c1:symbol,c2:symbol) .decl expr_ultimate_method(c0:symbol,c1:symbol,c2:symbol) @@ -487,8 +489,10 @@ .decl implements_member(c0:symbol,c1:symbol,c2:symbol) .decl implements_params_conflict(c0:symbol,c1:symbol) .decl implements_shape(c0:symbol,c1:symbol,c2:symbol) +.decl implicit_base_ctor(c0:symbol,c1:symbol) .decl implicit_base_group(c0:symbol,c1:symbol) .decl implicit_base_name(c0:symbol) +.decl implicit_base_site(c0:symbol,c1:symbol) .decl implicit_new_untargeted(c0:symbol,c1:symbol,c2:symbol) .decl indexer_access(c0:symbol,c1:symbol,c2:symbol) .decl indexer_getter(c0:symbol,c1:symbol,c2:symbol) @@ -663,6 +667,7 @@ .decl module_lang(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl module_namespace_own(c0:symbol,c1:symbol,c2:symbol) .decl module_namespace_style(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl module_ns_chain(c0:symbol,c1:symbol,c2:symbol) .decl module_parse_errors(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl module_regime(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl module_target(c0:symbol,c1:symbol,c2:symbol,c3:symbol) @@ -948,6 +953,7 @@ .decl type_parent(c0:symbol,c1:symbol,c2:symbol) .decl type_placement(c0:symbol,c1:symbol,c2:symbol) .decl type_primary_ctor(c0:symbol,c1:symbol,c2:symbol) +.decl type_qn_tail(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_ref(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol,c6:symbol) .decl type_ref_arg(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_ref_argc(c0:symbol,c1:symbol,c2:symbol) diff --git a/graph/javascript/engine/call-edge-generation/calls.dl b/graph/javascript/engine/call-edge-generation/calls.dl index 39dbe789..5745b29d 100644 --- a/graph/javascript/engine/call-edge-generation/calls.dl +++ b/graph/javascript/engine/call-edge-generation/calls.dl @@ -170,6 +170,29 @@ call_chain_edge(ce, caller, "-", m, "client", "multi_inferred", kind) :- wrapper_call_target(ce, m), !call_over_cap(ce), call_from(ce, caller), invocation_site(ce, kind). variable_reassigned(v) :- expr_kind(_, "ASSIGNMENT", _, a), expr_child(_, a, "ASSIGNMENT_TARGET", _, tgt), expr_binding(_, v, tgt). +// ── a call of what a PROJECT wrapper returned runs what that site was handed ── +// `export default promisable(eachItem, 3)`, `const add = curry2(function add(a, b) {…})`: +// the wrapper returns a closure that calls its parameter, and every export of the module +// is that closure. The closure's call of the parameter is read context-insensitively, so +// it held every function any of the wrapper's sites passed (past the fan cap: nothing), +// and a test calling the export reached the closure and stopped there. The site is +// already a value of its own, ("wrap", site), beside the closure (value-flow.dl), and a +// call through it takes its RESULT from that site's argument alone; the same site says +// what the call RUNS: the function it was handed at the forwarded position, what the +// wrapper writes into that parameter, or what a wrapped argument itself runs +// (`memoize(once(f))`). One of a set, beside the edge to the closure the call keeps, and +// under the same fan cap: a callee that may hold more wrapped values than --dispatch-cap +// (a parameter every wrapped export is passed to) runs one of them, and naming all of +// them says nothing. +wrap_runs(s, g) :- wrap_site_arg(s, _, arg), expr_value(arg, "func", g). +wrap_runs(s, g) :- wrap_site_arg(s, _, arg), expr_value(arg, "wrap", s2), wrap_runs(s2, g). +wrap_runs(s, g) :- wrap_site(s, h, pos), wrapper_param_written(h, pos, o), expr_value(o, "func", g). +wrapped_call_target(ce, g) :- callee_value(ce, "wrap", s), wrap_runs(s, g), method_prov(g, "client"), + !call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, ce, _, _). +wrapped_target_count(ce, n) :- wrapped_call_target(ce, _), n = count : { wrapped_call_target(ce, _) }. +wrapped_over_cap(ce) :- wrapped_target_count(ce, n), dispatch_cap(c), n > to_number(c). +call_chain_edge(ce, caller, "-", g, "client", "multi_inferred", kind) :- + wrapped_call_target(ce, g), !call_over_cap(ce), !wrapped_over_cap(ce), call_from(ce, caller), invocation_site(ce, kind). // `(c ? a : b)()`, `(0, cb)()`, `make()()`: the callee is computed by an expression. // `new Function(s)()` / `Function(s)()` runs code built at run time. unresolved_value_callee(ce, "dynamic_code") :- value_callee_unresolved(ce, c), callee_builds_code(c). diff --git a/graph/javascript/engine/resolution/fan-cap.dl b/graph/javascript/engine/resolution/fan-cap.dl index af6bdd93..ad69acdb 100644 --- a/graph/javascript/engine/resolution/fan-cap.dl +++ b/graph/javascript/engine/resolution/fan-cap.dl @@ -80,14 +80,48 @@ direct_export_func(mod, n, m) :- export_decl(_, n, _, _, _, _, _, se, mod, _), s // shares its name with hundreds of unrelated sites and must not be capped by them, // while a module-level `each` or `merge` called under its own name from everywhere // is exactly the generic utility the cap exists for. +// +// The PROGRAM's arguments are decided by the program's sites alone. A suite calls the +// API it tests over and over, and counted with the program those calls made exactly the +// functions under test "generic utilities": their parameters went untracked, so a +// callback the program hands a constructor and stores on the instance (`this.decode = +// options.decode`) was lost, and the program's own call through that field resolved only +// to the fallback beside it. So there are two verdicts: +// method_is_hot every site counts (as before): decides whether a TEST's +// arguments flow in +// method_is_hot_in_program test files' sites do not count: decides whether the +// PROGRAM's arguments flow in +// Nothing that flowed before stops flowing (a test handing the program a plugin it calls +// back still reaches it). A function hot only because of its tests now receives what the +// program passes it and still nothing from the tests: their callbacks would be the widest +// values it holds, and with them the call through the stored field would be over the +// dispatch cap and answer nothing. param_flow_capped_at(n) :- dispatch_cap(s), n = to_number(s). direct_call_count(m, n) :- direct_call_site(_, m), n = count : { direct_call_site(_, m) }. method_is_hot(m, n) :- direct_call_count(m, n), param_flow_capped_at(c), n > c. name_call_count(cn, n) :- call_site(_, _, cn, _, _, _, _, _, _), cn != "", n = count : { call_site(_, _, cn, _, _, _, _, _, _) }. +program_direct_site(ce, m) :- direct_call_site(ce, m), !site_in_test_module(ce). +program_direct_count(m, n) :- program_direct_site(_, m), n = count : { program_direct_site(_, m) }. +method_is_hot_in_program(m, n) :- program_direct_count(m, n), param_flow_capped_at(c), n > c. +// The NAME count is the overcount for module-level functions. A test's member call that +// only shares the name (`lib.add(1, 2)` on an export that is a different function, a +// curried closure) stays in it, as before; a test's bare call or `new` of the name +// (`build(...)`, `new lib.Codec(...)`) denotes the function itself and leaves it. +test_names_itself(ce) :- site_in_test_module(ce), call_site(_, ck, _, _, _, _, ce, _, _), call_kind_is_callee_form(ck). +test_names_itself(ce) :- site_in_test_module(ce), call_site(_, "CONSTRUCTOR_CALL", _, _, _, _, ce, _, _). +program_name_site(m, ce) :- module_level_function(m, cn), call_site(_, _, cn, _, _, _, ce, _, _), !test_names_itself(ce). +program_name_count(m, n) :- program_name_site(m, _), n = count : { program_name_site(m, _) }. +// ── test_module / site_in_test_module ────────────── +// By path, as runners find their files: a `test`/`tests`/`__tests__`/`spec(s)` directory, +// or a `.test.` / `.spec.` file. +test_module(mod) :- module_file("client", fp, mod), + match("(.*/)?(tests?|__tests__|specs?)/.*|.*[.](test|spec)[.][cm]?[jt]sx?", fp). +site_in_test_module(ce) :- expr_owner(_, _, mod, ce), test_module(mod). module_level_function(m, n) :- method_decl(_, n, "FUNCTION_DECLARATION", _, _, "", mod, m), method_scopes(_, _, _, init, m), module_init(_, init, mod). module_level_function(m, n) :- var_decl(_, n, _, _, _, mod, v), function_binding_value(v, m), method_decl(_, _, _, _, _, "", mod, m), method_scopes(_, _, _, init, m), module_init(_, init, mod). method_is_hot(m, n) :- module_level_function(m, cn), name_call_count(cn, n), param_flow_capped_at(c), n > c. +method_is_hot_in_program(m, n) :- program_name_count(m, n), param_flow_capped_at(c), n > c. // ── param_fan_capped(ParamHash, SiteCount) — countable ────────────────────── -param_fan_capped(p, n) :- method_is_hot(m, n), param_decl(_, _, _, m, p). +param_fan_capped(p, n) :- method_is_hot_in_program(m, n), param_decl(_, _, _, m, p). diff --git a/graph/javascript/engine/resolution/value-flow.dl b/graph/javascript/engine/resolution/value-flow.dl index 2c79e52b..e78863e2 100644 --- a/graph/javascript/engine/resolution/value-flow.dl +++ b/graph/javascript/engine/resolution/value-flow.dl @@ -264,8 +264,15 @@ param_value(p, "inst", t) :- param_type_name(_, _, tr, p), type_ref_type(tr, t). // values on every export, a 20-minute solve — because a generic library is exactly // the shape whose parameters unify everything. What a library does with a callback it // is handed is a MODEL (resolution/frameworks.dl), stated per library, not inferred. -param_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !method_is_hot(m, _), - method_decl("client", _, _, _, _, _, _, m), +// The program's sites pass their arguments unless the PROGRAM calls the function from +// more places than the cap; a test file's sites only while every site together stays +// under it (fan-cap.dl). +param_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !site_in_test_module(ce), + !method_is_hot_in_program(m, _), method_decl("client", _, _, _, _, _, _, m), + param_decl(_, _, ppos, m, p), to_number(ppos) = pos, !param_is_rest(_, p), + expr_value(arg, k, i). +param_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), site_in_test_module(ce), + !method_is_hot(m, _), method_decl("client", _, _, _, _, _, _, m), param_decl(_, _, ppos, m, p), to_number(ppos) = pos, !param_is_rest(_, p), expr_value(arg, k, i). // THE ONE LIBRARY EXCEPTION: a UMD wrapper (#710). `(function (root, factory) { @@ -283,8 +290,12 @@ param_value(p, "func", f) :- call_site(_, "IIFE_CALL", _, _, _, _, ce, _, _), ca expr_value(arg, "func", f). // A rest parameter IS an array; the arguments at and past its position are its elements. param_value(p, "arr", p) :- param_is_rest(_, p). -elem_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !method_is_hot(m, _), - method_decl("client", _, _, _, _, _, _, m), +elem_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !site_in_test_module(ce), + !method_is_hot_in_program(m, _), method_decl("client", _, _, _, _, _, _, m), + param_decl(_, _, ppos, m, p), param_is_rest(_, p), to_number(ppos) <= pos, + expr_value(arg, k, i). +elem_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), site_in_test_module(ce), + !method_is_hot(m, _), method_decl("client", _, _, _, _, _, _, m), param_decl(_, _, ppos, m, p), param_is_rest(_, p), to_number(ppos) <= pos, expr_value(arg, k, i). // A default value: `cb = () => {}`. The parser gives the parameter the default's diff --git a/graph/javascript/souffle/decls_all.dl b/graph/javascript/souffle/decls_all.dl index b7b56647..bc489cb6 100644 --- a/graph/javascript/souffle/decls_all.dl +++ b/graph/javascript/souffle/decls_all.dl @@ -112,6 +112,10 @@ .decl wrapper_holder_call(c0:symbol, c1:symbol) .decl wrapper_runs(c0:symbol, c1:symbol) .decl wrapper_call_target(c0:symbol, c1:symbol) +.decl wrap_runs(c0:symbol, c1:symbol) +.decl wrapped_call_target(c0:symbol, c1:symbol) +.decl wrapped_target_count(c0:symbol, c1:number) +.decl wrapped_over_cap(c0:symbol) .decl live_export_variable(c0:symbol, c1:symbol) .decl this_type_open(c0:symbol) @@ -462,6 +466,14 @@ .decl param_fan_capped(c0:symbol, c1:number) .decl import_binding_is_default_like(c0:symbol) .decl name_call_count(c0:symbol, c1:number) +.decl test_module(c0:symbol) +.decl site_in_test_module(c0:symbol) +.decl program_direct_site(c0:symbol, c1:symbol) +.decl program_direct_count(c0:symbol, c1:number) +.decl method_is_hot_in_program(c0:symbol, c1:number) +.decl test_names_itself(c0:symbol) +.decl program_name_site(c0:symbol, c1:symbol) +.decl program_name_count(c0:symbol, c1:number) .decl module_level_function(c0:symbol, c1:symbol) .decl ambient_collection(c0:symbol) .decl coll_method_call(c0:symbol, c1:symbol, c2:symbol) diff --git a/graph/python/engine/call-edge-generation/call_chain.dl b/graph/python/engine/call-edge-generation/call_chain.dl index b1c334c5..6c35b6cf 100644 --- a/graph/python/engine/call-edge-generation/call_chain.dl +++ b/graph/python/engine/call-edge-generation/call_chain.dl @@ -156,6 +156,31 @@ method_returns_method("client", m, r) :- expr_call_candidate(site, g), method_returns_method("client", g, r). +// ── a call that hands back one of its arguments (py_returns_arg, resolution/builtins.dl) ── +// Matched through the import that binds the callee, never by the bare name: +// `functools.update_wrapper(...)` / `t.cast(...)` where the receiver is the name an `import` +// bound, or `update_wrapper(...)` bound by `from functools import update_wrapper`. A local +// function that happens to share the name does neither. +py_passthrough_arg(site, pos) :- + call_name(site, fn), call_receiver_object(site, obj), + expr_binding("client", rb, ctx, obj), ctx != "STORE", binding_lookup("client", rb, rb2), + import_binding("client", rb2, i), import_decl("client", k, mod, _, i), + (k = "MODULE_IMPORT" ; k = "MODULE_IMPORT_ALIAS"), // `import typing as t` is the alias kind + py_returns_arg(path, pos), cat(mod, cat(".", fn)) = path. +py_passthrough_arg(site, pos) :- + call_callee_is_value(site), call_callee_expr(site, callee), + expr_binding("client", rb, ctx, callee), ctx != "STORE", binding_lookup("client", rb, rb2), + import_binding("client", rb2, i), import_decl("client", _, path, _, i), + py_returns_arg(path, pos). +// the expression a value really is, through any number of such calls +py_passthrough_root(e, e) :- method_return_value_expr("client", _, e). +py_passthrough_root(e, x) :- + py_passthrough_root(e, c), call_of_expr(c, site), py_passthrough_arg(site, pos), + call_arg(site, pos, x). +method_returns_method("client", m, r) :- + method_return_value_expr("client", m, e), py_passthrough_root(e, x), x != e, + expr_names_method("client", x, r). + // ── decorator_hits_lib(SiteKey, LibMethodHash) ─────────────────────────────── // `@abstractmethod` names `abc.abstractmethod`, which HAS Python source and is in the // staged stdlib IR — it is a library boundary, not a blind spot. A BARE decorator has no @@ -320,6 +345,25 @@ iter_protocol_edge(src, caller, m) :- expr_type("client", src, t), iter_protocol_target(t, m), expr_ultimate_method("client", src, caller). +// ── property_write_edge(WriteExprHash, CallerMethodHash, AccessorMethodHash) ── +// The store and delete halves of the property protocol: `obj.x = v` calls x's setter, +// `del obj.x` its deleter (type_property_accessor, resolution/attribute-lookup.dl). Same +// shape as a read, keyed on the access's own name context. +property_write_edge(e, caller, m) :- + expr_node("client", "ATTRIBUTE_ACCESS", _, n, e), + expr_name_context("client", "STORE", e), + expr_parent("client", e, "ATTRIBUTE_OBJECT", _, obj), + expr_type("client", obj, t), + type_property_accessor("client", t, n, "PROPERTY_SETTER", m), + expr_ultimate_method("client", e, caller). +property_write_edge(e, caller, m) :- + expr_node("client", "ATTRIBUTE_ACCESS", _, n, e), + expr_name_context("client", "DEL", e), + expr_parent("client", e, "ATTRIBUTE_OBJECT", _, obj), + expr_type("client", obj, t), + type_property_accessor("client", t, n, "PROPERTY_DELETER", m), + expr_ultimate_method("client", e, caller). + // ── property_read_edge(ReadExprHash, CallerMethodHash, GetterMethodHash) ───── property_read_edge(e, caller, getter) :- expr_node("client", "ATTRIBUTE_ACCESS", _, n, e), @@ -668,6 +712,7 @@ subscript_protocol_edge(sub, caller, m) :- // from the graph rather than being re-tiered. A dropped edge is worse than a mislabelled // one, and the golden caught it. protocol_edge(e, d, caller, m) :- property_read_edge(e, caller, m), method_decl(_, d, _, _, _, m). +protocol_edge(e, d, caller, m) :- property_write_edge(e, caller, m), method_decl(_, d, _, _, _, m). protocol_edge(e, d, caller, m) :- with_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m). protocol_edge(e, d, caller, m) :- iter_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m). protocol_edge(e, d, caller, m) :- subscript_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m). @@ -731,6 +776,12 @@ call_chain_edge(e, caller, "-", getter, "client", cls, "PROPERTY_READ") :- property_read_edge(e, caller, getter), method_decl(_, d, _, _, _, getter), protocol_edge_class(e, d, cls). +// PROPERTY WRITE — `obj.x = v` runs x's setter and `del obj.x` its deleter. Likewise no +// call site, and its own kind so it is never counted as a written call. +call_chain_edge(e, caller, "-", m, "client", cls, "PROPERTY_WRITE") :- + property_write_edge(e, caller, m), method_decl(_, d, _, _, _, m), + protocol_edge_class(e, d, cls). + // CONTEXT MANAGER — the same shape: an edge with no call site, its own kind so it can // never be mistaken for a written call. call_chain_edge(cm, caller, "-", m, "client", cls, "CONTEXT_MANAGER") :- diff --git a/graph/python/engine/expression-resolution/expr-type.dl b/graph/python/engine/expression-resolution/expr-type.dl index d46b6eca..ed530619 100644 --- a/graph/python/engine/expression-resolution/expr-type.dl +++ b/graph/python/engine/expression-resolution/expr-type.dl @@ -585,6 +585,15 @@ binding_declared_nominal(p, bind, d) :- binding_declared_ref(p, bind, r), type_ref_resolved(p, d, r), !type_is_structural(p, d). +// `x: Optional[Order] = ...` / `x: Order | None = ...` IS an Order (or None) — the rule the +// parameter, return and field annotations already have (resolution/annotations.dl, +// "Optional[X] IS X"), which the local annotation alone was missing: a local written +// with the commonest modern spelling stayed untyped while the same annotation on a +// parameter resolved. +binding_declared_nominal(p, bind, t) :- + type_ref_owner(p, bind, "BINDING", r), + type_ref(p, k, "VARIABLE_ANNOTATION", _, _, r), annotation_optional_kind(k), + type_ref_element(p, r, t), !type_is_structural(p, t). // The FK is resolved on only 14 of 479 variable annotations, so the name-based lookup // beside it is what carries this -- the same two clauses the parameter path uses, with // the same kind restriction. A SUBSCRIPT or a UNION names no single type and is served by diff --git a/graph/python/engine/framework-behavior/dispatch.dl b/graph/python/engine/framework-behavior/dispatch.dl index 0ede5dbf..c2c8dc38 100644 --- a/graph/python/engine/framework-behavior/dispatch.dl +++ b/graph/python/engine/framework-behavior/dispatch.dl @@ -412,6 +412,53 @@ py_fixture_requested(m) :- py_fixture_injection(_, m, _). framework_edge(from, to, "fixture_injection", name, "by_name") :- py_fixture_injection(from, to, name). +// ── 2a. THE SAME LOOKUP, FROM SYNTAX ALONE ────────────────────────────────────── +// resolution/value-flow.dl types a parameter by the fixture serving it, so it needs the +// runner's lookup INSIDE the resolution fixpoint. py_fixture_injection cannot be read +// there: a fixture a conftest star-imports is found through module_member_method, which +// is resolution, and the nearest-conftest MAX over it would then be a cyclic aggregate +// souffle refuses to stratify. This chain keeps the runner's order (class, own module, +// nearest conftest by its declaring file) and leaves out the two clauses that need +// resolution — a star-imported fixture and a pytest_plugins one. Those still reach their +// tests through the injection edge; they only go untyped. +.decl py_fixs_in_file(fix:symbol, file:symbol) +py_fixs_in_file(fix, p) :- py_fixture_decl(fix, _), method_file("client", p, fix). +.decl py_fixs_same_module(req:symbol, name:symbol, fix:symbol) +py_fixs_same_module(req, name, fix) :- py_fixture_request(req, name), + py_fixture_decl(fix, name), fix != req, !py_fixture_class(fix, _), + !py_fixture_class_shadowed(req, name), + method_file("client", p, req), py_fixs_in_file(fix, p). +.decl py_fixs_conftest_cand(req:symbol, name:symbol, fix:symbol, depth:number) +py_fixs_conftest_cand(req, name, fix, dl) :- py_fixture_request(req, name), + py_fixture_decl(fix, name), fix != req, !py_fixture_class(fix, _), + py_fixs_in_file(fix, cp), py_fixture_scope_file(c), + strlen(cp) >= strlen(c), substr(cp, strlen(cp) - strlen(c), strlen(c)) = c, + d = substr(cp, 0, strlen(cp) - strlen(c)), + method_file("client", p, req), + strlen(p) > strlen(d), substr(p, 0, strlen(d)) = d, dl = strlen(d). +.decl py_fixs_nearest(req:symbol, name:symbol, depth:number) +py_fixs_nearest(req, name, m) :- py_fixs_conftest_cand(req, name, _, _), + m = max dl : { py_fixs_conftest_cand(req, name, _, dl) }. +.decl py_fixs_injection(from:symbol, to:symbol, name:symbol) +py_fixs_injection(req, fix, name) :- py_fixture_in_class(req, name, fix). +py_fixs_injection(req, fix, name) :- py_fixs_same_module(req, name, fix). +py_fixs_injection(req, fix, name) :- py_fixs_conftest_cand(req, name, fix, dl), + !py_fixture_class_shadowed(req, name), !py_fixs_same_module(req, name, _), + py_fixs_nearest(req, name, dl). + +// ── 2b. FIXTURE VALUE (what the runner hands the parameter) ──────────────────── +// py_fixture_value_type(Fixture, Type) — the value a fixture hands over: its return or +// yield value's type, or its declared return. Consumed by resolution/value-flow.dl, where +// the runner's call `test(fixture_value)` is one more argument reaching a parameter. +.decl py_fixture_value_type(fix:symbol, t:symbol) +py_fixture_value_type(fix, t) :- py_fixture_decl(fix, _), + method_return_value_expr("client", fix, e), expr_type("client", e, t). +py_fixture_value_type(fix, t) :- py_fixture_decl(fix, _), + method_declared_return_type("client", fix, d), declared_dispatch("client", d, t). +py_fixture_value_type(fix, t) :- py_fixture_decl(fix, _), + expr_node("client", "YIELD", _, _, y), expr_ultimate_method("client", y, fix), + expr_parent("client", y, "YIELD_VALUE", _, v), expr_type("client", v, t). + // ───────────────────────────────────────────────────────────────────────────── // 3. URL DISPATCH (route table -> view) // ───────────────────────────────────────────────────────────────────────────── diff --git a/graph/python/engine/resolution/annotations.dl b/graph/python/engine/resolution/annotations.dl index 0d671aa9..2b9087a4 100644 --- a/graph/python/engine/resolution/annotations.dl +++ b/graph/python/engine/resolution/annotations.dl @@ -199,6 +199,25 @@ type_ref_element(p, parentRef, t) :- annotation_owner_module(p, owner, ok, mod), type_name_in_module(p, mod, tn, t). +// ── union_operand(Prov, UnionRefHash, OperandRefHash) — every operand of a union ── +// `|` is LEFT-associative, so `A | B | None` is `(A | B) | None`: A and B are not +// children of the annotation but grandchildren, under a nested UNION_PEP604, and the +// depth-1 clauses above saw only that nested union (which names no type) and None. Every +// rule reading "Optional[X] IS X" therefore lost the members of any union with three or +// more operands, on a parameter, a return, a field or a local alike. +union_operand(p, r, c) :- + type_ref(p, k, _, _, _, r), annotation_optional_kind(k), type_ref_nesting(p, r, _, _, c). +union_operand(p, r, c) :- + union_operand(p, r, u), type_ref(p, "UNION_PEP604", _, _, _, u), type_ref_nesting(p, u, _, _, c). +// the operands BELOW depth 1 (depth 1 is the clauses above), resolved, and by name +type_ref_element(p, r, t) :- + union_operand(p, r, c), type_ref_nesting(p, _, _, d, c), d != "1", type_ref_resolved(p, t, c). +type_ref_element(p, r, t) :- + union_operand(p, r, c), type_ref_nesting(p, _, _, d, c), d != "1", + type_ref(p, _, "GENERIC_ARGUMENT", tn, _, c), tn != "", + type_ref_owner(p, owner, ok, r), annotation_owner_module(p, owner, ok, mod), + type_name_in_module(p, mod, tn, t). + // ── annotation_owner_module(Prov, OwnerHash, OwnerKind, ModuleHash) ────────── // The module an annotation was written in, whatever kind of declaration owns it. annotation_owner_module(p, ph, "METHOD_PARAM", mod) :- diff --git a/graph/python/engine/resolution/attribute-lookup.dl b/graph/python/engine/resolution/attribute-lookup.dl index 3c032d13..7b44fe2d 100644 --- a/graph/python/engine/resolution/attribute-lookup.dl +++ b/graph/python/engine/resolution/attribute-lookup.dl @@ -317,6 +317,15 @@ type_call_target(p, t, m) :- mro_lookup(p, t, "__call__", m). type_property_getter(p, t, n, m) :- mro_lookup(p, t, n, m), method_kind(p, "PROPERTY_GETTER", _, m). +// ── type_property_accessor(Prov, TypeHash, Name, Kind, MethodHash) ─────────── +// `@x.setter` and `@x.deleter` are the other two halves: `obj.x = v` runs the setter and +// `del obj.x` the deleter, and like the getter neither has a call site. They share the +// getter's name and binding, so they are found on the class that WINS the name in the +// receiver's MRO, by their method kind, rather than through mro_lookup's one answer. +type_property_accessor(p, t, n, k, m) :- + mro_winner(p, t, n, c), method_owner(p, c, m), method_decl(p, n, _, _, _, m), + method_kind(p, k, _, m), (k = "PROPERTY_SETTER" ; k = "PROPERTY_DELETER"). + // ── A USER-WRITTEN DATA DESCRIPTOR IS THE SAME PROTOCOL (issue #326) ───────── // `@property` IS a data descriptor; the decorator is sugar over `__get__`/`__set__`. So // the clause above reads one spelling of the protocol and a class-level diff --git a/graph/python/engine/resolution/builtins.dl b/graph/python/engine/resolution/builtins.dl index ca1e14d1..02da0163 100644 --- a/graph/python/engine/resolution/builtins.dl +++ b/graph/python/engine/resolution/builtins.dl @@ -182,6 +182,18 @@ py_copy_module("copy"). py_copy_function("copy"). py_copy_function("deepcopy"). +// ── py_returns_arg(DottedName, Position) — a library call that hands back an argument ── +// Each returns, unchanged, the argument at Position (documented behaviour, not inference): +// functools.update_wrapper(wrapper, wrapped) -> wrapper (copies __name__/__doc__ onto it) +// typing.cast(T, value) -> value (a no-op at run time) +// A decorator written `return update_wrapper(wrapper, f)`, or the typed spelling +// `return t.cast(F, update_wrapper(wrapper, f))`, is therefore the same shape as one +// returning `wrapper` under @functools.wraps. Without these every method it decorates was +// decorator_replaced_target, and no call through the attribute reached anything +// (call-edge-generation/call_chain.dl). Neither module is staged in a client-only run. +py_returns_arg("functools.update_wrapper", "0"). +py_returns_arg("typing.cast", "1"). + // ── py_builtin_dynamic(Name) — the escape hatches ──────────────────────────── // These do not merely lack Python source; they make the PROGRAM unanalysable at that // point. A call to getattr/eval/exec means the engine cannot know what runs, and the diff --git a/graph/python/engine/resolution/value-flow.dl b/graph/python/engine/resolution/value-flow.dl index 14ef74b5..69fb4ae4 100644 --- a/graph/python/engine/resolution/value-flow.dl +++ b/graph/python/engine/resolution/value-flow.dl @@ -97,6 +97,21 @@ param_arg_type(ph, t) :- param_decl("client", kw, _, _, m, ph), expr_type("client", a, t). +// ── a parameter the TEST RUNNER fills ───────────────────────────────────────── +// pytest calls `test_commit(session=)`: the call is the runner's, +// so no call site spells it, but it is an argument reaching a parameter all the same, and +// the parameter is typed exactly as one passed at a call site would be. Which fixture +// serves the parameter is framework-behavior/dispatch.dl's py_fixs_injection (2a), the +// runner's own lookup (same class, same module, nearest conftest, a pytest_plugins +// module), so this is as deterministic as a name the LEGB walk resolves. +// MEASURED across ten public suites: 1,529 method calls on such parameters were +// ambiguous_unknown, and everything derived from them (`tx = session.begin(); tx.commit()`) +// with them; a suite whose fixtures carry annotations resolved its own. +param_arg_type(ph, t) :- + py_fixs_injection(req, fix, pn), + param_decl("client", pn, _, _, req, ph), + py_fixture_value_type(fix, t). + // ── param_arg_method(ParamHash, MethodHash) — a CALLABLE reaching a parameter ── // `Delegator(target)` puts the FUNCTION `target` into the parameter `fn`. Java would // need a functional interface for this; in Python it is an ordinary assignment. diff --git a/graph/python/souffle/decls_all.dl b/graph/python/souffle/decls_all.dl index d6b069c6..6f1fd864 100644 --- a/graph/python/souffle/decls_all.dl +++ b/graph/python/souffle/decls_all.dl @@ -238,6 +238,7 @@ .decl element_lib_type_of(c0:symbol,c1:symbol) .decl binding_element_lib_type(c0:symbol,c1:symbol) .decl param_declared_type_by_parser(c0:symbol,c1:symbol,c2:symbol) +.decl union_operand(c0:symbol,c1:symbol,c2:symbol) .decl type_ref_element(c0:symbol,c1:symbol,c2:symbol) .decl annotation_owner_module(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl annotation_container_kind(c0:symbol) @@ -313,6 +314,9 @@ .decl py_path_join_function(c0:symbol) .decl py_path_join_operator(c0:symbol) .decl py_copy_function(c0:symbol) +.decl py_returns_arg(c0:symbol,c1:symbol) +.decl py_passthrough_arg(c0:symbol,c1:symbol) +.decl py_passthrough_root(c0:symbol,c1:symbol) .decl dict_lookup_method(c0:symbol) .decl builtin_target(c0:symbol,c1:symbol) .decl builtin_object_init_target(c0:symbol) @@ -594,6 +598,8 @@ .decl decorator_hits_lib(c0:symbol,c1:symbol) .decl decorator_hits_builtin(c0:symbol,c1:symbol) .decl property_read_edge(c0:symbol,c1:symbol,c2:symbol) +.decl property_write_edge(c0:symbol,c1:symbol,c2:symbol) +.decl type_property_accessor(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl with_protocol_edge(c0:symbol,c1:symbol,c2:symbol) .decl iteration_protocol_sync(c0:symbol) .decl iteration_protocol_of(c0:symbol,c1:symbol) diff --git a/graph/test/javascript/expected/64-memoize-wrapper-result.edges b/graph/test/javascript/expected/64-memoize-wrapper-result.edges index c3d46e25..46a76a11 100644 --- a/graph/test/javascript/expected/64-memoize-wrapper-result.edges +++ b/graph/test/javascript/expected/64-memoize-wrapper-result.edges @@ -1,6 +1,8 @@ app.js:11:3 FUNCTION_CALL getAlpha -> known_edge memoize.js:5:10 +app.js:11:3 FUNCTION_CALL getAlpha -> multi_inferred app.js:7:26 app.js:11:3 METHOD_CALL getAlpha().getHooks -> known_edge alpha.js:2:3 getHooks app.js:12:3 FUNCTION_CALL getBeta -> known_edge memoize.js:5:10 +app.js:12:3 FUNCTION_CALL getBeta -> multi_inferred app.js:8:25 app.js:12:3 METHOD_CALL getBeta().getHooks -> known_edge beta.js:2:3 getHooks app.js:16:20 FUNCTION_CALL once -> callback_registered app.js:16:25 app.js:16:20 FUNCTION_CALL once -> known_edge once.js:2:1 once @@ -15,12 +17,16 @@ app.js:19:19 FUNCTION_CALL lazy -> callback_registered app.js:19:24 app.js:19:19 FUNCTION_CALL lazy -> known_edge once.js:14:1 lazy app.js:19:30 CONSTRUCTOR_CALL AlphaPlugin -> implicit_constructor - app.js:22:3 FUNCTION_CALL firstAlpha -> known_edge once.js:5:10 +app.js:22:3 FUNCTION_CALL firstAlpha -> multi_inferred app.js:16:25 app.js:22:3 METHOD_CALL firstAlpha().run -> known_edge alpha.js:3:3 run app.js:23:3 FUNCTION_CALL makeBeta -> known_edge once.js:15:10 +app.js:23:3 FUNCTION_CALL makeBeta -> multi_inferred app.js:17:23 app.js:23:3 METHOD_CALL makeBeta().run -> known_edge beta.js:3:3 run app.js:24:3 FUNCTION_CALL firstBeta -> known_edge once.js:5:10 +app.js:24:3 FUNCTION_CALL firstBeta -> multi_inferred app.js:18:24 app.js:24:3 METHOD_CALL firstBeta().run -> known_edge beta.js:3:3 run app.js:25:3 FUNCTION_CALL makeAlpha -> known_edge once.js:15:10 +app.js:25:3 FUNCTION_CALL makeAlpha -> multi_inferred app.js:19:24 app.js:25:3 METHOD_CALL makeAlpha().run -> known_edge alpha.js:3:3 run app.js:29:18 FUNCTION_CALL constant -> known_edge once.js:18:1 constant app.js:29:27 CONSTRUCTOR_CALL BetaPlugin -> implicit_constructor - @@ -57,10 +63,14 @@ declared.js:27:16 FUNCTION_CALL pickShared -> callback_registered declared.js declared.js:27:16 FUNCTION_CALL pickShared -> known_edge declared.js:20:1 pickShared declared.js:27:33 CONSTRUCTOR_CALL BetaPlugin -> implicit_constructor - declared.js:30:3 FUNCTION_CALL declAlpha -> known_edge declared.js:9:3 inner +declared.js:30:3 FUNCTION_CALL declAlpha -> multi_inferred declared.js:22:28 declared.js:30:3 METHOD_CALL declAlpha().run -> known_edge alpha.js:3:3 run declared.js:31:3 FUNCTION_CALL declBeta -> known_edge declared.js:9:3 inner +declared.js:31:3 FUNCTION_CALL declBeta -> multi_inferred declared.js:23:27 declared.js:31:3 METHOD_CALL declBeta().run -> known_edge beta.js:3:3 run declared.js:32:3 FUNCTION_CALL declOfGetter -> known_edge declared.js:9:3 inner +declared.js:32:3 FUNCTION_CALL declOfGetter -> multi_inferred declared.js:24:26 +declared.js:32:3 FUNCTION_CALL declOfGetter -> multi_inferred memoize.js:5:10 declared.js:32:3 METHOD_CALL declOfGetter().run -> known_edge alpha.js:3:3 run declared.js:33:3 FUNCTION_CALL tappedDecl -> known_edge declared.js:15:3 inner declared.js:33:3 METHOD_CALL tappedDecl().markOnly -> known_edge declared.js:13:14 markOnly @@ -202,24 +212,33 @@ shapes.js:45:27 FUNCTION_CALL memoize -> callback_registered shapes.js:45:35 shapes.js:45:27 FUNCTION_CALL memoize -> known_edge memoize.js:2:17 shapes.js:45:41 CONSTRUCTOR_CALL BetaPlugin -> implicit_constructor - shapes.js:48:3 FUNCTION_CALL getFb -> known_edge shapes.js:15:10 +shapes.js:48:3 FUNCTION_CALL getFb -> multi_inferred shapes.js:36:28 shapes.js:48:3 METHOD_CALL getFb().run -> known_edge alpha.js:3:3 run shapes.js:49:3 FUNCTION_CALL getFb -> known_edge shapes.js:15:10 +shapes.js:49:3 FUNCTION_CALL getFb -> multi_inferred shapes.js:36:28 shapes.js:49:3 METHOD_CALL getFb().fallbackOnly -> known_edge shapes.js:8:18 fallbackOnly shapes.js:50:3 FUNCTION_CALL getOr -> known_edge shapes.js:22:10 +shapes.js:50:3 FUNCTION_CALL getOr -> multi_inferred shapes.js:38:22 shapes.js:50:3 METHOD_CALL getOr().run -> ambient_terminal - shapes.js:50:3 METHOD_CALL getOr().run -> multi_inferred beta.js:3:3 run shapes.js:51:3 FUNCTION_CALL getOr -> known_edge shapes.js:22:10 +shapes.js:51:3 FUNCTION_CALL getOr -> multi_inferred shapes.js:38:22 shapes.js:51:3 METHOD_CALL getOr().mockOnly -> ambient_terminal - shapes.js:51:3 METHOD_CALL getOr().mockOnly -> multi_inferred shapes.js:9:14 mockOnly shapes.js:52:3 FUNCTION_CALL getD -> known_edge shapes.js:29:10 +shapes.js:52:3 FUNCTION_CALL getD -> multi_inferred shapes.js:39:26 shapes.js:52:3 METHOD_CALL getD().run -> known_edge alpha.js:3:3 run shapes.js:53:3 FUNCTION_CALL getD -> known_edge shapes.js:29:10 +shapes.js:53:3 FUNCTION_CALL getD -> multi_inferred shapes.js:39:26 shapes.js:53:3 METHOD_CALL getD().defaultsOnly -> known_edge shapes.js:25:20 defaultsOnly shapes.js:54:3 FUNCTION_CALL getDefault -> known_edge shapes.js:33:44 shapes.js:54:3 METHOD_CALL getDefault().defaultOnly -> known_edge shapes.js:10:19 defaultOnly shapes.js:55:3 FUNCTION_CALL getSpread -> known_edge shapes.js:33:44 shapes.js:55:3 METHOD_CALL getSpread().run -> ambiguous_unknown - shapes.js:56:3 FUNCTION_CALL warm -> known_edge shapes.js:34:32 +shapes.js:56:3 FUNCTION_CALL warm -> multi_inferred shapes.js:43:20 shapes.js:56:3 METHOD_CALL warm().run -> known_edge alpha.js:3:3 run shapes.js:57:3 FUNCTION_CALL getNested -> known_edge memoize.js:5:10 +shapes.js:57:3 FUNCTION_CALL getNested -> multi_inferred memoize.js:5:10 +shapes.js:57:3 FUNCTION_CALL getNested -> multi_inferred shapes.js:45:35 shapes.js:57:3 METHOD_CALL getNested().run -> known_edge beta.js:3:3 run diff --git a/graph/test/javascript/expected/65-wrapper-param-reassigned.edges b/graph/test/javascript/expected/65-wrapper-param-reassigned.edges index d56c695d..8373c4e3 100644 --- a/graph/test/javascript/expected/65-wrapper-param-reassigned.edges +++ b/graph/test/javascript/expected/65-wrapper-param-reassigned.edges @@ -2,51 +2,66 @@ app.js:10:23 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:10:3 FUNCTION_CALL lazyOrDefault -> callback_registered app.js:10:17 app.js:10:3 FUNCTION_CALL lazyOrDefault -> known_edge wrap.js:17:1 lazyOrDefault app.js:10:3 FUNCTION_CALL lazyOrDefault(() => new A()) -> known_edge wrap.js:19:10 +app.js:10:3 FUNCTION_CALL lazyOrDefault(() => new A()) -> multi_inferred app.js:10:17 +app.js:10:3 FUNCTION_CALL lazyOrDefault(() => new A()) -> multi_inferred wrap.js:18:15 app.js:10:3 METHOD_CALL lazyOrDefault(() => new A())().aOnly -> known_edge wrap.js:2:11 aOnly app.js:11:3 FUNCTION_CALL lazyNullish -> known_edge wrap.js:22:1 lazyNullish app.js:11:3 FUNCTION_CALL lazyNullish() -> known_edge wrap.js:24:10 +app.js:11:3 FUNCTION_CALL lazyNullish() -> multi_inferred wrap.js:23:10 app.js:11:3 METHOD_CALL lazyNullish()().dfltOnly -> known_edge wrap.js:4:14 dfltOnly app.js:15:15 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:15:3 FUNCTION_CALL plain -> callback_registered app.js:15:9 app.js:15:3 FUNCTION_CALL plain -> known_edge wrap.js:27:1 plain app.js:15:3 FUNCTION_CALL plain(() => new A()) -> known_edge wrap.js:28:10 +app.js:15:3 FUNCTION_CALL plain(() => new A()) -> multi_inferred app.js:15:9 app.js:15:3 METHOD_CALL plain(() => new A())().aOnly -> known_edge wrap.js:2:11 aOnly app.js:16:15 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:16:3 FUNCTION_CALL plain -> callback_registered app.js:16:9 app.js:16:3 FUNCTION_CALL plain -> known_edge wrap.js:27:1 plain app.js:16:3 FUNCTION_CALL plain(() => new A()) -> known_edge wrap.js:28:10 +app.js:16:3 FUNCTION_CALL plain(() => new A()) -> multi_inferred app.js:16:9 app.js:16:3 METHOD_CALL plain(() => new A())().replacedOnly -> ambiguous_unknown - app.js:17:22 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:17:3 FUNCTION_CALL otherWritten -> callback_registered app.js:17:16 app.js:17:3 FUNCTION_CALL otherWritten -> known_edge wrap.js:31:1 otherWritten app.js:17:3 FUNCTION_CALL otherWritten(() => new A(), null) -> known_edge wrap.js:33:10 +app.js:17:3 FUNCTION_CALL otherWritten(() => new A(), null) -> multi_inferred app.js:17:16 app.js:17:3 METHOD_CALL otherWritten(() => new A(), null)().aOnly -> known_edge wrap.js:2:11 aOnly app.js:18:22 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:18:3 FUNCTION_CALL otherWritten -> callback_registered app.js:18:16 app.js:18:3 FUNCTION_CALL otherWritten -> known_edge wrap.js:31:1 otherWritten app.js:18:3 FUNCTION_CALL otherWritten(() => new A(), null) -> known_edge wrap.js:33:10 +app.js:18:3 FUNCTION_CALL otherWritten(() => new A(), null) -> multi_inferred app.js:18:16 app.js:18:3 METHOD_CALL otherWritten(() => new A(), null)().replacedOnly -> ambiguous_unknown - app.js:4:14 FUNCTION_CALL swapped -> callback_registered app.js:4:22 app.js:4:14 FUNCTION_CALL swapped -> known_edge wrap.js:7:1 swapped app.js:4:28 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:5:3 FUNCTION_CALL sw -> known_edge wrap.js:9:10 +app.js:5:3 FUNCTION_CALL sw -> multi_inferred wrap.js:8:8 app.js:5:3 METHOD_CALL sw().replacedOnly -> known_edge wrap.js:3:18 replacedOnly app.js:6:3 FUNCTION_CALL sw -> known_edge wrap.js:9:10 +app.js:6:3 FUNCTION_CALL sw -> multi_inferred wrap.js:8:8 app.js:6:3 METHOD_CALL sw().aOnly -> ambiguous_unknown - app.js:7:3 FUNCTION_CALL lazyGuard -> known_edge wrap.js:12:1 lazyGuard app.js:7:3 FUNCTION_CALL lazyGuard(undefined) -> known_edge wrap.js:14:10 +app.js:7:3 FUNCTION_CALL lazyGuard(undefined) -> multi_inferred wrap.js:13:38 app.js:7:3 METHOD_CALL lazyGuard(undefined)().replacedOnly -> known_edge wrap.js:3:18 replacedOnly app.js:8:19 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:8:3 FUNCTION_CALL lazyGuard -> callback_registered app.js:8:13 app.js:8:3 FUNCTION_CALL lazyGuard -> known_edge wrap.js:12:1 lazyGuard app.js:8:3 FUNCTION_CALL lazyGuard(() => new A()) -> known_edge wrap.js:14:10 +app.js:8:3 FUNCTION_CALL lazyGuard(() => new A()) -> multi_inferred app.js:8:13 +app.js:8:3 FUNCTION_CALL lazyGuard(() => new A()) -> multi_inferred wrap.js:13:38 app.js:8:3 METHOD_CALL lazyGuard(() => new A())().aOnly -> known_edge wrap.js:2:11 aOnly app.js:9:3 FUNCTION_CALL lazyOrDefault -> known_edge wrap.js:17:1 lazyOrDefault app.js:9:3 FUNCTION_CALL lazyOrDefault() -> known_edge wrap.js:19:10 +app.js:9:3 FUNCTION_CALL lazyOrDefault() -> multi_inferred wrap.js:18:15 app.js:9:3 METHOD_CALL lazyOrDefault()().dfltOnly -> known_edge wrap.js:4:14 dfltOnly many.js:11:3 FUNCTION_CALL s21 -> known_edge wrap.js:9:10 +many.js:11:3 FUNCTION_CALL s21 -> multi_inferred wrap.js:8:8 many.js:11:3 METHOD_CALL s21().replacedOnly -> known_edge wrap.js:3:18 replacedOnly many.js:12:3 FUNCTION_CALL s21 -> known_edge wrap.js:9:10 +many.js:12:3 FUNCTION_CALL s21 -> multi_inferred wrap.js:8:8 many.js:12:3 METHOD_CALL s21().aOnly -> ambiguous_unknown - many.js:3:13 FUNCTION_CALL swapped -> callback_registered many.js:3:21 many.js:3:13 FUNCTION_CALL swapped -> known_edge wrap.js:7:1 swapped diff --git a/graph/test/python/torture/client/f43_def_rebind.py b/graph/test/python/torture/client/f43_def_rebind.py index 107989af..3a454e7e 100644 --- a/graph/test/python/torture/client/f43_def_rebind.py +++ b/graph/test/python/torture/client/f43_def_rebind.py @@ -89,10 +89,10 @@ def call_branched() -> str: def read_property() -> int: """The getter survives — the control that keeps this rule off property pairs. - EXPECT: miss — `h.value = 5` invokes the SETTER, and the engine emits a - PROPERTY_READ edge for a property read and nothing at all for a property WRITE. - That is a pre-existing gap this fixture happens to expose, not something #383 - changed; the getter edge on the next line is what this family is asserting. + `h.value = 5` invokes the SETTER, and the engine now emits a PROPERTY_WRITE + edge for it beside the PROPERTY_READ for the read; both halves of the pair + are live, which is exactly what this control exists to keep true. The getter + edge on the next line is what this family is asserting. """ h = Holder() h.value = 5 diff --git a/graph/test/python/torture/expected/coverage.txt b/graph/test/python/torture/expected/coverage.txt index 06b88e32..daf7e130 100644 --- a/graph/test/python/torture/expected/coverage.txt +++ b/graph/test/python/torture/expected/coverage.txt @@ -1,4 +1,4 @@ -=== per-family coverage (tier-4, 471 scored sites) === +=== per-family coverage (tier-4, 474 scored sites) === f01 inheritance & MRO links= 10 found= 10 (100.0%) missed= 0 wide= 0 WRONG= 0 f02 callables & closures links= 10 found= 9 (90.0%) missed= 1 wide= 0 WRONG= 0 f03 generics links= 14 found= 14 (100.0%) missed= 0 wide= 0 WRONG= 0 @@ -38,16 +38,16 @@ f40 data descriptor links= 14 found= 14 (100.0%) missed= 0 wide= 0 WRONG= 0 f41 class attribute absent links= 3 found= 3 (100.0%) missed= 0 wide= 0 WRONG= 0 f42 f42 links= 6 found= 6 (100.0%) missed= 0 wide= 0 WRONG= 0 - f43 f43 links= 7 found= 7 (100.0%) missed= 0 wide= 1 WRONG= 0 + f43 f43 links= 10 found= 10 (100.0%) missed= 0 wide= 1 WRONG= 0 nestmod.py nestmod.py links= 1 found= 1 (100.0%) missed= 0 wide= 0 WRONG= 0 pkgmod relative imports (subpackage) links= 3 found= 3 (100.0%) missed= 0 wide= 0 WRONG= 0 - TOTAL links=434 found=428 (98.6%) missed= 6 wide=37 WRONG= 0 - recall 428/434 = 98.6% of the links that actually ran + TOTAL links=437 found=431 (98.6%) missed= 6 wide=37 WRONG= 0 + recall 431/437 = 98.6% of the links that actually ran wide 37 a member of a SOUND SET that did not run on this pass WRONG 0 a single target asserted as certain that never ran - EXPECTED-MISS cases: {'FOUND': 20, 'MISSED': 24} (a CONCRETE here means a known blind spot closed) + EXPECTED-MISS cases: {'FOUND': 18, 'MISSED': 23} (a CONCRETE here means a known blind spot closed) --- non-concrete sites --- f02_callables.py:31 MISSED true=[('lib', 'callables.py', 22)] engine=[] diff --git a/graph/test/python/torture/expected/torture.edges b/graph/test/python/torture/expected/torture.edges index 4d85cc1a..557d3208 100644 --- a/graph/test/python/torture/expected/torture.edges +++ b/graph/test/python/torture/expected/torture.edges @@ -635,6 +635,7 @@ known_edge PROPERTY_READ f40_data_descriptor.via_data_descriptor -> f40_data_des known_edge PROPERTY_READ f40_data_descriptor.via_delete_descriptor -> f40_data_descriptor.Deletable.__get__ known_edge PROPERTY_READ f40_data_descriptor.via_property -> f40_data_descriptor.Prop.slot known_edge PROPERTY_READ f43_def_rebind.read_property -> f43_def_rebind.Holder.value +known_edge PROPERTY_WRITE f43_def_rebind.read_property -> f43_def_rebind.Holder.value known_edge SELF_CALL f02_callables.HoldsCallables.run -> f02_callables.LocalCallable.__call__ known_edge SELF_CALL f28_await.Registry.into_a_field -> f28_await.Registry.build known_edge SELF_CALL f28_await.Registry.via_self_receiver -> f28_await.Registry.build diff --git a/graph/test/python/torture/expected/torture.oracle b/graph/test/python/torture/expected/torture.oracle index 95fd8d90..4267008a 100644 --- a/graph/test/python/torture/expected/torture.oracle +++ b/graph/test/python/torture/expected/torture.oracle @@ -1 +1 @@ -oracle=634 engine=624 agree=587 missing=47 extra=37 +oracle=634 engine=625 agree=588 missing=46 extra=37 diff --git a/graph/test/typescript/expected/74-jsx-component-forms.edges b/graph/test/typescript/expected/74-jsx-component-forms.edges index 8a9a7700..1215ed2b 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 332f0630..15c58497 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 40aed3c8..92c54f4d 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 56518799..890a08ad 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 f90275c3..b4b87c8f 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 c9f675e1..99b05b5a 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 2c8e5ef2..0d3cff9f 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 75e9d129..284c6e88 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 00000000..e5c8fd2d --- /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 00000000..46ad4ace --- /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 00000000..f75ffe5b --- /dev/null +++ b/tests/cases/typescript/jsx-handed-functions/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "jsx": "preserve", "strict": true } }