Skip to main content

code_moniker_core/lang/cs/
mod.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3
4use tree_sitter::{Language, Parser, Tree};
5
6use crate::core::code_graph::CodeGraph;
7use crate::core::moniker::Moniker;
8
9use crate::lang::canonical_walker::CanonicalWalker;
10
11pub mod build;
12mod canonicalize;
13mod kinds;
14mod strategy;
15
16use canonicalize::compute_module_moniker;
17use strategy::{Strategy, collect_callable_table, collect_type_table};
18
19#[derive(Clone, Debug, Default)]
20pub struct Presets {}
21
22pub fn parse(source: &str) -> Tree {
23	let mut parser = Parser::new();
24	let language: Language = tree_sitter_c_sharp::LANGUAGE.into();
25	parser
26		.set_language(&language)
27		.expect("failed to load tree-sitter C# grammar");
28	parser
29		.parse(source, None)
30		.expect("tree-sitter parse returned None on a non-cancelled call")
31}
32
33pub fn extract(
34	uri: &str,
35	source: &str,
36	anchor: &Moniker,
37	deep: bool,
38	_presets: &Presets,
39) -> CodeGraph {
40	let tree = parse(source);
41	let module = compute_module_moniker(anchor, uri);
42	let (def_cap, ref_cap) = CodeGraph::capacity_for_source(source.len());
43	let mut graph = CodeGraph::with_capacity(module.clone(), kinds::MODULE, def_cap, ref_cap);
44	let mut type_table: HashMap<&[u8], Moniker> = HashMap::new();
45	collect_type_table(
46		tree.root_node(),
47		source.as_bytes(),
48		&module,
49		&mut type_table,
50	);
51	let mut callable_table: HashMap<(Moniker, Vec<u8>), Vec<u8>> = HashMap::new();
52	collect_callable_table(
53		tree.root_node(),
54		source.as_bytes(),
55		&module,
56		&mut callable_table,
57	);
58	let strat = Strategy {
59		module: module.clone(),
60		source_bytes: source.as_bytes(),
61		deep,
62		imports: RefCell::new(HashMap::new()),
63		local_scope: RefCell::new(Vec::new()),
64		type_table,
65		callable_table,
66	};
67	let walker = CanonicalWalker::new(&strat, source.as_bytes());
68	walker.walk(tree.root_node(), &module, &mut graph);
69	graph
70}
71
72pub struct Lang;
73
74impl crate::lang::LangExtractor for Lang {
75	type Presets = Presets;
76	const LANG_TAG: &'static str = "cs";
77	const ALLOWED_KINDS: &'static [&'static str] = &[
78		"class",
79		"interface",
80		"struct",
81		"record",
82		"enum",
83		"delegate",
84		"method",
85		"constructor",
86		"field",
87		"property",
88		"event",
89	];
90	const ALLOWED_VISIBILITIES: &'static [&'static str] =
91		&["public", "protected", "package", "private"];
92
93	fn extract(
94		uri: &str,
95		source: &str,
96		anchor: &Moniker,
97		deep: bool,
98		presets: &Self::Presets,
99	) -> CodeGraph {
100		extract(uri, source, anchor, deep, presets)
101	}
102}
103
104#[cfg(test)]
105mod tests {
106	use super::*;
107	use crate::core::moniker::MonikerBuilder;
108	use crate::lang::assert_conformance;
109
110	fn make_anchor() -> Moniker {
111		MonikerBuilder::new().project(b"app").build()
112	}
113
114	fn extract_default(uri: &str, source: &str, anchor: &Moniker, deep: bool) -> CodeGraph {
115		let g = extract(uri, source, anchor, deep, &Presets::default());
116		assert_conformance::<super::Lang>(&g, anchor);
117		g
118	}
119
120	#[test]
121	fn parse_empty_returns_compilation_unit() {
122		let tree = parse("");
123		assert_eq!(tree.root_node().kind(), "compilation_unit");
124	}
125
126	#[test]
127	fn extract_emits_comment_def_per_comment_node() {
128		let src = "// a\n/// b\nclass Foo {}\n";
129		let g = extract_default("Foo.cs", src, &make_anchor(), false);
130		let n = g.defs().filter(|d| d.kind == b"comment").count();
131		assert_eq!(n, 2);
132	}
133
134	#[test]
135	fn extract_module_uses_path_segments() {
136		let g = extract_default(
137			"Acme/Util/Text.cs",
138			"namespace Acme.Util;\n",
139			&make_anchor(),
140			false,
141		);
142		let expected = MonikerBuilder::new()
143			.project(b"app")
144			.segment(b"lang", b"cs")
145			.segment(b"package", b"Acme")
146			.segment(b"package", b"Util")
147			.segment(b"module", b"Text")
148			.build();
149		assert_eq!(g.root(), &expected);
150	}
151
152	#[test]
153	fn extract_class_emits_class_def() {
154		let src = "namespace Foo;\npublic class Bar {}\n";
155		let g = extract_default("F.cs", src, &make_anchor(), false);
156		let bar = g.defs().find(|d| d.kind == b"class").expect("class def");
157		assert_eq!(
158			bar.moniker.as_view().segments().last().unwrap().name,
159			b"Bar"
160		);
161		assert_eq!(bar.visibility, b"public".to_vec());
162	}
163
164	#[test]
165	fn extract_struct_emits_struct_def() {
166		let src = "namespace Foo;\npublic struct Bar {}\n";
167		let g = extract_default("F.cs", src, &make_anchor(), false);
168		assert!(g.defs().any(|d| d.kind == b"struct"
169			&& d.moniker.as_view().segments().last().unwrap().name == b"Bar"));
170	}
171
172	#[test]
173	fn extract_interface_emits_interface_def() {
174		let src = "namespace Foo;\npublic interface IBar {}\n";
175		let g = extract_default("F.cs", src, &make_anchor(), false);
176		let i = g
177			.defs()
178			.find(|d| d.kind == b"interface")
179			.expect("interface def");
180		assert_eq!(i.moniker.as_view().segments().last().unwrap().name, b"IBar");
181	}
182
183	#[test]
184	fn extract_enum_emits_enum_def() {
185		let src = "namespace Foo;\npublic enum Color { Red, Green }\n";
186		let g = extract_default("F.cs", src, &make_anchor(), false);
187		let e = g.defs().find(|d| d.kind == b"enum").expect("enum def");
188		assert_eq!(
189			e.moniker.as_view().segments().last().unwrap().name,
190			b"Color"
191		);
192	}
193
194	#[test]
195	fn extract_top_level_type_default_visibility_is_internal() {
196		let src = "namespace Foo;\nclass Bar {}\n";
197		let g = extract_default("F.cs", src, &make_anchor(), false);
198		let bar = g.defs().find(|d| d.kind == b"class").expect("class def");
199		assert_eq!(
200			bar.visibility,
201			b"package".to_vec(),
202			"top-level C# class without modifier defaults to internal (= VIS_PACKAGE)"
203		);
204	}
205
206	#[test]
207	fn extract_block_namespace_descends_into_body() {
208		let src = "namespace Foo {\n    public class Bar {}\n}\n";
209		let g = extract_default("F.cs", src, &make_anchor(), false);
210		assert!(g.defs().any(|d| d.kind == b"class"));
211	}
212
213	#[test]
214	fn extract_method_reparented_under_class() {
215		let src = "namespace Foo;\npublic class Bar {\n    public int Add(int a, int b) { return a + b; }\n}\n";
216		let g = extract_default("F.cs", src, &make_anchor(), false);
217		let m = MonikerBuilder::new()
218			.project(b"app")
219			.segment(b"lang", b"cs")
220			.segment(b"module", b"F")
221			.segment(b"class", b"Bar")
222			.segment(b"method", b"Add(a:int,b:int)")
223			.build();
224		assert!(
225			g.contains(&m),
226			"expected {m:?}, defs: {:?}",
227			g.def_monikers()
228		);
229	}
230
231	#[test]
232	fn extract_method_signature_excludes_return_type() {
233		let src = "namespace Foo;\npublic class Bar {\n    public string Greet(string n) { return n; }\n}\n";
234		let g = extract_default("F.cs", src, &make_anchor(), false);
235		let m = g.defs().find(|d| d.kind == b"method").expect("method def");
236		assert_eq!(m.signature, b"n:string".to_vec());
237	}
238
239	#[test]
240	fn extract_method_default_visibility_is_private() {
241		let src = "namespace Foo;\npublic class Bar {\n    int Hidden() { return 0; }\n}\n";
242		let g = extract_default("F.cs", src, &make_anchor(), false);
243		let m = g.defs().find(|d| d.kind == b"method").expect("method def");
244		assert_eq!(m.visibility, b"private".to_vec());
245	}
246
247	#[test]
248	fn extract_constructor_emits_constructor_def() {
249		let src = "namespace Foo;\npublic class Bar {\n    public Bar(int x) {}\n}\n";
250		let g = extract_default("F.cs", src, &make_anchor(), false);
251		let ctor = MonikerBuilder::new()
252			.project(b"app")
253			.segment(b"lang", b"cs")
254			.segment(b"module", b"F")
255			.segment(b"class", b"Bar")
256			.segment(b"constructor", b"Bar(x:int)")
257			.build();
258		assert!(
259			g.contains(&ctor),
260			"constructor expected at {ctor:?}; defs: {:?}",
261			g.def_monikers()
262		);
263	}
264
265	#[test]
266	fn extract_method_no_params_emits_empty_parens() {
267		let src = "namespace Foo;\npublic class Bar {\n    public void Boot() {}\n}\n";
268		let g = extract_default("F.cs", src, &make_anchor(), false);
269		let m = g.defs().find(|d| d.kind == b"method").expect("method def");
270		assert_eq!(
271			m.moniker.as_view().segments().last().unwrap().name,
272			b"Boot()"
273		);
274	}
275
276	#[test]
277	fn extract_method_params_modifier_emits_ellipsis() {
278		let src =
279			"namespace Foo;\npublic class Bar {\n    public void Log(params object[] args) {}\n}\n";
280		let g = extract_default("F.cs", src, &make_anchor(), false);
281		let m = g.defs().find(|d| d.kind == b"method").expect("method def");
282		assert_eq!(
283			m.moniker.as_view().segments().last().unwrap().name,
284			b"Log(...)"
285		);
286	}
287
288	#[test]
289	fn extract_record_emits_record_plus_primary_constructor() {
290		let src = "namespace Foo;\npublic record Person(int Age, string Name);\n";
291		let g = extract_default("F.cs", src, &make_anchor(), false);
292		let record = MonikerBuilder::new()
293			.project(b"app")
294			.segment(b"lang", b"cs")
295			.segment(b"module", b"F")
296			.segment(b"record", b"Person")
297			.build();
298		let ctor = MonikerBuilder::new()
299			.project(b"app")
300			.segment(b"lang", b"cs")
301			.segment(b"module", b"F")
302			.segment(b"record", b"Person")
303			.segment(b"constructor", b"Person(Age:int,Name:string)")
304			.build();
305		assert!(g.contains(&record));
306		assert!(
307			g.contains(&ctor),
308			"record primary constructor expected at {ctor:?}; defs: {:?}",
309			g.def_monikers()
310		);
311	}
312
313	#[test]
314	fn extract_nested_class_attached_to_outer_class() {
315		let src = "namespace Foo;\npublic class Outer {\n    public class Inner {}\n}\n";
316		let g = extract_default("F.cs", src, &make_anchor(), false);
317		let inner = MonikerBuilder::new()
318			.project(b"app")
319			.segment(b"lang", b"cs")
320			.segment(b"module", b"F")
321			.segment(b"class", b"Outer")
322			.segment(b"class", b"Inner")
323			.build();
324		assert!(g.contains(&inner));
325	}
326
327	#[test]
328	fn extract_field_emits_field_def() {
329		let src = "namespace Foo;\npublic class Bar {\n    private int _count;\n    public string Name = \"x\";\n}\n";
330		let g = extract_default("F.cs", src, &make_anchor(), false);
331		let names: Vec<&[u8]> = g
332			.defs()
333			.filter(|d| d.kind == b"field")
334			.map(|d| d.moniker.as_view().segments().last().unwrap().name)
335			.collect();
336		assert!(names.contains(&&b"_count"[..]));
337		assert!(names.contains(&&b"Name"[..]));
338		let count = g
339			.defs()
340			.find(|d| d.moniker.as_view().segments().last().unwrap().name == b"_count")
341			.unwrap();
342		assert_eq!(count.visibility, b"private".to_vec());
343		let name_def = g
344			.defs()
345			.find(|d| d.moniker.as_view().segments().last().unwrap().name == b"Name")
346			.unwrap();
347		assert_eq!(name_def.visibility, b"public".to_vec());
348	}
349
350	#[test]
351	fn extract_field_with_user_type_emits_uses_type() {
352		let src = "namespace Foo;\npublic class Other {}\npublic class Bar {\n    private Other _ref;\n}\n";
353		let g = extract_default("F.cs", src, &make_anchor(), false);
354		let r = g
355			.refs()
356			.find(|r| {
357				r.kind == b"uses_type"
358					&& r.target.as_view().segments().last().unwrap().name == b"Other"
359			})
360			.expect("uses_type Other");
361		assert!(matches!(
362			r.confidence.as_slice(),
363			b"name_match" | b"resolved"
364		));
365	}
366
367	#[test]
368	fn extract_property_emits_property_def() {
369		let src = "namespace Foo;\npublic class Bar {\n    public string Name { get; set; }\n}\n";
370		let g = extract_default("F.cs", src, &make_anchor(), false);
371		let p = g
372			.defs()
373			.find(|d| {
374				d.kind == b"property"
375					&& d.moniker.as_view().segments().last().unwrap().name == b"Name"
376			})
377			.expect("property def");
378		assert_eq!(p.visibility, b"public".to_vec());
379	}
380
381	#[test]
382	fn extract_expression_bodied_property_emits_property_def() {
383		let src = "namespace Foo;\npublic class Bar {\n    public int N => 42;\n}\n";
384		let g = extract_default("F.cs", src, &make_anchor(), false);
385		assert!(g.defs().any(|d| d.kind == b"property"
386			&& d.moniker.as_view().segments().last().unwrap().name == b"N"));
387	}
388
389	#[test]
390	fn extract_property_with_user_type_emits_uses_type() {
391		let src = "namespace Foo;\npublic class Other {}\npublic class Bar {\n    public Other Item { get; set; }\n}\n";
392		let g = extract_default("F.cs", src, &make_anchor(), false);
393		assert!(g.refs().any(|r| r.kind == b"uses_type"
394			&& r.target.as_view().segments().last().unwrap().name == b"Other"));
395	}
396
397	#[test]
398	fn extract_base_list_emits_extends_per_entry() {
399		let src = "namespace Foo;\npublic class Base {}\npublic class Foo : Base, IBar {}\n";
400		let g = extract_default("F.cs", src, &make_anchor(), false);
401		let names: Vec<&[u8]> = g
402			.refs()
403			.filter(|r| r.kind == b"extends")
404			.map(|r| r.target.as_view().segments().last().unwrap().name)
405			.collect();
406		assert!(names.contains(&&b"Base"[..]));
407		assert!(names.contains(&&b"IBar"[..]));
408	}
409
410	#[test]
411	fn extract_generic_base_emits_extends_on_head_and_uses_type_on_arg() {
412		let src = "namespace Foo;\npublic class List<T> {}\npublic class Bar : List<int> {}\n";
413		let g = extract_default("F.cs", src, &make_anchor(), false);
414		assert!(g.refs().any(|r| r.kind == b"extends"
415			&& r.target.as_view().segments().last().unwrap().name == b"List"));
416	}
417
418	#[test]
419	fn extract_interface_base_emits_extends_per_entry() {
420		let src = "namespace Foo;\npublic interface IFoo : IBar, IBaz {}\n";
421		let g = extract_default("F.cs", src, &make_anchor(), false);
422		let count = g.refs().filter(|r| r.kind == b"extends").count();
423		assert_eq!(count, 2);
424	}
425
426	#[test]
427	fn extract_method_param_user_type_emits_uses_type() {
428		let src = "namespace Foo;\npublic class Other {}\npublic class Bar {\n    public void Take(Other o) {}\n}\n";
429		let g = extract_default("F.cs", src, &make_anchor(), false);
430		assert!(
431			g.refs().any(|r| r.kind == b"uses_type"
432				&& r.target.as_view().segments().last().unwrap().name == b"Other"),
433			"refs: {:?}",
434			g.refs().map(|r| r.kind.clone()).collect::<Vec<_>>()
435		);
436	}
437
438	#[test]
439	fn extract_using_simple_emits_imports_module_external() {
440		let g = extract_default("F.cs", "using System;\n", &make_anchor(), false);
441		let r = g
442			.refs()
443			.find(|r| r.kind == b"imports_module")
444			.expect("imports_module ref");
445		assert_eq!(r.confidence, b"external".to_vec());
446		let target = MonikerBuilder::new()
447			.project(b"app")
448			.segment(b"external_pkg", b"System")
449			.build();
450		assert_eq!(r.target, target);
451	}
452
453	#[test]
454	fn extract_using_dotted_path_segments() {
455		let g = extract_default(
456			"F.cs",
457			"using System.Collections.Generic;\n",
458			&make_anchor(),
459			false,
460		);
461		let r = g
462			.refs()
463			.find(|r| r.kind == b"imports_module")
464			.expect("imports_module ref");
465		let names: Vec<&[u8]> = r.target.as_view().segments().map(|s| s.name).collect();
466		assert_eq!(
467			names,
468			vec![&b"System"[..], &b"Collections"[..], &b"Generic"[..]]
469		);
470	}
471
472	#[test]
473	fn extract_using_third_party_marks_imported() {
474		let g = extract_default("F.cs", "using Newtonsoft.Json;\n", &make_anchor(), false);
475		let r = g
476			.refs()
477			.find(|r| r.kind == b"imports_module")
478			.expect("imports_module ref");
479		assert_eq!(r.confidence, b"imported".to_vec());
480	}
481
482	#[test]
483	fn extract_using_alias_records_alias_attr() {
484		let g = extract_default("F.cs", "using IO = System.IO;\n", &make_anchor(), false);
485		let r = g
486			.refs()
487			.find(|r| r.kind == b"imports_module")
488			.expect("imports_module ref");
489		assert_eq!(r.alias, b"IO".to_vec());
490	}
491
492	#[test]
493	fn extract_global_using_emits_imports_module() {
494		let g = extract_default("F.cs", "global using System;\n", &make_anchor(), false);
495		assert!(
496			g.refs()
497				.any(|r| r.kind == b"imports_module" && r.confidence == b"external".to_vec())
498		);
499	}
500
501	#[test]
502	fn extract_using_static_emits_imports_module() {
503		let g = extract_default("F.cs", "using static System.Math;\n", &make_anchor(), false);
504		assert!(g.refs().any(|r| r.kind == b"imports_module"));
505	}
506
507	#[test]
508	fn extract_simple_invocation_to_unresolved_callee_uses_name_only() {
509		let src = "class B {\n    void M() { Helper(1, 2); }\n}\n";
510		let g = extract_default("F.cs", src, &make_anchor(), false);
511		let r = g
512			.refs()
513			.find(|r| {
514				r.kind == b"calls"
515					&& r.target.as_view().segments().last().unwrap().name == b"Helper"
516			})
517			.expect("calls Helper (name-only)");
518		assert_eq!(r.confidence, b"name_match".to_vec());
519	}
520
521	#[test]
522	fn extract_simple_invocation_to_same_module_resolves_slots() {
523		let src = "class B {\n    void M() { Helper(1); }\n    void Helper(int n) {}\n}\n";
524		let g = extract_default("F.cs", src, &make_anchor(), false);
525		let r = g
526			.refs()
527			.find(|r| {
528				r.kind == b"calls"
529					&& r.target.as_view().segments().last().unwrap().name == b"Helper(n:int)"
530			})
531			.expect("calls Helper(n:int)");
532		assert_eq!(r.confidence, b"name_match".to_vec());
533	}
534
535	#[test]
536	fn extract_member_invocation_emits_method_call_with_receiver_hint() {
537		let src = "class B {\n    void M() { Console.WriteLine(\"hi\"); }\n}\n";
538		let g = extract_default("F.cs", src, &make_anchor(), false);
539		let r = g
540			.refs()
541			.find(|r| r.kind == b"method_call")
542			.expect("method_call ref");
543		assert_eq!(r.receiver_hint, b"Console".to_vec());
544		assert_eq!(
545			r.target.as_view().segments().last().unwrap().name,
546			b"WriteLine"
547		);
548	}
549
550	#[test]
551	fn extract_chained_member_call_receiver_hint_is_call() {
552		let src = "class B {\n    void M() { foo().bar(); }\n}\n";
553		let g = extract_default("F.cs", src, &make_anchor(), false);
554		let r = g
555			.refs()
556			.find(|r| {
557				r.kind == b"method_call"
558					&& r.target.as_view().segments().last().unwrap().name == b"bar"
559			})
560			.expect("method_call bar");
561		assert_eq!(r.receiver_hint, b"call".to_vec());
562	}
563
564	#[test]
565	fn extract_object_creation_emits_instantiates() {
566		let src = "namespace Foo;\npublic class Bar {}\nclass C {\n    void M() { var x = new Bar(); }\n}\n";
567		let g = extract_default("F.cs", src, &make_anchor(), false);
568		let r = g
569			.refs()
570			.find(|r| r.kind == b"instantiates")
571			.expect("instantiates ref");
572		assert_eq!(r.target.as_view().segments().last().unwrap().name, b"Bar");
573		assert_eq!(r.confidence, b"resolved".to_vec());
574	}
575
576	#[test]
577	fn extract_object_creation_unresolved_marks_name_match() {
578		let src = "class C {\n    void M() { var x = new Unknown(); }\n}\n";
579		let g = extract_default("F.cs", src, &make_anchor(), false);
580		let r = g
581			.refs()
582			.find(|r| r.kind == b"instantiates")
583			.expect("instantiates ref");
584		assert_eq!(r.confidence, b"name_match".to_vec());
585	}
586
587	#[test]
588	fn extract_invocation_visits_arguments_for_nested_calls() {
589		let src = "class B {\n    void M() { Outer(Inner()); }\n}\n";
590		let g = extract_default("F.cs", src, &make_anchor(), false);
591		let names: Vec<&[u8]> = g
592			.refs()
593			.filter(|r| r.kind == b"calls")
594			.map(|r| r.target.as_view().segments().last().unwrap().name)
595			.collect();
596		assert!(names.contains(&&b"Outer"[..]));
597		assert!(names.contains(&&b"Inner"[..]));
598	}
599
600	#[test]
601	fn extract_class_attribute_emits_annotates() {
602		let src = "namespace Foo;\n[Serializable]\npublic class Bar {}\n";
603		let g = extract_default("F.cs", src, &make_anchor(), false);
604		let r = g
605			.refs()
606			.find(|r| r.kind == b"annotates")
607			.expect("annotates ref");
608		assert_eq!(
609			r.target.as_view().segments().last().unwrap().name,
610			b"Serializable"
611		);
612	}
613
614	#[test]
615	fn extract_method_attribute_emits_annotates() {
616		let src = "namespace Foo;\npublic class Bar {\n    [HttpGet] public void M() {}\n}\n";
617		let g = extract_default("F.cs", src, &make_anchor(), false);
618		let r = g
619			.refs()
620			.find(|r| r.kind == b"annotates")
621			.expect("annotates ref");
622		assert_eq!(
623			r.target.as_view().segments().last().unwrap().name,
624			b"HttpGet"
625		);
626	}
627
628	#[test]
629	fn extract_multiple_attribute_lists_each_emit_annotates() {
630		let src =
631			"namespace Foo;\npublic class Bar {\n    [Required] [Range(1,9)] public int N;\n}\n";
632		let g = extract_default("F.cs", src, &make_anchor(), false);
633		let names: Vec<&[u8]> = g
634			.refs()
635			.filter(|r| r.kind == b"annotates")
636			.map(|r| r.target.as_view().segments().last().unwrap().name)
637			.collect();
638		assert!(names.contains(&&b"Required"[..]));
639		assert!(names.contains(&&b"Range"[..]));
640	}
641
642	#[test]
643	fn extract_qualified_attribute_resolves_leaf_name() {
644		let src = "namespace Foo;\n[System.Serializable]\npublic class Bar {}\n";
645		let g = extract_default("F.cs", src, &make_anchor(), false);
646		assert!(g.refs().any(|r| r.kind == b"annotates"
647			&& r.target.as_view().segments().last().unwrap().name == b"Serializable"));
648	}
649
650	#[test]
651	fn extract_shallow_skips_param_and_local_defs() {
652		let src = "class B {\n    void M(int x) { int y = 1; var z = \"\"; }\n}\n";
653		let g = extract_default("F.cs", src, &make_anchor(), false);
654		assert!(
655			g.defs().all(|d| d.kind != b"param" && d.kind != b"local"),
656			"shallow extraction must not emit param/local defs"
657		);
658	}
659
660	#[test]
661	fn extract_deep_emits_param_def() {
662		let src = "class B {\n    void M(int a, string b) {}\n}\n";
663		let g = extract_default("F.cs", src, &make_anchor(), true);
664		let pa = MonikerBuilder::new()
665			.project(b"app")
666			.segment(b"lang", b"cs")
667			.segment(b"module", b"F")
668			.segment(b"class", b"B")
669			.segment(b"method", b"M(a:int,b:string)")
670			.segment(b"param", b"a")
671			.build();
672		assert!(g.contains(&pa));
673	}
674
675	#[test]
676	fn extract_deep_emits_local_def_for_typed_var() {
677		let src = "class B {\n    void M() { int x = 5; }\n}\n";
678		let g = extract_default("F.cs", src, &make_anchor(), true);
679		let lx = MonikerBuilder::new()
680			.project(b"app")
681			.segment(b"lang", b"cs")
682			.segment(b"module", b"F")
683			.segment(b"class", b"B")
684			.segment(b"method", b"M()")
685			.segment(b"local", b"x")
686			.build();
687		assert!(g.contains(&lx));
688	}
689
690	#[test]
691	fn extract_deep_emits_local_def_for_implicit_var() {
692		let src = "class B {\n    void M() { var s = \"hi\"; }\n}\n";
693		let g = extract_default("F.cs", src, &make_anchor(), true);
694		assert!(
695			g.defs().any(|d| d.kind == b"local"
696				&& d.moniker.as_view().segments().last().unwrap().name == b"s")
697		);
698	}
699
700	#[test]
701	fn extract_deep_emits_local_def_for_foreach_iter() {
702		let src = "class B {\n    void M(int[] items) { foreach (var item in items) {} }\n}\n";
703		let g = extract_default("F.cs", src, &make_anchor(), true);
704		assert!(g.defs().any(|d| d.kind == b"local"
705			&& d.moniker.as_view().segments().last().unwrap().name == b"item"));
706	}
707
708	#[test]
709	fn extract_deep_skips_blank_local() {
710		let src = "class B {\n    void M() { var _ = 1; var y = 2; }\n}\n";
711		let g = extract_default("F.cs", src, &make_anchor(), true);
712		let names: Vec<&[u8]> = g
713			.defs()
714			.filter(|d| d.kind == b"local")
715			.map(|d| d.moniker.as_view().segments().last().unwrap().name)
716			.collect();
717		assert_eq!(names, vec![&b"y"[..]]);
718	}
719}