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