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/engine/containment/type-nesting.dl b/graph/csharp/engine/containment/type-nesting.dl index e584db41..8ac8b94d 100644 --- a/graph/csharp/engine/containment/type-nesting.dl +++ b/graph/csharp/engine/containment/type-nesting.dl @@ -149,3 +149,27 @@ module_in_scope(prov, mod, outer) :- // The GLOBAL namespace ("") is always in scope. module_in_scope(prov, mod, "") :- module_decl(prov, _, _, _, mod). + +// module_ns_chain(Prov, ModuleHash, Namespace) -- the namespaces a name written in this +// file is read from: the file's own namespaces and every dotted prefix of them, as +// text. A prefix that holds no type of its own (`Acme` over `Acme.Shop`) still +// qualifies a name written below it, so this is not ns_encloses, which needs one. +module_ns_chain(prov, mod, ns) :- module_namespace_own(prov, mod, ns), ns != "". +module_ns_chain(prov, mod, p) :- module_namespace_own(prov, mod, inner), ns_prefix(prov, inner, p). + +// ── A USING WRITTEN INSIDE A NAMESPACE ───────────────────────────────────── +// `namespace Shop.Tests; using Results;` imports Shop.Results. C# reads the name in +// a using directive from the namespace the directive sits in, innermost outward, +// exactly as it reads any other name there. Taken as written it names a namespace +// that does not exist, and every type the file names through it reads as a library +// type. +// +// The IR does not record whether a using sits inside the namespace or above it. +// The language decides it instead: a using whose name is no namespace at all only +// compiles inside a namespace that has it as a child. So the relative reading is +// taken only when the name as written names nothing, and only for a child that +// exists -- a file whose namespaces have no such child gains nothing. +module_in_scope(prov, mod, full) :- + using_namespace(prov, mod, n), !ns_exists(prov, n), + module_ns_chain(prov, mod, e), full = cat(e, cat(".", n)), + ns_exists(prov, full). diff --git a/graph/csharp/engine/expression-resolution/expr-type.dl b/graph/csharp/engine/expression-resolution/expr-type.dl index 5c962379..fe5e35e7 100644 --- a/graph/csharp/engine/expression-resolution/expr-type.dl +++ b/graph/csharp/engine/expression-resolution/expr-type.dl @@ -459,6 +459,25 @@ expr_type(prov, e, gk) :- expr_type(prov, q, qgk), member_lookup(prov, qgk, n, "field", f), field_type(prov, f, gk). +// A STATIC member read through its TYPE NAME: `Context.Current.Factory.Create()`. +// The qualifier `Context` names a type, not a value, so it has no expr_type and the +// two clauses above cannot start from it: the getter of `Current` is an edge +// (properties.dl reads the type name), and these give its RESULT a type, so that +// `.Factory` and every call after it resolve. ref_names_type already refuses a name +// that also binds a value, which is the language's member-over-type rule. +expr_type(prov, e, gk) :- + expr_kind(prov, e, "MEMBER_ACCESS"), + expr_qualifier_child(prov, e, q), expr_member_name_child(prov, e, nameExpr), + expr_written_name(prov, nameExpr, n), + ref_names_type(prov, q, qgk), + member_lookup(prov, qgk, n, "property", p), property_type(prov, p, gk). +expr_type(prov, e, gk) :- + expr_kind(prov, e, "MEMBER_ACCESS"), + expr_qualifier_child(prov, e, q), expr_member_name_child(prov, e, nameExpr), + expr_written_name(prov, nameExpr, n), + ref_names_type(prov, q, qgk), + member_lookup(prov, qgk, n, "field", f), field_type(prov, f, gk). + // ── A GENERIC ARGUMENT, SUBSTITUTED FOR THE PARAMETER IT FILLS ────────────── // `IWrap w; w.Value.Flag` -- `Value` is declared `T`, and without binding // T to Settings the access resolves and the type of the RESULT is the type parameter, diff --git a/graph/csharp/engine/projections/expressions.dl b/graph/csharp/engine/projections/expressions.dl index 46e28c76..8a82e1ba 100644 --- a/graph/csharp/engine/projections/expressions.dl +++ b/graph/csharp/engine/projections/expressions.dl @@ -117,6 +117,15 @@ expr_written_name("lib", e, n) :- lib_cs_expression(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, n, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e), n != "". +// ── expr_type_argc(Prov, ExprHash, TypeArgumentCount) ────────────────────── +// The type arguments written on a name: `Cache` is the name `Cache` at arity 1. +// A generic name used as a receiver or qualifier is resolved at this arity, since +// `Cache` and `Cache` are two types. +expr_type_argc("client", e, tac) :- + cs_expression(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, tac, _, _, _, _, _, _, _, _, _, _, _, _, e). +expr_type_argc("lib", e, tac) :- + lib_cs_expression(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, tac, _, _, _, _, _, _, _, _, _, _, _, _, e). + // ── expr_literal(Prov, ExprHash, LiteralKind, LiteralValue) ───────────────── expr_literal("client", e, lk, lv) :- cs_expression(_, _, _, _, _, _, _, _, _, _, lk, lv, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e), diff --git a/graph/csharp/engine/projections/variables.dl b/graph/csharp/engine/projections/variables.dl index 27be6bf5..841c1f86 100644 --- a/graph/csharp/engine/projections/variables.dl +++ b/graph/csharp/engine/projections/variables.dl @@ -61,6 +61,10 @@ var_initializer("lib", v, e) :- // The initializer IS a value of the variable's type. var_value_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "LOCAL", _, v). var_value_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "CONST", _, v). +// A `using` resource -- `using (var w = new Writer())` and `using var w = ...;` -- is +// the disposable itself, so its initializer is a value of its type exactly as a +// local's is. Without it every call on such a resource has an untyped receiver. +var_value_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "USING", _, v). // The initializer is a COLLECTION and the variable takes its element type. var_iterated_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "FOREACH", _, v). diff --git a/graph/csharp/engine/resolution/extensions.dl b/graph/csharp/engine/resolution/extensions.dl index 23516414..5a09820c 100644 --- a/graph/csharp/engine/resolution/extensions.dl +++ b/graph/csharp/engine/resolution/extensions.dl @@ -118,6 +118,25 @@ call_recv_type_name(prov, e, tn) :- call_recv_type_name(prov, e, tn) :- call_receiver_expr(prov, e, r), ref_denotes(prov, r, "LAMBDA_PARAMETER", p), param_type_name(prov, p, tn, _). +// AN IMPLICITLY TYPED LAMBDA PARAMETER takes its type from the delegate it is +// converted to (lambda-parameters.dl): `Configure(s => s.AddRules())` with +// `Configure(Action c)` makes `s` an IServiceCollection. Where +// that type is in source, param_type already says so; where it is unstaged, the +// NAME is all there is, and without it an extension written for +// `this IServiceCollection` cannot match the lambda that calls it. +call_recv_type_name(prov, e, tn) :- + call_receiver_expr(prov, e, r), + ref_denotes(prov, r, "LAMBDA_PARAMETER", p), + lambda_param_arg_ref(prov, p, ar), type_ref_name(prov, ar, tn), tn != "". +// AN IMPLICITLY TYPED LOCAL made by `new T(..)` holds a T, resolved or not: +// `var services = new ServiceCollection(); services.AddRules();`. The name is the +// one written at the creation (external-types.dl labels the boundary from the same +// fact); without it the extension rungs have nothing to compare. +call_recv_type_name(prov, e, tn) :- + call_receiver_expr(prov, e, r), + ref_denotes(prov, r, "LOCAL_VARIABLE", v), var_is_implicit(prov, v), + var_value_initializer(prov, v, init), expr_kind(prov, init, "OBJECT_CREATION"), + call_callee_name(prov, init, tn), tn != "". // A receiver that is itself a call: the callee's declared return type name. The ROOT // reference's name where there is one (`List`, not `List`), which is how every // other receiver is named; the written text only where no reference was recorded. diff --git a/graph/csharp/engine/resolution/local-binding.dl b/graph/csharp/engine/resolution/local-binding.dl index f2ec9440..e7bc391f 100644 --- a/graph/csharp/engine/resolution/local-binding.dl +++ b/graph/csharp/engine/resolution/local-binding.dl @@ -220,7 +220,8 @@ ref_unbound(prov, e, n) :- !ref_names_type(prov, e, _). // A reference that names a TYPE rather than a value: the receiver of a static call. -// Resolved through type_name_resolves in the file's scope. +// Resolved through type_name_resolves in the file's scope, at the arity written on +// it: `Cache.Get()` names Cache`1, not a non-generic Cache. // // NOT WHERE THE NAME ALSO BINDS TO A VALUE. C# allows a member and a type to share a // name (the language's own "Color Color" rule) and prefers the MEMBER in an @@ -235,10 +236,10 @@ ref_binds_value(prov, e) :- ref_denotes(prov, e, _, _). ref_binds_value(prov, e) :- local_binds(prov, e, _). ref_names_type(prov, e, gk) :- - expr_written_name(prov, e, n), + expr_written_name(prov, e, n), expr_type_argc(prov, e, ar), expr_ultimate_module(prov, e, mod), !ref_binds_value(prov, e), - type_name_resolves(prov, mod, n, "0", gk). + type_name_resolves(prov, mod, n, ar, gk). // ── IMPLICIT `this` ───────────────────────────────────────────────────────── // An unqualified instance member reference has an implicit `this` receiver whose diff --git a/graph/csharp/engine/resolution/type-resolution.dl b/graph/csharp/engine/resolution/type-resolution.dl index fb90ddc4..f4d1b803 100644 --- a/graph/csharp/engine/resolution/type-resolution.dl +++ b/graph/csharp/engine/resolution/type-resolution.dl @@ -89,6 +89,26 @@ type_name_cand(prov, mod, qn, ar, gk, 1) :- type_decl(prov, _, qn, ar, _, t), type_group(prov, t, gk), !type_is_top_level(prov, gk). +// ── A QUALIFIED NAME READ FROM ITS NAMESPACE ──────────────────────────────── +// `Reflection.Info.Create(..)` written inside namespace Shop names +// Shop.Reflection.Info: the first segment of a qualified name is a +// simple name, looked up in the enclosing namespaces like any other. Matched only +// in full, such a receiver stays untyped and every call through it unresolved. +// +// Keyed from the DECLARATION side, so nothing is enumerated per written name: each +// type's qualified name is split at each dot into the namespace before it and the +// dotted tail after it, and the tail is a candidate in every file whose namespace +// chain holds that prefix. A tail with no dot is a simple name, which ranks 2-5 +// already answer. +type_qn_tail(prov, qn, p, s) :- + type_decl(prov, _, qn, _, _, _), + i = range(1, strlen(qn)), substr(qn, i, 1) = ".", + p = substr(qn, 0, i), s = substr(qn, i + 1, strlen(qn) - i - 1), + contains(".", s). +type_name_cand(prov, mod, s, ar, gk, 1) :- + type_qn_tail(prov, qn, p, s), module_ns_chain(prov, mod, p), + type_decl(prov, _, qn, ar, _, t), type_group(prov, t, gk). + // ── A CLIENT FILE NAMING A STAGED LIBRARY TYPE ────────────────────────────── // Every clause above is SINGLE-PROVENANCE: it joins a module and a declaration // under one `prov`, so a client module and a library namespace are never brought @@ -211,10 +231,10 @@ type_ref_demand(prov, mod, n, ar) :- // a type name and produced `external:s.Describe` -- an external target named after a // local variable, which is a wrong answer rather than a missing one. The !ref_denotes // term is what makes the guard "unbound by anyone". -type_ref_demand(prov, mod, n, "0") :- +type_ref_demand(prov, mod, n, ar) :- call_receiver_expr(prov, e, r), expr_ref_unknown(prov, r), - expr_written_name(prov, r, n), + expr_written_name(prov, r, n), expr_type_argc(prov, r, ar), !ref_denotes(prov, r, _, _), call_module(prov, e, mod). diff --git a/graph/csharp/souffle/decls_all.dl b/graph/csharp/souffle/decls_all.dl index b1faa46b..89a6ff36 100644 --- a/graph/csharp/souffle/decls_all.dl +++ b/graph/csharp/souffle/decls_all.dl @@ -65,9 +65,9 @@ .decl call_callee_name(c0:symbol,c1:symbol,c2:symbol) .decl call_caller_unknown(c0:symbol,c1:symbol) .decl call_chain_edge(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol,c6:symbol) -.decl call_class_kind(c0:symbol) .decl call_chain_summary(c0:symbol,c1:number) .decl call_class(c0:symbol,c1:symbol,c2:symbol) +.decl call_class_kind(c0:symbol) .decl call_context(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl call_ctor_implicit(c0:symbol,c1:symbol,c2:symbol) .decl call_ctor_target(c0:symbol,c1:symbol,c2:symbol) @@ -265,6 +265,7 @@ .decl ctl_route_c(c0:symbol,c1:symbol,c2:symbol) .decl ctl_route_tok(c0:symbol,c1:symbol,c2:symbol) .decl ctor_accepts_argc(c0:symbol,c1:symbol,c2:symbol) +.decl ctor_has_initializer(c0:symbol) .decl ctor_implicit_type(c0:symbol,c1:symbol) .decl decl_return_param(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl delegate_arg_expr(c0:symbol,c1:symbol) @@ -359,6 +360,7 @@ .decl expr_target_any(c0:symbol,c1:symbol,c2:symbol) .decl expr_target_type(c0:symbol,c1:symbol,c2:symbol) .decl expr_type(c0:symbol,c1:symbol,c2:symbol) +.decl expr_type_argc(c0:symbol,c1:symbol,c2:symbol) .decl expr_type_owner(c0:symbol,c1:symbol,c2:symbol) .decl expr_type_via_ref(c0:symbol,c1:symbol,c2:symbol) .decl expr_ultimate_method(c0:symbol,c1:symbol,c2:symbol) @@ -487,8 +489,10 @@ .decl implements_member(c0:symbol,c1:symbol,c2:symbol) .decl implements_params_conflict(c0:symbol,c1:symbol) .decl implements_shape(c0:symbol,c1:symbol,c2:symbol) +.decl implicit_base_ctor(c0:symbol,c1:symbol) .decl implicit_base_group(c0:symbol,c1:symbol) .decl implicit_base_name(c0:symbol) +.decl implicit_base_site(c0:symbol,c1:symbol) .decl implicit_new_untargeted(c0:symbol,c1:symbol,c2:symbol) .decl indexer_access(c0:symbol,c1:symbol,c2:symbol) .decl indexer_getter(c0:symbol,c1:symbol,c2:symbol) @@ -663,6 +667,7 @@ .decl module_lang(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl module_namespace_own(c0:symbol,c1:symbol,c2:symbol) .decl module_namespace_style(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl module_ns_chain(c0:symbol,c1:symbol,c2:symbol) .decl module_parse_errors(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl module_regime(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl module_target(c0:symbol,c1:symbol,c2:symbol,c3:symbol) @@ -948,6 +953,7 @@ .decl type_parent(c0:symbol,c1:symbol,c2:symbol) .decl type_placement(c0:symbol,c1:symbol,c2:symbol) .decl type_primary_ctor(c0:symbol,c1:symbol,c2:symbol) +.decl type_qn_tail(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_ref(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol,c6:symbol) .decl type_ref_arg(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_ref_argc(c0:symbol,c1:symbol,c2:symbol) diff --git a/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/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/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-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"); } + } +} 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"); } + } +} 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 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); } + } +} 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(); } + } +} 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 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; +}