Skip to main content

code_moniker_core/lang/cs/
mod.rs

1use tree_sitter::{Language, Parser, Tree};
2
3use crate::core::code_graph::CodeGraph;
4use crate::core::moniker::Moniker;
5use crate::core::shape::Shape;
6
7use crate::lang::{ExtractionContext, KindSpec, LangExtractor, ParsedDocument};
8pub mod build;
9mod canonicalize;
10mod kinds;
11mod sdk_pipeline;
12
13#[derive(Clone, Debug, Default)]
14pub struct Presets {}
15
16pub fn parse(source: &str) -> Tree {
17	let mut parser = Parser::new();
18	let language: Language = tree_sitter_c_sharp::LANGUAGE.into();
19	parser.set_language(&language).unwrap_or_else(|err| {
20		panic!("failed to load tree-sitter C# grammar: {err}");
21	});
22	parser.parse(source, None).unwrap_or_else(|| {
23		panic!("tree-sitter parse returned None on a non-cancelled call");
24	})
25}
26
27pub fn extract(
28	uri: &str,
29	source: &str,
30	anchor: &Moniker,
31	deep: bool,
32	_presets: &Presets,
33) -> CodeGraph {
34	<Lang as LangExtractor>::extract(uri, source, anchor, deep, _presets)
35}
36
37pub struct Lang;
38
39const DEF_KINDS: &[&str] = &[
40	"class",
41	"interface",
42	"struct",
43	"record",
44	"enum",
45	"enum_constant",
46	"delegate",
47	"method",
48	"constructor",
49	"field",
50	"property",
51	"event",
52];
53
54const DEF_KIND_SPECS: &[KindSpec] = &[
55	KindSpec::new("class", Shape::Type, 20, "class"),
56	KindSpec::new("interface", Shape::Type, 21, "interface"),
57	KindSpec::new("struct", Shape::Type, 22, "struct"),
58	KindSpec::new("record", Shape::Type, 23, "record"),
59	KindSpec::new("enum", Shape::Type, 24, "enum"),
60	KindSpec::new("delegate", Shape::Type, 25, "delegate"),
61	KindSpec::new("constructor", Shape::Callable, 40, "constructor"),
62	KindSpec::new("method", Shape::Callable, 41, "method"),
63	KindSpec::new("enum_constant", Shape::Value, 60, "enum_constant"),
64	KindSpec::new("property", Shape::Value, 61, "property"),
65	KindSpec::new("field", Shape::Value, 62, "field"),
66	KindSpec::new("event", Shape::Value, 63, "event"),
67];
68
69impl crate::lang::LangExtractor for Lang {
70	type Presets = Presets;
71	const LANG_TAG: &'static str = "cs";
72	const ALLOWED_KINDS: &'static [&'static str] = DEF_KINDS;
73	const KIND_SPECS: &'static [KindSpec] = DEF_KIND_SPECS;
74	const ALLOWED_VISIBILITIES: &'static [&'static str] =
75		&["public", "protected", "package", "private"];
76
77	fn parse(_uri: &str, source: &str) -> ParsedDocument {
78		ParsedDocument::new(parse(source))
79	}
80
81	fn file_root(uri: &str, anchor: &Moniker) -> Option<Moniker> {
82		Some(canonicalize::compute_module_moniker(anchor, uri))
83	}
84
85	fn extract_parsed(
86		context: ExtractionContext<'_, Self::Presets>,
87		document: &ParsedDocument,
88	) -> CodeGraph {
89		sdk_pipeline::extract(
90			context.uri,
91			context.source,
92			document,
93			context.anchor,
94			context.deep,
95		)
96	}
97}
98
99#[cfg(test)]
100mod tests {
101	use super::*;
102	use crate::core::moniker::MonikerBuilder;
103	use crate::lang::assert_conformance;
104
105	fn make_anchor() -> Moniker {
106		MonikerBuilder::new().project(b"app").build()
107	}
108
109	fn extract_default(uri: &str, source: &str, anchor: &Moniker, deep: bool) -> CodeGraph {
110		let g = extract(uri, source, anchor, deep, &Presets::default());
111		assert_conformance::<super::Lang>(&g, anchor);
112		g
113	}
114
115	#[test]
116	fn parse_empty_returns_compilation_unit() {
117		let tree = parse("");
118		assert_eq!(tree.root_node().kind(), "compilation_unit");
119	}
120
121	#[test]
122	fn extract_struct_emits_struct_def() {
123		let src = "namespace Foo;\npublic struct Bar {}\n";
124		let g = extract_default("F.cs", src, &make_anchor(), false);
125		assert!(g.defs().any(|d| d.kind == b"struct"
126			&& d.moniker.as_view().segments().last().unwrap().name == b"Bar"));
127	}
128
129	#[test]
130	fn extract_enum_emits_enum_def() {
131		let src = "namespace Foo;\npublic enum Color { Red, Green }\n";
132		let g = extract_default("F.cs", src, &make_anchor(), false);
133		let e = g.defs().find(|d| d.kind == b"enum").expect("enum def");
134		assert_eq!(
135			e.moniker.as_view().segments().last().unwrap().name,
136			b"Color"
137		);
138	}
139
140	#[test]
141	fn extract_enum_emits_enum_constants() {
142		let src = "namespace Foo;\npublic enum Color { Red, Green }\n";
143		let g = extract_default("F.cs", src, &make_anchor(), false);
144		let red = MonikerBuilder::new()
145			.project(b"app")
146			.segment(b"lang", b"cs")
147			.segment(b"module", b"F")
148			.segment(b"enum", b"Color")
149			.segment(b"enum_constant", b"Red")
150			.build();
151		let green = MonikerBuilder::new()
152			.project(b"app")
153			.segment(b"lang", b"cs")
154			.segment(b"module", b"F")
155			.segment(b"enum", b"Color")
156			.segment(b"enum_constant", b"Green")
157			.build();
158		assert!(g.contains(&red), "missing enum_constant:Red");
159		assert!(g.contains(&green), "missing enum_constant:Green");
160	}
161
162	#[test]
163	fn extract_top_level_type_default_visibility_is_internal() {
164		let src = "namespace Foo;\nclass Bar {}\n";
165		let g = extract_default("F.cs", src, &make_anchor(), false);
166		let bar = g.defs().find(|d| d.kind == b"class").expect("class def");
167		assert_eq!(
168			bar.visibility,
169			b"package".to_vec(),
170			"top-level C# class without modifier defaults to internal (= VIS_PACKAGE)"
171		);
172	}
173
174	#[test]
175	fn extract_block_namespace_descends_into_body() {
176		let src = "namespace Foo {\n    public class Bar {}\n}\n";
177		let g = extract_default("F.cs", src, &make_anchor(), false);
178		assert!(g.defs().any(|d| d.kind == b"class"));
179	}
180
181	#[test]
182	fn extract_method_default_visibility_is_private() {
183		let src = "namespace Foo;\npublic class Bar {\n    int Hidden() { return 0; }\n}\n";
184		let g = extract_default("F.cs", src, &make_anchor(), false);
185		let m = g.defs().find(|d| d.kind == b"method").expect("method def");
186		assert_eq!(m.visibility, b"private".to_vec());
187	}
188
189	#[test]
190	fn extract_method_params_modifier_emits_ellipsis() {
191		let src =
192			"namespace Foo;\npublic class Bar {\n    public void Log(params object[] args) {}\n}\n";
193		let g = extract_default("F.cs", src, &make_anchor(), false);
194		let m = g.defs().find(|d| d.kind == b"method").expect("method def");
195		assert_eq!(
196			m.moniker.as_view().segments().last().unwrap().name,
197			b"Log(...)"
198		);
199	}
200
201	#[test]
202	fn extract_nested_class_attached_to_outer_class() {
203		let src = "namespace Foo;\npublic class Outer {\n    public class Inner {}\n}\n";
204		let g = extract_default("F.cs", src, &make_anchor(), false);
205		let inner = MonikerBuilder::new()
206			.project(b"app")
207			.segment(b"lang", b"cs")
208			.segment(b"module", b"F")
209			.segment(b"class", b"Outer")
210			.segment(b"class", b"Inner")
211			.build();
212		assert!(g.contains(&inner));
213	}
214
215	#[test]
216	fn extract_expression_bodied_property_emits_property_def() {
217		let src = "namespace Foo;\npublic class Bar {\n    public int N => 42;\n}\n";
218		let g = extract_default("F.cs", src, &make_anchor(), false);
219		assert!(g.defs().any(|d| d.kind == b"property"
220			&& d.moniker.as_view().segments().last().unwrap().name == b"N"));
221	}
222
223	#[test]
224	fn extract_property_with_user_type_emits_uses_type() {
225		let src = "namespace Foo;\npublic class Other {}\npublic class Bar {\n    public Other Item { get; set; }\n}\n";
226		let g = extract_default("F.cs", src, &make_anchor(), false);
227		assert!(g.refs().any(|r| r.kind == b"uses_type"
228			&& r.target.as_view().segments().last().unwrap().name == b"Other"));
229	}
230
231	#[test]
232	fn extract_global_qualified_system_type_is_external() {
233		let src = "class B { global::System.Resources.ResourceManager Resources { get; } }";
234		let g = extract_default("F.cs", src, &make_anchor(), false);
235		let reference = g
236			.refs()
237			.find(|reference| reference.kind == b"uses_type")
238			.expect("uses_type reference");
239		assert_eq!(reference.confidence, b"external".to_vec());
240		let segments = reference.target.as_view().segments().collect::<Vec<_>>();
241		assert!(
242			segments
243				.iter()
244				.any(|segment| { segment.kind == b"sdk" && segment.name == b"cs" })
245		);
246		assert!(
247			segments
248				.iter()
249				.any(|segment| { segment.kind == b"path" && segment.name == b"System" })
250		);
251		assert!(
252			segments
253				.iter()
254				.any(|segment| { segment.kind == b"path" && segment.name == b"ResourceManager" })
255		);
256	}
257
258	#[test]
259	fn microsoft_namespace_is_not_implicitly_owned_by_the_sdk() {
260		let src = "using Microsoft.Extensions.Logging; class Service {}";
261		let g = extract_default("Service.cs", src, &make_anchor(), false);
262		let reference = g
263			.refs()
264			.find(|reference| reference.kind == b"imports_module")
265			.expect("using reference");
266		let first = reference.target.as_view().segments().next().unwrap();
267		assert_eq!(first.kind, b"external_pkg");
268		assert_eq!(first.name, b"Microsoft");
269	}
270
271	#[test]
272	fn extract_base_list_emits_extends_per_entry() {
273		let src = "namespace Foo;\npublic class Base {}\npublic class Foo : Base, IBar {}\n";
274		let g = extract_default("F.cs", src, &make_anchor(), false);
275		let names: Vec<&[u8]> = g
276			.refs()
277			.filter(|r| r.kind == b"extends")
278			.map(|r| r.target.as_view().segments().last().unwrap().name)
279			.collect();
280		assert!(names.contains(&&b"Base"[..]));
281		assert!(names.contains(&&b"IBar"[..]));
282	}
283
284	#[test]
285	fn extract_generic_base_emits_extends_on_head_and_uses_type_on_arg() {
286		let src = "namespace Foo;\npublic class List<T> {}\npublic class Bar : List<int> {}\n";
287		let g = extract_default("F.cs", src, &make_anchor(), false);
288		assert!(g.refs().any(|r| r.kind == b"extends"
289			&& r.target.as_view().segments().last().unwrap().name == b"List"));
290	}
291
292	#[test]
293	fn extract_interface_base_emits_extends_per_entry() {
294		let src = "namespace Foo;\npublic interface IFoo : IBar, IBaz {}\n";
295		let g = extract_default("F.cs", src, &make_anchor(), false);
296		let count = g.refs().filter(|r| r.kind == b"extends").count();
297		assert_eq!(count, 2);
298	}
299
300	#[test]
301	fn extract_using_third_party_marks_imported() {
302		let g = extract_default("F.cs", "using Newtonsoft.Json;\n", &make_anchor(), false);
303		let r = g
304			.refs()
305			.find(|r| r.kind == b"imports_module")
306			.expect("imports_module ref");
307		assert_eq!(r.confidence, b"imported".to_vec());
308	}
309
310	#[test]
311	fn extract_system_prefixed_nuget_namespace_stays_manifest_owned() {
312		for namespace in [
313			"System.Reactive.Linq",
314			"System.CommandLine",
315			"System.IO.Abstractions",
316			"System.Linq.Dynamic.Core",
317		] {
318			let source = format!("using {namespace};\n");
319			let g = extract_default("F.cs", &source, &make_anchor(), false);
320			let reference = g
321				.refs()
322				.find(|reference| reference.kind == b"imports_module")
323				.expect("imports_module ref");
324			assert_eq!(
325				reference.confidence,
326				b"imported".to_vec(),
327				"{namespace} must be resolved through the project manifest",
328			);
329			assert_eq!(
330				reference.target.as_view().segments().next().unwrap().kind,
331				b"external_pkg",
332			);
333		}
334	}
335
336	#[test]
337	fn extract_qualified_bcl_types_outside_the_short_catalog_use_sdk_targets() {
338		let source = concat!(
339			"class Service { ",
340			"System.IO.StreamReader reader; ",
341			"System.Net.Http.HttpClient client = new System.Net.Http.HttpClient(); ",
342			"}",
343		);
344		let g = extract_default("Service.cs", source, &make_anchor(), false);
345		for type_name in ["StreamReader", "HttpClient"] {
346			let reference = g
347				.refs()
348				.find(|reference| {
349					reference.kind == b"uses_type"
350						&& reference
351							.target
352							.as_view()
353							.segments()
354							.last()
355							.is_some_and(|segment| segment.name == type_name.as_bytes())
356				})
357				.unwrap_or_else(|| panic!("missing uses_type for {type_name}"));
358			assert_eq!(
359				reference.target.as_view().segments().next().unwrap().kind,
360				b"sdk",
361				"{type_name} must be owned by the target framework",
362			);
363			assert_eq!(reference.confidence, b"external".to_vec());
364		}
365	}
366
367	#[test]
368	fn extract_using_alias_records_alias_attr() {
369		let g = extract_default("F.cs", "using IO = System.IO;\n", &make_anchor(), false);
370		let r = g
371			.refs()
372			.find(|r| r.kind == b"imports_module")
373			.expect("imports_module ref");
374		assert_eq!(r.alias, b"IO".to_vec());
375	}
376
377	#[test]
378	fn extract_global_using_emits_imports_module() {
379		let g = extract_default("F.cs", "global using System;\n", &make_anchor(), false);
380		assert!(
381			g.refs()
382				.any(|r| r.kind == b"imports_module" && r.confidence == b"external".to_vec())
383		);
384	}
385
386	#[test]
387	fn extract_using_static_emits_imports_module() {
388		let g = extract_default("F.cs", "using static System.Math;\n", &make_anchor(), false);
389		assert!(g.refs().any(|r| r.kind == b"imports_module"));
390	}
391
392	#[test]
393	fn extract_simple_invocation_to_unresolved_callee_uses_name_only() {
394		let src = "class B {\n    void M() { Helper(1, 2); }\n}\n";
395		let g = extract_default("F.cs", src, &make_anchor(), false);
396		let r = g
397			.refs()
398			.find(|r| {
399				r.kind == b"calls"
400					&& r.target.as_view().segments().last().unwrap().name == b"Helper"
401			})
402			.expect("calls Helper (name-only)");
403		assert_eq!(r.confidence, b"name_match".to_vec());
404		assert_eq!(r.call_name, b"Helper".to_vec());
405		assert_eq!(r.call_arity, Some(2));
406	}
407
408	#[test]
409	fn extract_chained_member_call_receiver_hint_is_call() {
410		let src = "class B {\n    void M() { foo().bar(); }\n}\n";
411		let g = extract_default("F.cs", src, &make_anchor(), false);
412		let r = g
413			.refs()
414			.find(|r| {
415				r.kind == b"method_call"
416					&& r.target.as_view().segments().last().unwrap().name == b"bar"
417			})
418			.expect("method_call bar");
419		assert_eq!(r.receiver_hint, b"call".to_vec());
420		assert_eq!(r.call_name, b"bar".to_vec());
421		assert_eq!(r.call_arity, Some(0));
422	}
423
424	#[test]
425	fn extract_callable_defs_expose_linkage_metadata() {
426		let src = "class B { int Sum(int left, int right) => left + right; }";
427		let g = extract_default("F.cs", src, &make_anchor(), false);
428		let def = g
429			.defs()
430			.find(|def| def.kind == b"method")
431			.expect("method definition");
432		assert_eq!(def.call_name, b"Sum".to_vec());
433		assert_eq!(def.call_arity, Some(2));
434	}
435
436	#[test]
437	fn extract_callable_arity_comes_from_syntax_not_type_commas() {
438		let src = "using System.Collections.Generic; class B { void One(Dictionary<string, int> value) {} void Many(params\nobject[] values) {} }";
439		let g = extract_default("F.cs", src, &make_anchor(), false);
440		let one = g
441			.defs()
442			.find(|def| def.call_name == b"One")
443			.expect("One definition");
444		let many = g
445			.defs()
446			.find(|def| def.call_name == b"Many")
447			.expect("Many definition");
448		assert_eq!(one.call_arity, Some(1));
449		assert_eq!(many.call_arity, None);
450	}
451
452	#[test]
453	fn extract_parameter_type_emits_typed_binding() {
454		let src = "class Worker {} class B { void Run(Worker worker) {} }";
455		let g = extract_default("F.cs", src, &make_anchor(), false);
456		let reference = g
457			.refs()
458			.find(|reference| reference.kind == b"typed_as")
459			.expect("typed_as reference");
460		assert_eq!(reference.alias, b"worker".to_vec());
461		assert_eq!(reference.confidence, b"resolved".to_vec());
462	}
463
464	#[test]
465	fn extract_field_and_inferred_local_types_emit_typed_bindings() {
466		let src =
467			"class Worker {} class B { Worker worker; void Run() { var local = new Worker(); } }";
468		let g = extract_default("F.cs", src, &make_anchor(), false);
469		let typed = g
470			.refs()
471			.filter(|reference| reference.kind == b"typed_as")
472			.collect::<Vec<_>>();
473		assert!(typed.iter().any(|reference| {
474			reference.alias.is_empty() && g.def_at(reference.source).kind == b"field"
475		}));
476		assert!(
477			typed
478				.iter()
479				.any(|reference| reference.alias == b"local".to_vec())
480		);
481	}
482
483	#[test]
484	fn extract_object_creation_unresolved_marks_name_match() {
485		let src = "class C {\n    void M() { var x = new Unknown(); }\n}\n";
486		let g = extract_default("F.cs", src, &make_anchor(), false);
487		let r = g
488			.refs()
489			.find(|r| r.kind == b"instantiates")
490			.expect("instantiates ref");
491		assert_eq!(r.confidence, b"name_match".to_vec());
492	}
493
494	#[test]
495	fn extract_class_attribute_emits_annotates() {
496		let src = "namespace Foo;\n[Serializable]\npublic class Bar {}\n";
497		let g = extract_default("F.cs", src, &make_anchor(), false);
498		let r = g
499			.refs()
500			.find(|r| r.kind == b"annotates")
501			.expect("annotates ref");
502		assert_eq!(
503			r.target.as_view().segments().last().unwrap().name,
504			b"Serializable"
505		);
506	}
507
508	#[test]
509	fn extract_method_attribute_emits_annotates() {
510		let src = "namespace Foo;\npublic class Bar {\n    [HttpGet] public void M() {}\n}\n";
511		let g = extract_default("F.cs", src, &make_anchor(), false);
512		let r = g
513			.refs()
514			.find(|r| r.kind == b"annotates")
515			.expect("annotates ref");
516		assert_eq!(
517			r.target.as_view().segments().last().unwrap().name,
518			b"HttpGet"
519		);
520	}
521
522	#[test]
523	fn extract_multiple_attribute_lists_each_emit_annotates() {
524		let src =
525			"namespace Foo;\npublic class Bar {\n    [Required] [Range(1,9)] public int N;\n}\n";
526		let g = extract_default("F.cs", src, &make_anchor(), false);
527		let names: Vec<&[u8]> = g
528			.refs()
529			.filter(|r| r.kind == b"annotates")
530			.map(|r| r.target.as_view().segments().last().unwrap().name)
531			.collect();
532		assert!(names.contains(&&b"Required"[..]));
533		assert!(names.contains(&&b"Range"[..]));
534	}
535
536	#[test]
537	fn extract_qualified_attribute_resolves_leaf_name() {
538		let src = "namespace Foo;\n[System.Serializable]\npublic class Bar {}\n";
539		let g = extract_default("F.cs", src, &make_anchor(), false);
540		assert!(g.refs().any(|r| r.kind == b"annotates"
541			&& r.target.as_view().segments().last().unwrap().name == b"Serializable"));
542	}
543
544	#[test]
545	fn extract_shallow_skips_param_and_local_defs() {
546		let src = "class B {\n    void M(int x) { int y = 1; var z = \"\"; }\n}\n";
547		let g = extract_default("F.cs", src, &make_anchor(), false);
548		assert!(
549			g.defs().all(|d| d.kind != b"param" && d.kind != b"local"),
550			"shallow extraction must not emit param/local defs"
551		);
552	}
553
554	#[test]
555	fn extract_deep_skips_blank_local() {
556		let src = "class B {\n    void M() { var _ = 1; var y = 2; }\n}\n";
557		let g = extract_default("F.cs", src, &make_anchor(), true);
558		let names: Vec<&[u8]> = g
559			.defs()
560			.filter(|d| d.kind == b"local")
561			.map(|d| d.moniker.as_view().segments().last().unwrap().name)
562			.collect();
563		assert_eq!(names, vec![&b"y"[..]]);
564	}
565}