use super::*;
#[test]
fn stage5_receiver_rule_an_external_receiver_refuses_a_candidate_not_assignable_to_it() {
let files = fragments_for(&[
(
"Logging/DbUpLogAdapter.cs",
"namespace App.Logging { public class DbUpLogAdapter : IUpgradeLog { public void LogInformation(string m) { } } }",
),
(
"Consumers/Runner.cs",
"\nnamespace App.Consumers;\n\npublic class Runner\n{\n private ILogger _logger;\n public void Run() => _logger.LogInformation(\"x\");\n}\n",
),
]);
let g = resolve_graph(&no_git_root(), &files);
assert!(
heuristic_member_edges_from(&g, "Consumers/Runner.cs").is_empty(),
"DbUpLogAdapter implements IUpgradeLog and nothing in its closure names ILogger -- the receiver's own type disproves the guess"
);
assert_eq!(g.stats.heuristic_edge_count, 0);
}
#[test]
fn stage5_receiver_rule_a_candidate_whose_base_closure_names_the_receiver_type_still_emits() {
let files = fragments_for(&[
(
"Logging/FileLogger.cs",
"namespace App.Logging { public class FileLogger : ILogger { public void LogInformation(string m) { } } }",
),
(
"Logging/Base.cs",
"namespace App.Logging { public class Base : ILogger { } }",
),
(
"Logging/Derived.cs",
"namespace App.Logging { public class Derived : Base { public void LogInformation(string m) { } } }",
),
(
"Consumers/Runner.cs",
"\nnamespace App.Consumers;\n\npublic class Runner\n{\n private ILogger _logger;\n public void Run() => _logger.LogInformation(\"x\");\n}\n",
),
]);
let g = resolve_graph(&no_git_root(), &files);
assert_eq!(
heuristic_member_edges_from(&g, "Consumers/Runner.cs"),
vec![("App.Logging.Derived", 7), ("App.Logging.FileLogger", 7)],
"FileLogger names ILogger directly; Derived reaches it one in-graph hop up, through Base"
);
}
#[test]
fn stage5_receiver_rule_generic_arguments_must_unify_on_the_matched_base() {
let files = fragments_for(&[
(
"Logging/Adapter.cs",
"namespace App.Logging { public class Adapter : ILogger { public void LogInformation(string m) { } } }",
),
(
"Logging/Typed.cs",
"namespace App.Logging { public class Typed : ILogger<Worker> { public void LogInformation(string m) { } } }",
),
(
"Logging/Open.cs",
"namespace App.Logging { public class Open<T> : ILogger<T> { public void LogInformation(string m) { } } }",
),
(
"Consumers/Runner.cs",
"\nnamespace App.Consumers;\n\npublic class Runner\n{\n private ILogger<Worker> _logger;\n public void Run() => _logger.LogInformation(\"x\");\n}\n",
),
]);
let g = resolve_graph(&no_git_root(), &files);
assert_eq!(
heuristic_member_edges_from(&g, "Consumers/Runner.cs"),
vec![("App.Logging.Open", 7), ("App.Logging.Typed", 7)],
"the base NAME matching is not enough: the non-generic `: ILogger` never binds an ILogger<Worker> receiver, while a closed and an open implementation both do"
);
}
#[test]
fn stage5_receiver_rule_a_call_hop_receiver_with_unknown_args_compares_by_name_only() {
let files = fragments_for(&[
(
"Logging/LoggerFactory.cs",
"namespace App.Logging { public class LoggerFactory { public static ILogger Make() { return null; } } }",
),
(
"Logging/Typed.cs",
"namespace App.Logging { public class Typed : ILogger<Worker> { public void LogInformation(string m) { } } }",
),
(
"Consumers/Runner.cs",
"\nusing App.Logging;\n\nnamespace App.Consumers;\n\npublic class Runner\n{\n public void Run()\n {\n var l = LoggerFactory.Make();\n l.LogInformation(\"x\");\n }\n}\n",
),
]);
let g = resolve_graph(&no_git_root(), &files);
assert_eq!(
heuristic_member_edges_from(&g, "Consumers/Runner.cs"),
vec![("App.Logging.Typed", 11)],
"a method's recorded RETURN type carries a name and no type arguments, so the rule compares names only rather than refusing every generic implementation"
);
}
#[test]
fn stage5_receiver_rule_readmits_an_extension_of_the_receiver_type_declined_on_namespace() {
let files = fragments_for(&[
(
"Ext/LogExt.cs",
"namespace App.Ext { public static class LogExt { public static void LogInformation(this IOtherLogger l, string m) { } } }",
),
(
"Registration/WidgetServiceExtensions.cs",
"namespace App.Registration { public static class WidgetServiceExtensions { public static void AddWidgets(this IServiceCollection s) { } } }",
),
(
"Consumers/Startup.cs",
"\nnamespace App.Consumers;\n\npublic class Startup\n{\n public void Run(ILogger logger, IServiceCollection services)\n {\n logger.LogInformation(\"x\");\n services.AddWidgets();\n }\n}\n",
),
]);
let g = resolve_graph(&no_git_root(), &files);
assert_eq!(
heuristic_member_edges_from(&g, "Consumers/Startup.cs"),
vec![("App.Registration.WidgetServiceExtensions", 9)],
"LogExt extends IOtherLogger, not ILogger, so no this-type of its own answers the receiver and nothing else connects it -- while AddWidgets extends the receiver type exactly and only tier (f)'s namespace test (App.Registration is not imported here) kept it out"
);
}
#[test]
fn stage5_receiver_rule_an_extension_tier_f_declined_on_arity_is_not_readmitted_as_a_guess() {
let files = fragments_for(&[
(
"Ext/LogExt.cs",
"namespace App.Ext { public static class LogExt { public static void Trace(this IThing t, string a, string b) { } } }",
),
(
"Consumers/Runner.cs",
"using App.Ext;\n\nnamespace App.Consumers;\n\npublic class Runner\n{\n public void Run(IThing thing)\n {\n thing.Trace();\n thing.Trace(\"a\", \"b\");\n }\n}\n",
),
]);
let g = resolve_graph(&no_git_root(), &files);
let edges: Vec<(&str, usize, Option<HeuristicTier>)> = g
.edges
.iter()
.filter_map(|e| match e {
Edge::UsesMember {
from_file,
from_line,
to,
tier,
..
} if from_file == "Consumers/Runner.cs" => Some((to.as_str(), *from_line, *tier)),
_ => None,
})
.collect();
assert_eq!(
edges,
vec![("App.Ext.LogExt", 10, Some(HeuristicTier::Ext))],
"line 9 has no binding and no edge; line 10 binds through tier (f) as before"
);
}
#[test]
fn stage5_receiver_rule_a_generic_receiver_binds_the_generic_sibling_not_the_first_indexed() {
let files = fragments_for(&[
(
"Contexts/Context.cs",
"namespace App.Contexts { public class Context { public void Publish() { } } }",
),
(
"Contexts/ContextOfT.cs",
"namespace App.Contexts { public class Context<T> : Context { public T Message { get; } } }",
),
(
"Consumers/Handler.cs",
"\nusing App.Contexts;\n\nnamespace App.Consumers;\n\npublic class Handler\n{\n public void Handle(Context<Order> ctx)\n {\n var m = ctx.Message;\n ctx.Publish();\n }\n}\n",
),
]);
let g = resolve_graph(&no_git_root(), &files);
let edges = member_edges_with_file(&g, "Consumers/Handler.cs");
assert_eq!(
edges,
vec![
("App.Contexts.Context", 10, "Contexts/ContextOfT.cs", None),
("App.Contexts.Context", 11, "Contexts/Context.cs", None),
],
"ctx.Message binds to the generic sibling (the only one declaring Message); \
ctx.Publish binds through the generic sibling's own base to the non-generic \
sibling -- neither answer is the first-indexed def by accident"
);
}
#[test]
fn stage5_receiver_rule_a_non_generic_receiver_binds_the_non_generic_sibling_when_the_generic_was_indexed_first(
) {
let files = fragments_for(&[
(
"Contexts/ContextOfT.cs",
"namespace App.Contexts { public interface Context<T> : Context { T Message { get; } } }",
),
(
"Contexts/Context.cs",
"namespace App.Contexts { public interface Context { void Publish(); } }",
),
(
"Consumers/Handler.cs",
"\nusing App.Contexts;\n\nnamespace App.Consumers;\n\npublic class Handler\n{\n public void Handle(Context ctx)\n {\n ctx.Publish();\n var m = ctx.Message;\n }\n}\n",
),
]);
let g = resolve_graph(&no_git_root(), &files);
let edges = member_edges_with_file(&g, "Consumers/Handler.cs");
assert_eq!(
edges
.iter()
.filter(|(_, _, _, tier)| tier.is_none())
.cloned()
.collect::<Vec<_>>(),
vec![("App.Contexts.Context", 10, "Contexts/Context.cs", None)],
"a bare Context receiver binds Publish to the non-generic sibling, and line 11 \
(ctx.Message) has no precise edge -- Message is declared only on the generic \
sibling, which a bare, arity-0 receiver is not assignable to; a guess-tier edge \
there, if the graph produces one, is not asserted against here"
);
}
#[test]
fn stage5_receiver_rule_a_base_written_without_arguments_walks_to_the_non_generic_sibling() {
let files = fragments_for(&[
(
"Contexts/ContextOfT.cs",
"namespace App.Contexts { public class Context<T> : Context { public T Message { get; } } }",
),
(
"Contexts/Context.cs",
"namespace App.Contexts { public class Context { public void Publish() { } } }",
),
(
"Consumers/Handler.cs",
"\nusing App.Contexts;\n\nnamespace App.Consumers;\n\npublic class Handler\n{\n public void Handle(Context<Order> ctx)\n {\n ctx.Publish();\n }\n}\n",
),
]);
let g = resolve_graph(&no_git_root(), &files);
let edges = member_edges_with_file(&g, "Consumers/Handler.cs");
assert_eq!(
edges,
vec![("App.Contexts.Context", 10, "Contexts/Context.cs", None)],
"the bare base name Context, carrying no argument list, walks to the \
non-generic sibling regardless of index order"
);
}
#[test]
fn stage5_receiver_rule_a_receiver_arity_with_no_in_graph_def_keeps_the_arity_blind_answer() {
let files = fragments_for(&[
(
"Data/Repository.cs",
"namespace App.Data { public class Repository { public void Save() { } } }",
),
(
"Consumers/Handler.cs",
"\nusing App.Data;\n\nnamespace App.Consumers;\n\npublic class Handler\n{\n public void Handle(Repository<Order> repo)\n {\n repo.Save();\n }\n}\n",
),
]);
let g = resolve_graph(&no_git_root(), &files);
assert_eq!(
member_edges_with_file(&g, "Consumers/Handler.cs"),
vec![("App.Data.Repository", 10, "Data/Repository.cs", None)],
"no def of arity 1 exists, so the arity-blind answer stands"
);
}
#[test]
fn stage5_receiver_rule_an_extension_stops_binding_when_the_generic_sibling_declares_the_member() {
let files = fragments_for(&[
(
"Contexts/Context.cs",
"namespace App.Contexts { public interface Context { void Publish(); } }",
),
(
"Contexts/ContextOfT.cs",
"namespace App.Contexts { public interface Context<T> : Context { void Respond(string s); } }",
),
(
"Ext/ContextExt.cs",
"namespace App.Ext { public static class ContextExt { public static void Respond<T>(this App.Contexts.Context<T> c, string s) { } } }",
),
(
"Consumers/Handler.cs",
"\nusing App.Contexts;\nusing App.Ext;\n\nnamespace App.Consumers;\n\npublic class Handler\n{\n public void Handle(Context<Order> ctx)\n {\n ctx.Respond(\"x\");\n }\n}\n",
),
]);
let g = resolve_graph(&no_git_root(), &files);
let edges = member_edges_with_file(&g, "Consumers/Handler.cs");
assert_eq!(
edges,
vec![("App.Contexts.Context", 11, "Contexts/ContextOfT.cs", None)],
"the generic sibling declares Respond itself, so the receiver binds precisely \
to it rather than falling through to the extension"
);
assert!(
!g.edges.iter().any(|e| matches!(
e,
Edge::UsesMember { from_file, tier, .. }
if from_file == "Consumers/Handler.cs" && *tier == Some(HeuristicTier::Ext)
)),
"no extension-tier edge from the consumer file -- tier (e) already claimed the call"
);
}
#[test]
fn stage5_receiver_rule_an_in_graph_receiver_still_resolves_precisely() {
let files = fragments_for(&[
(
"Widgets/Widget.cs",
"namespace App.Widgets { public class Widget { public void Render() { } } }",
),
(
"Consumers/UsesWidget.cs",
"\nusing App.Widgets;\n\nnamespace App.Consumers;\n\npublic class UsesWidget\n{\n private Widget _widget;\n public void Run() => _widget.Render();\n}\n",
),
]);
let g = resolve_graph(&no_git_root(), &files);
assert_eq!(
member_edges_from(&g, "Consumers/UsesWidget.cs"),
vec![("App.Widgets.Widget", 9)],
"an in-graph receiver never reaches the scored tier at all -- tier (e) answers it precisely"
);
assert_eq!(g.stats.heuristic_edge_count, 0);
}
#[test]
fn stage5_receiver_rule_a_dotted_base_name_meets_a_dotted_receiver_type_by_bare_identifier() {
let files = fragments_for(&[
(
"Io/Handle.cs",
"namespace App.Io { public class Handle : System.IDisposable { public void Dispose() { } } }",
),
(
"Consumers/Closer.cs",
"\nnamespace App.Consumers;\n\npublic class Closer\n{\n public void Run(System.IDisposable d) => d.Dispose();\n}\n",
),
]);
let g = resolve_graph(&no_git_root(), &files);
assert_eq!(
heuristic_member_edges_from(&g, "Consumers/Closer.cs"),
vec![("App.Io.Handle", 6)],
"both sides reduce to the bare identifier IDisposable, so the raw base string answers the receiver type"
);
}