diff --git a/gemini-extension.json b/gemini-extension.json index 6327e15c..9b59561d 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "axiomcode", - "version": "0.1.7", + "version": "0.1.8", "description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.", "contextFileName": "plugins/axiomcode/AGENTS.md", "mcpServers": { diff --git a/graph/bundle/SCHEMA.md b/graph/bundle/SCHEMA.md index 7bd4051a..625aac8d 100644 --- a/graph/bundle/SCHEMA.md +++ b/graph/bundle/SCHEMA.md @@ -483,6 +483,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h | `ctor_delegate` | java | `this(…)` / `super(…)` inside a constructor. | | `anon_new` | java | `new X() { … }` — an anonymous class creation. | | `event` | java | Synthesised: the publishEvent call site to a listener it runs (tier event_dispatch). Not a written call to that method. | +| `entity_callback` | java | Synthesised: a call that writes an entity (`repository.save(e)`, `em.persist(e)`, `delete(e)`) to a JPA lifecycle callback it runs (tier event_dispatch). Not a written call to that method. | | `record_accessor` | java | Synthesised: a record pattern `case Pair(var l, var r)` calls each accessor. Not a written call; the site is the pattern expression. | | `resource_close` | java | Synthesised: a try-with-resources resource `try (var w = open())` is closed when the block exits, so its close() is called. Not a written call; the site is the resource local. | | `FUNCTION_CALL` | typescript | `f(…)` — a bare callee. | @@ -511,6 +512,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h | `DYNAMIC_CODE_CALL` | javascript | `eval(…)` / `new Function(…)` — unknowable by construction. | | `DYNAMIC_IMPORT_CALL` | javascript | `import(…)` — a module load that is also a site. | | `JSX_ELEMENT` | javascript | `` — the renderer runs the component (a function component, or a class component's constructor and `render`) with the element's attributes as its props. Engine-authored: no written call; the site is the JSX element expression. An intrinsic tag (`
`) is no site. | +| `JSX_ATTRIBUTE` | javascript | `
`, `` — a function handed over as a JSX attribute, on any tag; always tier callback_registered. Engine-authored: the site is the attribute. | | `SIMPLE_CALL` | python | `f(…)` — a bare name. | | `METHOD_CALL` | python | `obj.m(…)`. | | `CHAINED_CALL` | python | `a.b().c(…)` — the receiver is itself a call. | @@ -554,7 +556,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h | `fan_capped` | javascript, java, csharp | More targets than --dispatch-cap: the set was refused rather than emitted. JavaScript: callee is NULL. Java and C#: callee is the declared base method the fan would have started from; dispatch-capped-sites.csv carries the refused count. | | `callback_registered` | javascript, typescript | The site HANDS the callee this function (`xs.forEach(f)`, `p.then(f)`, `emitter.on('x', h)`, `setTimeout(f)`), which may invoke it. Not the site's own callee; a reachability edge, labelled so it is never read as a resolved call. | | `event_dispatch` | javascript | `x.emit('name')` reaching a handler registered by `x.on('name', h)` on a value x may hold — name-sensitive for literal names, every handler on that value for a computed one. | -| `event_dispatch` | java | A Spring application event: `publishEvent(e)` reaching each listener (`@EventListener`, `@TransactionalEventListener`, `ApplicationListener.onApplicationEvent`) whose declared event type e's static type is, or is a subtype of. Added beside the publishEvent boundary row, never in place of it (call-edge-generation/event_dispatch.dl). | +| `event_dispatch` | java | A Spring application event: `publishEvent(e)` reaching each listener (`@EventListener`, `@TransactionalEventListener`, `ApplicationListener.onApplicationEvent`) whose declared event type e's static type is, or is a subtype of. Added beside the publishEvent boundary row, never in place of it (call-edge-generation/event_dispatch.dl). Also a JPA entity write (`save`, `persist`, `merge`, `delete` on a repository or EntityManager) reaching the `@PrePersist`/`@PreUpdate`/`@PreRemove`-style callbacks of the written entity's type and of the listeners `@EntityListeners` names on it or a superclass (kind entity_callback; call-edge-generation/entity_lifecycle.dl). | | `intrinsic_terminal` | typescript | The site is a JSX intrinsic element or a dynamic `import()` — a runtime intrinsic, not a function the graph can name. | **`call_edges.callee_provenance` values** diff --git a/graph/bundle/schema.ts b/graph/bundle/schema.ts index 8467c946..87b31156 100644 --- a/graph/bundle/schema.ts +++ b/graph/bundle/schema.ts @@ -513,7 +513,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'call_edges', column: 'tier', value: 'fan_capped', languages: ['javascript', 'java', 'csharp'], meaning: 'More targets than --dispatch-cap: the set was refused rather than emitted. JavaScript: callee is NULL. Java and C#: callee is the declared base method the fan would have started from; dispatch-capped-sites.csv carries the refused count.' }, { table: 'call_edges', column: 'tier', value: 'callback_registered', languages: ['javascript', 'typescript'], meaning: 'The site HANDS the callee this function (`xs.forEach(f)`, `p.then(f)`, `emitter.on(\'x\', h)`, `setTimeout(f)`), which may invoke it. Not the site\'s own callee; a reachability edge, labelled so it is never read as a resolved call.' }, { table: 'call_edges', column: 'tier', value: 'event_dispatch', languages: S, meaning: '`x.emit(\'name\')` reaching a handler registered by `x.on(\'name\', h)` on a value x may hold — name-sensitive for literal names, every handler on that value for a computed one.' }, - { table: 'call_edges', column: 'tier', value: 'event_dispatch', languages: J, meaning: 'A Spring application event: `publishEvent(e)` reaching each listener (`@EventListener`, `@TransactionalEventListener`, `ApplicationListener.onApplicationEvent`) whose declared event type e\'s static type is, or is a subtype of. Added beside the publishEvent boundary row, never in place of it (call-edge-generation/event_dispatch.dl).' }, + { table: 'call_edges', column: 'tier', value: 'event_dispatch', languages: J, meaning: 'A Spring application event: `publishEvent(e)` reaching each listener (`@EventListener`, `@TransactionalEventListener`, `ApplicationListener.onApplicationEvent`) whose declared event type e\'s static type is, or is a subtype of. Added beside the publishEvent boundary row, never in place of it (call-edge-generation/event_dispatch.dl). Also a JPA entity write (`save`, `persist`, `merge`, `delete` on a repository or EntityManager) reaching the `@PrePersist`/`@PreUpdate`/`@PreRemove`-style callbacks of the written entity\'s type and of the listeners `@EntityListeners` names on it or a superclass (kind entity_callback; call-edge-generation/entity_lifecycle.dl).' }, { table: 'call_edges', column: 'tier', value: 'intrinsic_terminal', languages: T, meaning: 'The site is a JSX intrinsic element or a dynamic `import()` — a runtime intrinsic, not a function the graph can name.' }, // fields.kind / field_access.access / field_access.tier (#663) @@ -615,6 +615,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'call_edges', column: 'kind', value: 'ctor_delegate', languages: J, meaning: '`this(…)` / `super(…)` inside a constructor.' }, { table: 'call_edges', column: 'kind', value: 'anon_new', languages: J, meaning: '`new X() { … }` — an anonymous class creation.' }, { table: 'call_edges', column: 'kind', value: 'event', languages: J, meaning: 'Synthesised: the publishEvent call site to a listener it runs (tier event_dispatch). Not a written call to that method.' }, + { table: 'call_edges', column: 'kind', value: 'entity_callback', languages: J, meaning: 'Synthesised: a call that writes an entity (`repository.save(e)`, `em.persist(e)`, `delete(e)`) to a JPA lifecycle callback it runs (tier event_dispatch). Not a written call to that method.' }, { table: 'call_edges', column: 'kind', value: 'record_accessor', languages: J, meaning: 'Synthesised: a record pattern `case Pair(var l, var r)` calls each accessor. Not a written call; the site is the pattern expression.' }, { table: 'call_edges', column: 'kind', value: 'resource_close', languages: J, meaning: 'Synthesised: a try-with-resources resource `try (var w = open())` is closed when the block exits, so its close() is called. Not a written call; the site is the resource local.' }, // — TypeScript (the parser's callKind) @@ -645,6 +646,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'call_edges', column: 'kind', value: 'DYNAMIC_CODE_CALL', languages: S, meaning: '`eval(…)` / `new Function(…)` — unknowable by construction.' }, { table: 'call_edges', column: 'kind', value: 'DYNAMIC_IMPORT_CALL', languages: S, meaning: '`import(…)` — a module load that is also a site.' }, { table: 'call_edges', column: 'kind', value: 'JSX_ELEMENT', languages: S, meaning: '`` — the renderer runs the component (a function component, or a class component\'s constructor and `render`) with the element\'s attributes as its props. Engine-authored: no written call; the site is the JSX element expression. An intrinsic tag (`
`) is no site.' }, + { table: 'call_edges', column: 'kind', value: 'JSX_ATTRIBUTE', languages: S, meaning: '``, `` — a function handed over as a JSX attribute, on any tag; always tier callback_registered. Engine-authored: the site is the attribute.' }, // — Python (the parser's callKind, plus engine-authored decorator/metaclass forms) { table: 'call_edges', column: 'kind', value: 'SIMPLE_CALL', languages: P, meaning: '`f(…)` — a bare name.' }, { table: 'call_edges', column: 'kind', value: 'METHOD_CALL', languages: P, meaning: '`obj.m(…)`.' }, diff --git a/graph/csharp/engine/framework-behavior/knobs.dl b/graph/csharp/engine/framework-behavior/knobs.dl index a44b0be6..1ead8281 100644 --- a/graph/csharp/engine/framework-behavior/knobs.dl +++ b/graph/csharp/engine/framework-behavior/knobs.dl @@ -338,3 +338,51 @@ cs_framework_save_hook("SaveChangesCanceled"). cs_framework_save_hook("SaveChang cs_framework_save_hook("ThrowingConcurrencyException"). cs_framework_save_hook("ThrowingConcurrencyExceptionAsync"). cs_framework_callback("SaveChangesInterceptor", n, "orm_hook") :- cs_framework_save_hook(n). cs_framework_callback("ISaveChangesInterceptor", n, "orm_hook") :- cs_framework_save_hook(n). +// EF Core: a command interceptor runs around every command the context sends. +cs_framework_command_hook("ReaderExecuting"). cs_framework_command_hook("ReaderExecutingAsync"). +cs_framework_command_hook("ReaderExecuted"). cs_framework_command_hook("ReaderExecutedAsync"). +cs_framework_command_hook("NonQueryExecuting"). cs_framework_command_hook("NonQueryExecutingAsync"). +cs_framework_command_hook("NonQueryExecuted"). cs_framework_command_hook("NonQueryExecutedAsync"). +cs_framework_command_hook("ScalarExecuting"). cs_framework_command_hook("ScalarExecutingAsync"). +cs_framework_command_hook("ScalarExecuted"). cs_framework_command_hook("ScalarExecutedAsync"). +cs_framework_command_hook("CommandFailed"). cs_framework_command_hook("CommandFailedAsync"). +cs_framework_callback("DbCommandInterceptor", n, "orm_hook") :- cs_framework_command_hook(n). +cs_framework_callback("IDbCommandInterceptor", n, "orm_hook") :- cs_framework_command_hook(n). +// ASP.NET Core: an endpoint filter runs around every endpoint it is added to, and a +// factory-activated middleware around every request. +cs_framework_callback("IEndpointFilter", "InvokeAsync", "framework_hook"). +cs_framework_callback("IMiddleware", "InvokeAsync", "framework_hook"). + +// ── PIPELINE REGISTRATION (pipelines.dl) ─────────────────────────────────── +// cs_pipeline_register(Method, ReceiverMember, Form): a call that puts a component into a +// framework pipeline, whose hooks (cs_framework_callback above) the framework then runs. +// ReceiverMember "*" is any receiver; otherwise the receiver must be a member access of +// that name (`options.Interceptors.Add()`: a bare `Add` is every list in the program). +// Form "type" is the call's type argument, `AddEndpointFilter()`; "value" is an +// argument whose type is known, `AddEndpointFilter(new T(..))`, +// `AddInterceptors(sp.GetRequiredService())`. +cs_pipeline_register("AddHostedService", "*", "type"). +cs_pipeline_register("AddEndpointFilter", "*", "type"). +cs_pipeline_register("AddEndpointFilter", "*", "value"). +cs_pipeline_register("UseMiddleware", "*", "type"). +cs_pipeline_register("AddInterceptor", "*", "type"). // a gRPC client's builder +cs_pipeline_register("AddInterceptor", "*", "value"). +cs_pipeline_register("Add", "Interceptors", "type"). // a gRPC server's options +cs_pipeline_register("Add", "Interceptors", "value"). +cs_pipeline_register("AddInterceptors", "*", "value"). // an EF Core context's options +// What a registered component WRAPS, beyond being called: (Method, ReceiverMember, Scope). +// "route": the endpoint it is added to, or every endpoint of the route group it is added +// to (nested groups too). "grpc_host": every rpc of every service the same file maps +// with cs_grpc_map_service, which is where a host both configures AddGrpc and maps. +cs_pipeline_wraps("AddEndpointFilter", "*", "route"). +cs_pipeline_wraps("Add", "Interceptors", "grpc_host"). +cs_grpc_map_service("MapGrpcService"). +// EF Core runs every save interceptor added with AddInterceptors when a context saves: +// the synchronous hooks on SaveChanges, the Async ones on SaveChangesAsync. +cs_ef_interceptor_register("AddInterceptors"). +cs_ef_save_call("SaveChanges", "sync"). +cs_ef_save_call("SaveChangesAsync", "async"). +// the framework bases a DbContext is written against +cs_ef_context_base("DbContext"). +cs_ef_context_base("IdentityDbContext"). +cs_ef_context_base("IdentityUserContext"). diff --git a/graph/csharp/engine/framework-behavior/pipelines.dl b/graph/csharp/engine/framework-behavior/pipelines.dl new file mode 100644 index 00000000..2529b9f9 --- /dev/null +++ b/graph/csharp/engine/framework-behavior/pipelines.dl @@ -0,0 +1,133 @@ +// ============================================================================ +// FRAMEWORK-BEHAVIOR · PIPELINES (a component registered into a framework pipeline) +// +// group.AddEndpointFilter(); // ASP.NET runs InvokeAsync +// services.AddGrpc(o => o.Interceptors.Add()); // gRPC runs UnaryServerHandler +// options.AddInterceptors(new AuditInterceptor()); // EF runs SavingChangesAsync +// await db.SaveChangesAsync(); // ...here +// +// The registration names the component's TYPE (or hands over an instance) and never +// its hook, so no call site names InvokeAsync, UnaryServerHandler or SavingChangesAsync. +// entry-points.dl already says the framework calls them; nothing said WHO put them where +// they are called, so `impact` on the hook had no caller and `path` from a save to the +// interceptor found no chain. +// +// Three facts, each ADDED beside what the resolution phases derived: +// 1. the registration site calls every hook of the registered type. A call_chain_edge +// of tier `callback_registered` (a component handed over and run by whoever holds +// it): consumers rank it below a resolved call and print it as `registered`. +// 2. an EF save calls the save hooks of every interceptor the project registers with +// AddInterceptors. Tier `event_dispatch`, as a mediator's Send reaching its handler +// (dispatch.dl): same call stack, no call site names the hook. +// 3. a filter or a server interceptor WRAPS the endpoints it is added to: the endpoint +// depends on it (a request to the endpoint runs it first). framework_edge(Endpoint, +// Hook, Mechanism, Detail, Confidence), the relation python/ emits for the same kind +// of hop, which impact reads as a `framework` dependent of the hook. Not a call. +// +// Every framework name is a knob (knobs.dl: cs_pipeline_register, cs_pipeline_wraps, +// cs_ef_*), over the cs_framework_callback table that names each base's hooks. +// ============================================================================ + +// ── the registration sites ────────────────────────────────────────────────── +pl_site(e, mn, rm) :- csite("client", e), call_callee_name("client", e, mn), cs_pipeline_register(mn, rm, _), + rm = "*". +pl_site(e, mn, rm) :- csite("client", e), call_callee_name("client", e, mn), cs_pipeline_register(mn, rm, _), + rm != "*", call_receiver_expr("client", e, r), expr_kind("client", r, "MEMBER_ACCESS"), + expr_member_name_child("client", r, nm), expr_written_name("client", nm, rm). + +// the registered type: the call's own type argument... +pl_registers(e, gk) :- pl_site(e, mn, rm), cs_pipeline_register(mn, rm, "type"), + type_ref_owner("client", tr, e, "EXPRESSION"), type_ref_context("client", tr, "METHOD_TYPE_ARGUMENT"), + !type_ref_parent("client", tr, _, _, _), type_ref_resolves("client", tr, gk). +// ...or the type of an argument: `new T(..)`, a typed local, parameter or field, and +// `sp.GetRequiredService()`, whose type is its type argument. +pl_registers(e, gk) :- pl_site(e, mn, rm), cs_pipeline_register(mn, rm, "value"), http_arg(e, _, a), + expr_type("client", a, gk). +pl_registers(e, gk) :- pl_site(e, mn, rm), cs_pipeline_register(mn, rm, "value"), http_arg(e, _, a), + csite("client", a), call_callee_name("client", a, f), cs_di_resolve(f), + type_ref_owner("client", tr, a, "EXPRESSION"), type_ref_context("client", tr, "METHOD_TYPE_ARGUMENT"), + !type_ref_parent("client", tr, _, _, _), type_ref_resolves("client", tr, gk). + +// ── the hooks of a registered type ────────────────────────────────────────── +// A method the framework calls on the type because of the base it derives from +// (entry-points.dl, cs_framework_type), declared on the type or, when the type does not +// declare one of that name, inherited from the project's own base. +pl_hook_name(gk, mn) :- pl_registers(_, gk), cs_framework_type(gk, bn), cs_framework_callback(bn, mn, _), + !contains("*", mn), !contains("<", mn). +pl_own_hook(gk, mn, m) :- pl_hook_name(gk, mn), method_in_type("client", gk, m), method_name("client", m, mn). +pl_has_own(gk, mn) :- pl_own_hook(gk, mn, _). +pl_hook(gk, m) :- pl_own_hook(gk, _, m). +pl_hook(gk, m) :- pl_hook_name(gk, mn), !pl_has_own(gk, mn), type_ancestor("client", gk, agk), + method_in_type("client", agk, m), method_name("client", m, mn). + +// 1. the registration site reaches the hooks +call_chain_edge(e, from, "-", m, "client", "callback_registered", kind) :- pl_registers(e, gk), pl_hook(gk, m), + call_from("client", e, from), from != m, invocation_site("client", e, kind). + +// ── 2. an EF save runs the registered save interceptors ───────────────────── +pl_ef_interceptor(gk) :- pl_site(e, mn, _), cs_ef_interceptor_register(mn), pl_registers(e, gk). +pl_async_name(n) :- cs_framework_save_hook(n), strlen(n) > 5, substr(n, strlen(n) - 5, 5) = "Async". +pl_save_hook(m, "async") :- pl_ef_interceptor(gk), pl_hook(gk, m), method_name("client", m, n), + cs_framework_save_hook(n), pl_async_name(n). +pl_save_hook(m, "sync") :- pl_ef_interceptor(gk), pl_hook(gk, m), method_name("client", m, n), + cs_framework_save_hook(n), !pl_async_name(n). + +// a context: a type deriving from a DbContext base, directly or through the project's own +pl_context(gk) :- cs_framework_type(gk, bn), cs_ef_context_base(bn). +// A save: SaveChanges(Async) on a context, `db.SaveChangesAsync()`, `SaveChanges()` +// inside one, `base.SaveChangesAsync()` in its override... +pl_save_call(e, how) :- call_callee_name("client", e, n), cs_ef_save_call(n, how), call_recv_type("client", e, gk), + pl_context(gk). +pl_save_call(e, how) :- call_callee_name("client", e, n), cs_ef_save_call(n, how), call_is_base("client", e), + expr_ultimate_type_group("client", e, gk), pl_context(gk). +// ...and through the project's own abstraction over it: `IUnitOfWork.SaveChangesAsync`, +// which a context implements by inheriting DbContext's. The call resolves to the +// abstraction's method, which is not a context's own: a context that overrides the save +// is reached through the dispatch hop and its own `base.` call above. +// (Read off the resolution relations, not call_chain_edge, which this adds to.) +pl_save_target(e, mm) :- call_callee_name("client", e, n), cs_ef_save_call(n, _), client_calls_client("client", e, "client", mm). +pl_save_target(e, mm) :- call_callee_name("client", e, n), cs_ef_save_call(n, _), call_names_bodiless(e, mm). +pl_save_call(e, how) :- call_callee_name("client", e, n), cs_ef_save_call(n, how), pl_save_target(e, mm), + method_name("client", mm, n), method_owner("client", ot, mm), type_group("client", ot, ogk), !pl_context(ogk), + pl_context(cgk), type_ancestor("client", cgk, ogk). + +call_chain_edge(e, from, "-", m, "client", "event_dispatch", kind) :- pl_save_call(e, how), pl_save_hook(m, how), + call_from("client", e, from), from != m, invocation_site("client", e, kind). + +// ── 3. what a filter or server interceptor wraps ──────────────────────────── +pl_wrap_site(e, sc) :- pl_site(e, mn, rm), cs_pipeline_wraps(mn, rm, sc). + +// "route": walk back from a builder expression through locals and fluent calls to the +// endpoint (MapGet..) or route group (MapGroup) it configures, as group_prefix does in +// destinations.dl. +pl_map_call(e) :- map_call(e, _). +pl_up(x, x) :- pl_wrap_site(e, "route"), call_receiver_expr("client", e, x). +pl_up(x, x) :- map_call(e, _), call_receiver_expr("client", e, x). +pl_up(x, x) :- map_group_call(e), call_receiver_expr("client", e, x). +pl_up(x, z) :- pl_up(x, y), ref_denotes("client", y, "LOCAL_VARIABLE", v), var_initializer("client", v, z). +pl_up(x, z) :- pl_up(x, y), csite("client", y), !pl_map_call(y), !map_group_call(y), call_receiver_expr("client", y, z). +pl_anchor(x, n) :- pl_up(x, n), map_call(n, _). +pl_anchor(x, n) :- pl_up(x, n), map_group_call(n). + +// the endpoints of a group, and of the groups nested in it +pl_in_group(e, g) :- map_call(e, _), call_receiver_expr("client", e, r), pl_anchor(r, g), map_group_call(g). +pl_in_group(e, g) :- pl_in_group(e, g2), call_receiver_expr("client", g2, r), pl_anchor(r, g), map_group_call(g). + +pl_wraps(h, e) :- pl_wrap_site(e, "route"), call_receiver_expr("client", e, r), pl_anchor(r, n), map_call(n, _), + map_handler(n, h). +pl_wraps(h, e) :- pl_wrap_site(e, "route"), call_receiver_expr("client", e, r), pl_anchor(r, g), map_group_call(g), + pl_in_group(n, g), map_handler(n, h). + +// "grpc_host": every rpc of every service mapped in the file that registers it +pl_grpc_mapped(mod, gk) :- csite("client", e), call_callee_name("client", e, mn), cs_grpc_map_service(mn), + call_module("client", e, mod), + type_ref_owner("client", tr, e, "EXPRESSION"), type_ref_context("client", tr, "METHOD_TYPE_ARGUMENT"), + !type_ref_parent("client", tr, _, _, _), type_ref_resolves("client", tr, gk). +pl_wraps(h, e) :- pl_wrap_site(e, "grpc_host"), call_module("client", e, mod), pl_grpc_mapped(mod, gk), + grpc_serves(h, _), method_owner("client", t, h), type_group("client", t, gk). + +// the detail names the registration as written: `AddEndpointFilter`, `Interceptors.Add` +pl_wrap_detail(e, mn) :- pl_wrap_site(e, _), pl_site(e, mn, "*"). +pl_wrap_detail(e, d) :- pl_wrap_site(e, _), pl_site(e, mn, rm), rm != "*", d = cat(rm, cat(".", mn)). +framework_edge(h, m, "pipeline_wrap", d, "registered") :- pl_wraps(h, e), pl_registers(e, gk), pl_hook(gk, m), + pl_wrap_detail(e, d), h != m. diff --git a/graph/csharp/souffle/decls_all.dl b/graph/csharp/souffle/decls_all.dl index 37161bfd..4372d0de 100644 --- a/graph/csharp/souffle/decls_all.dl +++ b/graph/csharp/souffle/decls_all.dl @@ -142,9 +142,13 @@ .decl cs_amqp_publish(c0:symbol) .decl cs_attr_suffix(c0:symbol,c1:symbol) .decl cs_di_resolve(c0:symbol) +.decl cs_ef_context_base(c0:symbol) +.decl cs_ef_interceptor_register(c0:symbol) +.decl cs_ef_save_call(c0:symbol,c1:symbol) .decl cs_framework_base(c0:symbol) .decl cs_framework_callback(c0:symbol,c1:symbol,c2:symbol) .decl cs_framework_callback_prefix(c0:symbol,c1:symbol,c2:symbol) +.decl cs_framework_command_hook(c0:symbol) .decl cs_framework_namespace_root(c0:symbol) .decl cs_framework_save_hook(c0:symbol) .decl cs_framework_type(c0:symbol,c1:symbol) @@ -152,6 +156,7 @@ .decl cs_grpc_async_suffix(c0:symbol) .decl cs_grpc_client_member_not_rpc(c0:symbol) .decl cs_grpc_client_suffix(c0:symbol) +.decl cs_grpc_map_service(c0:symbol) .decl cs_grpc_server_suffix(c0:symbol) .decl cs_heritage_in_source(c0:symbol,c1:symbol) .decl cs_http_client_factory_method(c0:symbol) @@ -181,6 +186,8 @@ .decl cs_msg_name_serve(c0:symbol,c1:symbol,c2:number,c3:symbol) .decl cs_msg_request_client(c0:symbol,c1:symbol) .decl cs_msg_request_send(c0:symbol) +.decl cs_pipeline_register(c0:symbol,c1:symbol,c2:symbol) +.decl cs_pipeline_wraps(c0:symbol,c1:symbol,c2:symbol) .decl cs_predefined_alias(c0:symbol) .decl cs_remote_confidence_binding(c0:symbol) .decl cs_remote_confidence_exact(c0:symbol) @@ -325,6 +332,7 @@ .decl field_type(c0:symbol,c1:symbol,c2:symbol) .decl field_type_name(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl field_type_ref(c0:symbol,c1:symbol,c2:symbol) +.decl framework_edge(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl framework_rooted(c0:symbol) .decl generated_accessor_read(c0:symbol,c1:symbol,c2:symbol) .decl generated_property(c0:symbol,c1:symbol,c2:symbol,c3:symbol) @@ -437,8 +445,8 @@ .decl med_send_concrete(c0:symbol,c1:symbol) .decl med_send_declared(c0:symbol,c1:symbol) .decl med_send_has_named_type(c0:symbol,c1:symbol) -.decl med_send_named_type(c0:symbol,c1:symbol,c2:symbol) .decl med_send_matcher(c0:symbol) +.decl med_send_named_type(c0:symbol,c1:symbol,c2:symbol) .decl med_send_new_ref(c0:symbol,c1:symbol) .decl med_send_qualified(c0:symbol,c1:symbol) .decl med_send_sees_new(c0:symbol) @@ -625,6 +633,26 @@ .decl parse_gap_bytes(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl path_norm(c0:symbol,c1:symbol) .decl path_raw(c0:symbol) +.decl pl_anchor(c0:symbol,c1:symbol) +.decl pl_async_name(c0:symbol) +.decl pl_context(c0:symbol) +.decl pl_ef_interceptor(c0:symbol) +.decl pl_grpc_mapped(c0:symbol,c1:symbol) +.decl pl_has_own(c0:symbol,c1:symbol) +.decl pl_hook(c0:symbol,c1:symbol) +.decl pl_hook_name(c0:symbol,c1:symbol) +.decl pl_in_group(c0:symbol,c1:symbol) +.decl pl_map_call(c0:symbol) +.decl pl_own_hook(c0:symbol,c1:symbol,c2:symbol) +.decl pl_registers(c0:symbol,c1:symbol) +.decl pl_save_call(c0:symbol,c1:symbol) +.decl pl_save_hook(c0:symbol,c1:symbol) +.decl pl_save_target(c0:symbol,c1:symbol) +.decl pl_site(c0:symbol,c1:symbol,c2:symbol) +.decl pl_up(c0:symbol,c1:symbol) +.decl pl_wrap_detail(c0:symbol,c1:symbol) +.decl pl_wrap_site(c0:symbol,c1:symbol) +.decl pl_wraps(c0:symbol,c1:symbol) .decl pn(c0:symbol) .decl pn_count(c0:symbol,c1:number) .decl pn_has_literal(c0:symbol) diff --git a/graph/csharp/souffle/export_manifest.tsv b/graph/csharp/souffle/export_manifest.tsv index 1b4583e2..a48d6138 100644 --- a/graph/csharp/souffle/export_manifest.tsv +++ b/graph/csharp/souffle/export_manifest.tsv @@ -38,3 +38,4 @@ entry_point entry-point.csv entry_reachable entry-reachable.csv type_ancestor_type resolution-type-ancestor-type.csv override_pair resolution-virtual-override.csv +framework_edge framework-edge.csv diff --git a/graph/java/engine/call-edge-generation/entity_lifecycle.dl b/graph/java/engine/call-edge-generation/entity_lifecycle.dl new file mode 100644 index 00000000..b4d032ef --- /dev/null +++ b/graph/java/engine/call-edge-generation/entity_lifecycle.dl @@ -0,0 +1,86 @@ +// ============================================================================ +// Call-edge generation · ENTITY LIFECYCLE CALLBACKS (save(entity) -> the callbacks it runs) +// +// orders.save(order); // written here +// @Entity @EntityListeners(Stamps.class) class Order { @PrePersist void check() { … } } +// class Stamps { @PrePersist void onCreate(Order o) { … } } // run from there +// +// A JPA provider fires an entity's lifecycle callbacks when the entity is written through +// the persistence context: the methods of the entity (and of its mapped superclasses) +// annotated @PrePersist / @PostPersist / @PreUpdate / @PostUpdate / @PreRemove / +// @PostRemove, and the same-annotated methods of every listener class that +// @EntityListeners names on the entity or on a superclass. No call site names any of +// them, so without this file `path` from the code that saves an entity to its listener +// said the two were independent, and `impact` on a listener said the change was local. +// +// WHY A CALL EDGE WITH TIER event_dispatch. It is the application-event shape +// (event_dispatch.dl): one JVM, keyed on a TYPE (the entity's), the callback running +// before or right after the write returns. The provider publishes a lifecycle event and +// runs its handlers, which is what the event_dispatch tier already says to every tool +// that walks call_chain_edge; the KIND column, `entity_callback`, tells the two apart. +// The boundary row for the save call itself is kept: the edge is ADDED beside it. +// +// THE KEY IS THE STATIC TYPE OF THE WRITTEN ENTITY, with the subtype rule JPA applies: +// callbacks declared on the entity type or any ancestor of it, and listeners named on +// the entity type or any ancestor of it. An entity that names no listener and declares +// no callback gets no edge, and a listener class that no entity names is never linked. +// +// WHICH CALLBACKS, BY OPERATION (cfg_entity_write): a save or merge may insert or update, +// so it reaches the persist and the update callbacks; persist reaches the persist ones; +// delete / remove the remove ones. +// +// THE STORE is a knob table of persistence interfaces (cfg_entity_store), matched against +// the receiver's declared type or any ancestor of it — a Spring Data repository interface +// in the project reaches it through `extends JpaRepository<…>`. A project class that +// declares its own save(entity) is neither, so a call on it stays the ordinary call it is. +// +// WHAT IS NOT MODELLED, and stays unlinked rather than guessed: +// - saveAll(Iterable) / deleteAll(…) / deleteById(id): the argument is not the entity. +// - an update of a managed entity at flush or commit with no write call (dirty checking). +// - @PostLoad (a read, not a write), @ExcludeSuperclassListeners, default listeners +// declared in orm.xml. +// ============================================================================ + +// ── the store types, by name ─────────────────────────────────────────────── +// An external node (`external:`, or `external:` from an +// on-demand import) or a staged library type, as event_dispatch.dl matches publishers. +entity_store_type(x) :- cfg_entity_store(n), external_type(x), x = cat("external:", n). +entity_store_type(x) :- cfg_entity_store(n), external_type(x), + strlen(x) > strlen(n) + 1, + substr(x, max(0, strlen(x) - strlen(n) - 1), strlen(n) + 1) = cat(".", n). +entity_store_type(t) :- cfg_entity_store(n), lib_type(n, _, _, _, _, _, _, _, _, _, _, _, _, t). + +entity_store_recv_type(t) :- entity_store_type(t). +entity_store_recv_type(t) :- entity_store_type(a), type_ancestor(t, a). + +// ── the write sites, and the static type of the entity each one writes ────── +entity_write_site(call, phase) :- call_site(call, name, recv), cfg_entity_write(name, phase), + expr_type(_, recv, t), entity_store_recv_type(t). + +entity_written(call, caller, et, phase) :- entity_write_site(call, phase), + expr_child("client", call, "ARGUMENT", arg), expr_position("client", arg, "0"), + expr_type(_, arg, et), call_from(call, caller). + +// ── the callbacks each entity type runs, by phase ────────────────────────── +entity_callback_method(m, owner, phase) :- ann_on_method(_, _, n, m, _), cfg_entity_callback_ann(n, phase), + method_owner("client", owner, m). + +// the listener classes a type names with @EntityListeners (one row per class; an array +// argument is stored as one row per element) +entity_names_listener(t, l) :- ann_on_type(_, a, n, t), cfg_entity_listeners_ann(n), + ann_arg(_, a, _, v, "CLASS_REFERENCE", _), config_class_ref("annotation", a, v, l, _). + +// the types whose callbacks and named listeners apply to an entity of type et: et itself +// and every ancestor of it +entity_scope(et, et) :- entity_written(_, _, et, _). +entity_scope(et, a) :- entity_written(_, _, et, _), type_ancestor(et, a). + +entity_runs(et, m, phase) :- entity_scope(et, s), entity_callback_method(m, s, phase). +entity_runs(et, m, phase) :- entity_scope(et, s), entity_names_listener(s, l), + entity_callback_method(m, l, phase). + +// ── the edge ──────────────────────────────────────────────────────────────── +entity_callback_edge(call, caller, m) :- entity_written(call, caller, et, phase), entity_runs(et, m, phase). + +call_chain_edge(call, caller, "-", m, "client", "event_dispatch", "entity_callback") :- + entity_callback_edge(call, caller, m). diff --git a/graph/java/engine/config-resolution/knobs.dl b/graph/java/engine/config-resolution/knobs.dl index 82f96b0b..2ec0de97 100644 --- a/graph/java/engine/config-resolution/knobs.dl +++ b/graph/java/engine/config-resolution/knobs.dl @@ -375,3 +375,29 @@ cfg_event_listener_type("ApplicationListener", "onApplicationEvent"). // is wrapped in a PayloadApplicationEvent, which is an ApplicationEvent. cfg_event_catch_all("ApplicationEvent"). cfg_event_catch_all("Object"). + +// ── (21) ENTITY LIFECYCLE CALLBACKS: save(entity) runs its @PrePersist … listeners ── +// A JPA provider runs an entity's lifecycle callbacks, and those of the listener classes +// @EntityListeners names, when the entity is written. No call site names them +// (call-edge-generation/entity_lifecycle.dl). +// +// cfg_entity_store(TypeSimpleName): a write call on a receiver declared as this type, or +// as a subtype of it (a project repository interface), writes its first argument. +cfg_entity_store("Repository"). cfg_entity_store("CrudRepository"). +cfg_entity_store("ListCrudRepository"). cfg_entity_store("PagingAndSortingRepository"). +cfg_entity_store("JpaRepository"). cfg_entity_store("EntityManager"). +cfg_entity_store("Session"). +// cfg_entity_write(Method, Phase): the callbacks of which phase a write method runs. A +// save or merge may insert or update, so it runs both. +cfg_entity_write("save", "persist"). cfg_entity_write("save", "update"). +cfg_entity_write("saveAndFlush", "persist"). cfg_entity_write("saveAndFlush", "update"). +cfg_entity_write("merge", "persist"). cfg_entity_write("merge", "update"). +cfg_entity_write("saveOrUpdate", "persist"). cfg_entity_write("saveOrUpdate", "update"). +cfg_entity_write("persist", "persist"). cfg_entity_write("update", "update"). +cfg_entity_write("delete", "remove"). cfg_entity_write("remove", "remove"). +// cfg_entity_callback_ann(Annotation, Phase) +cfg_entity_callback_ann("PrePersist", "persist"). cfg_entity_callback_ann("PostPersist", "persist"). +cfg_entity_callback_ann("PreUpdate", "update"). cfg_entity_callback_ann("PostUpdate", "update"). +cfg_entity_callback_ann("PreRemove", "remove"). cfg_entity_callback_ann("PostRemove", "remove"). +// a type annotation whose class arguments are listener classes +cfg_entity_listeners_ann("EntityListeners"). diff --git a/graph/java/souffle/decls_all.dl b/graph/java/souffle/decls_all.dl index c27f23b3..625f6581 100644 --- a/graph/java/souffle/decls_all.dl +++ b/graph/java/souffle/decls_all.dl @@ -932,6 +932,20 @@ .decl event_listener_class(c0:symbol,c1:symbol) .decl event_listener_method(c0:symbol) .decl event_dispatch_edge(c0:symbol,c1:symbol,c2:symbol) +// call-edge-generation/entity_lifecycle.dl + knobs.dl (21): JPA entity lifecycle callbacks +.decl cfg_entity_store(c0:symbol) +.decl cfg_entity_write(c0:symbol,c1:symbol) +.decl cfg_entity_callback_ann(c0:symbol,c1:symbol) +.decl cfg_entity_listeners_ann(c0:symbol) +.decl entity_store_type(c0:symbol) +.decl entity_store_recv_type(c0:symbol) +.decl entity_write_site(c0:symbol,c1:symbol) +.decl entity_written(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl entity_callback_method(c0:symbol,c1:symbol,c2:symbol) +.decl entity_names_listener(c0:symbol,c1:symbol) +.decl entity_scope(c0:symbol,c1:symbol) +.decl entity_runs(c0:symbol,c1:symbol,c2:symbol) +.decl entity_callback_edge(c0:symbol,c1:symbol,c2:symbol) // #1401: `var` in an enhanced for, a try-with-resources resource and a lambda parameter .decl iter_written_ref(c0:symbol,c1:symbol) // (iterable expr, its written declaration ref) .decl foreach_platform_iterable(c0:symbol,c1:symbol) // (package, simple name) knob diff --git a/graph/javascript/engine/call-edge-generation/jsx.dl b/graph/javascript/engine/call-edge-generation/jsx.dl index 4e1180cd..f3c31375 100644 --- a/graph/javascript/engine/call-edge-generation/jsx.dl +++ b/graph/javascript/engine/call-edge-generation/jsx.dl @@ -144,3 +144,20 @@ call_chain_edge(je, caller, "-", m, "lib", "boundary_lib", "JSX_ELEMENT") :- jsx // A tag that names something the value layer could not follow: a declared unknown. call_chain_edge(je, caller, "-", "-", "-", "ambiguous_unknown", "JSX_ELEMENT") :- jsx_target_count(je, 0), jsx_caller(je, caller). + +// ── a function handed over as an attribute ────────────────────────────────── +// ``, ` + + label()}>hover + + + ); +} diff --git a/graph/test/javascript/cases/70-jsx-attribute-handler/src/UserForm.jsx b/graph/test/javascript/cases/70-jsx-attribute-handler/src/UserForm.jsx new file mode 100644 index 00000000..e0ba9847 --- /dev/null +++ b/graph/test/javascript/cases/70-jsx-attribute-handler/src/UserForm.jsx @@ -0,0 +1,3 @@ +export default function UserForm({ action }) { + return
; +} diff --git a/graph/test/javascript/cases/70-jsx-attribute-handler/src/actions.js b/graph/test/javascript/cases/70-jsx-attribute-handler/src/actions.js new file mode 100644 index 00000000..22631c0b --- /dev/null +++ b/graph/test/javascript/cases/70-jsx-attribute-handler/src/actions.js @@ -0,0 +1 @@ +export async function createUser(data) { return data; } diff --git a/graph/test/javascript/cases/70-jsx-attribute-handler/src/main.jsx b/graph/test/javascript/cases/70-jsx-attribute-handler/src/main.jsx new file mode 100644 index 00000000..51bd35b3 --- /dev/null +++ b/graph/test/javascript/cases/70-jsx-attribute-handler/src/main.jsx @@ -0,0 +1,2 @@ +import LoginForm from './LoginForm'; +export const root = ; diff --git a/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Legacy.vue b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Legacy.vue new file mode 100644 index 00000000..a5280d51 --- /dev/null +++ b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Legacy.vue @@ -0,0 +1,10 @@ + + + diff --git a/graph/test/javascript/cases/70-single-file-component-scripts/src/components/List.svelte b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/List.svelte new file mode 100644 index 00000000..7d673914 --- /dev/null +++ b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/List.svelte @@ -0,0 +1,8 @@ + +{#each items as p}
  • {show(p)} {onlyInMarkup(p)}
  • {/each} diff --git a/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Price.vue b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Price.vue new file mode 100644 index 00000000..28d5800c --- /dev/null +++ b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Price.vue @@ -0,0 +1,9 @@ + + + diff --git a/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Typed.vue b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Typed.vue new file mode 100644 index 00000000..e695a8ce --- /dev/null +++ b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Typed.vue @@ -0,0 +1,5 @@ + + diff --git a/graph/test/javascript/cases/70-single-file-component-scripts/src/lib/format.js b/graph/test/javascript/cases/70-single-file-component-scripts/src/lib/format.js new file mode 100644 index 00000000..46a90a21 --- /dev/null +++ b/graph/test/javascript/cases/70-single-file-component-scripts/src/lib/format.js @@ -0,0 +1,8 @@ +// Helpers called only from single-file components: every call to them lives in a +// diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/jsconfig.json b/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/jsconfig.json new file mode 100644 index 00000000..e8b41875 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/jsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { "checkJs": true } +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/lib/api.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/lib/api.js new file mode 100644 index 00000000..33e6187d --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/lib/api.js @@ -0,0 +1,7 @@ +export function get(path) { + return path; +} + +export function post(path, body) { + return [path, body]; +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/routes/page.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/routes/page.js new file mode 100644 index 00000000..6b70d40f --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/routes/page.js @@ -0,0 +1,11 @@ +// SvelteKit's $lib, mapped by the generated .svelte-kit/tsconfig.json that a checkout lacks +import * as api from '$lib/api.js'; +import { post } from '$lib/api'; + +export function load({ params }) { + return api.get(params.slug); +} + +export function save(body) { + return post('/save', body); +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/server/api/price.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/server/api/price.js new file mode 100644 index 00000000..1e7bf1b8 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/server/api/price.js @@ -0,0 +1,7 @@ +// Nuxt's ~ and @, mapped by the generated .nuxt/tsconfig.json that a checkout lacks +import { formatPrice } from '~/utils/price'; +import { formatPrice as viaAt } from '@/utils/price'; + +export default function handler() { + return formatPrice(1) + viaAt(2); +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/tsconfig.json b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/tsconfig.json new file mode 100644 index 00000000..5e4206d0 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "./.nuxt/tsconfig.json" } diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/utils/price.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/utils/price.js new file mode 100644 index 00000000..c32b777c --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/utils/price.js @@ -0,0 +1,3 @@ +export function formatPrice(n) { + return n; +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/pages/index.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/pages/index.js new file mode 100644 index 00000000..714e6b72 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/pages/index.js @@ -0,0 +1,6 @@ +// Nuxt 4: the source dir is app/, and the root config only references the generated ones +import { formatDate } from '~/utils/date'; + +export function page(d) { + return formatDate(d); +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/utils/date.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/utils/date.js new file mode 100644 index 00000000..6ba492fd --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/utils/date.js @@ -0,0 +1,3 @@ +export function formatDate(d) { + return d; +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/tsconfig.json b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/tsconfig.json new file mode 100644 index 00000000..84bef86a --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./.nuxt/tsconfig.app.json" }, { "path": "./.nuxt/tsconfig.server.json" }] +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/jsconfig.json b/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/jsconfig.json new file mode 100644 index 00000000..1025badb --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/jsconfig.json @@ -0,0 +1 @@ +{ "extends": "./.config/tsconfig.json" } diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/probe.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/probe.js new file mode 100644 index 00000000..5cca172c --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/probe.js @@ -0,0 +1,6 @@ +// CONTROL: a missing extended config that no framework generates maps nothing +import { helper } from '$lib/helper'; + +export function probe() { + return helper(); +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/src/lib/helper.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/src/lib/helper.js new file mode 100644 index 00000000..ee46f6df --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/src/lib/helper.js @@ -0,0 +1,3 @@ +export function helper() { + return 1; +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/jsconfig.json b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/jsconfig.json new file mode 100644 index 00000000..5e845477 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/jsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { "baseUrl": ".", "paths": { "$lib/*": ["./shared/*"] } } +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/probe.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/probe.js new file mode 100644 index 00000000..3de05691 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/probe.js @@ -0,0 +1,6 @@ +// CONTROL: the project maps $lib itself, and its own paths win over any framework default +import { pick } from '$lib/util'; + +export function chosen() { + return pick(); +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/shared/util.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/shared/util.js new file mode 100644 index 00000000..19f5e2f8 --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/shared/util.js @@ -0,0 +1,3 @@ +export function pick() { + return 2; +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/src/lib/util.js b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/src/lib/util.js new file mode 100644 index 00000000..f063568e --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/src/lib/util.js @@ -0,0 +1,3 @@ +export function pick() { + return 1; +} diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/package.json b/graph/test/javascript/cases/71-framework-generated-config-alias/src/package.json new file mode 100644 index 00000000..162f64bf --- /dev/null +++ b/graph/test/javascript/cases/71-framework-generated-config-alias/src/package.json @@ -0,0 +1 @@ +{ "name": "framework-alias-fixture", "type": "module" } diff --git a/graph/test/javascript/expected/62-jsx-component-render.edges b/graph/test/javascript/expected/62-jsx-component-render.edges index 05290ffb..5ec30d2d 100644 --- a/graph/test/javascript/expected/62-jsx-component-render.edges +++ b/graph/test/javascript/expected/62-jsx-component-render.edges @@ -5,12 +5,15 @@ App.jsx:20:27 FUNCTION_CALL format -> known_edge App.jsx:26:1 format App.jsx:20:7 JSX_ELEMENT {(item) => format(item)} -> known_edge components/ui.jsx:3:1 List App.jsx:25:30 FUNCTION_CALL div -> known_edge App.jsx:7:1 div App.jsx:26:32 FUNCTION_CALL String -> ambient_terminal - +components/Panel.jsx:5:21 JSX_ATTRIBUTE onClick={() => this.props.onClose()} -> callback_registered components/Panel.jsx:5:30 components/Panel.jsx:5:36 METHOD_CALL this.props.onClose -> known_edge App.jsx:25:1 closeAll +components/TodoItem.jsx:3:14 JSX_ATTRIBUTE onClick={() => onToggle(todo)} -> callback_registered components/TodoItem.jsx:3:23 components/TodoItem.jsx:3:29 FUNCTION_CALL onToggle -> known_edge components/TodoList.jsx:4:3 handleToggle components/TodoList.jsx:4:40 FUNCTION_CALL save -> known_edge components/TodoList.jsx:9:1 save components/TodoList.jsx:6:15 METHOD_CALL todos.map -> ambient_terminal - components/TodoList.jsx:6:15 METHOD_CALL todos.map -> callback_registered components/TodoList.jsx:6:25 components/TodoList.jsx:6:32 JSX_ELEMENT -> known_edge components/TodoItem.jsx:2:1 TodoItem +components/TodoList.jsx:6:64 JSX_ATTRIBUTE onToggle={handleToggle} -> callback_registered components/TodoList.jsx:4:3 handleToggle components/ui.jsx:3:43 METHOD_CALL [1, 2].map -> ambient_terminal - components/ui.jsx:3:43 METHOD_CALL [1, 2].map -> callback_registered components/ui.jsx:3:54 components/ui.jsx:3:61 METHOD_CALL props.children -> known_edge App.jsx:20:17 diff --git a/graph/test/javascript/expected/69-vue-sfc.diag b/graph/test/javascript/expected/69-vue-sfc.diag new file mode 100644 index 00000000..3ef2afad --- /dev/null +++ b/graph/test/javascript/expected/69-vue-sfc.diag @@ -0,0 +1 @@ +package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> - diff --git a/graph/test/javascript/expected/69-vue-sfc.edges b/graph/test/javascript/expected/69-vue-sfc.edges new file mode 100644 index 00000000..d7efd225 --- /dev/null +++ b/graph/test/javascript/expected/69-vue-sfc.edges @@ -0,0 +1,5 @@ +Comp.vue:10:34 FUNCTION_CALL sfcHelper -> known_edge Comp.vue:3:1 sfcHelper +Comp.vue:10:6 FUNCTION_CALL Child -> known_edge child.js:1:1 Child +Comp.vue:11:6 FUNCTION_CALL MyBadge -> known_edge child.js:5:1 MyBadge +Comp.vue:12:21 FUNCTION_CALL onSave -> known_edge Comp.vue:4:1 onSave +Comp.vue:12:40 FUNCTION_CALL sfcHelper -> known_edge Comp.vue:3:1 sfcHelper diff --git a/graph/test/javascript/expected/69-vue-sfc.oracle b/graph/test/javascript/expected/69-vue-sfc.oracle new file mode 100644 index 00000000..8675b52d --- /dev/null +++ b/graph/test/javascript/expected/69-vue-sfc.oracle @@ -0,0 +1 @@ +# defects: 0 diff --git a/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.diag b/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.diag new file mode 100644 index 00000000..3ef2afad --- /dev/null +++ b/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.diag @@ -0,0 +1 @@ +package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> - diff --git a/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.edges b/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.edges new file mode 100644 index 00000000..f7aad7a0 --- /dev/null +++ b/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.edges @@ -0,0 +1,40 @@ +main.js:14:34 FUNCTION_CALL (table[k] || deep) -> multi_inferred main.js:7:1 fromA +main.js:14:34 FUNCTION_CALL (table[k] || deep) -> multi_inferred main.js:8:1 fromB +main.js:14:34 FUNCTION_CALL (table[k] || deep) -> multi_inferred main.js:9:1 deep +main.js:17:39 FUNCTION_CALL (k in table ? table[k] : deep) -> multi_inferred main.js:7:1 fromA +main.js:17:39 FUNCTION_CALL (k in table ? table[k] : deep) -> multi_inferred main.js:8:1 fromB +main.js:17:39 FUNCTION_CALL (k in table ? table[k] : deep) -> multi_inferred main.js:9:1 deep +main.js:23:10 FUNCTION_CALL h -> multi_inferred main.js:7:1 fromA +main.js:23:10 FUNCTION_CALL h -> multi_inferred main.js:8:1 fromB +main.js:23:10 FUNCTION_CALL h -> multi_inferred main.js:9:1 deep +main.js:30:10 FUNCTION_CALL h -> multi_inferred main.js:7:1 fromA +main.js:30:10 FUNCTION_CALL h -> multi_inferred main.js:8:1 fromB +main.js:30:10 FUNCTION_CALL h -> multi_inferred main.js:9:1 deep +main.js:37:10 FUNCTION_CALL h -> multi_inferred main.js:7:1 fromA +main.js:37:10 FUNCTION_CALL h -> multi_inferred main.js:8:1 fromB +main.js:37:10 FUNCTION_CALL h -> multi_inferred main.js:9:1 deep +main.js:41:40 FUNCTION_CALL (remote[k] || deep) -> multi_inferred main.js:9:1 deep +main.js:41:40 FUNCTION_CALL (remote[k] || deep) -> multi_inferred table.js:1:1 fromC +main.js:41:40 FUNCTION_CALL (remote[k] || deep) -> multi_inferred table.js:2:1 fromD +main.js:45:10 FUNCTION_CALL h -> multi_inferred main.js:9:1 deep +main.js:45:10 FUNCTION_CALL h -> multi_inferred table.js:1:1 fromC +main.js:45:10 FUNCTION_CALL h -> multi_inferred table.js:2:1 fromD +main.js:49:39 FUNCTION_CALL (table['a'] || deep) -> multi_inferred main.js:7:1 fromA +main.js:49:39 FUNCTION_CALL (table['a'] || deep) -> multi_inferred main.js:9:1 deep +main.js:55:10 FUNCTION_CALL h -> multi_inferred main.js:8:1 fromB +main.js:55:10 FUNCTION_CALL h -> multi_inferred main.js:9:1 deep +main.js:62:10 FUNCTION_CALL h -> multi_inferred main.js:7:1 fromA +main.js:62:10 FUNCTION_CALL h -> multi_inferred main.js:9:1 deep +main.js:66:43 FUNCTION_CALL (k || deep) -> ambient_terminal - +main.js:66:43 FUNCTION_CALL (k || deep) -> multi_inferred main.js:9:1 deep +main.js:69:1 FUNCTION_CALL inlineOr -> known_edge main.js:14:1 inlineOr +main.js:70:1 FUNCTION_CALL inlineTernary -> known_edge main.js:17:1 inlineTernary +main.js:71:1 FUNCTION_CALL reassigned -> known_edge main.js:20:1 reassigned +main.js:72:1 FUNCTION_CALL nullishAssigned -> known_edge main.js:27:1 nullishAssigned +main.js:73:1 FUNCTION_CALL readAssigned -> known_edge main.js:34:1 readAssigned +main.js:74:1 FUNCTION_CALL importedInline -> known_edge main.js:41:1 importedInline +main.js:75:1 FUNCTION_CALL importedReassigned -> known_edge main.js:42:1 importedReassigned +main.js:76:1 FUNCTION_CALL inlineLiteralKey -> known_edge main.js:49:1 inlineLiteralKey +main.js:77:1 FUNCTION_CALL reassignedDotted -> known_edge main.js:52:1 reassignedDotted +main.js:78:1 FUNCTION_CALL reassignedPlain -> known_edge main.js:59:1 reassignedPlain +main.js:79:1 FUNCTION_CALL inlineDefaultOnly -> known_edge main.js:66:1 inlineDefaultOnly diff --git a/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.oracle b/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.oracle new file mode 100644 index 00000000..927704eb --- /dev/null +++ b/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.oracle @@ -0,0 +1,15 @@ +main.js:37:10 FUNCTION_CALL h SOUND_SUPERSET main.js:7:1;main.js:8:1;main.js:9:1 +main.js:55:10 FUNCTION_CALL h SOUND_SUPERSET main.js:8:1;main.js:9:1 +main.js:62:10 FUNCTION_CALL h SOUND_SUPERSET main.js:7:1;main.js:9:1 +main.js:69:1 FUNCTION_CALL inlineOr EXACT main.js:14:1 +main.js:70:1 FUNCTION_CALL inlineTernary EXACT main.js:17:1 +main.js:71:1 FUNCTION_CALL reassigned EXACT main.js:20:1 +main.js:72:1 FUNCTION_CALL nullishAssigned EXACT main.js:27:1 +main.js:73:1 FUNCTION_CALL readAssigned EXACT main.js:34:1 +main.js:74:1 FUNCTION_CALL importedInline EXACT main.js:41:1 +main.js:75:1 FUNCTION_CALL importedReassigned EXACT main.js:42:1 +main.js:76:1 FUNCTION_CALL inlineLiteralKey EXACT main.js:49:1 +main.js:77:1 FUNCTION_CALL reassignedDotted EXACT main.js:52:1 +main.js:78:1 FUNCTION_CALL reassignedPlain EXACT main.js:59:1 +main.js:79:1 FUNCTION_CALL inlineDefaultOnly EXACT main.js:66:1 +# defects: 0 diff --git a/graph/test/javascript/expected/70-jsconfig-path-alias.diag b/graph/test/javascript/expected/70-jsconfig-path-alias.diag new file mode 100644 index 00000000..74d4aa41 --- /dev/null +++ b/graph/test/javascript/expected/70-jsconfig-path-alias.diag @@ -0,0 +1,3 @@ +import_cause noalias/probe.js:2:10 @/check not_staged +package_entry path-alias-fixture . [] DEFAULT_INDEX index.js MISSING_FILE -> - +unresolved noalias/probe.js:5:10 FUNCTION_CALL checkA callee_untyped diff --git a/graph/test/javascript/expected/70-jsconfig-path-alias.edges b/graph/test/javascript/expected/70-jsconfig-path-alias.edges new file mode 100644 index 00000000..09a8c77c --- /dev/null +++ b/graph/test/javascript/expected/70-jsconfig-path-alias.edges @@ -0,0 +1,4 @@ +app.js:11:10 FUNCTION_CALL checkB -> known_edge lib/check.js:5:1 checkB +app.js:7:10 FUNCTION_CALL checkA -> known_edge lib/check.js:1:1 checkA +noalias/probe.js:5:10 FUNCTION_CALL checkA -> ambiguous_unknown - +remix/app/route.js:5:10 FUNCTION_CALL getNote -> known_edge remix/app/models/note.server.js:1:1 getNote diff --git a/graph/test/javascript/expected/70-jsconfig-path-alias.oracle b/graph/test/javascript/expected/70-jsconfig-path-alias.oracle new file mode 100644 index 00000000..16df3e53 --- /dev/null +++ b/graph/test/javascript/expected/70-jsconfig-path-alias.oracle @@ -0,0 +1,2 @@ +app.js:11:10 FUNCTION_CALL checkB EXACT lib/check.js:5:1 +# defects: 0 diff --git a/graph/test/javascript/expected/70-jsx-attribute-handler.diag b/graph/test/javascript/expected/70-jsx-attribute-handler.diag new file mode 100644 index 00000000..a21ec6f5 --- /dev/null +++ b/graph/test/javascript/expected/70-jsx-attribute-handler.diag @@ -0,0 +1,2 @@ +package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> - +unresolved LoginForm.jsx:8:27 METHOD_CALL preventDefault receiver_untyped diff --git a/graph/test/javascript/expected/70-jsx-attribute-handler.edges b/graph/test/javascript/expected/70-jsx-attribute-handler.edges new file mode 100644 index 00000000..1b0d890b --- /dev/null +++ b/graph/test/javascript/expected/70-jsx-attribute-handler.edges @@ -0,0 +1,10 @@ +LoginForm.jsx:11:11 JSX_ATTRIBUTE onSubmit={submit} -> callback_registered LoginForm.jsx:8:18 +LoginForm.jsx:12:29 JSX_ATTRIBUTE onClick={logout} -> callback_registered LoginForm.jsx:4:1 logout +LoginForm.jsx:13:53 FUNCTION_CALL label -> known_edge LoginForm.jsx:5:1 label +LoginForm.jsx:14:13 JSX_ATTRIBUTE onMouseEnter={() => label()} -> callback_registered LoginForm.jsx:14:27 +LoginForm.jsx:14:33 FUNCTION_CALL label -> known_edge LoginForm.jsx:5:1 label +LoginForm.jsx:15:17 JSX_ATTRIBUTE action={createUser} -> callback_registered actions.js:1:1 createUser +LoginForm.jsx:15:7 JSX_ELEMENT -> known_edge UserForm.jsx:1:1 UserForm +LoginForm.jsx:8:27 METHOD_CALL e.preventDefault -> ambiguous_unknown - +UserForm.jsx:2:16 JSX_ATTRIBUTE action={action} -> callback_registered actions.js:1:1 createUser +main.jsx:2:21 JSX_ELEMENT -> known_edge LoginForm.jsx:7:1 LoginForm diff --git a/graph/test/javascript/expected/70-jsx-attribute-handler.oracle b/graph/test/javascript/expected/70-jsx-attribute-handler.oracle new file mode 100644 index 00000000..b9e235e7 --- /dev/null +++ b/graph/test/javascript/expected/70-jsx-attribute-handler.oracle @@ -0,0 +1,3 @@ +LoginForm.jsx:13:53 FUNCTION_CALL label EXACT LoginForm.jsx:5:1 +LoginForm.jsx:14:33 FUNCTION_CALL label EXACT LoginForm.jsx:5:1 +# defects: 0 diff --git a/graph/test/javascript/expected/70-single-file-component-scripts.diag b/graph/test/javascript/expected/70-single-file-component-scripts.diag new file mode 100644 index 00000000..f01b3a10 --- /dev/null +++ b/graph/test/javascript/expected/70-single-file-component-scripts.diag @@ -0,0 +1,14 @@ +import_cause components/List.svelte:2:12 svelte not_staged +import_cause components/Price.vue:4:10 vue not_staged +import_cause main.js:2:10 vue not_staged +package_entry single-file-component-scripts . [] DEFAULT_INDEX index.js MISSING_FILE -> - +unresolved components/Legacy.vue:7:26 METHOD_CALL $emit member_absent +unresolved components/List.svelte:5:3 FUNCTION_CALL onMount callee_untyped +unresolved components/Price.vue:1:50 FUNCTION_CALL onlyInMarkup callee_untyped +unresolved components/Price.vue:6:15 FUNCTION_CALL defineProps callee_untyped +unresolved components/Price.vue:7:15 FUNCTION_CALL computed callee_untyped +unresolved components/Price.vue:8:22 METHOD_CALL log no_target +unresolved lib/format.js:3:47 METHOD_CALL toFixed no_target +unresolved lib/format.js:4:37 METHOD_CALL toLowerCase no_target +unresolved main.js:5:1 FUNCTION_CALL createApp callee_untyped +unresolved main.js:5:1 METHOD_CALL mount receiver_untyped diff --git a/graph/test/javascript/expected/70-single-file-component-scripts.edges b/graph/test/javascript/expected/70-single-file-component-scripts.edges new file mode 100644 index 00000000..0bb4c4cb --- /dev/null +++ b/graph/test/javascript/expected/70-single-file-component-scripts.edges @@ -0,0 +1,22 @@ +components/Legacy.vue:6:31 FUNCTION_CALL slugify -> known_edge lib/format.js:4:1 slugify +components/Legacy.vue:7:26 METHOD_CALL this.$emit -> ambiguous_unknown - +components/Legacy.vue:7:48 FUNCTION_CALL slugify -> known_edge lib/format.js:4:1 slugify +components/List.svelte:5:3 FUNCTION_CALL onMount -> ambiguous_unknown - +components/List.svelte:5:3 FUNCTION_CALL onMount -> callback_registered components/List.svelte:5:11 +components/List.svelte:5:39 FUNCTION_CALL fetchProducts -> known_edge lib/format.js:5:1 fetchProducts +components/List.svelte:6:29 FUNCTION_CALL formatPrice -> known_edge lib/format.js:3:1 formatPrice +components/Price.vue:1:25 FUNCTION_CALL onClick -> known_edge components/Price.vue:8:1 onClick +components/Price.vue:1:50 FUNCTION_CALL onlyInMarkup -> ambiguous_unknown - +components/Price.vue:6:15 FUNCTION_CALL defineProps -> ambiguous_unknown - +components/Price.vue:7:15 FUNCTION_CALL computed -> ambiguous_unknown - +components/Price.vue:7:15 FUNCTION_CALL computed -> callback_registered components/Price.vue:7:24 +components/Price.vue:7:30 FUNCTION_CALL formatPrice -> known_edge lib/format.js:3:1 formatPrice +components/Price.vue:8:22 METHOD_CALL console.log -> ambient_terminal - +lib/format.js:3:47 METHOD_CALL n.toFixed -> ambient_terminal - +lib/format.js:4:37 METHOD_CALL s.toLowerCase -> ambient_terminal - +main.js:5:1 FUNCTION_CALL createApp -> ambiguous_unknown - +main.js:5:1 METHOD_CALL createApp(Price).mount -> ambiguous_unknown - +main.js:5:30 FUNCTION_CALL slugify -> known_edge lib/format.js:4:1 slugify +pages/index.astro:3:18 FUNCTION_CALL fetchProducts -> known_edge lib/format.js:5:1 fetchProducts +pages/index.astro:4:14 FUNCTION_CALL slugify -> known_edge lib/format.js:4:1 slugify +pages/index.astro:9:20 FUNCTION_CALL formatPrice -> known_edge lib/format.js:3:1 formatPrice diff --git a/graph/test/javascript/expected/70-single-file-component-scripts.oracle b/graph/test/javascript/expected/70-single-file-component-scripts.oracle new file mode 100644 index 00000000..72b4acd0 --- /dev/null +++ b/graph/test/javascript/expected/70-single-file-component-scripts.oracle @@ -0,0 +1,2 @@ +main.js:5:30 FUNCTION_CALL slugify EXACT lib/format.js:4:1 +# defects: 0 diff --git a/graph/test/javascript/expected/71-framework-generated-config-alias.diag b/graph/test/javascript/expected/71-framework-generated-config-alias.diag new file mode 100644 index 00000000..008e2a73 --- /dev/null +++ b/graph/test/javascript/expected/71-framework-generated-config-alias.diag @@ -0,0 +1,3 @@ +import_cause other/probe.js:2:10 $lib/helper not_staged +package_entry framework-alias-fixture . [] DEFAULT_INDEX index.js MISSING_FILE -> - +unresolved other/probe.js:5:10 FUNCTION_CALL helper callee_untyped diff --git a/graph/test/javascript/expected/71-framework-generated-config-alias.edges b/graph/test/javascript/expected/71-framework-generated-config-alias.edges new file mode 100644 index 00000000..916bc000 --- /dev/null +++ b/graph/test/javascript/expected/71-framework-generated-config-alias.edges @@ -0,0 +1,7 @@ +kit/src/routes/page.js:10:10 FUNCTION_CALL post -> known_edge kit/src/lib/api.js:5:1 post +kit/src/routes/page.js:6:10 METHOD_CALL api.get -> known_edge kit/src/lib/api.js:1:1 get +nuxt/server/api/price.js:6:10 FUNCTION_CALL formatPrice -> known_edge nuxt/utils/price.js:1:1 formatPrice +nuxt/server/api/price.js:6:27 FUNCTION_CALL viaAt -> known_edge nuxt/utils/price.js:1:1 formatPrice +nuxt4/app/pages/index.js:5:10 FUNCTION_CALL formatDate -> known_edge nuxt4/app/utils/date.js:1:1 formatDate +other/probe.js:5:10 FUNCTION_CALL helper -> ambiguous_unknown - +own/probe.js:5:10 FUNCTION_CALL pick -> known_edge own/shared/util.js:1:1 pick diff --git a/graph/test/javascript/expected/71-framework-generated-config-alias.oracle b/graph/test/javascript/expected/71-framework-generated-config-alias.oracle new file mode 100644 index 00000000..8675b52d --- /dev/null +++ b/graph/test/javascript/expected/71-framework-generated-config-alias.oracle @@ -0,0 +1 @@ +# defects: 0 diff --git a/graph/test/javascript/tools/normalize_edges.py b/graph/test/javascript/tools/normalize_edges.py index 8c515c32..47679a64 100644 --- a/graph/test/javascript/tools/normalize_edges.py +++ b/graph/test/javascript/tools/normalize_edges.py @@ -51,8 +51,10 @@ def methods(d, tag): if h: i = {c: h.index(c) for c in ('jsExpressionUniqueHash', 'ownerModuleLinkHash', 'startLine', 'startColumn', 'expressionKind', 'text')} for r in exprs: - # A JSX element edge is keyed on the JSX_ELEMENT expression, likewise no call site. - if r[i['expressionKind']] in ('PROPERTY_ACCESS', 'OPTIONAL_ACCESS', 'ELEMENT_ACCESS', 'JSX_ELEMENT'): + # A JSX element edge is keyed on the JSX_ELEMENT expression, likewise no call site; + # a handler handed over as an attribute, on the JSX_ATTRIBUTE_VALUE expression. + if r[i['expressionKind']] in ('PROPERTY_ACCESS', 'OPTIONAL_ACCESS', 'ELEMENT_ACCESS', 'JSX_ELEMENT', + 'JSX_ATTRIBUTE_VALUE'): access[r[i['jsExpressionUniqueHash']]] = '%s:%s:%s %%s %s' % ( modfile.get(r[i['ownerModuleLinkHash']], '?'), r[i['startLine']], r[i['startColumn']], r[i['text']].replace('\n', ' ')[:60]) diff --git a/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/__init__.py b/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/graph-core.py b/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/graph-core.py new file mode 100644 index 00000000..5e705c8c --- /dev/null +++ b/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/graph-core.py @@ -0,0 +1,12 @@ +# A hyphenated file name is not importable with `import`; importlib loads it by path. +class G: + def __init__(self, repo): + self.repo = repo + + def lambda_label(self, i): + return str(i) + + +class H: + def lambda_label(self, i): + return i diff --git a/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/loader.py b/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/loader.py new file mode 100644 index 00000000..58b9cd83 --- /dev/null +++ b/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/loader.py @@ -0,0 +1,91 @@ +import importlib +import importlib.util +import os +from importlib.machinery import SourceFileLoader + +HERE = os.path.dirname(os.path.abspath(__file__)) + +# spec_from_file_location + module_from_spec + exec_module, the path a literal file name +_spec = importlib.util.spec_from_file_location('graph_core', os.path.join(HERE, 'graph-core.py')) +core = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(core) +G, H = core.G, core.H + +# spec_from_loader over a SourceFileLoader +_tspec = importlib.util.spec_from_loader('tools_mod', SourceFileLoader('tools_mod', os.path.join(HERE, 'tools-mod.py'))) +tools = importlib.util.module_from_spec(_tspec); _tspec.loader.exec_module(tools) + +# import_module with a literal dotted name +named = importlib.import_module('pkg.named') + +# CONTROL: the name is not a literal, so which module it is cannot be known +which = os.environ.get('WHICH', 'pkg.other') +dyn = importlib.import_module(which) + +# CONTROL: a literal path that names no file beside this one +_mspec = importlib.util.spec_from_file_location('missing', os.path.join(HERE, 'no-such-file.py')) +missing = importlib.util.module_from_spec(_mspec) + + +class Changed: + # the receiver is an attribute assigned from a class the loaded module declares + def __init__(self, repo): + self.g = G(repo); self.repo = self.g.repo + + def file_changes(self): + return self.g.lambda_label(1) + + +class Impact: + # an annotated parameter whose annotation is that same module-level alias + def __init__(self, g: G): + self.g = g + + def show(self): + return self.g.lambda_label(2) + + +class Rebound: + # CONTROL: the attribute is reassigned to a different type, so the call stays a set + def __init__(self, repo): + self.g = G(repo) + + def swap(self): + self.g = H() + + def show(self): + return self.g.lambda_label(3) + + +class UsesTools: + def __init__(self): + self.t = tools.Tool() + self.u = tools.make_tool() + + def go(self): + return self.t.run() + + +def use_named(): + return named.helper(), named.Named().ping() + + +def use_dyn(): + return dyn.helper() + + +def use_missing(): + return missing.helper() + + + +def use_core_directly(): + return core.G(0).lambda_label(0) + + +# CONTROL: a starred target is not paired by position, so neither name is typed from it +*RestK, LastK = core.H, core.G + + +def use_starred(): + return LastK(0).lambda_label(0) diff --git a/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/named.py b/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/named.py new file mode 100644 index 00000000..2873a559 --- /dev/null +++ b/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/named.py @@ -0,0 +1,7 @@ +def helper(): + return 2 + + +class Named: + def ping(self): + return 3 diff --git a/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/other.py b/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/other.py new file mode 100644 index 00000000..acd4f0f8 --- /dev/null +++ b/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/other.py @@ -0,0 +1,2 @@ +def helper(): + return 4 diff --git a/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/tools-mod.py b/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/tools-mod.py new file mode 100644 index 00000000..b1966d2a --- /dev/null +++ b/graph/test/python/cases/34-importlib-loaded-modules/src/pkg/tools-mod.py @@ -0,0 +1,7 @@ +class Tool: + def run(self): + return 1 + + +def make_tool(): + return Tool() diff --git a/graph/test/python/expected/34-importlib-loaded-modules.edges b/graph/test/python/expected/34-importlib-loaded-modules.edges new file mode 100644 index 00000000..c915da1a --- /dev/null +++ b/graph/test/python/expected/34-importlib-loaded-modules.edges @@ -0,0 +1,31 @@ +ambiguous_unknown CHAINED_CALL pkg.loader.use_starred -> - +ambiguous_unknown METHOD_CALL pkg.loader. -> - +ambiguous_unknown METHOD_CALL pkg.loader.use_dyn -> - +ambiguous_unknown METHOD_CALL pkg.loader.use_missing -> - +ambiguous_unknown SIMPLE_CALL pkg.loader.use_starred -> - +boundary_lib METHOD_CALL pkg.loader. -> external:importlib.import_module +boundary_lib METHOD_CALL pkg.loader. -> external:importlib.util.module_from_spec +boundary_lib METHOD_CALL pkg.loader. -> external:importlib.util.spec_from_file_location +boundary_lib METHOD_CALL pkg.loader. -> external:importlib.util.spec_from_loader +boundary_lib METHOD_CALL pkg.loader. -> external:os.environ.get +boundary_lib METHOD_CALL pkg.loader. -> external:os.path.abspath +boundary_lib METHOD_CALL pkg.loader. -> external:os.path.dirname +boundary_lib METHOD_CALL pkg.loader. -> external:os.path.join +boundary_lib METHOD_CALL pkg.loader.UsesTools.__init__ -> builtin:object.__init__ +boundary_lib METHOD_CALL pkg.loader.use_named -> builtin:object.__init__ +boundary_lib SIMPLE_CALL pkg.graph-core.G.lambda_label -> builtin:str +boundary_lib SIMPLE_CALL pkg.loader. -> external:SourceFileLoader +boundary_lib SIMPLE_CALL pkg.loader.Rebound.swap -> builtin:object.__init__ +boundary_lib SIMPLE_CALL pkg.tools-mod.make_tool -> builtin:object.__init__ +known_edge CHAINED_CALL pkg.loader.use_core_directly -> pkg.graph-core.G.lambda_label +known_edge CHAINED_CALL pkg.loader.use_named -> pkg.named.Named.ping +known_edge METHOD_CALL pkg.loader.Changed.file_changes -> pkg.graph-core.G.lambda_label +known_edge METHOD_CALL pkg.loader.Impact.show -> pkg.graph-core.G.lambda_label +known_edge METHOD_CALL pkg.loader.UsesTools.__init__ -> pkg.tools-mod.make_tool +known_edge METHOD_CALL pkg.loader.UsesTools.go -> pkg.tools-mod.Tool.run +known_edge METHOD_CALL pkg.loader.use_core_directly -> pkg.graph-core.G.__init__ +known_edge METHOD_CALL pkg.loader.use_named -> pkg.named.helper +known_edge SIMPLE_CALL pkg.loader.Changed.__init__ -> pkg.graph-core.G.__init__ +known_edge SIMPLE_CALL pkg.loader.Rebound.__init__ -> pkg.graph-core.G.__init__ +multi_inferred METHOD_CALL pkg.loader.Rebound.show -> pkg.graph-core.G.lambda_label +multi_inferred METHOD_CALL pkg.loader.Rebound.show -> pkg.graph-core.H.lambda_label diff --git a/graph/test/python/expected/34-importlib-loaded-modules.entries b/graph/test/python/expected/34-importlib-loaded-modules.entries new file mode 100644 index 00000000..276e39fa --- /dev/null +++ b/graph/test/python/expected/34-importlib-loaded-modules.entries @@ -0,0 +1 @@ +── entry_point (0) ── diff --git a/graph/test/python/expected/34-importlib-loaded-modules.framework b/graph/test/python/expected/34-importlib-loaded-modules.framework new file mode 100644 index 00000000..b9cbca06 --- /dev/null +++ b/graph/test/python/expected/34-importlib-loaded-modules.framework @@ -0,0 +1,5 @@ +── framework_edge (0) ── +── framework_unjoined (0) ── +── remote_edge (0) ── +── remote_unserved (0) ── +── remote_unsent (0) ── diff --git a/graph/test/python/expected/34-importlib-loaded-modules.tiers b/graph/test/python/expected/34-importlib-loaded-modules.tiers new file mode 100644 index 00000000..dc415a0a --- /dev/null +++ b/graph/test/python/expected/34-importlib-loaded-modules.tiers @@ -0,0 +1,31 @@ +distinct call sites emitted: 37 + +--- by tier: edge ROWS, and the distinct SITES they cover --- + 6 rows 6 sites ambiguous_unknown + 20 rows 20 sites boundary_lib + 10 rows 10 sites known_edge + 2 rows 1 sites multi_inferred + +--- edge rows by call kind --- + 3 CHAINED_CALL + 28 METHOD_CALL + 7 SIMPLE_CALL + +--- unresolved reasons --- + 1 no_rule + 2 untyped_receiver:attribute_object_untyped + 1 untyped_receiver:call_result_untyped + 2 untyped_receiver:local_untyped + +--- the engine's own conservation ledger --- + 37 _total_sites + 6 ambiguous_unknown + 20 boundary_lib + 10 known_edge + 1 multi_inferred + +--- reconciling rows against the conserved site count --- + edge rows 38 + minus extra rows from multi-target sites 1 + = tier/site pairs 37 + engine's conserved site total 37 diff --git a/graph/test/python/tools/literal_gate.py b/graph/test/python/tools/literal_gate.py index dd88f708..db44b130 100644 --- a/graph/test/python/tools/literal_gate.py +++ b/graph/test/python/tools/literal_gate.py @@ -79,7 +79,11 @@ # framework hop -- the registration decorator, the dispatch method, the # route table, the marker -- are catalogues in config-resolution/knobs.dl, # which is the one file a reviewer reads to see every name assumed. - "framework_edge(", "framework_unjoined(") + "framework_edge(", "framework_unjoined(", + # unresolved_value_callee's second column is `binding`: WHAT the callee of an + # unresolved call is ("parameter", "loop_variable", "field", ...), the same words + # the JavaScript rules write. Written into the output and never joined on. + "unresolved_value_callee(") GROUND_FACT = re.compile(r'^[a-z_]+\((?:\s*"[^"]*"\s*,?)+\)\.\s*$') diff --git a/graph/test/typescript/cases/80-contextual-lambda-param/src/globals.d.ts b/graph/test/typescript/cases/80-contextual-lambda-param/src/globals.d.ts new file mode 100644 index 00000000..bf77e799 --- /dev/null +++ b/graph/test/typescript/cases/80-contextual-lambda-param/src/globals.d.ts @@ -0,0 +1,35 @@ +// The case's OWN standard library, for the reason 21 gives: under `noLib` the target has +// to be declared HERE or neither side of the comparison can name it. +// +// Every signature below is copied from lib.es5.d.ts / lib.es2015.promise.d.ts as written, +// because the shape of the callback parameter IS the case: `then` takes a UNION of a +// function type with `undefined | null`, and `map` declares its own `U` next to Array's `T`. +interface Array { + length: number; + forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void; + map(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; + filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T[]; + some(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): boolean; +} +interface ReadonlyArray { + readonly length: number; + forEach(callbackfn: (value: T, index: number, array: readonly T[]) => void, thisArg?: any): void; + map(callbackfn: (value: T, index: number, array: readonly T[]) => U, thisArg?: any): U[]; + some(predicate: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: any): boolean; +} +interface PromiseLike { + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): PromiseLike; +} +interface Promise { + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise; + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise; +} +interface Object { toString(): string; } +interface Boolean {} +interface Number {} +interface String {} +interface Function {} +interface CallableFunction extends Function {} +interface NewableFunction extends Function {} +interface IArguments {} +interface RegExp {} diff --git a/graph/test/typescript/cases/80-contextual-lambda-param/src/tsconfig.json b/graph/test/typescript/cases/80-contextual-lambda-param/src/tsconfig.json new file mode 100644 index 00000000..c901fced --- /dev/null +++ b/graph/test/typescript/cases/80-contextual-lambda-param/src/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022", "module": "commonjs" } } diff --git a/graph/test/typescript/cases/80-contextual-lambda-param/src/use.ts b/graph/test/typescript/cases/80-contextual-lambda-param/src/use.ts new file mode 100644 index 00000000..0184ceed --- /dev/null +++ b/graph/test/typescript/cases/80-contextual-lambda-param/src/use.ts @@ -0,0 +1,97 @@ +// A callback parameter with no annotation takes its type from the signature it is passed to: +// `Array.map((value: T) => U)`, `forEach`, `Promise.then((value: T) => …)`. Two classes +// share every member name, so a name match cannot pass for a resolution — the owner is the answer. +// +// Each shape is written twice, `(u) => …` and `u => …`, because a parameter written without +// parentheses is the commoner spelling and the two must type alike. +export class User { + display(): string { + return 'd'; + } + save(): string { + return 's'; + } + touch(): string { + return 't'; + } +} +export class Admin { + display(): string { + return 'D'; + } + save(): string { + return 'S'; + } + touch(): string { + return 'T'; + } +} + +export class Repo { + constructor(private readonly pending: Promise, private readonly users: User[]) {} + all(): User[] { + return this.users; + } + one(): Promise { + return this.pending; + } + readonlyAll(): readonly User[] { + return this.users; + } +} + +export interface Handler { + handle(n: number): number; +} + +// ── map: the method declares its own `U` beside Array's `T` ───────────────── +export function mapParen(r: Repo): string[] { + return r.all().map((u) => u.display()); +} +export function mapBare(r: Repo): string[] { + return r.all().map(u => u.display()); +} +export function mapDeclared(hs: Handler[]): number[] { + return hs.map(h => h.handle(1)); +} + +// ── forEach / filter / some: no type parameter of their own ─────────────── +export function forEachParen(r: Repo): void { + r.all().forEach((u) => { + u.save(); + }); +} +export function forEachBare(r: Repo): void { + r.all().forEach(u => { + u.save(); + }); +} +export function filterBare(r: Repo): User[] { + return r.all().filter(u => u.save() !== ''); +} +export function someReadonly(r: Repo): boolean { + return r.readonlyAll().some(u => u.touch() === 't'); +} + +// ── then: the callback's parameter type is `((value: T) => …) | undefined | null` ── +export function thenParen(r: Repo): Promise { + return r.one().then((u) => u.touch()); +} +export function thenBare(r: Repo): Promise { + return r.one().then(u => u.touch()); +} +export function thenDeclared(p: Promise): Promise { + return p.then(a => a.touch()); +} + +// ── CONTROLS: these must keep resolving, and only to their own owner ──────── +export function ctlAnnotated(r: Repo): string[] { + return r.all().map((u: User) => u.save()); +} +export function ctlOtherOwner(as: Admin[]): void { + as.forEach((a) => a.display()); +} +// `catch`'s parameter is `any`: nothing to type, so `e.touch()` must stay unresolved. +export function ctlCatchAny(r: Repo): Promise { + return r.one().catch(e => e.touch()); +} diff --git a/graph/test/typescript/cases/80-vue-definecomponent-jsx/lib/vue.ts b/graph/test/typescript/cases/80-vue-definecomponent-jsx/lib/vue.ts new file mode 100644 index 00000000..036ca918 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-definecomponent-jsx/lib/vue.ts @@ -0,0 +1,29 @@ +// The Vue entry points that matter, declared with their shapes. No Vue dependency. +declare global { + namespace JSX { + type Element = string; + interface ElementAttributesProperty { + props: unknown; + } + interface IntrinsicElements { + [name: string]: unknown; + } + } +} + +export interface ComponentOptions { + name?: string; + props?: Record; + setup?: (props: any) => unknown; + render?: () => string; +} +// A constructor type, as Vue's own DefineComponent is: nothing here says what renders. +export interface Component { + new (...args: any[]): { props: any }; +} +export declare function defineComponent(options: ComponentOptions): Component; +export declare function defineComponent(setup: (props: any) => () => string): Component; +export declare function defineNuxtComponent(options: ComponentOptions): Component; + +// NOT a component definer: it takes an options-shaped object and returns something else. +export declare function makeStore(options: ComponentOptions): Component; diff --git a/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/app.tsx b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/app.tsx new file mode 100644 index 00000000..0facbff3 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/app.tsx @@ -0,0 +1,26 @@ +import { defineComponent } from "../lib/vue"; +import Card from "./card"; +import { Child, Panel, FnForm, NuxtCard, Store } from "./parts"; +import { Button, Local } from "./reg"; + +function helper(): number { + return 42; +} + +export const App = defineComponent({ + setup() { + const onPing = () => helper(); + return () => ( +
    + + + + + + +
    + ); + }, +}); diff --git a/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/button.tsx b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/button.tsx new file mode 100644 index 00000000..b3626705 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/button.tsx @@ -0,0 +1,12 @@ +import { defineComponent } from "../lib/vue"; + +export function leafButton(): number { + return 8; +} + +export default defineComponent({ + name: "Button", + setup() { + return () => {leafButton()}; + }, +}); diff --git a/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/card.tsx b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/card.tsx new file mode 100644 index 00000000..63e84e1a --- /dev/null +++ b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/card.tsx @@ -0,0 +1,15 @@ +import { defineComponent } from "../lib/vue"; +import { Child } from "./parts"; + +export function leafCard(): number { + return 7; +} + +// the module's default export, rendered through a default import +export default defineComponent({ + name: "Card", + render() { + leafCard(); + return ; + }, +}); diff --git a/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/parts.tsx b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/parts.tsx new file mode 100644 index 00000000..1f4193a9 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/parts.tsx @@ -0,0 +1,53 @@ +import { defineComponent, defineNuxtComponent, makeStore } from "../lib/vue"; + +export function leafSetup(): number { + return 1; +} +export function leafRendered(): number { + return 2; +} +export function leafRender(): number { + return 3; +} +export function leafFn(): number { + return 4; +} +export function leafNuxt(): number { + return 5; +} +export function leafStore(): number { + return 6; +} + +// setup() returns the render function +export const Child = defineComponent({ + props: { msg: String }, + setup(props) { + leafSetup(); + return () => String(leafRendered()); + }, +}); + +// the render option, no setup +export const Panel = defineComponent({ + name: "Panel", + render() { + return String(leafRender()); + }, +}); + +// the function form: the function IS setup +export const FnForm = defineComponent((props) => () => String(leafFn())); + +export const NuxtCard = defineNuxtComponent({ + setup() { + return () => String(leafNuxt()); + }, +}); + +// CONTROL: the same object handed to another function is not a component +export const Store = makeStore({ + setup() { + return () => String(leafStore()); + }, +}); diff --git a/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/reg.ts b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/reg.ts new file mode 100644 index 00000000..ab73a3cf --- /dev/null +++ b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/reg.ts @@ -0,0 +1,24 @@ +import { defineComponent } from "../lib/vue"; +import _Button from "./button"; + +export function leafLocal(): number { + return 9; +} + +// the registration idiom: a component library re-exports each component through an +// installer that returns its argument +export function withInstall(component: T): T { + return component; +} + +// …of a component default-imported from its own module +export const Button = withInstall(_Button); + +// …and of one defined in this module +const _Local = defineComponent({ + setup() { + leafLocal(); + return () => "local"; + }, +}); +export const Local = withInstall(_Local); diff --git a/graph/test/typescript/cases/80-vue-sfc/src/Comp.vue b/graph/test/typescript/cases/80-vue-sfc/src/Comp.vue new file mode 100644 index 00000000..b4f89c5b --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/Comp.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/graph/test/typescript/cases/80-vue-sfc/src/Plain.vue b/graph/test/typescript/cases/80-vue-sfc/src/Plain.vue new file mode 100644 index 00000000..a8b318cc --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/Plain.vue @@ -0,0 +1,4 @@ + + diff --git a/graph/test/typescript/cases/80-vue-sfc/src/TemplateFirst.vue b/graph/test/typescript/cases/80-vue-sfc/src/TemplateFirst.vue new file mode 100644 index 00000000..d4ab960c --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/TemplateFirst.vue @@ -0,0 +1,14 @@ + + + diff --git a/graph/test/typescript/cases/80-vue-sfc/src/TemplateOnly.vue b/graph/test/typescript/cases/80-vue-sfc/src/TemplateOnly.vue new file mode 100644 index 00000000..25d1e389 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/TemplateOnly.vue @@ -0,0 +1,3 @@ + diff --git a/graph/test/typescript/cases/80-vue-sfc/src/child.ts b/graph/test/typescript/cases/80-vue-sfc/src/child.ts new file mode 100644 index 00000000..157bb63e --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/child.ts @@ -0,0 +1,11 @@ +export function Child(props: { msg: string }): string { + return props.msg; +} + +export function MyBadge(): string { + return 'badge'; +} + +export function format(n: number): string { + return n.toFixed(2); +} diff --git a/graph/test/typescript/cases/80-vue-sfc/src/main.ts b/graph/test/typescript/cases/80-vue-sfc/src/main.ts new file mode 100644 index 00000000..c804793d --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/main.ts @@ -0,0 +1,5 @@ +import Comp from './Comp.vue'; + +export function mount(): unknown { + return Comp; +} diff --git a/graph/test/typescript/cases/80-vue-sfc/src/shims-vue.d.ts b/graph/test/typescript/cases/80-vue-sfc/src/shims-vue.d.ts new file mode 100644 index 00000000..821cca50 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/shims-vue.d.ts @@ -0,0 +1,4 @@ +declare module '*.vue' { + const component: unknown; + export default component; +} diff --git a/graph/test/typescript/cases/80-vue-sfc/src/tsconfig.json b/graph/test/typescript/cases/80-vue-sfc/src/tsconfig.json new file mode 100644 index 00000000..98ff2e00 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true + }, + "include": ["**/*.ts", "**/*.vue"] +} diff --git a/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/shadow.ts b/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/shadow.ts new file mode 100644 index 00000000..dddc6bcb --- /dev/null +++ b/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/shadow.ts @@ -0,0 +1,37 @@ +// CONTROLS: a module that declares its own `Set` and `Promise`. They are not the built-ins, +// so `forEach` / `then` callbacks take their parameter from these classes' own signatures +// (a Key), never from the type argument (an Item). +export class Item { + size = 1; + display(): string { + return 'i'; + } +} +export class Key { + display(): string { + return 'k'; + } +} +export class Set { + forEach(cb: (k: Key) => void): void { + cb(new Key()); + } +} +export class Promise { + then(cb: (k: Key) => void): void { + cb(new Key()); + } +} +export function ctlOwnSet(s: Set): void { + s.forEach(k => k.display()); +} +export function ctlOwnPromise(p: Promise): void { + p.then(k => k.display()); +} +// A union of two function types over DIFFERENT parameter types gives no contextual type +// (the compiler reports the parameter as implicitly `any`), so neither class is picked. +export function either(cb: ((i: Item) => void) | ((k: Key) => void)): void {} +export function ctlUnionOfFns(): void { + // @ts-expect-error z is implicitly any + either(z => z.display()); +} diff --git a/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/tsconfig.json b/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/tsconfig.json new file mode 100644 index 00000000..c3d35633 --- /dev/null +++ b/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "target": "es2022", "module": "commonjs" } } diff --git a/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/use.ts b/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/use.ts new file mode 100644 index 00000000..e48b591f --- /dev/null +++ b/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/use.ts @@ -0,0 +1,114 @@ +// 80's shapes with NO standard library in the graph, which is what a default index is: +// `map`, `forEach` and `then` have no declaration to read a callback signature from, so +// the parameter's type has to come off the receiver's own reference, the way a `for…of` +// binding over the same receiver already does. Two classes share every member name, so a +// name match cannot pass for a resolution. +export class User { + display(): string { + return 'd'; + } + save(): string { + return 's'; + } + touch(): string { + return 't'; + } +} +export class Admin { + display(): string { + return 'D'; + } + save(): string { + return 'S'; + } + touch(): string { + return 'T'; + } +} + +export class Repo { + constructor(private readonly pending: Promise, private readonly users: User[]) {} + all(): User[] { + return this.users; + } + one(): Promise { + return this.pending; + } + readonlyAll(): ReadonlyArray { + return this.users; + } +} + +export function mapBare(r: Repo): string[] { + return r.all().map(u => u.display()); +} +export function forEachParen(r: Repo): void { + r.all().forEach((u) => { + u.save(); + }); +} +export function findReadonly(r: Repo): User | undefined { + return r.readonlyAll().find(u => u.touch() === 't'); +} +export function everyParam(as: Admin[]): boolean { + return as.every(a => a.save() !== ''); +} +// `reduce`'s first parameter is the accumulator: only the second is the element. +export function reduceElement(us: User[]): string { + return us.reduce((acc, u) => acc + u.save(), ''); +} +export function thenBare(r: Repo): Promise { + return r.one().then(u => u.touch()); +} +export function thenParam(p: Promise): Promise { + return p.then(a => a.display()); +} +// Through a type alias, plain and generic. +export type Users = User[]; +export type Many = Array; +export function aliasPlain(us: Users): void { + us.forEach(u => u.save()); +} +export function aliasGeneric(as: Many): void { + as.forEach(a => a.touch()); +} +// A callback over a parameter that was itself typed from context. +export function nestedArrays(xss: Admin[][]): void { + xss.forEach(xs => { + xs.forEach(a => a.save()); + }); +} +export function thenThenMap(p: Promise): void { + p.then(us => { + us.map(u => u.touch()); + }); +} +// `Map.forEach((value, key) => …)`: the value is type argument 1, the key argument 0. +export function mapForEach(m: Map): void { + m.forEach((v, k) => { + v.display(); + k.touch(); + }); +} + +// ── CONTROLS ──────────────────────────────────────────────────────────────── +// A class of the user's own with a `then` and a `map`: its own signatures type the +// callback, and nothing about Promise or Array may be added to them. +export class Lazy { + then(cb: (a: Admin) => string): string { + return cb(new Admin()); + } + map(cb: (u: User) => string): string { + return cb(new User()); + } +} +export function ctlOwnThen(l: Lazy): string { + return l.then(a => a.touch()); +} +export function ctlOwnMap(l: Lazy): string { + return l.map(u => u.touch()); +} +// An annotated parameter keeps its annotation. +export function ctlAnnotated(us: User[]): string[] { + return us.map((u: User) => u.save()); +} diff --git a/graph/test/typescript/cases/81-vue-component-tag/src/components/Child.vue b/graph/test/typescript/cases/81-vue-component-tag/src/components/Child.vue new file mode 100644 index 00000000..52cc63ef --- /dev/null +++ b/graph/test/typescript/cases/81-vue-component-tag/src/components/Child.vue @@ -0,0 +1,4 @@ + + diff --git a/graph/test/typescript/cases/81-vue-component-tag/src/components/Legacy.vue b/graph/test/typescript/cases/81-vue-component-tag/src/components/Legacy.vue new file mode 100644 index 00000000..30c1e158 --- /dev/null +++ b/graph/test/typescript/cases/81-vue-component-tag/src/components/Legacy.vue @@ -0,0 +1,5 @@ + + diff --git a/graph/test/typescript/cases/81-vue-component-tag/src/components/MyWidget.vue b/graph/test/typescript/cases/81-vue-component-tag/src/components/MyWidget.vue new file mode 100644 index 00000000..a705d455 --- /dev/null +++ b/graph/test/typescript/cases/81-vue-component-tag/src/components/MyWidget.vue @@ -0,0 +1,5 @@ + + diff --git a/graph/test/typescript/cases/81-vue-component-tag/src/components/badge.ts b/graph/test/typescript/cases/81-vue-component-tag/src/components/badge.ts new file mode 100644 index 00000000..7dba71a8 --- /dev/null +++ b/graph/test/typescript/cases/81-vue-component-tag/src/components/badge.ts @@ -0,0 +1 @@ +export function Badge(): string { return 'b'; } diff --git a/graph/test/typescript/cases/81-vue-component-tag/src/components/store.ts b/graph/test/typescript/cases/81-vue-component-tag/src/components/store.ts new file mode 100644 index 00000000..c29d0438 --- /dev/null +++ b/graph/test/typescript/cases/81-vue-component-tag/src/components/store.ts @@ -0,0 +1,2 @@ +function storeInit(): number { return 1; } +export default { value: storeInit() }; diff --git a/graph/test/typescript/cases/81-vue-component-tag/src/main.ts b/graph/test/typescript/cases/81-vue-component-tag/src/main.ts new file mode 100644 index 00000000..5283f2d1 --- /dev/null +++ b/graph/test/typescript/cases/81-vue-component-tag/src/main.ts @@ -0,0 +1,5 @@ +import Parent from './views/Parent.vue'; + +export function mount(): unknown { + return Parent; +} diff --git a/graph/test/typescript/cases/81-vue-component-tag/src/shims-vue.d.ts b/graph/test/typescript/cases/81-vue-component-tag/src/shims-vue.d.ts new file mode 100644 index 00000000..821cca50 --- /dev/null +++ b/graph/test/typescript/cases/81-vue-component-tag/src/shims-vue.d.ts @@ -0,0 +1,4 @@ +declare module '*.vue' { + const component: unknown; + export default component; +} diff --git a/graph/test/typescript/cases/81-vue-component-tag/src/tsconfig.json b/graph/test/typescript/cases/81-vue-component-tag/src/tsconfig.json new file mode 100644 index 00000000..98ff2e00 --- /dev/null +++ b/graph/test/typescript/cases/81-vue-component-tag/src/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true + }, + "include": ["**/*.ts", "**/*.vue"] +} diff --git a/graph/test/typescript/cases/81-vue-component-tag/src/views/Parent.vue b/graph/test/typescript/cases/81-vue-component-tag/src/views/Parent.vue new file mode 100644 index 00000000..40e92d4a --- /dev/null +++ b/graph/test/typescript/cases/81-vue-component-tag/src/views/Parent.vue @@ -0,0 +1,15 @@ + + diff --git a/graph/test/typescript/expected/80-contextual-lambda-param.edges b/graph/test/typescript/expected/80-contextual-lambda-param.edges new file mode 100644 index 00000000..709759a5 --- /dev/null +++ b/graph/test/typescript/expected/80-contextual-lambda-param.edges @@ -0,0 +1,49 @@ +ambiguous_unknown METHOD_CALL use#(?) @L96 -> - +callback_registered METHOD_CALL use#ctlAnnotated(Repo) @L89 -> use#(User) +callback_registered METHOD_CALL use#ctlCatchAny(Repo) @L96 -> use#(?) +callback_registered METHOD_CALL use#ctlOtherOwner(Admin[]) @L92 -> use#(?) +callback_registered METHOD_CALL use#filterBare(Repo) @L70 -> use#(?) +callback_registered METHOD_CALL use#forEachBare(Repo) @L65 -> use#(?) +callback_registered METHOD_CALL use#forEachParen(Repo) @L60 -> use#(?) +callback_registered METHOD_CALL use#mapBare(Repo) @L52 -> use#(?) +callback_registered METHOD_CALL use#mapDeclared(Handler[]) @L55 -> use#(?) +callback_registered METHOD_CALL use#mapParen(Repo) @L49 -> use#(?) +callback_registered METHOD_CALL use#someReadonly(Repo) @L73 -> use#(?) +callback_registered METHOD_CALL use#thenBare(Repo) @L81 -> use#(?) +callback_registered METHOD_CALL use#thenDeclared(Promise) @L84 -> use#(?) +callback_registered METHOD_CALL use#thenParen(Repo) @L78 -> use#(?) +known_edge METHOD_CALL use#(?) @L49 -> User#display() +known_edge METHOD_CALL use#(?) @L52 -> User#display() +known_edge METHOD_CALL use#(?) @L55 -> Handler#handle(number) +known_edge METHOD_CALL use#(?) @L61 -> User#save() +known_edge METHOD_CALL use#(?) @L66 -> User#save() +known_edge METHOD_CALL use#(?) @L70 -> User#save() +known_edge METHOD_CALL use#(?) @L73 -> User#touch() +known_edge METHOD_CALL use#(?) @L78 -> User#touch() +known_edge METHOD_CALL use#(?) @L81 -> User#touch() +known_edge METHOD_CALL use#(?) @L84 -> Admin#touch() +known_edge METHOD_CALL use#(User) @L89 -> User#save() +known_edge METHOD_CALL use#(?) @L92 -> Admin#display() +known_edge METHOD_CALL use#ctlAnnotated(Repo) @L89 -> Array#map((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#ctlAnnotated(Repo) @L89 -> Repo#all() +known_edge METHOD_CALL use#ctlCatchAny(Repo) @L96 -> Promise#catch(((reason: any) =TResult) +known_edge METHOD_CALL use#ctlCatchAny(Repo) @L96 -> Repo#one() +known_edge METHOD_CALL use#ctlOtherOwner(Admin[]) @L92 -> Array#forEach((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#filterBare(Repo) @L70 -> Array#filter((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#filterBare(Repo) @L70 -> Repo#all() +known_edge METHOD_CALL use#forEachBare(Repo) @L65 -> Array#forEach((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#forEachBare(Repo) @L65 -> Repo#all() +known_edge METHOD_CALL use#forEachParen(Repo) @L60 -> Array#forEach((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#forEachParen(Repo) @L60 -> Repo#all() +known_edge METHOD_CALL use#mapBare(Repo) @L52 -> Array#map((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#mapBare(Repo) @L52 -> Repo#all() +known_edge METHOD_CALL use#mapDeclared(Handler[]) @L55 -> Array#map((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#mapParen(Repo) @L49 -> Array#map((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#mapParen(Repo) @L49 -> Repo#all() +known_edge METHOD_CALL use#someReadonly(Repo) @L73 -> ReadonlyArray#some((value: T, index: number, array: readonly T[]) =,any) +known_edge METHOD_CALL use#someReadonly(Repo) @L73 -> Repo#readonlyAll() +known_edge METHOD_CALL use#thenBare(Repo) @L81 -> Promise#then(((value: T) =TResult1,((reason: any) =TResult2) +known_edge METHOD_CALL use#thenBare(Repo) @L81 -> Repo#one() +known_edge METHOD_CALL use#thenDeclared(Promise) @L84 -> Promise#then(((value: T) =TResult1,((reason: any) =TResult2) +known_edge METHOD_CALL use#thenParen(Repo) @L78 -> Promise#then(((value: T) =TResult1,((reason: any) =TResult2) +known_edge METHOD_CALL use#thenParen(Repo) @L78 -> Repo#one() diff --git a/graph/test/typescript/expected/80-contextual-lambda-param.entries b/graph/test/typescript/expected/80-contextual-lambda-param.entries new file mode 100644 index 00000000..bec0b3b1 --- /dev/null +++ b/graph/test/typescript/expected/80-contextual-lambda-param.entries @@ -0,0 +1,16 @@ +── entry_point (15) ── + exported_from_entry_module use#ctlAnnotated use.ts:88 + exported_from_entry_module use#ctlCatchAny use.ts:95 + exported_from_entry_module use#ctlOtherOwner use.ts:91 + exported_from_entry_module use#filterBare use.ts:69 + exported_from_entry_module use#forEachBare use.ts:64 + exported_from_entry_module use#forEachParen use.ts:59 + exported_from_entry_module use#mapBare use.ts:51 + exported_from_entry_module use#mapDeclared use.ts:54 + exported_from_entry_module use#mapParen use.ts:48 + exported_from_entry_module use#someReadonly use.ts:72 + exported_from_entry_module use#thenBare use.ts:80 + exported_from_entry_module use#thenDeclared use.ts:83 + exported_from_entry_module use#thenParen use.ts:77 + unimported_module globals# globals.d.ts:1 + unimported_module use# use.ts:1 diff --git a/graph/test/typescript/expected/80-contextual-lambda-param.fields b/graph/test/typescript/expected/80-contextual-lambda-param.fields new file mode 100644 index 00000000..70b51f31 --- /dev/null +++ b/graph/test/typescript/expected/80-contextual-lambda-param.fields @@ -0,0 +1,3 @@ +known_edge read Repo#all() -> Repo#users +known_edge read Repo#one() -> Repo#pending +known_edge read Repo#readonlyAll() -> Repo#users diff --git a/graph/test/typescript/expected/80-contextual-lambda-param.fields-oracle b/graph/test/typescript/expected/80-contextual-lambda-param.fields-oracle new file mode 100644 index 00000000..50318431 --- /dev/null +++ b/graph/test/typescript/expected/80-contextual-lambda-param.fields-oracle @@ -0,0 +1,7 @@ +80-contextual-lambda-param [fields] + precision 1.0000 (3 correct, 0 wrong) + recall 1.0000 (3 of 3 the compiler resolved) + sites 3 resolved 3 (100.0%) + tiers known_edge=3 + access read=3 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/80-contextual-lambda-param.oracle b/graph/test/typescript/expected/80-contextual-lambda-param.oracle new file mode 100644 index 00000000..540c068b --- /dev/null +++ b/graph/test/typescript/expected/80-contextual-lambda-param.oracle @@ -0,0 +1 @@ +oracle=35 engine=35 agree=35 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/80-contextual-lambda-param.type-use b/graph/test/typescript/expected/80-contextual-lambda-param.type-use new file mode 100644 index 00000000..c5660109 --- /dev/null +++ b/graph/test/typescript/expected/80-contextual-lambda-param.type-use @@ -0,0 +1,22 @@ +known_edge METHOD_PARAM 0 Repo [METHOD_PARAM] -> Promise +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Promise +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Repo +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> User +known_edge METHOD_RETURN 0 Promise [METHOD] -> Promise +known_edge METHOD_RETURN 0 PromiseLike [METHOD] -> PromiseLike +known_edge METHOD_RETURN 0 Repo [METHOD] -> Promise +known_edge METHOD_RETURN 0 use [METHOD] -> Promise +known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function +known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function +known_edge TYPE_ARGUMENT 1 Repo [METHOD] -> User +known_edge TYPE_ARGUMENT 1 Repo [METHOD_PARAM] -> User +known_edge TYPE_ARGUMENT 1 use [METHOD_PARAM] -> Admin +known_edge TYPE_ELEMENT 1 Repo [METHOD] -> User +known_edge TYPE_ELEMENT 1 Repo [METHOD_PARAM] -> User +known_edge TYPE_ELEMENT 1 use [METHOD] -> User +known_edge TYPE_ELEMENT 1 use [METHOD_PARAM] -> Admin +known_edge TYPE_ELEMENT 1 use [METHOD_PARAM] -> Handler +known_edge TYPE_ELEMENT 2 Repo [METHOD] -> User +known_edge TYPE_ELEMENT 2 use [METHOD] -> User +known_edge TYPE_ELEMENT 4 Promise [METHOD_PARAM] -> PromiseLike +known_edge TYPE_ELEMENT 4 PromiseLike [METHOD_PARAM] -> PromiseLike diff --git a/graph/test/typescript/expected/80-contextual-lambda-param.types-oracle b/graph/test/typescript/expected/80-contextual-lambda-param.types-oracle new file mode 100644 index 00000000..75a68397 --- /dev/null +++ b/graph/test/typescript/expected/80-contextual-lambda-param.types-oracle @@ -0,0 +1,7 @@ +80-contextual-lambda-param [types] + precision 1.0000 (12 correct, 0 wrong) + recall 1.0000 (12 of 12 the compiler resolved) + sites 38 resolved 38 (100.0%) + tiers known_edge=38 + contexts METHOD_PARAM=13 METHOD_RETURN=8 SUPER_TYPE=2 TYPE_ARGUMENT=3 TYPE_ELEMENT=12 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges b/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges new file mode 100644 index 00000000..332f0630 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges @@ -0,0 +1,36 @@ +ambiguous_unknown FUNCTION_CALL app#() @L10 -> - +ambiguous_unknown FUNCTION_CALL button#() @L7 -> - +ambiguous_unknown FUNCTION_CALL card#() @L9 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L27 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L40 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L44 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L51 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L23 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L32 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L40 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L42 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L49 -> - +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#(?) +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() +known_edge FUNCTION_CALL parts#() @L27 -> parts#leafRendered() +known_edge FUNCTION_CALL parts#() @L40 -> parts#leafFn() +known_edge FUNCTION_CALL parts#() @L44 -> parts#leafNuxt() +known_edge FUNCTION_CALL parts#() @L51 -> parts#leafStore() +known_edge FUNCTION_CALL parts#render() @L35 -> parts#leafRender() +known_edge FUNCTION_CALL parts#setup(?) @L26 -> parts#leafSetup() +known_edge FUNCTION_CALL reg#() @L15 -> reg#withInstall(T) +known_edge FUNCTION_CALL reg#() @L24 -> reg#withInstall(T) +known_edge FUNCTION_CALL reg#setup() @L20 -> reg#leafLocal() +known_edge JSX_COMPONENT_CALL app#() @L15 -> card#render() +known_edge JSX_COMPONENT_CALL app#() @L16 -> parts#setup(?) +known_edge JSX_COMPONENT_CALL app#() @L17 -> parts#render() +known_edge JSX_COMPONENT_CALL app#() @L18 -> parts#(?) +known_edge JSX_COMPONENT_CALL app#() @L19 -> parts#setup()@L43 +known_edge JSX_COMPONENT_CALL app#() @L21 -> button#setup() +known_edge JSX_COMPONENT_CALL app#() @L22 -> reg#setup() +known_edge JSX_COMPONENT_CALL card#render() @L13 -> parts#setup(?) diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.entries b/graph/test/typescript/expected/80-vue-definecomponent-jsx.entries new file mode 100644 index 00000000..e98e9f55 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.entries @@ -0,0 +1,2 @@ +── entry_point (1) ── + unimported_module app# app.tsx:1 diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields b/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields new file mode 100644 index 00000000..e69de29b diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields-oracle b/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields-oracle new file mode 100644 index 00000000..7aa79d05 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields-oracle @@ -0,0 +1,7 @@ +80-vue-definecomponent-jsx [fields] + precision 0.0000 (0 correct, 0 wrong) + recall 0.0000 (0 of 0 the compiler resolved) + sites 0 resolved 0 + tiers + access + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges new file mode 100644 index 00000000..40aed3c8 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges @@ -0,0 +1,36 @@ +ambiguous_unknown FUNCTION_CALL parts#() @L27 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L40 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L44 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L51 -> - +ambiguous_unknown FUNCTION_CALL parts#render() @L35 -> - +ambiguous_unknown JSX_COMPONENT_CALL app#() @L20 -> - +boundary_lib FUNCTION_CALL app#() @L10 -> vue#defineComponent(ComponentOptions) +boundary_lib FUNCTION_CALL button#() @L7 -> vue#defineComponent(ComponentOptions) +boundary_lib FUNCTION_CALL card#() @L9 -> vue#defineComponent(ComponentOptions) +boundary_lib FUNCTION_CALL parts#() @L23 -> vue#defineComponent(ComponentOptions) +boundary_lib FUNCTION_CALL parts#() @L32 -> vue#defineComponent(ComponentOptions) +boundary_lib FUNCTION_CALL parts#() @L40 -> vue#defineComponent((props: any) =) +boundary_lib FUNCTION_CALL parts#() @L42 -> vue#defineNuxtComponent(ComponentOptions) +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#(?) +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() +known_edge FUNCTION_CALL parts#() @L27 -> parts#leafRendered() +known_edge FUNCTION_CALL parts#() @L40 -> parts#leafFn() +known_edge FUNCTION_CALL parts#() @L44 -> parts#leafNuxt() +known_edge FUNCTION_CALL parts#() @L51 -> parts#leafStore() +known_edge FUNCTION_CALL parts#render() @L35 -> parts#leafRender() +known_edge FUNCTION_CALL parts#setup(?) @L26 -> parts#leafSetup() +known_edge FUNCTION_CALL reg#() @L15 -> reg#withInstall(T) +known_edge FUNCTION_CALL reg#() @L24 -> reg#withInstall(T) +known_edge FUNCTION_CALL reg#setup() @L20 -> reg#leafLocal() +known_edge JSX_COMPONENT_CALL app#() @L15 -> card#render() +known_edge JSX_COMPONENT_CALL app#() @L16 -> parts#setup(?) +known_edge JSX_COMPONENT_CALL app#() @L17 -> parts#render() +known_edge JSX_COMPONENT_CALL app#() @L18 -> parts#(?) +known_edge JSX_COMPONENT_CALL app#() @L19 -> parts#setup()@L43 +known_edge JSX_COMPONENT_CALL app#() @L21 -> button#setup() +known_edge JSX_COMPONENT_CALL app#() @L22 -> reg#setup() +known_edge JSX_COMPONENT_CALL card#render() @L13 -> parts#setup(?) diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.known-missing b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.known-missing new file mode 100644 index 00000000..a25fb513 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.known-missing @@ -0,0 +1,9 @@ +# Accepted gaps for 80-vue-definecomponent-jsx — each line is an edge the TypeScript compiler +# resolves and this engine does not. A NEW missing edge fails the suite; +# a line here that STARTS working also fails, so the debt cannot rot. +# +# The compiler binds a Vue tag to the library's component CONSTRUCT signature, which says +# nothing about what renders; the engine binds it to the setup / render the options object +# defines instead. Both were missing before that rule too. +app#() -> Component#(any[]) +card#render() -> Component#(any[]) diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.oracle b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.oracle new file mode 100644 index 00000000..618f3a51 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.oracle @@ -0,0 +1,11 @@ +oracle=21 engine=27 agree=19 missing=2 (known 2, NEW 0) extra=8 + known app#() -> Component#(any[]) + known card#render() -> Component#(any[]) + extra app#() -> button#setup() + extra app#() -> card#render() + extra app#() -> parts#(?) + extra app#() -> parts#render() + extra app#() -> parts#setup()@L43 + extra app#() -> parts#setup(?) + extra app#() -> reg#setup() + extra card#render() -> parts#setup(?) diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.oracle b/graph/test/typescript/expected/80-vue-definecomponent-jsx.oracle new file mode 100644 index 00000000..3279e3f8 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.oracle @@ -0,0 +1,9 @@ +oracle=11 engine=19 agree=11 missing=0 (known 0, NEW 0) extra=8 + extra app#() -> button#setup() + extra app#() -> card#render() + extra app#() -> parts#(?) + extra app#() -> parts#render() + extra app#() -> parts#setup()@L43 + extra app#() -> parts#setup(?) + extra app#() -> reg#setup() + extra card#render() -> parts#setup(?) diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.type-use b/graph/test/typescript/expected/80-vue-definecomponent-jsx.type-use new file mode 100644 index 00000000..e69de29b diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.types-oracle b/graph/test/typescript/expected/80-vue-definecomponent-jsx.types-oracle new file mode 100644 index 00000000..8be94480 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.types-oracle @@ -0,0 +1,7 @@ +80-vue-definecomponent-jsx [types] + precision 0.0000 (0 correct, 0 wrong) + recall 0.0000 (0 of 0 the compiler resolved) + sites 0 resolved 0 + tiers + contexts + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/80-vue-sfc.edges b/graph/test/typescript/expected/80-vue-sfc.edges new file mode 100644 index 00000000..f0d4508a --- /dev/null +++ b/graph/test/typescript/expected/80-vue-sfc.edges @@ -0,0 +1,8 @@ +ambiguous_unknown METHOD_CALL child#format(number) @L10 -> - +known_edge FUNCTION_CALL Comp.vue#() @L10 -> Comp.vue#sfcHelper() +known_edge FUNCTION_CALL Comp.vue#() @L11 -> Comp.vue#onSave(number) +known_edge FUNCTION_CALL Comp.vue#() @L11 -> child#format(number) +known_edge FUNCTION_CALL TemplateFirst.vue#() @L3 -> TemplateFirst.vue#shown() +known_edge FUNCTION_CALL TemplateFirst.vue#() @L5 -> TemplateFirst.vue#onInput() +known_edge JSX_COMPONENT_CALL Comp.vue#() @L10 -> child#Child({ msg: string }) +known_edge JSX_COMPONENT_CALL TemplateFirst.vue#() @L3 -> child#MyBadge() diff --git a/graph/test/typescript/expected/80-vue-sfc.entries b/graph/test/typescript/expected/80-vue-sfc.entries new file mode 100644 index 00000000..3d43a844 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-sfc.entries @@ -0,0 +1,5 @@ +── entry_point (4) ── + exported_from_entry_module main#mount main.ts:3 + unimported_module TemplateFirst.vue# TemplateFirst.vue:1 + unimported_module main# main.ts:1 + unimported_module shims-vue# shims-vue.d.ts:1 diff --git a/graph/test/typescript/expected/80-vue-sfc.fields b/graph/test/typescript/expected/80-vue-sfc.fields new file mode 100644 index 00000000..2c586263 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-sfc.fields @@ -0,0 +1 @@ +known_edge read child#Child({ msg: string }) -> { msg: string }#msg diff --git a/graph/test/typescript/expected/80-vue-sfc.fields-oracle b/graph/test/typescript/expected/80-vue-sfc.fields-oracle new file mode 100644 index 00000000..706b5d61 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-sfc.fields-oracle @@ -0,0 +1,9 @@ +80-vue-sfc [fields] + precision 0.0000 (0 correct, 1 wrong) + recall 0.0000 (0 of 1 the compiler resolved) + sites 1 resolved 1 (100.0%) + tiers known_edge=1 + access read=1 + not scored: 0 rows whose target is not a client declaration + WRONG child#Child({ msg: string }) READ { msg: string }#msg + MISSING child#Child({ msg: string }) READ child#msg diff --git a/graph/test/typescript/expected/80-vue-sfc.oracle b/graph/test/typescript/expected/80-vue-sfc.oracle new file mode 100644 index 00000000..8f740ea8 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-sfc.oracle @@ -0,0 +1,8 @@ +oracle=0 engine=7 agree=0 missing=0 (known 0, NEW 0) extra=7 + extra Comp.vue#() -> Comp.vue#onSave(number) + extra Comp.vue#() -> Comp.vue#sfcHelper() + extra Comp.vue#() -> child#Child({ msg: string }) + extra Comp.vue#() -> child#format(number) + extra TemplateFirst.vue#() -> TemplateFirst.vue#onInput() + extra TemplateFirst.vue#() -> TemplateFirst.vue#shown() + extra TemplateFirst.vue#() -> child#MyBadge() diff --git a/graph/test/typescript/expected/80-vue-sfc.type-use b/graph/test/typescript/expected/80-vue-sfc.type-use new file mode 100644 index 00000000..e69de29b diff --git a/graph/test/typescript/expected/80-vue-sfc.types-oracle b/graph/test/typescript/expected/80-vue-sfc.types-oracle new file mode 100644 index 00000000..01d62a42 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-sfc.types-oracle @@ -0,0 +1,7 @@ +80-vue-sfc [types] + precision 0.0000 (0 correct, 0 wrong) + recall 0.0000 (0 of 0 the compiler resolved) + sites 0 resolved 0 + tiers + contexts + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.edges b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.edges new file mode 100644 index 00000000..f055468c --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.edges @@ -0,0 +1,66 @@ +ambiguous_unknown METHOD_CALL shadow#(?) @L36 -> - +ambiguous_unknown METHOD_CALL use#(?) @L78 -> - +ambiguous_unknown METHOD_CALL use#(?) @L83 -> - +ambiguous_unknown METHOD_CALL use#aliasGeneric(Many) @L73 -> - +ambiguous_unknown METHOD_CALL use#aliasPlain(Users) @L70 -> - +ambiguous_unknown METHOD_CALL use#ctlAnnotated(User[]) @L113 -> - +ambiguous_unknown METHOD_CALL use#everyParam(Admin[]) @L54 -> - +ambiguous_unknown METHOD_CALL use#findReadonly(Repo) @L51 -> - +ambiguous_unknown METHOD_CALL use#forEachParen(Repo) @L46 -> - +ambiguous_unknown METHOD_CALL use#mapBare(Repo) @L43 -> - +ambiguous_unknown METHOD_CALL use#mapForEach(Map) @L88 -> - +ambiguous_unknown METHOD_CALL use#nestedArrays(Admin[][]) @L77 -> - +ambiguous_unknown METHOD_CALL use#reduceElement(User[]) @L58 -> - +ambiguous_unknown METHOD_CALL use#thenBare(Repo) @L61 -> - +ambiguous_unknown METHOD_CALL use#thenParam(Promise) @L64 -> - +ambiguous_unknown METHOD_CALL use#thenThenMap(Promise) @L82 -> - +callback_registered METHOD_CALL use#(?) @L78 -> use#(?) +callback_registered METHOD_CALL use#(?) @L83 -> use#(?) +callback_registered METHOD_CALL use#aliasGeneric(Many) @L73 -> use#(?) +callback_registered METHOD_CALL use#aliasPlain(Users) @L70 -> use#(?) +callback_registered METHOD_CALL use#ctlAnnotated(User[]) @L113 -> use#(User) +callback_registered METHOD_CALL use#everyParam(Admin[]) @L54 -> use#(?) +callback_registered METHOD_CALL use#findReadonly(Repo) @L51 -> use#(?) +callback_registered METHOD_CALL use#forEachParen(Repo) @L46 -> use#(?) +callback_registered METHOD_CALL use#mapBare(Repo) @L43 -> use#(?) +callback_registered METHOD_CALL use#mapForEach(Map) @L88 -> use#(?,?) +callback_registered METHOD_CALL use#nestedArrays(Admin[][]) @L77 -> use#(?) +callback_registered METHOD_CALL use#reduceElement(User[]) @L58 -> use#(?,?) +callback_registered METHOD_CALL use#thenBare(Repo) @L61 -> use#(?) +callback_registered METHOD_CALL use#thenParam(Promise) @L64 -> use#(?) +callback_registered METHOD_CALL use#thenThenMap(Promise) @L82 -> use#(?) +known_edge CONSTRUCTOR_CALL Lazy#map((u: User) =) @L102 -> User#() +known_edge CONSTRUCTOR_CALL Lazy#then((a: Admin) =) @L99 -> Admin#() +known_edge CONSTRUCTOR_CALL Promise#then((k: Key) =) @L22 -> Key#() +known_edge CONSTRUCTOR_CALL Set#forEach((k: Key) =) @L17 -> Key#() +known_edge FUNCTION_CALL Lazy#map((u: User) =) @L102 -> Lazy#(User) +known_edge FUNCTION_CALL Lazy#then((a: Admin) =) @L99 -> Lazy#(Admin) +known_edge FUNCTION_CALL Promise#then((k: Key) =) @L22 -> Promise#(Key) +known_edge FUNCTION_CALL Set#forEach((k: Key) =) @L17 -> Set#(Key) +known_edge FUNCTION_CALL shadow#ctlUnionOfFns() @L36 -> shadow#either(((i: Item) =) +known_edge METHOD_CALL shadow#(?) @L26 -> Key#display() +known_edge METHOD_CALL shadow#(?) @L29 -> Key#display() +known_edge METHOD_CALL shadow#ctlOwnPromise(Promise) @L29 -> Promise#then((k: Key) =) +known_edge METHOD_CALL shadow#ctlOwnSet(Set) @L26 -> Set#forEach((k: Key) =) +known_edge METHOD_CALL use#(?) @L106 -> Admin#touch() +known_edge METHOD_CALL use#(?) @L109 -> User#touch() +known_edge METHOD_CALL use#(User) @L113 -> User#save() +known_edge METHOD_CALL use#(?) @L43 -> User#display() +known_edge METHOD_CALL use#(?) @L47 -> User#save() +known_edge METHOD_CALL use#(?) @L51 -> User#touch() +known_edge METHOD_CALL use#(?) @L54 -> Admin#save() +known_edge METHOD_CALL use#(?,?) @L58 -> User#save() +known_edge METHOD_CALL use#(?) @L61 -> User#touch() +known_edge METHOD_CALL use#(?) @L64 -> Admin#display() +known_edge METHOD_CALL use#(?) @L70 -> User#save() +known_edge METHOD_CALL use#(?) @L73 -> Admin#touch() +known_edge METHOD_CALL use#(?) @L78 -> Admin#save() +known_edge METHOD_CALL use#(?) @L83 -> User#touch() +known_edge METHOD_CALL use#(?,?) @L89 -> User#display() +known_edge METHOD_CALL use#(?,?) @L90 -> Admin#touch() +known_edge METHOD_CALL use#ctlOwnMap(Lazy) @L109 -> Lazy#map((u: User) =) +known_edge METHOD_CALL use#ctlOwnThen(Lazy) @L106 -> Lazy#then((a: Admin) =) +known_edge METHOD_CALL use#findReadonly(Repo) @L51 -> Repo#readonlyAll() +known_edge METHOD_CALL use#forEachParen(Repo) @L46 -> Repo#all() +known_edge METHOD_CALL use#mapBare(Repo) @L43 -> Repo#all() +known_edge METHOD_CALL use#thenBare(Repo) @L61 -> Repo#one() diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.entries b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.entries new file mode 100644 index 00000000..687a9aea --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.entries @@ -0,0 +1,22 @@ +── entry_point (21) ── + exported_from_entry_module shadow#ctlOwnPromise shadow.ts:28 + exported_from_entry_module shadow#ctlOwnSet shadow.ts:25 + exported_from_entry_module shadow#ctlUnionOfFns shadow.ts:34 + exported_from_entry_module shadow#either shadow.ts:33 + exported_from_entry_module use#aliasGeneric use.ts:72 + exported_from_entry_module use#aliasPlain use.ts:69 + exported_from_entry_module use#ctlAnnotated use.ts:112 + exported_from_entry_module use#ctlOwnMap use.ts:108 + exported_from_entry_module use#ctlOwnThen use.ts:105 + exported_from_entry_module use#everyParam use.ts:53 + exported_from_entry_module use#findReadonly use.ts:50 + exported_from_entry_module use#forEachParen use.ts:45 + exported_from_entry_module use#mapBare use.ts:42 + exported_from_entry_module use#mapForEach use.ts:87 + exported_from_entry_module use#nestedArrays use.ts:76 + exported_from_entry_module use#reduceElement use.ts:57 + exported_from_entry_module use#thenBare use.ts:60 + exported_from_entry_module use#thenParam use.ts:63 + exported_from_entry_module use#thenThenMap use.ts:81 + unimported_module shadow# shadow.ts:1 + unimported_module use# use.ts:1 diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.envelope b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.envelope new file mode 100644 index 00000000..8c7dd7b7 --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.envelope @@ -0,0 +1,10 @@ +value shadow#@16:15 -> shadow#@26 +value shadow#@21:12 -> shadow#@29 +value shadow#@33:29 -> shadow#@36 +value shadow#@33:51 -> shadow#@36 +value use#@101:11 -> use#@109 +value use#@98:12 -> use#@106 +value_call shadow#Promise.then -> shadow#@29 +value_call shadow#Set.forEach -> shadow#@26 +value_call use#Lazy.map -> use#@109 +value_call use#Lazy.then -> use#@106 diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.fields b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.fields new file mode 100644 index 00000000..70b51f31 --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.fields @@ -0,0 +1,3 @@ +known_edge read Repo#all() -> Repo#users +known_edge read Repo#one() -> Repo#pending +known_edge read Repo#readonlyAll() -> Repo#users diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.fields-oracle b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.fields-oracle new file mode 100644 index 00000000..7af09bb2 --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.fields-oracle @@ -0,0 +1,7 @@ +81-contextual-lambda-param-no-lib [fields] + precision 1.0000 (3 correct, 0 wrong) + recall 1.0000 (3 of 3 the compiler resolved) + sites 3 resolved 3 (100.0%) + tiers known_edge=3 + access read=3 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.oracle b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.oracle new file mode 100644 index 00000000..540c068b --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.oracle @@ -0,0 +1 @@ +oracle=35 engine=35 agree=35 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.type-use b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.type-use new file mode 100644 index 00000000..0622fd29 --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.type-use @@ -0,0 +1,35 @@ +ambiguous_unknown METHOD_PARAM 0 Repo [METHOD_PARAM] -> - +ambiguous_unknown METHOD_PARAM 0 use [METHOD_PARAM] -> - +ambiguous_unknown METHOD_RETURN 0 Repo [METHOD] -> - +ambiguous_unknown METHOD_RETURN 0 use [METHOD] -> - +ambiguous_unknown TYPE_ALIAS_RHS 0 Many [TYPE] -> - +known_edge METHOD_PARAM 0 shadow [METHOD_PARAM] -> Promise +known_edge METHOD_PARAM 0 shadow [METHOD_PARAM] -> Set +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Lazy +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Many +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Repo +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> User +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Users +known_edge METHOD_PARAM 1 Lazy [METHOD_PARAM] -> Admin +known_edge METHOD_PARAM 1 Lazy [METHOD_PARAM] -> User +known_edge METHOD_PARAM 1 Promise [METHOD_PARAM] -> Key +known_edge METHOD_PARAM 1 Set [METHOD_PARAM] -> Key +known_edge METHOD_PARAM 3 shadow [METHOD_PARAM] -> Item +known_edge METHOD_PARAM 3 shadow [METHOD_PARAM] -> Key +known_edge OBJECT_CREATION_TYPE 0 Lazy [EXPRESSION] -> Admin +known_edge OBJECT_CREATION_TYPE 0 Lazy [EXPRESSION] -> User +known_edge OBJECT_CREATION_TYPE 0 Promise [EXPRESSION] -> Key +known_edge OBJECT_CREATION_TYPE 0 Set [EXPRESSION] -> Key +known_edge TYPE_ARGUMENT 1 Repo [METHOD] -> User +known_edge TYPE_ARGUMENT 1 Repo [METHOD_PARAM] -> User +known_edge TYPE_ARGUMENT 1 shadow [METHOD_PARAM] -> Item +known_edge TYPE_ARGUMENT 1 use [METHOD_PARAM] -> Admin +known_edge TYPE_ARGUMENT 1 use [METHOD_PARAM] -> User +known_edge TYPE_ELEMENT 1 Repo [METHOD] -> User +known_edge TYPE_ELEMENT 1 Repo [METHOD_PARAM] -> User +known_edge TYPE_ELEMENT 1 Users [TYPE] -> User +known_edge TYPE_ELEMENT 1 use [METHOD] -> User +known_edge TYPE_ELEMENT 1 use [METHOD_PARAM] -> Admin +known_edge TYPE_ELEMENT 1 use [METHOD_PARAM] -> User +known_edge TYPE_ELEMENT 2 use [METHOD_PARAM] -> Admin +known_edge TYPE_ELEMENT 2 use [METHOD_PARAM] -> User diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.types-oracle b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.types-oracle new file mode 100644 index 00000000..e4ceb413 --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.types-oracle @@ -0,0 +1,7 @@ +81-contextual-lambda-param-no-lib [types] + precision 1.0000 (16 correct, 0 wrong) + recall 1.0000 (16 of 16 the compiler resolved) + sites 48 resolved 39 (81.2%) + tiers ambiguous_unknown=9 known_edge=39 + contexts METHOD_PARAM=21 METHOD_RETURN=4 OBJECT_CREATION_TYPE=4 TYPE_ALIAS_RHS=1 TYPE_ARGUMENT=9 TYPE_ELEMENT=9 + not scored: 9 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/81-vue-component-tag.edges b/graph/test/typescript/expected/81-vue-component-tag.edges new file mode 100644 index 00000000..2d88b089 --- /dev/null +++ b/graph/test/typescript/expected/81-vue-component-tag.edges @@ -0,0 +1,10 @@ +ambiguous_unknown FUNCTION_CALL components/MyWidget.vue#() @L2 -> - +ambiguous_unknown JSX_COMPONENT_CALL views/Parent.vue#() @L13 -> - +known_edge FUNCTION_CALL components/Child.vue#() @L4 -> components/Child.vue#childLocal() +known_edge FUNCTION_CALL components/Legacy.vue#data() @L3 -> components/Legacy.vue#legacyInit() +known_edge FUNCTION_CALL components/MyWidget.vue#() @L5 -> components/MyWidget.vue#widgetLabel() +known_edge FUNCTION_CALL components/store#() @L2 -> components/store#storeInit() +known_edge JSX_COMPONENT_CALL views/Parent.vue#() @L10 -> components/MyWidget.vue#() +known_edge JSX_COMPONENT_CALL views/Parent.vue#() @L11 -> components/Legacy.vue#() +known_edge JSX_COMPONENT_CALL views/Parent.vue#() @L12 -> components/badge#Badge() +known_edge JSX_COMPONENT_CALL views/Parent.vue#() @L9 -> components/Child.vue#() diff --git a/graph/test/typescript/expected/81-vue-component-tag.entries b/graph/test/typescript/expected/81-vue-component-tag.entries new file mode 100644 index 00000000..44defd18 --- /dev/null +++ b/graph/test/typescript/expected/81-vue-component-tag.entries @@ -0,0 +1,4 @@ +── entry_point (3) ── + exported_from_entry_module main#mount main.ts:3 + unimported_module main# main.ts:1 + unimported_module shims-vue# shims-vue.d.ts:1 diff --git a/graph/test/typescript/expected/81-vue-component-tag.fields b/graph/test/typescript/expected/81-vue-component-tag.fields new file mode 100644 index 00000000..e69de29b diff --git a/graph/test/typescript/expected/81-vue-component-tag.fields-oracle b/graph/test/typescript/expected/81-vue-component-tag.fields-oracle new file mode 100644 index 00000000..7a65896b --- /dev/null +++ b/graph/test/typescript/expected/81-vue-component-tag.fields-oracle @@ -0,0 +1,7 @@ +81-vue-component-tag [fields] + precision 0.0000 (0 correct, 0 wrong) + recall 0.0000 (0 of 0 the compiler resolved) + sites 0 resolved 0 + tiers + access + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/81-vue-component-tag.oracle b/graph/test/typescript/expected/81-vue-component-tag.oracle new file mode 100644 index 00000000..c5bc7b40 --- /dev/null +++ b/graph/test/typescript/expected/81-vue-component-tag.oracle @@ -0,0 +1,8 @@ +oracle=1 engine=8 agree=1 missing=0 (known 0, NEW 0) extra=7 + extra components/Child.vue#() -> components/Child.vue#childLocal() + extra components/Legacy.vue#data() -> components/Legacy.vue#legacyInit() + extra components/MyWidget.vue#() -> components/MyWidget.vue#widgetLabel() + extra views/Parent.vue#() -> components/Child.vue#() + extra views/Parent.vue#() -> components/Legacy.vue#() + extra views/Parent.vue#() -> components/MyWidget.vue#() + extra views/Parent.vue#() -> components/badge#Badge() diff --git a/graph/test/typescript/expected/81-vue-component-tag.type-use b/graph/test/typescript/expected/81-vue-component-tag.type-use new file mode 100644 index 00000000..e69de29b diff --git a/graph/test/typescript/expected/81-vue-component-tag.types-oracle b/graph/test/typescript/expected/81-vue-component-tag.types-oracle new file mode 100644 index 00000000..cf012188 --- /dev/null +++ b/graph/test/typescript/expected/81-vue-component-tag.types-oracle @@ -0,0 +1,7 @@ +81-vue-component-tag [types] + precision 0.0000 (0 correct, 0 wrong) + recall 0.0000 (0 of 0 the compiler resolved) + sites 0 resolved 0 + tiers + contexts + not scored: 0 rows whose target is not a client declaration diff --git a/graph/typescript/engine/expression-resolution/callee-resolution.dl b/graph/typescript/engine/expression-resolution/callee-resolution.dl index b1de05bf..db0a0bd1 100644 --- a/graph/typescript/engine/expression-resolution/callee-resolution.dl +++ b/graph/typescript/engine/expression-resolution/callee-resolution.dl @@ -653,6 +653,47 @@ jsx_wrapper_renders(w, m) :- jsx_component_wrapper(w), expr_child("client", w, "ARGUMENT", "0", a), jsx_component_wrapper(a), jsx_wrapper_renders(a, m). +// …or, when that argument NAMES a component defined elsewhere, what that one renders: the +// registration idiom `import _Button from "./Button"; export const Button = +// withInstall(_Button)`, where Button.tsx is `export default defineComponent({…})`, and +// its local and named-import forms. +jsx_wrapper_renders(w, m) :- jsx_component_wrapper(w), + expr_child("client", w, "ARGUMENT", "0", a), + expr_referenced("client", "VARIABLE", v, a), + jsx_wrapped_component(v, m). +jsx_wrapper_renders(w, m) :- jsx_component_wrapper(w), + expr_child("client", w, "ARGUMENT", "0", a), + expr_referenced("client", "IMPORT_BINDING", ih, a), + import_binds(ih, _, "VARIABLE", v), + jsx_wrapped_component(v, m). +jsx_wrapper_renders(w, m) :- jsx_component_wrapper(w), + expr_child("client", w, "ARGUMENT", "0", a), + expr_referenced("client", "IMPORT_BINDING", ih, a), + import_binding("client", "DEFAULT", _, _, _, ih), + import_target_module(ih, "client", mod2), + jsx_default_export_renders(mod2, m). +// A Vue component, `defineComponent({ setup() {…} })` / `defineComponent({ render() {…} })` +// (Nuxt: `defineNuxtComponent`). What the tag invokes with its props is the options +// object's `setup`, or its `render` option when there is no setup; a render function setup +// RETURNS is reached from setup's body, as memo's argument reaches what it defines. One +// target, not both: the site's candidates are an overload set to arity selection, and +// `setup(props)` beside a zero-parameter render arrow would lose one of them. The function +// form, `defineComponent((props) => () => …)`, is argument 0 itself, as for memo. Named by +// the definer, so the same object handed to any other call (`makeStore({ setup() {…} })`) +// stays out. +jsx_vue_definer_name("defineComponent"). +jsx_vue_definer_name("defineNuxtComponent"). +jsx_vue_definer(w) :- call_site("client", _, n, _, _, w, _), + jsx_vue_definer_name(n). +jsx_component_wrapper(w) :- jsx_vue_definer(w). +jsx_vue_has_setup(a) :- objlit_member_fn(a, "setup", _). +jsx_wrapper_renders(w, m) :- jsx_vue_definer(w), + expr_child("client", w, "ARGUMENT", "0", a), + objlit_member_fn(a, "setup", m). +jsx_wrapper_renders(w, m) :- jsx_vue_definer(w), + expr_child("client", w, "ARGUMENT", "0", a), + !jsx_vue_has_setup(a), + objlit_member_fn(a, "render", m). // A wrapper call that IS the module's default export, `export default memo(Card)`, // binds no variable: the tag reaches it through a default import of that module. jsx_default_export_renders(mod, m) :- export_decl("client", "default", _, "DEFAULT_EXPRESSION", mod, h), @@ -665,6 +706,24 @@ expr_call_candidate(ce, m) :- call_site("client", "JSX_COMPONENT_CALL", cn, _, _ import_binding("client", "DEFAULT", cn, _, mod, ih), import_target_module(ih, "client", mod2), jsx_default_export_renders(mod2, m). +// A Vue single-file component, `import Child from "./Child.vue"` rendered as `` +// (a tag of a `.vue` template, or JSX). Its script is the module's top-level code — a +// `\n'], + ['a.vue', '\n\n'], + ['a.svelte', '{g()}'], + ['a.astro', '---\n', 'h()\n', '---\n

    {h()}

    \n'], + ]; + for (const [name, ...parts] of COMPONENTS) { + const text = parts.join(''); + const want = parts.map((part, i) => (i % 2 === 1 ? part : part.replace(/[^\n]/g, ' '))).join(''); + const got = sfcScriptText(name, text).text; + if (got !== want) { + failures += fail(`sfcScriptText(${name}, ${JSON.stringify(text)}) is ${JSON.stringify(got)}, ` + + `expected ${JSON.stringify(want)}`); + } + } + // ---- 2. source-level: nobody rolls their own. const KNOWN = ['.js', '.jsx', '.mjs', '.cjs', '.flow']; const files: string[] = []; @@ -7187,6 +7277,7 @@ const CHECKS: Check[] = [ { name: 'module-edge 1:1', proves: 'every module-edge expression is pointed at by exactly one import or export, so the second pass cannot double-mint', run: moduleEdgeOneToOne }, { name: 'call-site 1:1', proves: 'one call site per call-like expression, and require() has none because it is a module edge', run: callSiteOneToOne }, { name: 'package specifiers resolve under the site conditions', proves: "a require() of an exports-only package resolves to its `require` build and an ES import to its `import` build, a subpath export and a main-only package either way, so a dual package links to the build the runtime loads (#601)", run: packageSpecifiersResolveUnderTheSiteConditions }, + { name: 'path aliases resolve through the nearest config', proves: "an import through a jsconfig/tsconfig `paths` alias ('@/lib/x', '~/models/x') resolves to the project file it maps to, and a directory whose nearest config maps nothing leaves the alias unresolved", run: pathAliasesResolveThroughTheNearestConfig }, { name: 'computed member names link their key', proves: "a member declared under a computed name links its key expression (rooted COMPUTED_NAME, bound to the key's const) and a literal key fills name, on class methods, a getter, class fields, object-literal and prototype-literal members (#598)", run: computedMemberNamesLinkTheirKey }, { name: 'pattern binding defaults are linked', proves: "a reference to a binding declared inside a destructuring pattern with a default links the default's root expression (c35), for object, nested and array parameter patterns and a variable pattern; a top-level parameter default and a binding without one link nothing (#673)", run: patternBindingDefaultsAreLinked }, { name: 'several roots produce one set', proves: 'overlapping discovered roots merge into one flat fact base with no file extracted twice — the failure the real entry point found and a single-root harness cannot', run: severalRootsProduceOneSet }, diff --git a/parser/src/utils/javascript/javascript-module-utils.ts b/parser/src/utils/javascript/javascript-module-utils.ts index f301664b..fad52cfd 100644 --- a/parser/src/utils/javascript/javascript-module-utils.ts +++ b/parser/src/utils/javascript/javascript-module-utils.ts @@ -1,4 +1,4 @@ -import { JS_SOURCE_EXTENSIONS } from '@/constants/javascript-constants'; +import { JS_COMPONENT_EXTENSIONS, JS_SOURCE_EXTENSIONS } from '@/constants/javascript-constants'; /** * What counts as a platform module, in one place. @@ -129,7 +129,96 @@ export function jsExtensionOf(fileName: string): string { // table, which is the argument for having written one: the function was in use // at eight call sites and every one of them happened to pass a `.js` family // name, so nothing else would ever have shown it. - return (JS_SOURCE_EXTENSIONS as readonly string[]).includes(extension) ? extension : ''; + if ((JS_SOURCE_EXTENSIONS as readonly string[]).includes(extension)) { + return extension; + } + // A component has no `.flow` spelling: `a.vue.flow` is not a declaration file. + return base === name && (JS_COMPONENT_EXTENSIONS as readonly string[]).includes(extension) + ? extension + : ''; +} + +/** Is this a single-file component (`.vue`, `.svelte`, `.astro`), by name? */ +export function isJsComponentFile(fileName: string): boolean { + return (JS_COMPONENT_EXTENSIONS as readonly string[]).includes(jsExtensionOf(fileName)); +} + +/** One `\n" + "\n"), + ('src/List.svelte', '\n'), ('src/pages/index.astro', '---\n---\n'), + ('src/index.html', '
    \n'), ('src/app.css', 'a {}\n'), ('README.md', '# x\n')): + write(root, f, body) + got = {os.path.relpath(p, root) for p in ax_fresh.watched(root, lang)} + check(f"prune: {lang} watches .vue, .svelte and .astro components, not the .html, .css or .md beside them", + got == {'src/lib.js', 'src/views/Home.vue', 'src/List.svelte', 'src/pages/index.astro'}, sorted(got)) + table = dict(lang=lang, lang_auto=True, src='', files=ax_fresh.snapshot(root, lang, root)) + with open(os.path.join(root, 'src/views/Home.vue'), 'w') as fh: + fh.write("\n" + "\n") + write(root, 'src/views/About.vue', '\n') + write(root, 'src/index.html', '
    changed
    \n') + c = ax_fresh.changes(root, table) + check(f"prune: {lang}: an edited .vue makes the graph stale and a new one is added; an edited .html does neither", + c == (['src/views/Home.vue'], ['src/views/About.vue'], []), c) + only = os.path.join(work, 'sfc-only') + write(only, 'src/App.vue', '\n') + n = {l: sum(1 for p in ax_fresh.watched(only, l) if p.endswith(ax_fresh.SOURCE[l])) for l in ('javascript', 'typescript')} + check("prune: a repository of .vue files alone counts no JavaScript or TypeScript source", n == {'javascript': 0, 'typescript': 0}, n) finally: shutil.rmtree(work, ignore_errors=True) diff --git a/tests/hooks_from_path.py b/tests/hooks_from_path.py index fd80ef88..a771dd73 100644 --- a/tests/hooks_from_path.py +++ b/tests/hooks_from_path.py @@ -118,14 +118,16 @@ def write(base, files): fire(app, 'c7', 'Read', {'file_path': os.path.join(app, 'vendored', P, 'OrderStore.java')}) == '') # ── the directive (PreToolUse) ──────────────────────────────────────────────────────────────────────── - d = fire(ws, 'd1', 'Read', {'file_path': svc}, hook='direct.py', event='PreToolUse') - check('the directive speaks before a Read by absolute path from a directory with no graph', 'axiomcode_impact' in d, d) - check('control: the directive is silent before a Read in a tree with no graph', - fire(ws, 'd2', 'Read', {'file_path': os.path.join(plain, P, 'OrderStore.java')}, hook='direct.py', event='PreToolUse') == '') - check('the directive is silent on a shell `cat` of a file that is not source', - fire(app, 'd3', 'Bash', {'command': 'cat notes.md'}, hook='direct.py', event='PreToolUse') == '') - d = fire(app, 'd4', 'Bash', {'command': f'cat {os.path.relpath(store, app)}'}, hook='direct.py', event='PreToolUse') - check('control: the directive speaks on a shell `cat` of a source file', 'axiomcode_impact' in d, d) + # it speaks once per session (stamped in the temp directory), so each check gets a session no earlier run used + sid = lambda s: f'{s}-{os.getpid()}-{int(time.time() * 1000)}' + d = fire(ws, sid('d1'), 'Grep', {'pattern': 'findById', 'path': app}, hook='direct.py', event='PreToolUse') + check('the directive speaks before a Grep by absolute path from a directory with no graph', 'axiomcode_impact' in d and 'OrderStore.java' in d, d) + check('control: the directive is silent before a Grep of a tree with no graph', + fire(ws, sid('d2'), 'Grep', {'pattern': 'findById', 'path': plain}, hook='direct.py', event='PreToolUse') == '') + check('the directive is silent on a shell grep of a file that is not source', + fire(app, sid('d3'), 'Bash', {'command': 'grep -n findById notes.md'}, hook='direct.py', event='PreToolUse') == '') + d = fire(app, sid('d4'), 'Bash', {'command': f'grep -n findById {os.path.relpath(store, app)}'}, hook='direct.py', event='PreToolUse') + check('control: the directive speaks on a shell grep of a source file for a declared method', 'axiomcode_impact' in d, d) # ── orientation ────────────────────────────────────────────────────────────────────────────────────── for i in range(30): diff --git a/tests/hosts.py b/tests/hosts.py index e1f235d8..e581c385 100644 --- a/tests/hosts.py +++ b/tests/hosts.py @@ -156,8 +156,8 @@ def said_cursor(out, event): rc3 == 0 and first and said_gemini(out3, 'AfterTool') == first, out3[:160]) # PreToolUse: Cursor's preToolUse output has no context field, so the directive stays silent there. - rc, out = fire('direct.py', original('PreToolUse', 'Grep', {'pattern': 'greet'}, repo, 'd1')) - rc2, out2 = fire('direct.py', cursor('preToolUse', 'Grep', {'pattern': 'greet'}, repo, 'd2'), cursor=True) + rc, out = fire('direct.py', original('PreToolUse', 'Grep', {'pattern': r'greet\('}, repo, 'd1')) + rc2, out2 = fire('direct.py', cursor('preToolUse', 'Grep', {'pattern': r'greet\('}, repo, 'd2'), cursor=True) check('direct: the original host hears the directive', rc == 0 and 'impact' in said_original(out), out[:160]) check('direct: Cursor hears nothing, since preToolUse cannot carry context', rc2 == 0 and out2 == '', out2[:160]) diff --git a/tests/mcp.py b/tests/mcp.py index faef7ec7..9be19790 100644 --- a/tests/mcp.py +++ b/tests/mcp.py @@ -133,6 +133,7 @@ def check_arguments(label, cmd, cwd, lax=False): ('axiomcode_changed', {'repo': cwd, 'files': ['a.py']}), ('axiomcode_impact', {'targets': ['A.f'], 'repo': cwd, 'limit': 5, 'delete': True, 'in_path': 'src'}), ('axiomcode_context', {'task': 'x', 'repo': cwd, 'in_path': 'src', 'from_': 'main'}), + ('axiomcode_context', {'task': 'x', 'repo': cwd, 'limit': 5}), ('axiomcode_path', {'from_': 'a', 'to': 'b', 'repo': cwd, 'limit': 3})] for name, args in right: res = call(cmd, cwd, name, args) @@ -199,9 +200,15 @@ def check_grep_default(): for f in grep: seen.clear(); f() if '--grep' not in seen[0]: bad.append(f"sites answer without --grep: {seen[0]}") - seen.clear(); grep[-1]() - if '--grep-limit' not in seen[0]: bad.append(f"limit=5 did not cap the sites: {seen[0]}") - if '--limit' in seen[0]: bad.append(f"limit=5 passed as the prose's --limit under --grep: {seen[0]}") + # limit=N caps the sites on every tool whose footer says "limit=N lists more": context took no limit, and + # test_impact's went to the prose's --limit, so the sites were never capped + for f in (grep[-1], lambda: server.axiomcode_context('how', limit=5), lambda: server.axiomcode_test_impact(limit=5), + lambda: server.axiomcode_path('a', 'b', limit=5)): + seen.clear(); f() + if '--grep-limit' not in seen[0]: bad.append(f"limit=5 did not cap the sites: {seen[0]}") + if '--limit' in seen[0]: bad.append(f"limit=5 passed as the prose's --limit under --grep: {seen[0]}") + seen.clear(); server.axiomcode_test_impact(limit=5, why=True) # the control: the prose keeps its --limit + if '--limit' not in seen[0] or '--grep-limit' in seen[0]: bad.append(f"test_impact prose lost --limit: {seen[0]}") for f in prose: seen.clear(); f() if any(a.startswith('--grep') for a in seen[0]): bad.append(f"prose asked for, got --grep: {seen[0]}") diff --git a/tests/mcp_docs.py b/tests/mcp_docs.py new file mode 100644 index 00000000..45c67f3a --- /dev/null +++ b/tests/mcp_docs.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""tests/mcp_docs.py: every MCP argument the skill documents is one the tool it names accepts. + +SKILL.md and reference/*.md tell an agent which MCP arguments to pass (`full=True`, `limit=N`, `page="all"`, +`range='a..b'`, `files=[…]`). An argument the tool's schema does not take is refused, and the agent that followed the +docs is left with an error and no answer. The docs name the arguments in prose, so this reads them the way an agent +does: in each paragraph, list item or table row that speaks of MCP, every `name=value` belongs to the nearest tool named +before it (`axiomcode_impact`, MCP `impact`, `changed --range …`; a run like "`impact`, `path` and `context`" is one +group, and every tool in it must take the argument). The schemas are the server's own tools/list, from the SDK-free +fallback, so what is checked is what a client is offered. + +Both skill copies are read (plugins/axiomcode/skills/axiomcode and the root skills/axiomcode), and +plugins/axiomcode/AGENTS.md and rules/axiomcode.mdc, which name the same tools. An argument a schema +takes and the docs never mention is fine. A documented argument with no tool named before it is a failure too: the +agent cannot tell which tool takes it. + + python3 tests/mcp_docs.py +""" +import glob, json, os, re, subprocess, sys, threading + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SERVER = os.path.join(ROOT, 'plugins', 'axiomcode', 'mcp', 'server.py') +DOCS = sorted(p for d in (os.path.join(ROOT, 'plugins', 'axiomcode', 'skills', 'axiomcode'), + os.path.join(ROOT, 'skills', 'axiomcode')) + for p in [os.path.join(d, 'SKILL.md')] + glob.glob(os.path.join(d, 'reference', '*.md'))) + \ + [os.path.join(ROOT, 'plugins', 'axiomcode', 'AGENTS.md')] + \ + sorted(glob.glob(os.path.join(ROOT, 'plugins', 'axiomcode', 'rules', '*.mdc'))) + +VERB = r'(index|context|impact|path|changed|test[-_]impact|graph)' +# a tool named: `axiomcode_impact`, bare axiomcode_impact, MCP `impact`, or a command in backticks (`changed --range x`); +# `path:line: …` is an answer's shape, not the tool +MENTION = re.compile(r'`(?:axiomcode[ _])?' + VERB + r'(?:[ \t][^`\n]*)?`|\baxiomcode_' + VERB + r'\b') +# between two tools of one group: commas, "and", "or", a middle dot, a slash +JOIN = re.compile(r'^(?:[\s,·/]|\band\b|\bor\b)*$') +ARG = re.compile(r'(?