From 9e4379e036497c4bf7c4ac1c89dccf36037a674f Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:24:03 -0700 Subject: [PATCH 1/7] csharp: a name written inside a namespace is read from that namespace `namespace Shop.Tests; using Results;` imports Shop.Results, and `Reflection.Info.Create()` written inside namespace Shop names Shop.Reflection.Info: C# reads the first segment of a using's name, and of a qualified name, from the enclosing namespaces outward. The engine took both as written, so the using imported nothing and every client type named through it read as a library type (`new Failure(..)` was boundary_lib), and the qualified receiver stayed untyped. - module_in_scope: a using whose name is no namespace at all takes the child of the file's namespace chain that does exist (the IR does not say whether the using sits inside the namespace; a name that is no namespace only compiles there). - type_name_cand: a type's qualified name is split at each dot into namespace prefix and dotted tail; the tail is a rank-1 candidate in every file whose namespace chain holds the prefix. Keyed from the declaration side. Case a-namespace-name-is-read-from-its-namespace fails 4/6 on the base (the two controls, a file in an unrelated namespace, pass on both). Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../csharp/engine/containment/type-nesting.dl | 24 +++++++++++++++++++ .../engine/resolution/type-resolution.dl | 20 ++++++++++++++++ graph/csharp/souffle/decls_all.dl | 4 +++- .../case.json | 20 ++++++++++++++++ .../src/Audit.cs | 13 ++++++++++ .../src/Elsewhere.cs | 15 ++++++++++++ .../src/Machine.cs | 17 +++++++++++++ .../src/Reflection.cs | 7 ++++++ .../src/Results.cs | 8 +++++++ .../src/Shop.csproj | 1 + 10 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/case.json create mode 100644 tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Audit.cs create mode 100644 tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Elsewhere.cs create mode 100644 tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Machine.cs create mode 100644 tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Reflection.cs create mode 100644 tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Results.cs create mode 100644 tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Shop.csproj diff --git a/graph/csharp/engine/containment/type-nesting.dl b/graph/csharp/engine/containment/type-nesting.dl index e584db41..8ac8b94d 100644 --- a/graph/csharp/engine/containment/type-nesting.dl +++ b/graph/csharp/engine/containment/type-nesting.dl @@ -149,3 +149,27 @@ module_in_scope(prov, mod, outer) :- // The GLOBAL namespace ("") is always in scope. module_in_scope(prov, mod, "") :- module_decl(prov, _, _, _, mod). + +// module_ns_chain(Prov, ModuleHash, Namespace) -- the namespaces a name written in this +// file is read from: the file's own namespaces and every dotted prefix of them, as +// text. A prefix that holds no type of its own (`Acme` over `Acme.Shop`) still +// qualifies a name written below it, so this is not ns_encloses, which needs one. +module_ns_chain(prov, mod, ns) :- module_namespace_own(prov, mod, ns), ns != "". +module_ns_chain(prov, mod, p) :- module_namespace_own(prov, mod, inner), ns_prefix(prov, inner, p). + +// ── A USING WRITTEN INSIDE A NAMESPACE ───────────────────────────────────── +// `namespace Shop.Tests; using Results;` imports Shop.Results. C# reads the name in +// a using directive from the namespace the directive sits in, innermost outward, +// exactly as it reads any other name there. Taken as written it names a namespace +// that does not exist, and every type the file names through it reads as a library +// type. +// +// The IR does not record whether a using sits inside the namespace or above it. +// The language decides it instead: a using whose name is no namespace at all only +// compiles inside a namespace that has it as a child. So the relative reading is +// taken only when the name as written names nothing, and only for a child that +// exists -- a file whose namespaces have no such child gains nothing. +module_in_scope(prov, mod, full) :- + using_namespace(prov, mod, n), !ns_exists(prov, n), + module_ns_chain(prov, mod, e), full = cat(e, cat(".", n)), + ns_exists(prov, full). diff --git a/graph/csharp/engine/resolution/type-resolution.dl b/graph/csharp/engine/resolution/type-resolution.dl index fb90ddc4..1c76e1cb 100644 --- a/graph/csharp/engine/resolution/type-resolution.dl +++ b/graph/csharp/engine/resolution/type-resolution.dl @@ -89,6 +89,26 @@ type_name_cand(prov, mod, qn, ar, gk, 1) :- type_decl(prov, _, qn, ar, _, t), type_group(prov, t, gk), !type_is_top_level(prov, gk). +// ── A QUALIFIED NAME READ FROM ITS NAMESPACE ──────────────────────────────── +// `Reflection.Info.Create(..)` written inside namespace Shop names +// Shop.Reflection.Info: the first segment of a qualified name is a +// simple name, looked up in the enclosing namespaces like any other. Matched only +// in full, such a receiver stays untyped and every call through it unresolved. +// +// Keyed from the DECLARATION side, so nothing is enumerated per written name: each +// type's qualified name is split at each dot into the namespace before it and the +// dotted tail after it, and the tail is a candidate in every file whose namespace +// chain holds that prefix. A tail with no dot is a simple name, which ranks 2-5 +// already answer. +type_qn_tail(prov, qn, p, s) :- + type_decl(prov, _, qn, _, _, _), + i = range(1, strlen(qn)), substr(qn, i, 1) = ".", + p = substr(qn, 0, i), s = substr(qn, i + 1, strlen(qn) - i - 1), + contains(".", s). +type_name_cand(prov, mod, s, ar, gk, 1) :- + type_qn_tail(prov, qn, p, s), module_ns_chain(prov, mod, p), + type_decl(prov, _, qn, ar, _, t), type_group(prov, t, gk). + // ── A CLIENT FILE NAMING A STAGED LIBRARY TYPE ────────────────────────────── // Every clause above is SINGLE-PROVENANCE: it joins a module and a declaration // under one `prov`, so a client module and a library namespace are never brought diff --git a/graph/csharp/souffle/decls_all.dl b/graph/csharp/souffle/decls_all.dl index b1faa46b..2e454837 100644 --- a/graph/csharp/souffle/decls_all.dl +++ b/graph/csharp/souffle/decls_all.dl @@ -65,9 +65,9 @@ .decl call_callee_name(c0:symbol,c1:symbol,c2:symbol) .decl call_caller_unknown(c0:symbol,c1:symbol) .decl call_chain_edge(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol,c6:symbol) -.decl call_class_kind(c0:symbol) .decl call_chain_summary(c0:symbol,c1:number) .decl call_class(c0:symbol,c1:symbol,c2:symbol) +.decl call_class_kind(c0:symbol) .decl call_context(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl call_ctor_implicit(c0:symbol,c1:symbol,c2:symbol) .decl call_ctor_target(c0:symbol,c1:symbol,c2:symbol) @@ -663,6 +663,7 @@ .decl module_lang(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl module_namespace_own(c0:symbol,c1:symbol,c2:symbol) .decl module_namespace_style(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl module_ns_chain(c0:symbol,c1:symbol,c2:symbol) .decl module_parse_errors(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl module_regime(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl module_target(c0:symbol,c1:symbol,c2:symbol,c3:symbol) @@ -948,6 +949,7 @@ .decl type_parent(c0:symbol,c1:symbol,c2:symbol) .decl type_placement(c0:symbol,c1:symbol,c2:symbol) .decl type_primary_ctor(c0:symbol,c1:symbol,c2:symbol) +.decl type_qn_tail(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_ref(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol,c6:symbol) .decl type_ref_arg(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_ref_argc(c0:symbol,c1:symbol,c2:symbol) diff --git a/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/case.json b/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/case.json new file mode 100644 index 00000000..d5cdce43 --- /dev/null +++ b/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/case.json @@ -0,0 +1,20 @@ +{"lang": "csharp", "src": "src", + "checks": [ + {"why": "a qualified name written inside a namespace is read relative to it: Reflection.Info in namespace Shop is Shop.Reflection.Info", + "run": ["impact", "Info.Create"], + "want": ["[resolved] Machine.Guard"]}, + {"why": "the enclosing namespace of a nested one supplies the qualifier too (Shop.Orders sees Shop.Reflection)", + "run": ["impact", "Info.Create"], + "want": ["[resolved] Desk.Check"]}, + {"why": "control: from namespace Billing, Reflection.Info is no Shop type", + "run": ["impact", "Info.Create"], + "avoid": ["[resolved] Ledger.Post"]}, + {"why": "a using written inside a namespace names its namespace relative to it: `using Results;` in Shop.Audit imports Shop.Results", + "run": ["impact", "Failure.Describe"], + "want": ["[resolved] Auditor.Describes"]}, + {"why": "the constructor is reached through the same using", + "run": ["impact", "Failure."], + "want": ["Auditor.Describes"]}, + {"why": "control: `using Results;` in namespace Billing imports nothing of Shop", + "run": ["impact", "Failure.Describe"], + "avoid": ["[resolved] Ledger.Post"]}]} diff --git a/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Audit.cs b/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Audit.cs new file mode 100644 index 00000000..bc827cc6 --- /dev/null +++ b/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Audit.cs @@ -0,0 +1,13 @@ +namespace Shop.Audit; + +using Results; + +public class Auditor +{ + // `using Results;` written inside namespace Shop.Audit imports Shop.Results + public void Describes() + { + var failure = new Failure("x"); + failure.Describe(); + } +} diff --git a/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Elsewhere.cs b/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Elsewhere.cs new file mode 100644 index 00000000..a1921629 --- /dev/null +++ b/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Elsewhere.cs @@ -0,0 +1,15 @@ +// control: from a namespace with no Results or Reflection under it, the same names reach nothing in Shop +namespace Billing +{ + using Results; + + public class Ledger + { + public void Post() + { + var failure = new Failure("y"); + failure.Describe(); + var info = Reflection.Info.Create("ledger"); + } + } +} diff --git a/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Machine.cs b/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Machine.cs new file mode 100644 index 00000000..d6694f89 --- /dev/null +++ b/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Machine.cs @@ -0,0 +1,17 @@ +namespace Shop +{ + public class Machine + { + // `Reflection` is Shop.Reflection: a qualified name is read from the namespace it is written in + public void Guard() { var info = Reflection.Info.Create("guard"); } + } +} + +namespace Shop.Orders +{ + public class Desk + { + // from a namespace nested deeper, the enclosing Shop still supplies `Reflection` + public void Check() { var info = Reflection.Info.Create("desk"); } + } +} diff --git a/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Reflection.cs b/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Reflection.cs new file mode 100644 index 00000000..667bd0a5 --- /dev/null +++ b/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Reflection.cs @@ -0,0 +1,7 @@ +namespace Shop.Reflection +{ + public class Info + { + public static Info Create(string name) { return new Info(); } + } +} diff --git a/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Results.cs b/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Results.cs new file mode 100644 index 00000000..c6a5aed1 --- /dev/null +++ b/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Results.cs @@ -0,0 +1,8 @@ +namespace Shop.Results; + +public class Failure +{ + public Failure(string message) { Message = message; } + public string Message { get; } + public string Describe() { return Message; } +} diff --git a/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Shop.csproj b/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Shop.csproj new file mode 100644 index 00000000..d3b2f030 --- /dev/null +++ b/tests/cases/csharp/a-namespace-name-is-read-from-its-namespace/src/Shop.csproj @@ -0,0 +1 @@ +net8.0 From d867f4fe3d846f7b7e6fa07ba640cd881963ebc3 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:24:03 -0700 Subject: [PATCH 2/7] path: a C# source file is an endpoint `axiomcode path Tests/OrderTests.cs Order.Place` was refused with "nothing named 'Tests/OrderTests.cs' is declared" on every C# graph, although the file is indexed: the file-endpoint branch matched .ts/.tsx/.js/.mjs/.cjs/.py/.java only. Every extension a front end reads (ax_langs.BY_EXT) is now a file endpoint. An extension outside the old list falls through to the name lookup when no indexed file has that name, so `Owner.cs` still resolves a method `cs` of Owner (the case's control). Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../skills/axiomcode/scripts/axiomcode-path | 13 +++++++++---- .../a-source-file-is-a-path-endpoint/case.json | 14 ++++++++++++++ .../a-source-file-is-a-path-endpoint/src/Calc.cs | 8 ++++++++ .../src/Calc.csproj | 1 + .../a-source-file-is-a-path-endpoint/src/Checks.cs | 12 ++++++++++++ .../a-source-file-is-a-path-endpoint/src/Owner.cs | 8 ++++++++ 6 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 tests/cases/csharp/a-source-file-is-a-path-endpoint/case.json create mode 100644 tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Calc.cs create mode 100644 tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Calc.csproj create mode 100644 tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Checks.cs create mode 100644 tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Owner.cs diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path index 51c36cbd..bb67ac13 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path @@ -42,7 +42,7 @@ look at, never as a path. import contextlib, io, json, os, re, sys, sqlite3, subprocess, tempfile, shutil, collections, glob, time HERE = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, HERE); import dl_program, graph_sql, ax_edges, ax_contract, ax_text +sys.path.insert(0, HERE); import dl_program, graph_sql, ax_edges, ax_contract, ax_text, ax_langs # The certainty table used to live here with SEVEN entries and a default of 5 for everything else. The # engine emits ELEVEN tiers, and the five it did not list all fell BELOW `defines` — which is not a call # at all — so on a JavaScript graph, where 8,916 of 25,048 traversable edges are `callback_registered`, @@ -557,13 +557,18 @@ class G: if not near: die(f"no method carries {s}, and this graph records NO decoration at all — not that the code has none." " A front end that does not project decorations cannot answer a decoration endpoint; ask by name instead.") die(f"no method carries {s}. Decorations in this graph: " + ', '.join(f"@{x['name']} ({x['n']})" for x in near)) - if re.search(r'\.(ts|tsx|js|mjs|cjs|py|java)$', s): # a file → every method in it + # a file → every method in it, for every source extension a front end reads (ax_langs.BY_EXT). An extension + # outside the first list below falls through to the name lookup when no file matches, so `Owner.cs` can still + # name a method `cs` of Owner. + legacy = re.search(r'\.(ts|tsx|js|mjs|cjs|py|java)$', s) + if legacy or os.path.splitext(s)[1] in ax_langs.BY_EXT: r = self.q("SELECT id FROM symbols WHERE (file = ? OR file LIKE ?) AND method_id IS NOT NULL AND kind <> 'module'", s.lstrip('/'), f"%/{s.lstrip('/')}") if r: self.why_note('file name', [x['id'] for x in r], f"every method declared in a file named {s}") return f"{s} ({len(r)} methods)", [x['id'] for x in r] - self.why_note('file name', won=f"no indexed file named {s} declares a method") - die(f"no methods in a file named {s}") + if legacy: + self.why_note('file name', won=f"no indexed file named {s} declares a method") + die(f"no methods in a file named {s}") # the name as the agent writes it, normalised: Outer$Inner.m, Outer.Inner#m, m(int,String), pkg.Outer.Inner.m are all # Outer.Inner.m. Java's parser names a nested type without its outer (#667) and the index recovers most of them, so the # same name is tried with leading segments dropped — Outer.Inner.m, then Inner.m — against display AND qualified name, diff --git a/tests/cases/csharp/a-source-file-is-a-path-endpoint/case.json b/tests/cases/csharp/a-source-file-is-a-path-endpoint/case.json new file mode 100644 index 00000000..aeb09220 --- /dev/null +++ b/tests/cases/csharp/a-source-file-is-a-path-endpoint/case.json @@ -0,0 +1,14 @@ +{"lang": "csharp", "src": "src", + "checks": [ + {"why": "a C# source file is an endpoint: every method in it, as for a .java, .py or .ts file", + "run": ["path", "src/Checks.cs", "Adder.Twice"], + "want": ["Checker.Run", "Adder.Add", "Adder.Twice"], + "avoid": ["nothing named"]}, + {"why": "the file may be named by its last segments", + "run": ["path", "Checks.cs", "Adder.Add"], + "want": ["Checker.Run"], + "avoid": ["nothing named"]}, + {"why": "control: a method named like an extension (Owner.cs) is still that method when no file has the name", + "run": ["path", "Owner.cs", "Adder.Twice"], + "want": ["Owner.cs", "Adder.Twice"], + "avoid": ["no methods in a file named"]}]} diff --git a/tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Calc.cs b/tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Calc.cs new file mode 100644 index 00000000..dcc37da0 --- /dev/null +++ b/tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Calc.cs @@ -0,0 +1,8 @@ +namespace Calc +{ + public class Adder + { + public int Add(int a, int b) { return Twice(a) + b; } + int Twice(int a) { return a * 2; } + } +} diff --git a/tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Calc.csproj b/tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Calc.csproj new file mode 100644 index 00000000..d3b2f030 --- /dev/null +++ b/tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Calc.csproj @@ -0,0 +1 @@ +net8.0 diff --git a/tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Checks.cs b/tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Checks.cs new file mode 100644 index 00000000..63c0481c --- /dev/null +++ b/tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Checks.cs @@ -0,0 +1,12 @@ +namespace Calc +{ + public class Checker + { + public void Run() { new Adder().Add(1, 2); } + } + + public class Lone + { + public void Idle() { } + } +} diff --git a/tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Owner.cs b/tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Owner.cs new file mode 100644 index 00000000..3c5d7f26 --- /dev/null +++ b/tests/cases/csharp/a-source-file-is-a-path-endpoint/src/Owner.cs @@ -0,0 +1,8 @@ +namespace Calc +{ + public class Owner + { + // a method whose name reads like a file extension: `Owner.cs` names it, not a file + public void cs() { new Adder().Add(3, 4); } + } +} From ea42c61224de8d2604583dfeb779aea9ff5ce7f8 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:24:03 -0700 Subject: [PATCH 3/7] csharp: a using resource is typed by its initializer `using (var w = new Writer(..)) { w.Write(..); }` and `using var w = new Writer();` left the receiver untyped: var_value_initializer read LOCAL and CONST declarations only, and the IR gives these the declarationKind USING. The resource is the disposable itself, so its initializer is a value of its type exactly as a local's is. Case a-using-resource-is-typed-by-its-initializer: both shapes fail on the base; the control (a foreach variable is typed by its element, never by the collection) passes on both. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- graph/csharp/engine/projections/variables.dl | 4 ++ .../case.json | 11 +++++ .../src/Files.cs | 49 +++++++++++++++++++ .../src/Files.csproj | 1 + 4 files changed, 65 insertions(+) create mode 100644 tests/cases/csharp/a-using-resource-is-typed-by-its-initializer/case.json create mode 100644 tests/cases/csharp/a-using-resource-is-typed-by-its-initializer/src/Files.cs create mode 100644 tests/cases/csharp/a-using-resource-is-typed-by-its-initializer/src/Files.csproj diff --git a/graph/csharp/engine/projections/variables.dl b/graph/csharp/engine/projections/variables.dl index 27be6bf5..841c1f86 100644 --- a/graph/csharp/engine/projections/variables.dl +++ b/graph/csharp/engine/projections/variables.dl @@ -61,6 +61,10 @@ var_initializer("lib", v, e) :- // The initializer IS a value of the variable's type. var_value_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "LOCAL", _, v). var_value_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "CONST", _, v). +// A `using` resource -- `using (var w = new Writer())` and `using var w = ...;` -- is +// the disposable itself, so its initializer is a value of its type exactly as a +// local's is. Without it every call on such a resource has an untyped receiver. +var_value_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "USING", _, v). // The initializer is a COLLECTION and the variable takes its element type. var_iterated_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "FOREACH", _, v). diff --git a/tests/cases/csharp/a-using-resource-is-typed-by-its-initializer/case.json b/tests/cases/csharp/a-using-resource-is-typed-by-its-initializer/case.json new file mode 100644 index 00000000..759af318 --- /dev/null +++ b/tests/cases/csharp/a-using-resource-is-typed-by-its-initializer/case.json @@ -0,0 +1,11 @@ +{"lang": "csharp", "src": "src", + "checks": [ + {"why": "a using statement's resource is typed by its initializer: `using (var w = new Writer())` then w.Write", + "run": ["impact", "Writer.Write"], + "want": ["[resolved] Jobs.Statement"]}, + {"why": "a using declaration likewise: `using var w = new Writer();`", + "run": ["impact", "Writer.Write"], + "want": ["[resolved] Jobs.Declaration"]}, + {"why": "control: a foreach variable is not typed as the collection it iterates", + "run": ["impact", "Shelf.Read"], + "avoid": ["[resolved] Jobs.Loop"]}]} diff --git a/tests/cases/csharp/a-using-resource-is-typed-by-its-initializer/src/Files.cs b/tests/cases/csharp/a-using-resource-is-typed-by-its-initializer/src/Files.cs new file mode 100644 index 00000000..3f0e91e3 --- /dev/null +++ b/tests/cases/csharp/a-using-resource-is-typed-by-its-initializer/src/Files.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Files +{ + public abstract class Sink { } + + public class Writer : Sink, IDisposable + { + public void Write(string s) { } + public void Dispose() { } + } + + public class Book + { + public void Read() { } + } + + public class Shelf : IEnumerable + { + public void Read() { } + public IEnumerator GetEnumerator() { yield break; } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + public class Jobs + { + public void Statement() + { + using (var w = new Writer()) + { + w.Write("a"); + } + } + + public void Declaration() + { + using var w = new Writer(); + w.Write("b"); + } + + public void Loop(Shelf shelf) + { + // control: a foreach variable takes the ELEMENT type, never the collection's + foreach (var b in shelf) b.Read(); + } + } +} diff --git a/tests/cases/csharp/a-using-resource-is-typed-by-its-initializer/src/Files.csproj b/tests/cases/csharp/a-using-resource-is-typed-by-its-initializer/src/Files.csproj new file mode 100644 index 00000000..d3b2f030 --- /dev/null +++ b/tests/cases/csharp/a-using-resource-is-typed-by-its-initializer/src/Files.csproj @@ -0,0 +1 @@ +net8.0 From 59a1ce93cdc7ce99a0018d58265693dce0efa65c Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:24:04 -0700 Subject: [PATCH 4/7] csharp: a constructor runs its base class's parameterless constructor A constructor with no `: base(..)` / `: this(..)` runs `base()` before its body, and a class that declares no constructor gets an implicit one that does the same. Neither is written, so the base constructor had no caller: an abstract base whose constructor every derived class runs read as uncalled. New synthesised call_edges kind implicit_base_ctor (known_edge): - from a declared constructor without an initializer (and not a primary constructor whose heritage passes arguments), FromExpr = the base's heritage type reference, as primary_ctor_base does; - from a `new T()` site where T declares no constructor, beside its known_implicit_ctor row. The target is the base's constructor that takes no argument, walking up through bases that declare none. Registered in the bundle schema and ax_edges' kind vocabulary (`ctor`). Case a-constructor-runs-its-base-constructor: 4 positives fail on the base; the controls (`: base(name)` and `: this(..)` constructors gain no implicit edge) pass on both. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- graph/bundle/SCHEMA.md | 1 + graph/bundle/schema.ts | 1 + .../engine/call-edge-generation/call_chain.dl | 46 +++++++++++++++++++ graph/csharp/souffle/decls_all.dl | 3 ++ .../skills/axiomcode/scripts/ax_edges.py | 2 + .../case.json | 21 +++++++++ .../src/Maps.cs | 41 +++++++++++++++++ .../src/Maps.csproj | 1 + .../src/Use.cs | 9 ++++ 9 files changed, 125 insertions(+) create mode 100644 tests/cases/csharp/a-constructor-runs-its-base-constructor/case.json create mode 100644 tests/cases/csharp/a-constructor-runs-its-base-constructor/src/Maps.cs create mode 100644 tests/cases/csharp/a-constructor-runs-its-base-constructor/src/Maps.csproj create mode 100644 tests/cases/csharp/a-constructor-runs-its-base-constructor/src/Use.cs diff --git a/graph/bundle/SCHEMA.md b/graph/bundle/SCHEMA.md index 2c17b391..75b60d9b 100644 --- a/graph/bundle/SCHEMA.md +++ b/graph/bundle/SCHEMA.md @@ -467,6 +467,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h | `new` | csharp | An object creation. The target is the constructed type's constructor, and it is never dispatched. | | `ctor_delegate` | csharp | `: this(...)` or `: base(...)`. No name is written, so the target is structural. | | `primary_ctor_base` | csharp | SYNTHESISED. A primary constructor's base invocation, written in the heritage clause: `class D(int a) : B(a)`. There is no call syntax anywhere in the body. FromExpr is the heritage type reference. | +| `implicit_base_ctor` | csharp | SYNTHESISED. The base class's parameterless constructor that a constructor with no `: base(...)` / `: this(...)` runs before its body, or that the implicit constructor of a class declaring none runs at its `new`. FromExpr is the heritage type reference, or the `new` site. | | `delegate` | csharp | A call through a delegate value: `handler(x)` or `handler.Invoke(x)`. | | `operator` | csharp | A user-defined operator invoked by operator syntax. | | `conversion` | csharp | A user-defined conversion. An implicit one has no syntax at the call site. | diff --git a/graph/bundle/schema.ts b/graph/bundle/schema.ts index 2e9af322..b687c274 100644 --- a/graph/bundle/schema.ts +++ b/graph/bundle/schema.ts @@ -358,6 +358,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'call_edges', column: 'kind', value: 'new', languages: C, meaning: 'An object creation. The target is the constructed type\'s constructor, and it is never dispatched.' }, { table: 'call_edges', column: 'kind', value: 'ctor_delegate', languages: C, meaning: '`: this(...)` or `: base(...)`. No name is written, so the target is structural.' }, { table: 'call_edges', column: 'kind', value: 'primary_ctor_base', languages: C, meaning: 'SYNTHESISED. A primary constructor\'s base invocation, written in the heritage clause: `class D(int a) : B(a)`. There is no call syntax anywhere in the body. FromExpr is the heritage type reference.' }, + { table: 'call_edges', column: 'kind', value: 'implicit_base_ctor', languages: C, meaning: 'SYNTHESISED. The base class\'s parameterless constructor that a constructor with no `: base(...)` / `: this(...)` runs before its body, or that the implicit constructor of a class declaring none runs at its `new`. FromExpr is the heritage type reference, or the `new` site.' }, { table: 'call_edges', column: 'kind', value: 'delegate', languages: C, meaning: 'A call through a delegate value: `handler(x)` or `handler.Invoke(x)`.' }, { table: 'call_edges', column: 'kind', value: 'operator', languages: C, meaning: 'A user-defined operator invoked by operator syntax.' }, { table: 'call_edges', column: 'kind', value: 'conversion', languages: C, meaning: 'A user-defined conversion. An implicit one has no syntax at the call site.' }, diff --git a/graph/csharp/engine/call-edge-generation/call_chain.dl b/graph/csharp/engine/call-edge-generation/call_chain.dl index 69647979..c0e3de06 100644 --- a/graph/csharp/engine/call-edge-generation/call_chain.dl +++ b/graph/csharp/engine/call-edge-generation/call_chain.dl @@ -286,6 +286,52 @@ call_chain_edge(tr, caller, "-", ctor, "lib", "boundary_lib", "primary_ctor_base method_decl("lib", _, _, _, ctor), type_ctor("client", gk, caller). +// ── SYNTHESISED EDGES: THE IMPLICIT `base()` ──────────────────────────────── +// A constructor that writes neither `: base(..)` nor `: this(..)` runs its base +// class's parameterless constructor before its own body, and a class that declares +// no constructor at all gets one that does the same. Neither is written anywhere, so +// without these edges a base constructor that every derived class runs has no caller. +// +// implicit_base_ctor(TypeGroup, Ctor): the constructor `base()` runs for a type -- +// its base class's constructor that takes no argument, or, where the base declares +// no constructor either, the one the base's own implicit constructor runs. +implicit_base_ctor(gk, ctor) :- + type_extends("client", gk, bgk), + type_ctor("client", bgk, ctor), ctor_accepts_argc("client", ctor, "0"). +implicit_base_ctor(gk, ctor) :- + type_extends("client", gk, bgk), + !type_ctor("client", bgk, _), type_group("client", _, bgk), + implicit_base_ctor(bgk, ctor). + +// A constructor that hands off with `: this(..)` or `: base(..)`, or a primary +// constructor whose heritage passes arguments: the base constructor it runs is the +// one written there, already an edge. +ctor_has_initializer(m) :- call_is_base_ctor("client", e), expr_ultimate_method("client", e, m). +ctor_has_initializer(m) :- call_is_this_ctor("client", e), expr_ultimate_method("client", e, m). +ctor_has_initializer(m) :- + heritage_has_ctor_args("client", t, _), type_group("client", t, gk), type_ctor("client", gk, m). + +// The FromExpr is the base class's entry in the heritage clause, as for a primary +// constructor's base invocation: the closest thing to where the call is written. +implicit_base_site(gk, tr) :- + type_extends("client", gk, bgk), + heritage_resolves("client", gk, pos, bgk), + heritage_of_entity("client", gk, pos, _, _, tr), tr != "". + +// (1) A declared constructor with no initializer. +call_chain_edge(tr, m, "-", ctor, "client", "known_edge", "implicit_base_ctor") :- + type_ctor("client", gk, m), method_decl("client", _, _, _, m), + !ctor_has_initializer(m), + implicit_base_ctor(gk, ctor), + implicit_base_site(gk, tr). + +// (2) `new T()` where T declares no constructor: the implicit one runs T's base's. +// The edge sits on the `new` site itself, beside its known_implicit_ctor row. +call_chain_edge(e, caller, "-", ctor, "client", "known_edge", "implicit_base_ctor") :- + call_ctor_implicit("client", e, gk), + implicit_base_ctor(gk, ctor), + call_from_expr("client", e, caller). + // ── ACCESSOR EDGES: A PROPERTY READ IS A CALL ─────────────────────────────── // `x.Name` invokes get_Name, `a[i]` an indexer accessor, `e += h` an add accessor. // These have no cs_call_site row -- the parser cannot know whether `x.Name` is a diff --git a/graph/csharp/souffle/decls_all.dl b/graph/csharp/souffle/decls_all.dl index 2e454837..778520ef 100644 --- a/graph/csharp/souffle/decls_all.dl +++ b/graph/csharp/souffle/decls_all.dl @@ -265,6 +265,7 @@ .decl ctl_route_c(c0:symbol,c1:symbol,c2:symbol) .decl ctl_route_tok(c0:symbol,c1:symbol,c2:symbol) .decl ctor_accepts_argc(c0:symbol,c1:symbol,c2:symbol) +.decl ctor_has_initializer(c0:symbol) .decl ctor_implicit_type(c0:symbol,c1:symbol) .decl decl_return_param(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl delegate_arg_expr(c0:symbol,c1:symbol) @@ -487,8 +488,10 @@ .decl implements_member(c0:symbol,c1:symbol,c2:symbol) .decl implements_params_conflict(c0:symbol,c1:symbol) .decl implements_shape(c0:symbol,c1:symbol,c2:symbol) +.decl implicit_base_ctor(c0:symbol,c1:symbol) .decl implicit_base_group(c0:symbol,c1:symbol) .decl implicit_base_name(c0:symbol) +.decl implicit_base_site(c0:symbol,c1:symbol) .decl implicit_new_untargeted(c0:symbol,c1:symbol,c2:symbol) .decl indexer_access(c0:symbol,c1:symbol,c2:symbol) .decl indexer_getter(c0:symbol,c1:symbol,c2:symbol) diff --git a/plugins/axiomcode/skills/axiomcode/scripts/ax_edges.py b/plugins/axiomcode/skills/axiomcode/scripts/ax_edges.py index f5283b76..4e1a9965 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/ax_edges.py +++ b/plugins/axiomcode/skills/axiomcode/scripts/ax_edges.py @@ -103,6 +103,8 @@ 'new': 'new', 'CONSTRUCTOR_CALL': 'new', 'anon_new': 'new', 'METACLASS_CREATION': 'new', # one constructor to another 'ctor_delegate': 'ctor', 'SUPER_CALL': 'super', + # C#: a base constructor no syntax names: the implicit `base()` a constructor without an initializer runs + 'implicit_base_ctor': 'ctor', # the callable is named, not called at that line — it runs when whoever took it runs it 'ref': 'method-ref', # a declaration handed to a decorator, which is what wires most framework handlers up diff --git a/tests/cases/csharp/a-constructor-runs-its-base-constructor/case.json b/tests/cases/csharp/a-constructor-runs-its-base-constructor/case.json new file mode 100644 index 00000000..0b0cde40 --- /dev/null +++ b/tests/cases/csharp/a-constructor-runs-its-base-constructor/case.json @@ -0,0 +1,21 @@ +{"lang": "csharp", "src": "src", + "checks": [ + {"why": "a constructor with no initializer runs its base class's parameterless constructor", + "run": ["impact", "src/Maps.cs:7", "--depth", "1"], + "want": ["[resolved] Map. src/Maps.cs:15"]}, + {"why": "through a generic base: PersonMap() runs Map()", + "run": ["impact", "src/Maps.cs:15", "--depth", "1"], + "want": ["[resolved] PersonMap. src/Maps.cs:21"]}, + {"why": "a class that declares no constructor runs its base's at every `new`", + "run": ["impact", "src/Maps.cs:15", "--depth", "1"], + "want": ["[resolved] Use.Plain"]}, + {"why": "control: `: base(name)` runs the named constructor, so the parameterless one gains no resolved caller from it", + "run": ["impact", "src/Maps.cs:7", "--depth", "1"], + "avoid": ["[resolved] NamedMap."]}, + {"why": "control: a constructor that delegates with `: this(..)` runs no base constructor itself (the one it delegates to does)", + "run": ["impact", "src/Maps.cs:7", "--depth", "1"], + "want": ["[resolved] SelfMap. src/Maps.cs:39"], + "avoid": ["[resolved] SelfMap. src/Maps.cs:38"]}, + {"why": "the explicit base call keeps its own edge", + "run": ["impact", "src/Maps.cs:8"], + "want": ["[resolved] NamedMap."]}]} diff --git a/tests/cases/csharp/a-constructor-runs-its-base-constructor/src/Maps.cs b/tests/cases/csharp/a-constructor-runs-its-base-constructor/src/Maps.cs new file mode 100644 index 00000000..18c78d86 --- /dev/null +++ b/tests/cases/csharp/a-constructor-runs-its-base-constructor/src/Maps.cs @@ -0,0 +1,41 @@ +using System; + +namespace Maps +{ + public abstract class Map + { + protected Map() { Register(); } + protected Map(string name) { Register(); } + void Register() { } + } + + public abstract class Map : Map + { + // no initializer: runs Map() before its body + protected Map() { } + } + + public class PersonMap : Map + { + // no initializer, generic base: runs Map() and through it Map() + public PersonMap() { Console.WriteLine("person"); } + } + + public class PlainMap : Map + { + // no constructor at all: its implicit one runs Map() + } + + public class NamedMap : Map + { + // control: an explicit `: base(name)` runs that constructor, not the parameterless one + public NamedMap(string name) : base(name) { } + } + + public class SelfMap : Map + { + // control: `: this(..)` delegates; this constructor itself runs no base constructor + public SelfMap() : this(1) { } + public SelfMap(int x) { } + } +} diff --git a/tests/cases/csharp/a-constructor-runs-its-base-constructor/src/Maps.csproj b/tests/cases/csharp/a-constructor-runs-its-base-constructor/src/Maps.csproj new file mode 100644 index 00000000..d3b2f030 --- /dev/null +++ b/tests/cases/csharp/a-constructor-runs-its-base-constructor/src/Maps.csproj @@ -0,0 +1 @@ +net8.0 diff --git a/tests/cases/csharp/a-constructor-runs-its-base-constructor/src/Use.cs b/tests/cases/csharp/a-constructor-runs-its-base-constructor/src/Use.cs new file mode 100644 index 00000000..fc56a879 --- /dev/null +++ b/tests/cases/csharp/a-constructor-runs-its-base-constructor/src/Use.cs @@ -0,0 +1,9 @@ +namespace Maps +{ + public static class Use + { + public static void Person() { var m = new PersonMap(); } + public static void Plain() { var m = new PlainMap(); } + public static void Named() { var m = new NamedMap("n"); } + } +} From 0966ce7b07338e6599850d2f6cc3b2a3edbdc7d7 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:24:04 -0700 Subject: [PATCH 5/7] csharp: an unstaged receiver keeps its type name for extension matching `var services = new ServiceCollection(); services.AddWidgets();` and `Configure(s => s.AddWidgets())` with `Configure(Action c)` never reached the client's `AddWidgets(this IServiceCollection s)`: both receivers' types are unstaged, so they have no group, and call_recv_type_name -- the NAME the by-name rung (3) and the loose DI rung (7, #1472) compare -- had no clause for either. call_recv_type_name now also gives - an implicitly typed local initialised with `new T(..)`: the T written there; - an implicitly typed lambda parameter: the name of the delegate argument that types it (lambda_param_arg_ref, the same reference param_type reads). So the lambda's call matches by name (resolved) and the local's is a rung-7 candidate (one of a set, keeping its external label). Case an-unstaged-receiver-keeps-its-type-name: both positives fail on the base; controls (an extension on an in-source `this` type, a lambda typed as another unstaged type) pass on both; extension-on-unstaged-receiver unchanged. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- graph/csharp/engine/resolution/extensions.dl | 19 +++++++++++++ .../case.json | 14 ++++++++++ .../src/App.csproj | 10 +++++++ .../src/Program.cs | 27 +++++++++++++++++++ .../src/Widgets.cs | 16 +++++++++++ 5 files changed, 86 insertions(+) create mode 100644 tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/case.json create mode 100644 tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/src/App.csproj create mode 100644 tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/src/Program.cs create mode 100644 tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/src/Widgets.cs diff --git a/graph/csharp/engine/resolution/extensions.dl b/graph/csharp/engine/resolution/extensions.dl index 23516414..5a09820c 100644 --- a/graph/csharp/engine/resolution/extensions.dl +++ b/graph/csharp/engine/resolution/extensions.dl @@ -118,6 +118,25 @@ call_recv_type_name(prov, e, tn) :- call_recv_type_name(prov, e, tn) :- call_receiver_expr(prov, e, r), ref_denotes(prov, r, "LAMBDA_PARAMETER", p), param_type_name(prov, p, tn, _). +// AN IMPLICITLY TYPED LAMBDA PARAMETER takes its type from the delegate it is +// converted to (lambda-parameters.dl): `Configure(s => s.AddRules())` with +// `Configure(Action c)` makes `s` an IServiceCollection. Where +// that type is in source, param_type already says so; where it is unstaged, the +// NAME is all there is, and without it an extension written for +// `this IServiceCollection` cannot match the lambda that calls it. +call_recv_type_name(prov, e, tn) :- + call_receiver_expr(prov, e, r), + ref_denotes(prov, r, "LAMBDA_PARAMETER", p), + lambda_param_arg_ref(prov, p, ar), type_ref_name(prov, ar, tn), tn != "". +// AN IMPLICITLY TYPED LOCAL made by `new T(..)` holds a T, resolved or not: +// `var services = new ServiceCollection(); services.AddRules();`. The name is the +// one written at the creation (external-types.dl labels the boundary from the same +// fact); without it the extension rungs have nothing to compare. +call_recv_type_name(prov, e, tn) :- + call_receiver_expr(prov, e, r), + ref_denotes(prov, r, "LOCAL_VARIABLE", v), var_is_implicit(prov, v), + var_value_initializer(prov, v, init), expr_kind(prov, init, "OBJECT_CREATION"), + call_callee_name(prov, init, tn), tn != "". // A receiver that is itself a call: the callee's declared return type name. The ROOT // reference's name where there is one (`List`, not `List`), which is how every // other receiver is named; the written text only where no reference was recorded. diff --git a/tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/case.json b/tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/case.json new file mode 100644 index 00000000..e9a105ee --- /dev/null +++ b/tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/case.json @@ -0,0 +1,14 @@ +{"lang": "csharp", "src": "src", + "checks": [ + {"why": "a local initialised with `new ServiceCollection()` keeps that name, so a this-IServiceCollection extension is a candidate on it, one of a set (#1472's rung)", + "run": ["impact", "WidgetExtensions.AddWidgetRules"], + "want": ["[one of a set] Program.FromNew"]}, + {"why": "a lambda parameter typed by Action is an IServiceCollection: the extension written for it matches by name", + "run": ["impact", "WidgetExtensions.AddWidgetRules"], + "want": ["[resolved] Program. src/Program.cs:16"]}, + {"why": "control: an unstaged receiver never matches an extension on an in-source type", + "run": ["impact", "WidgetExtensions.AddGadgets"], + "avoid": ["Program.Control"]}, + {"why": "control: a lambda typed Action is not resolved to the IServiceCollection extension", + "run": ["impact", "WidgetExtensions.AddWidgetRules"], + "avoid": ["[resolved] Program. src/Program.cs:26"]}]} diff --git a/tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/src/App.csproj b/tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/src/App.csproj new file mode 100644 index 00000000..6240b341 --- /dev/null +++ b/tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/src/App.csproj @@ -0,0 +1,10 @@ + + + net8.0 + Exe + enable + + + + + diff --git a/tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/src/Program.cs b/tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/src/Program.cs new file mode 100644 index 00000000..581198a3 --- /dev/null +++ b/tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/src/Program.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using App.Widgets; + +public static class Program +{ + public static void Configure(Action configure) { } + public static void Tune(Action tune) { } + + public static void FromNew() + { + var services = new ServiceCollection(); + services.AddWidgetRules(); + } + + public static void FromLambda() => Configure(s => s.AddWidgetRules()); + + // control: an unstaged receiver never matches an extension whose `this` type is in source + public static void Control() + { + var g = new Gizmo(); + g.AddGadgets(); + } + + // control: a lambda typed as another unstaged type is no by-name match for this IServiceCollection + public static void OtherLambda() => Tune(z => z.AddWidgetRules()); +} diff --git a/tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/src/Widgets.cs b/tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/src/Widgets.cs new file mode 100644 index 00000000..83b04b43 --- /dev/null +++ b/tests/cases/csharp/an-unstaged-receiver-keeps-its-type-name/src/Widgets.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +namespace App.Widgets; + +public interface IRule { bool Check(); } +public class SizeRule : IRule { public bool Check() => true; } +public class Gadget { } + +public static class WidgetExtensions +{ + public static IServiceCollection AddWidgetRules(this IServiceCollection s) + { + s.AddSingleton(); + return s; + } + public static Gadget AddGadgets(this Gadget g) => g; +} From 21917afd7c6d602203beaf2ac0976b8c595116d5 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:24:04 -0700 Subject: [PATCH 6/7] csharp: a static member read through its type name types the chain `var f = Context.Current.Factory; f.Create()` -- the getter of the static property `Current` was an edge, but its result had no type: expr_type for a MEMBER_ACCESS needed an expr_type on the qualifier, and a qualifier that names a TYPE has none. So `.Factory` and every call after it were unresolved. Two clauses mirror the instance ones with ref_names_type on the qualifier (property and field). ref_names_type already refuses a name that also binds a value, the language's member-over-type rule. Case a-static-member-read-types-the-chain: four shapes (local, one chain, a var of the static read, a static field) fail on the base; the "Color Color" control -- a property named like the type is the value read -- passes on both. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../engine/expression-resolution/expr-type.dl | 19 ++++++ .../case.json | 66 +++++++++++++++++++ .../src/Ctx.cs | 17 +++++ .../src/Ctx.csproj | 1 + .../src/Use.cs | 17 +++++ 5 files changed, 120 insertions(+) create mode 100644 tests/cases/csharp/a-static-member-read-types-the-chain/case.json create mode 100644 tests/cases/csharp/a-static-member-read-types-the-chain/src/Ctx.cs create mode 100644 tests/cases/csharp/a-static-member-read-types-the-chain/src/Ctx.csproj create mode 100644 tests/cases/csharp/a-static-member-read-types-the-chain/src/Use.cs diff --git a/graph/csharp/engine/expression-resolution/expr-type.dl b/graph/csharp/engine/expression-resolution/expr-type.dl index 5c962379..fe5e35e7 100644 --- a/graph/csharp/engine/expression-resolution/expr-type.dl +++ b/graph/csharp/engine/expression-resolution/expr-type.dl @@ -459,6 +459,25 @@ expr_type(prov, e, gk) :- expr_type(prov, q, qgk), member_lookup(prov, qgk, n, "field", f), field_type(prov, f, gk). +// A STATIC member read through its TYPE NAME: `Context.Current.Factory.Create()`. +// The qualifier `Context` names a type, not a value, so it has no expr_type and the +// two clauses above cannot start from it: the getter of `Current` is an edge +// (properties.dl reads the type name), and these give its RESULT a type, so that +// `.Factory` and every call after it resolve. ref_names_type already refuses a name +// that also binds a value, which is the language's member-over-type rule. +expr_type(prov, e, gk) :- + expr_kind(prov, e, "MEMBER_ACCESS"), + expr_qualifier_child(prov, e, q), expr_member_name_child(prov, e, nameExpr), + expr_written_name(prov, nameExpr, n), + ref_names_type(prov, q, qgk), + member_lookup(prov, qgk, n, "property", p), property_type(prov, p, gk). +expr_type(prov, e, gk) :- + expr_kind(prov, e, "MEMBER_ACCESS"), + expr_qualifier_child(prov, e, q), expr_member_name_child(prov, e, nameExpr), + expr_written_name(prov, nameExpr, n), + ref_names_type(prov, q, qgk), + member_lookup(prov, qgk, n, "field", f), field_type(prov, f, gk). + // ── A GENERIC ARGUMENT, SUBSTITUTED FOR THE PARAMETER IT FILLS ────────────── // `IWrap w; w.Value.Flag` -- `Value` is declared `T`, and without binding // T to Settings the access resolves and the type of the RESULT is the type parameter, diff --git a/tests/cases/csharp/a-static-member-read-types-the-chain/case.json b/tests/cases/csharp/a-static-member-read-types-the-chain/case.json new file mode 100644 index 00000000..4170ca10 --- /dev/null +++ b/tests/cases/csharp/a-static-member-read-types-the-chain/case.json @@ -0,0 +1,66 @@ +{ + "lang": "csharp", + "src": "src", + "checks": [ + { + "why": "a static property read through its type name has the property's type: `var f = Ctx.Current.F; f.Create()`", + "run": [ + "impact", + "Factory.Create" + ], + "want": [ + "[resolved] Use.Local" + ] + }, + { + "why": "the same chain written in one expression", + "run": [ + "impact", + "Factory.Create" + ], + "want": [ + "[resolved] Use.Chain" + ] + }, + { + "why": "a local initialised from the static read takes its type", + "run": [ + "impact", + "Factory.Create" + ], + "want": [ + "[resolved] Use.ViaVar" + ] + }, + { + "why": "a static field read through its type name likewise", + "run": [ + "impact", + "Factory.Create" + ], + "want": [ + "[resolved] Use.Field" + ] + }, + { + "why": "control: a property named like the type is the value read, so its own type decides", + "run": [ + "impact", + "Factory.Create" + ], + "avoid": [ + "Holder.Other" + ] + }, + { + "why": "and that property's type does reach its own factory", + "run": [ + "impact", + "OtherFactory.Create" + ], + "want": [ + "[resolved] Holder.Other" + ] + } + ] +} \ No newline at end of file diff --git a/tests/cases/csharp/a-static-member-read-types-the-chain/src/Ctx.cs b/tests/cases/csharp/a-static-member-read-types-the-chain/src/Ctx.cs new file mode 100644 index 00000000..b1c00ba0 --- /dev/null +++ b/tests/cases/csharp/a-static-member-read-types-the-chain/src/Ctx.cs @@ -0,0 +1,17 @@ +namespace N +{ + public interface IFactory { object Create(); } + public class Factory : IFactory { public object Create() { return null; } } + public interface IOtherFactory { object Create(); } + public class OtherFactory : IOtherFactory { public object Create() { return null; } } + + public interface ICtx { IFactory F { get; } } + public interface IOther { IOtherFactory F { get; } } + + public class Ctx : ICtx + { + public static ICtx Current { get; set; } + public static ICtx Shared; + public IFactory F { get; } + } +} diff --git a/tests/cases/csharp/a-static-member-read-types-the-chain/src/Ctx.csproj b/tests/cases/csharp/a-static-member-read-types-the-chain/src/Ctx.csproj new file mode 100644 index 00000000..d3b2f030 --- /dev/null +++ b/tests/cases/csharp/a-static-member-read-types-the-chain/src/Ctx.csproj @@ -0,0 +1 @@ +net8.0 diff --git a/tests/cases/csharp/a-static-member-read-types-the-chain/src/Use.cs b/tests/cases/csharp/a-static-member-read-types-the-chain/src/Use.cs new file mode 100644 index 00000000..570d5ae9 --- /dev/null +++ b/tests/cases/csharp/a-static-member-read-types-the-chain/src/Use.cs @@ -0,0 +1,17 @@ +namespace N +{ + public static class Use + { + public static object Local() { var f = Ctx.Current.F; return f.Create(); } + public static object Chain() { return Ctx.Current.F.Create(); } + public static object ViaVar() { var c = Ctx.Current; return c.F.Create(); } + public static object Field() { return Ctx.Shared.F.Create(); } + } + + public static class Holder + { + // control: inside Holder, `Ctx` is this property (the member wins over the type of the same name) + public static IOther Ctx { get; } + public static object Other() { return Ctx.F.Create(); } + } +} From c0dab0b4471ea4a98764347af15be6b2093fe7fa Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:24:05 -0700 Subject: [PATCH 7/7] csharp: a constructed generic type is a static receiver `Cache.Get("k")`, `Registry.Create()` and `Pair.Make().Use()` were unresolved: the parser emitted a generic_name in expression position with typeArgumentCount set and NO potentialQualifiedName, so the receiver had no name to resolve. - parser: a generic_name row carries its identifier as the written name (its arity stays in typeArgumentCount). - engine: expr_type_argc projects the count; ref_names_type and the call receiver's type_ref_demand resolve the name at that arity, so `Cache` names Cache`1 and never a non-generic Cache. Every receiver written without type arguments has arity "0", which is what both clauses used before. Case a-generic-type-name-is-a-static-receiver: three positives fail on the base; the non-generic `Cache.Get` control resolves to its own type on both. Parser C# suite 62/62. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../csharp/engine/projections/expressions.dl | 9 ++++++++ .../csharp/engine/resolution/local-binding.dl | 7 +++--- .../engine/resolution/type-resolution.dl | 4 ++-- graph/csharp/souffle/decls_all.dl | 1 + .../extractors/cs-expression-extractor.ts | 7 ++++++ .../case.json | 18 +++++++++++++++ .../src/Cache.cs | 22 +++++++++++++++++++ .../src/Cache.csproj | 1 + .../src/Use.cs | 10 +++++++++ 9 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 tests/cases/csharp/a-generic-type-name-is-a-static-receiver/case.json create mode 100644 tests/cases/csharp/a-generic-type-name-is-a-static-receiver/src/Cache.cs create mode 100644 tests/cases/csharp/a-generic-type-name-is-a-static-receiver/src/Cache.csproj create mode 100644 tests/cases/csharp/a-generic-type-name-is-a-static-receiver/src/Use.cs diff --git a/graph/csharp/engine/projections/expressions.dl b/graph/csharp/engine/projections/expressions.dl index 46e28c76..8a82e1ba 100644 --- a/graph/csharp/engine/projections/expressions.dl +++ b/graph/csharp/engine/projections/expressions.dl @@ -117,6 +117,15 @@ expr_written_name("lib", e, n) :- lib_cs_expression(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, n, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e), n != "". +// ── expr_type_argc(Prov, ExprHash, TypeArgumentCount) ────────────────────── +// The type arguments written on a name: `Cache` is the name `Cache` at arity 1. +// A generic name used as a receiver or qualifier is resolved at this arity, since +// `Cache` and `Cache` are two types. +expr_type_argc("client", e, tac) :- + cs_expression(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, tac, _, _, _, _, _, _, _, _, _, _, _, _, e). +expr_type_argc("lib", e, tac) :- + lib_cs_expression(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, tac, _, _, _, _, _, _, _, _, _, _, _, _, e). + // ── expr_literal(Prov, ExprHash, LiteralKind, LiteralValue) ───────────────── expr_literal("client", e, lk, lv) :- cs_expression(_, _, _, _, _, _, _, _, _, _, lk, lv, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e), diff --git a/graph/csharp/engine/resolution/local-binding.dl b/graph/csharp/engine/resolution/local-binding.dl index f2ec9440..e7bc391f 100644 --- a/graph/csharp/engine/resolution/local-binding.dl +++ b/graph/csharp/engine/resolution/local-binding.dl @@ -220,7 +220,8 @@ ref_unbound(prov, e, n) :- !ref_names_type(prov, e, _). // A reference that names a TYPE rather than a value: the receiver of a static call. -// Resolved through type_name_resolves in the file's scope. +// Resolved through type_name_resolves in the file's scope, at the arity written on +// it: `Cache.Get()` names Cache`1, not a non-generic Cache. // // NOT WHERE THE NAME ALSO BINDS TO A VALUE. C# allows a member and a type to share a // name (the language's own "Color Color" rule) and prefers the MEMBER in an @@ -235,10 +236,10 @@ ref_binds_value(prov, e) :- ref_denotes(prov, e, _, _). ref_binds_value(prov, e) :- local_binds(prov, e, _). ref_names_type(prov, e, gk) :- - expr_written_name(prov, e, n), + expr_written_name(prov, e, n), expr_type_argc(prov, e, ar), expr_ultimate_module(prov, e, mod), !ref_binds_value(prov, e), - type_name_resolves(prov, mod, n, "0", gk). + type_name_resolves(prov, mod, n, ar, gk). // ── IMPLICIT `this` ───────────────────────────────────────────────────────── // An unqualified instance member reference has an implicit `this` receiver whose diff --git a/graph/csharp/engine/resolution/type-resolution.dl b/graph/csharp/engine/resolution/type-resolution.dl index 1c76e1cb..f4d1b803 100644 --- a/graph/csharp/engine/resolution/type-resolution.dl +++ b/graph/csharp/engine/resolution/type-resolution.dl @@ -231,10 +231,10 @@ type_ref_demand(prov, mod, n, ar) :- // a type name and produced `external:s.Describe` -- an external target named after a // local variable, which is a wrong answer rather than a missing one. The !ref_denotes // term is what makes the guard "unbound by anyone". -type_ref_demand(prov, mod, n, "0") :- +type_ref_demand(prov, mod, n, ar) :- call_receiver_expr(prov, e, r), expr_ref_unknown(prov, r), - expr_written_name(prov, r, n), + expr_written_name(prov, r, n), expr_type_argc(prov, r, ar), !ref_denotes(prov, r, _, _), call_module(prov, e, mod). diff --git a/graph/csharp/souffle/decls_all.dl b/graph/csharp/souffle/decls_all.dl index 778520ef..89a6ff36 100644 --- a/graph/csharp/souffle/decls_all.dl +++ b/graph/csharp/souffle/decls_all.dl @@ -360,6 +360,7 @@ .decl expr_target_any(c0:symbol,c1:symbol,c2:symbol) .decl expr_target_type(c0:symbol,c1:symbol,c2:symbol) .decl expr_type(c0:symbol,c1:symbol,c2:symbol) +.decl expr_type_argc(c0:symbol,c1:symbol,c2:symbol) .decl expr_type_owner(c0:symbol,c1:symbol,c2:symbol) .decl expr_type_via_ref(c0:symbol,c1:symbol,c2:symbol) .decl expr_ultimate_method(c0:symbol,c1:symbol,c2:symbol) diff --git a/parser/src/parsers/csharp/extractors/cs-expression-extractor.ts b/parser/src/parsers/csharp/extractors/cs-expression-extractor.ts index d0fd6d4d..b7abc63c 100644 --- a/parser/src/parsers/csharp/extractors/cs-expression-extractor.ts +++ b/parser/src/parsers/csharp/extractors/cs-expression-extractor.ts @@ -2142,6 +2142,13 @@ function describeExpression( const argumentList = childOfType(node, 'type_argument_list'); shape.typeArgumentCount = argumentList === undefined ? 0 : namedChildren(argumentList).length; + // The name without its arguments, as an `identifier` row carries it; the + // arity is typeArgumentCount. Without it the receiver of `Cache.Get()` + // has no name, and nothing can say which type it names. + const name = childOfType(node, 'identifier'); + if (name !== undefined) { + shape.potentialQualifiedName = normalizeCSharpIdentifier(name.text); + } break; } diff --git a/tests/cases/csharp/a-generic-type-name-is-a-static-receiver/case.json b/tests/cases/csharp/a-generic-type-name-is-a-static-receiver/case.json new file mode 100644 index 00000000..acc2d4e0 --- /dev/null +++ b/tests/cases/csharp/a-generic-type-name-is-a-static-receiver/case.json @@ -0,0 +1,18 @@ +{"lang": "csharp", "src": "src", + "checks": [ + {"why": "a static call through a constructed generic type, `Cache.Get()`, reaches the generic type's member", + "run": ["impact", "src/Cache.cs:5"], + "want": ["[resolved] Use.Typed"]}, + {"why": "control: the non-generic type of the same name is not reached from the generic receiver", + "run": ["impact", "src/Cache.cs:12"], + "want": ["[resolved] Use.Plain"], + "avoid": ["[resolved] Use.Typed"]}, + {"why": "and the generic member is not reached from the non-generic receiver", + "run": ["impact", "src/Cache.cs:5"], + "avoid": ["[resolved] Use.Plain"]}, + {"why": "a call on the result of a static call through a generic type", + "run": ["impact", "Pair.Use"], + "want": ["[resolved] Use.Chained"]}, + {"why": "a static field read through a generic type name types the chain", + "run": ["impact", "Store.Touch"], + "want": ["[resolved] Use.Member"]}]} diff --git a/tests/cases/csharp/a-generic-type-name-is-a-static-receiver/src/Cache.cs b/tests/cases/csharp/a-generic-type-name-is-a-static-receiver/src/Cache.cs new file mode 100644 index 00000000..5bf51e29 --- /dev/null +++ b/tests/cases/csharp/a-generic-type-name-is-a-static-receiver/src/Cache.cs @@ -0,0 +1,22 @@ +namespace N +{ + public static class Cache + { + public static T Get(string key) { return default(T); } + public static Store Shared = new Store(); + } + + public static class Cache + { + // a non-generic type of the same name: `Cache.Get` is this one + public static object Get(string key) { return null; } + } + + public class Store { public void Touch() { } } + + public class Pair + { + public static Pair Make() { return new Pair(); } + public void Use() { } + } +} diff --git a/tests/cases/csharp/a-generic-type-name-is-a-static-receiver/src/Cache.csproj b/tests/cases/csharp/a-generic-type-name-is-a-static-receiver/src/Cache.csproj new file mode 100644 index 00000000..d3b2f030 --- /dev/null +++ b/tests/cases/csharp/a-generic-type-name-is-a-static-receiver/src/Cache.csproj @@ -0,0 +1 @@ +net8.0 diff --git a/tests/cases/csharp/a-generic-type-name-is-a-static-receiver/src/Use.cs b/tests/cases/csharp/a-generic-type-name-is-a-static-receiver/src/Use.cs new file mode 100644 index 00000000..a0229463 --- /dev/null +++ b/tests/cases/csharp/a-generic-type-name-is-a-static-receiver/src/Use.cs @@ -0,0 +1,10 @@ +namespace N +{ + public static class Use + { + public static int Typed() { return Cache.Get("x"); } + public static void Chained() { Pair.Make().Use(); } + public static void Member() { Cache.Shared.Touch(); } + public static object Plain() { return Cache.Get("y"); } + } +}