diff --git a/graph/csharp/engine/expression-resolution/expr-type.dl b/graph/csharp/engine/expression-resolution/expr-type.dl index 72c20b462..2258c0e0b 100644 --- a/graph/csharp/engine/expression-resolution/expr-type.dl +++ b/graph/csharp/engine/expression-resolution/expr-type.dl @@ -281,7 +281,8 @@ expr_new_type(prov, e, gk) :- !expr_new_type_ref(prov, e, _), call_callee_name(prov, e, n), expr_ultimate_module(prov, e, mod), call_type_argc(prov, e, ar), - type_name_resolves(prov, mod, n, ar, gk). + type_name_resolves(prov, mod, n, ar, gk), + !expr_callee_name_dropped(prov, e, gk). // AN INVOCATION has its callee's return type. expr_type(prov, e, gk) :- diff --git a/graph/csharp/engine/resolution/local-binding.dl b/graph/csharp/engine/resolution/local-binding.dl index e7bc391fa..2c24e832a 100644 --- a/graph/csharp/engine/resolution/local-binding.dl +++ b/graph/csharp/engine/resolution/local-binding.dl @@ -239,7 +239,8 @@ ref_names_type(prov, e, gk) :- 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, ar, gk). + type_name_resolves(prov, mod, n, ar, gk), + !expr_name_dropped(prov, e, gk). // ── IMPLICIT `this` ───────────────────────────────────────────────────────── // An unqualified instance member reference has an implicit `this` receiver whose diff --git a/graph/csharp/engine/resolution/type-hierarchy.dl b/graph/csharp/engine/resolution/type-hierarchy.dl index 7c285be12..b329d00dd 100644 --- a/graph/csharp/engine/resolution/type-hierarchy.dl +++ b/graph/csharp/engine/resolution/type-hierarchy.dl @@ -73,6 +73,7 @@ heritage_resolves(prov, gk, pos, bgk) :- heritage_of_entity(prov, gk, pos, bn, ba, ""), type_group(prov, t, gk), type_module(prov, t, mod), type_name_resolves(prov, mod, bn, ba, bgk), + !heritage_name_dropped(prov, t, bn, bgk), bgk != gk. // ── THE SPLIT ─────────────────────────────────────────────────────────────── diff --git a/graph/csharp/engine/resolution/type-resolution.dl b/graph/csharp/engine/resolution/type-resolution.dl index f4d1b8031..75492a1d5 100644 --- a/graph/csharp/engine/resolution/type-resolution.dl +++ b/graph/csharp/engine/resolution/type-resolution.dl @@ -109,6 +109,107 @@ 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). +// ── ...FROM THE SITE'S NAMESPACE, NOT THE FILE'S ──────────────────────────── +// The candidate above is per FILE: module_ns_chain holds every namespace the file +// declares a type in. A file declaring P.Models, P.Other.Models and a class in P +// therefore offered `Models.Report` written in P both P.Models.Report and +// P.Other.Models.Report -- and P.Other is a child of P, not in scope there. +// +// C# reads the first segment from the namespaces enclosing the SITE, innermost +// outward, and the innermost level that has a match wins: it does not merge +// levels, and a name found at a level hides the same name read in full from the +// global namespace. So per (site namespace, written name), the candidates are +// ranked by the length of the prefix they were found under and only the longest +// survives; a full match is the outermost level and survives only when no +// enclosing namespace matched. +// +// PRUNE-ONLY. The file-level answer is kept as it is and a site drops from it only +// what its own namespace rules out; the site's own answer is always one of the +// file-level ones, so nothing correct is lost. A site whose enclosing type is not +// known drops nothing. +type_name_rel_at(prov, n, ar, gk, p) :- + type_qn_tail(prov, qn, p, n), + type_decl(prov, _, qn, ar, _, t), type_group(prov, t, gk). +type_name_rel_src(prov, n, ar, gk) :- type_name_rel_at(prov, n, ar, gk, _). + +// The namespaces a name written in `ns` is read from, with the length of each. +ns_read_level(prov, ns, ns, l) :- ns_exists(prov, ns), l = strlen(ns). +ns_read_level(prov, ns, p, l) :- ns_prefix(prov, ns, p), l = strlen(p). + +// A dotted name some site in this file actually writes. type_name_resolves answers +// every tail of every type in every file whose chain holds its prefix, written or +// not, so the scoping below is keyed on what is written. Keyed by FILE as well, so +// the probe into type_name_resolves binds (module, name) and reuses the index its +// consumers already need rather than building one on the name alone. +type_name_written_dotted(prov, mod, n) :- + type_ref_name(prov, tr, n), contains(".", n), type_ref_module(prov, tr, mod). +type_name_written_dotted(prov, mod, n) :- + expr_written_name(prov, e, n), contains(".", n), expr_ultimate_module(prov, e, mod). +type_name_written_dotted(prov, mod, n) :- + call_callee_name(prov, e, n), contains(".", n), call_module(prov, e, mod). +type_name_written_dotted(prov, mod, n) :- + heritage_slot(prov, t, _, n, _, ""), contains(".", n), type_module(prov, t, mod). + +type_name_scope_demand(prov, mod, ns, n, ar) :- + type_name_written_dotted(prov, mod, n), + type_name_resolves(prov, mod, n, ar, _), + module_namespace_own(prov, mod, ns). +type_name_scope_level(prov, ns, n, ar, gk, l) :- + type_name_scope_demand(prov, _, ns, n, ar), + ns_read_level(prov, ns, p, l), + type_name_rel_at(prov, n, ar, gk, p). +type_name_scope_best(prov, ns, n, ar, b) :- + type_name_scope_level(prov, ns, n, ar, _, _), + b = max l : { type_name_scope_level(prov, ns, n, ar, _, l) }. +type_name_scope_keep(prov, ns, n, ar, gk) :- + type_name_scope_level(prov, ns, n, ar, gk, b), + type_name_scope_best(prov, ns, n, ar, b). + +// type_name_out_of_scope(Prov, Namespace, Name, Arity, GroupKey) -- a file-level +// answer that a site in `ns` does not see: a relative candidate not at the +// innermost matching level (a child or sibling namespace, or a shallower level), +// or a full match hidden by a relative one. +type_name_out_of_scope(prov, ns, n, ar, gk) :- + type_name_scope_demand(prov, mod, ns, n, ar), + type_name_resolves(prov, mod, n, ar, gk), + type_name_rel_src(prov, n, ar, gk), + !type_name_scope_keep(prov, ns, n, ar, gk). +type_name_out_of_scope(prov, ns, n, ar, gk) :- + type_name_scope_demand(prov, mod, ns, n, ar), + type_name_resolves(prov, mod, n, ar, gk), + !type_name_rel_src(prov, n, ar, gk), + type_name_scope_best(prov, ns, n, ar, _). + +// The site side, one per consumer below: the written name, the site's enclosing +// type, and that type's namespace. Joined from the (small) out-of-scope set, so only +// sites that write a pruned name are visited. +type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "TYPE"), t = o. +type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "METHOD"), method_owner(prov, t, o). +type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "FIELD"), field_decl(prov, _, t, o). +type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "PROPERTY"), property_decl(prov, _, t, o). +type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "EVENT"), event_decl(prov, _, t, o). +type_ref_scope_type(prov, tr, t) :- + type_ref_owner(prov, tr, o, "METHOD_PARAMETER"), param_of(prov, o, m), method_owner(prov, t, m). +type_ref_scope_type(prov, tr, t) :- + type_ref_owner(prov, tr, o, "VARIABLE"), var_method(prov, o, m), method_owner(prov, t, m). +type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "EXPRESSION"), expr_ultimate_type(prov, o, t). + +type_ref_name_dropped(prov, tr, gk) :- + type_name_out_of_scope(prov, ns, n, ar, gk), + type_ref_name(prov, tr, n), type_ref_argc(prov, tr, ar), + type_ref_scope_type(prov, tr, t), type_namespace(prov, t, ns). +expr_name_dropped(prov, e, gk) :- + type_name_out_of_scope(prov, ns, n, ar, gk), + expr_written_name(prov, e, n), expr_type_argc(prov, e, ar), + expr_ultimate_type(prov, e, t), type_namespace(prov, t, ns). +expr_callee_name_dropped(prov, e, gk) :- + type_name_out_of_scope(prov, ns, n, ar, gk), + call_callee_name(prov, e, n), call_type_argc(prov, e, ar), + expr_ultimate_type(prov, e, t), type_namespace(prov, t, ns). +heritage_name_dropped(prov, t, n, gk) :- + type_name_out_of_scope(prov, ns, n, ar, gk), + heritage_slot(prov, t, _, n, ar, ""), type_namespace(prov, t, ns). + // ── 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 @@ -266,7 +367,8 @@ type_ref_resolves(prov, tr, gk) :- !type_ref_type_param(prov, tr, _), type_ref_argc(prov, tr, ar), type_ref_module(prov, tr, mod), - type_name_resolves(prov, mod, n, ar, gk). + type_name_resolves(prov, mod, n, ar, gk), + !type_ref_name_dropped(prov, tr, gk). // The module a type reference is written in, by owner kind. Every owner kind that // can carry a reference is listed; one that is missing makes every reference in that diff --git a/graph/csharp/souffle/decls_all.dl b/graph/csharp/souffle/decls_all.dl index 882cc9b7d..182506dd0 100644 --- a/graph/csharp/souffle/decls_all.dl +++ b/graph/csharp/souffle/decls_all.dl @@ -337,6 +337,7 @@ .decl expr_argument(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl expr_arity(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl expr_call_candidate(c0:symbol,c1:symbol,c2:symbol) +.decl expr_callee_name_dropped(c0:symbol,c1:symbol,c2:symbol) .decl expr_cast_ref(c0:symbol,c1:symbol,c2:symbol) .decl expr_child(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl expr_dynamic_typed(c0:symbol,c1:symbol) @@ -360,6 +361,7 @@ .decl expr_method_name_child(c0:symbol,c1:symbol,c2:symbol) .decl expr_method_ref(c0:symbol,c1:symbol,c2:symbol) .decl expr_module(c0:symbol,c1:symbol,c2:symbol) +.decl expr_name_dropped(c0:symbol,c1:symbol,c2:symbol) .decl expr_new_type(c0:symbol,c1:symbol,c2:symbol) .decl expr_new_type_ref(c0:symbol,c1:symbol,c2:symbol) .decl expr_node(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) @@ -481,6 +483,7 @@ .decl grpc_static_base(c0:symbol,c1:symbol,c2:symbol) .decl grpc_written_base(c0:symbol,c1:symbol) .decl heritage_has_ctor_args(c0:symbol,c1:symbol,c2:symbol) +.decl heritage_name_dropped(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl heritage_of_entity(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol) .decl heritage_resolves(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl heritage_slot(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol) @@ -745,6 +748,7 @@ .decl ns_encloses(c0:symbol,c1:symbol,c2:symbol) .decl ns_exists(c0:symbol,c1:symbol) .decl ns_prefix(c0:symbol,c1:symbol,c2:symbol) +.decl ns_read_level(c0:symbol,c1:symbol,c2:symbol,c3:number) .decl override_cand(c0:symbol,c1:symbol,c2:symbol,c3:number) .decl override_depth(c0:symbol,c1:symbol,c2:symbol,c3:number) .decl override_pair(c0:symbol,c1:symbol) @@ -980,10 +984,18 @@ .decl type_name_best_rank(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:number) .decl type_name_cand(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:number) .decl type_name_count(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:number) +.decl type_name_out_of_scope(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl type_name_predefined_unstaged(c0:symbol,c1:symbol,c2:symbol) +.decl type_name_rel_at(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl type_name_rel_src(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_name_resolves(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl type_name_scope_best(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:number) +.decl type_name_scope_demand(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl type_name_scope_keep(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl type_name_scope_level(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:number) .decl type_name_unresolved(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_name_visible(c0:symbol,c1:symbol,c2:symbol) +.decl type_name_written_dotted(c0:symbol,c1:symbol,c2:symbol) .decl type_namespace(c0:symbol,c1:symbol,c2:symbol) .decl type_nest_ancestor(c0:symbol,c1:symbol,c2:symbol) .decl type_ns(c0:symbol,c1:symbol,c2:symbol) @@ -1009,11 +1021,13 @@ .decl type_ref_is_tuple(c0:symbol,c1:symbol,c2:symbol) .decl type_ref_module(c0:symbol,c1:symbol,c2:symbol) .decl type_ref_name(c0:symbol,c1:symbol,c2:symbol) +.decl type_ref_name_dropped(c0:symbol,c1:symbol,c2:symbol) .decl type_ref_nullable(c0:symbol,c1:symbol) .decl type_ref_owner(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_ref_parent(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl type_ref_resolves(c0:symbol,c1:symbol,c2:symbol) .decl type_ref_root_position(c0:symbol,c1:symbol,c2:symbol) +.decl type_ref_scope_type(c0:symbol,c1:symbol,c2:symbol) .decl type_ref_type_param(c0:symbol,c1:symbol,c2:symbol) .decl type_satisfies(c0:symbol,c1:symbol,c2:symbol) .decl type_scope_key(c0:symbol,c1:symbol,c2:symbol) diff --git a/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/case.json b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/case.json new file mode 100644 index 000000000..9e5afadbd --- /dev/null +++ b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/case.json @@ -0,0 +1,22 @@ +{"lang": "csharp", "src": "src", + "checks": [ + {"why": "inside namespace P, Models.Report is P.Models.Report", + "run": ["impact", "P.Models.Report.Make"], + "want": ["[resolved] Calls.Partially"]}, + {"why": "control: a file that declares only namespace P keeps its one correct target", + "run": ["impact", "P.Models.Report.Make"], + "want": ["[resolved] Alone.Only"]}, + {"why": "a child namespace of the site's is not in scope: P.Other.Models.Report is not named from P, even when the same file declares P.Other", + "run": ["impact", "P.Other.Models.Report.Make"], + "avoid": ["[resolved] Calls.Partially", "[one of a set] Calls.Partially", "Alone.Only"]}, + {"why": "inside P.Other, Models is P.Other.Models: the innermost namespace with a match wins, for a nested type too", + "run": ["impact", "P.Other.Models.Report.Make"], + "want": ["[resolved] Deeper.Nearest", "[resolved] Deeper.Inner.Nested"]}, + {"why": "...and the outer P.Models.Report is not also named from P.Other", + "run": ["impact", "P.Models.Report.Make"], + "avoid": ["[resolved] Deeper.Nearest", "[one of a set] Deeper.Nearest", "[resolved] Deeper.Inner.Nested", "[one of a set] Deeper.Inner.Nested"]}, + {"why": "control: an unrelated namespace declared in the same file is never read", + "run": ["impact", "Z.Models.Report.Make"], + "avoid": ["[resolved] Calls.Partially", "[one of a set] Calls.Partially", "Alone.Only", + "[resolved] Deeper.Nearest", "[one of a set] Deeper.Nearest", + "[resolved] Deeper.Inner.Nested", "[one of a set] Deeper.Inner.Nested"]}]} diff --git a/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Mixed.cs b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Mixed.cs new file mode 100644 index 000000000..0b4cf7534 --- /dev/null +++ b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Mixed.cs @@ -0,0 +1,28 @@ +// one file declaring several namespaces: the namespaces a name is read from are the SITE's, not the file's +namespace P.Models { public sealed class Report { public static Report Make() { return new Report(); } } } +namespace P.Other.Models { public sealed class Report { public static Report Make() { return new Report(); } } } +namespace Z.Models { public sealed class Report { public static Report Make() { return new Report(); } } } + +namespace P +{ + public sealed class Calls + { + // inside P, Models is P.Models; P.Other is a child of P and not in scope here + public Models.Report Partially() => Models.Report.Make(); + } +} + +namespace P.Other +{ + public sealed class Deeper + { + // inside P.Other, Models is P.Other.Models: the innermost namespace with a match wins + public Models.Report Nearest() => Models.Report.Make(); + + // a nested type reads from the same namespace as the type around it + public sealed class Inner + { + public Models.Report Nested() => Models.Report.Make(); + } + } +} diff --git a/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Report.csproj b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Report.csproj new file mode 100644 index 000000000..d3b2f0306 --- /dev/null +++ b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Report.csproj @@ -0,0 +1 @@ +net8.0 diff --git a/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Single.cs b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Single.cs new file mode 100644 index 000000000..55e77d379 --- /dev/null +++ b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Single.cs @@ -0,0 +1,8 @@ +// control: a file that declares only namespace P keeps its one correct target +namespace P +{ + public sealed class Alone + { + public Models.Report Only() => Models.Report.Make(); + } +}