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;
8use crate::core::shape::Shape;
9
10use crate::lang::KindSpec;
11use crate::lang::canonical_walker::CanonicalWalker;
12
13pub mod build;
14mod canonicalize;
15mod kinds;
16mod strategy;
17
18use canonicalize::compute_module_moniker;
19use strategy::{Strategy, collect_callable_table, collect_type_table};
20
21#[derive(Clone, Debug, Default)]
22pub struct Presets {}
23
24pub fn parse(source: &str) -> Tree {
25	let mut parser = Parser::new();
26	let language: Language = tree_sitter_c_sharp::LANGUAGE.into();
27	parser.set_language(&language).unwrap_or_else(|err| {
28		panic!("failed to load tree-sitter C# grammar: {err}");
29	});
30	parser.parse(source, None).unwrap_or_else(|| {
31		panic!("tree-sitter parse returned None on a non-cancelled call");
32	})
33}
34
35pub fn extract(
36	uri: &str,
37	source: &str,
38	anchor: &Moniker,
39	deep: bool,
40	_presets: &Presets,
41) -> CodeGraph {
42	let tree = parse(source);
43	let module = compute_module_moniker(anchor, uri);
44	let (def_cap, ref_cap) = CodeGraph::capacity_for_source(source.len());
45	let mut graph = CodeGraph::with_capacity(module.clone(), kinds::MODULE, def_cap, ref_cap);
46	let mut type_table: HashMap<&[u8], Moniker> = HashMap::new();
47	collect_type_table(
48		tree.root_node(),
49		source.as_bytes(),
50		&module,
51		&mut type_table,
52	);
53	let mut callable_table: HashMap<(Moniker, Vec<u8>), Vec<u8>> = HashMap::new();
54	collect_callable_table(
55		tree.root_node(),
56		source.as_bytes(),
57		&module,
58		&mut callable_table,
59	);
60	let strat = Strategy {
61		module: module.clone(),
62		source_bytes: source.as_bytes(),
63		deep,
64		imports: RefCell::new(HashMap::new()),
65		local_scope: RefCell::new(Vec::new()),
66		type_table,
67		callable_table,
68	};
69	let walker = CanonicalWalker::new(&strat, source.as_bytes());
70	walker.walk(tree.root_node(), &module, &mut graph);
71	graph
72}
73
74pub struct Lang;
75
76const DEF_KINDS: &[&str] = &[
77	"class",
78	"interface",
79	"struct",
80	"record",
81	"enum",
82	"enum_constant",
83	"delegate",
84	"method",
85	"constructor",
86	"field",
87	"property",
88	"event",
89];
90
91const DEF_KIND_SPECS: &[KindSpec] = &[
92	KindSpec::new("class", Shape::Type, 20, "class"),
93	KindSpec::new("interface", Shape::Type, 21, "interface"),
94	KindSpec::new("struct", Shape::Type, 22, "struct"),
95	KindSpec::new("record", Shape::Type, 23, "record"),
96	KindSpec::new("enum", Shape::Type, 24, "enum"),
97	KindSpec::new("delegate", Shape::Type, 25, "delegate"),
98	KindSpec::new("constructor", Shape::Callable, 40, "constructor"),
99	KindSpec::new("method", Shape::Callable, 41, "method"),
100	KindSpec::new("enum_constant", Shape::Value, 60, "enum_constant"),
101	KindSpec::new("property", Shape::Value, 61, "property"),
102	KindSpec::new("field", Shape::Value, 62, "field"),
103	KindSpec::new("event", Shape::Value, 63, "event"),
104];
105
106impl crate::lang::LangExtractor for Lang {
107	type Presets = Presets;
108	const LANG_TAG: &'static str = "cs";
109	const ALLOWED_KINDS: &'static [&'static str] = DEF_KINDS;
110	const KIND_SPECS: &'static [KindSpec] = DEF_KIND_SPECS;
111	const ALLOWED_VISIBILITIES: &'static [&'static str] =
112		&["public", "protected", "package", "private"];
113
114	fn extract(
115		uri: &str,
116		source: &str,
117		anchor: &Moniker,
118		deep: bool,
119		presets: &Self::Presets,
120	) -> CodeGraph {
121		extract(uri, source, anchor, deep, presets)
122	}
123}
124
125#[cfg(test)]
126mod tests {
127	use super::*;
128	use crate::core::moniker::MonikerBuilder;
129	use crate::lang::assert_conformance;
130
131	fn make_anchor() -> Moniker {
132		MonikerBuilder::new().project(b"app").build()
133	}
134
135	fn extract_default(uri: &str, source: &str, anchor: &Moniker, deep: bool) -> CodeGraph {
136		let g = extract(uri, source, anchor, deep, &Presets::default());
137		assert_conformance::<super::Lang>(&g, anchor);
138		g
139	}
140
141	#[test]
142	fn parse_empty_returns_compilation_unit() {
143		let tree = parse("");
144		assert_eq!(tree.root_node().kind(), "compilation_unit");
145	}
146
147	#[test]
148	fn extract_struct_emits_struct_def() {
149		let src = "namespace Foo;\npublic struct Bar {}\n";
150		let g = extract_default("F.cs", src, &make_anchor(), false);
151		assert!(g.defs().any(|d| d.kind == b"struct"
152			&& d.moniker.as_view().segments().last().unwrap().name == b"Bar"));
153	}
154
155	#[test]
156	fn extract_enum_emits_enum_def() {
157		let src = "namespace Foo;\npublic enum Color { Red, Green }\n";
158		let g = extract_default("F.cs", src, &make_anchor(), false);
159		let e = g.defs().find(|d| d.kind == b"enum").expect("enum def");
160		assert_eq!(
161			e.moniker.as_view().segments().last().unwrap().name,
162			b"Color"
163		);
164	}
165
166	#[test]
167	fn extract_enum_emits_enum_constants() {
168		let src = "namespace Foo;\npublic enum Color { Red, Green }\n";
169		let g = extract_default("F.cs", src, &make_anchor(), false);
170		let red = MonikerBuilder::new()
171			.project(b"app")
172			.segment(b"lang", b"cs")
173			.segment(b"module", b"F")
174			.segment(b"enum", b"Color")
175			.segment(b"enum_constant", b"Red")
176			.build();
177		let green = MonikerBuilder::new()
178			.project(b"app")
179			.segment(b"lang", b"cs")
180			.segment(b"module", b"F")
181			.segment(b"enum", b"Color")
182			.segment(b"enum_constant", b"Green")
183			.build();
184		assert!(g.contains(&red), "missing enum_constant:Red");
185		assert!(g.contains(&green), "missing enum_constant:Green");
186	}
187
188	#[test]
189	fn extract_top_level_type_default_visibility_is_internal() {
190		let src = "namespace Foo;\nclass Bar {}\n";
191		let g = extract_default("F.cs", src, &make_anchor(), false);
192		let bar = g.defs().find(|d| d.kind == b"class").expect("class def");
193		assert_eq!(
194			bar.visibility,
195			b"package".to_vec(),
196			"top-level C# class without modifier defaults to internal (= VIS_PACKAGE)"
197		);
198	}
199
200	#[test]
201	fn extract_block_namespace_descends_into_body() {
202		let src = "namespace Foo {\n    public class Bar {}\n}\n";
203		let g = extract_default("F.cs", src, &make_anchor(), false);
204		assert!(g.defs().any(|d| d.kind == b"class"));
205	}
206
207	#[test]
208	fn extract_method_default_visibility_is_private() {
209		let src = "namespace Foo;\npublic class Bar {\n    int Hidden() { return 0; }\n}\n";
210		let g = extract_default("F.cs", src, &make_anchor(), false);
211		let m = g.defs().find(|d| d.kind == b"method").expect("method def");
212		assert_eq!(m.visibility, b"private".to_vec());
213	}
214
215	#[test]
216	fn extract_method_params_modifier_emits_ellipsis() {
217		let src =
218			"namespace Foo;\npublic class Bar {\n    public void Log(params object[] args) {}\n}\n";
219		let g = extract_default("F.cs", src, &make_anchor(), false);
220		let m = g.defs().find(|d| d.kind == b"method").expect("method def");
221		assert_eq!(
222			m.moniker.as_view().segments().last().unwrap().name,
223			b"Log(...)"
224		);
225	}
226
227	#[test]
228	fn extract_nested_class_attached_to_outer_class() {
229		let src = "namespace Foo;\npublic class Outer {\n    public class Inner {}\n}\n";
230		let g = extract_default("F.cs", src, &make_anchor(), false);
231		let inner = MonikerBuilder::new()
232			.project(b"app")
233			.segment(b"lang", b"cs")
234			.segment(b"module", b"F")
235			.segment(b"class", b"Outer")
236			.segment(b"class", b"Inner")
237			.build();
238		assert!(g.contains(&inner));
239	}
240
241	#[test]
242	fn extract_expression_bodied_property_emits_property_def() {
243		let src = "namespace Foo;\npublic class Bar {\n    public int N => 42;\n}\n";
244		let g = extract_default("F.cs", src, &make_anchor(), false);
245		assert!(g.defs().any(|d| d.kind == b"property"
246			&& d.moniker.as_view().segments().last().unwrap().name == b"N"));
247	}
248
249	#[test]
250	fn extract_property_with_user_type_emits_uses_type() {
251		let src = "namespace Foo;\npublic class Other {}\npublic class Bar {\n    public Other Item { get; set; }\n}\n";
252		let g = extract_default("F.cs", src, &make_anchor(), false);
253		assert!(g.refs().any(|r| r.kind == b"uses_type"
254			&& r.target.as_view().segments().last().unwrap().name == b"Other"));
255	}
256
257	#[test]
258	fn extract_base_list_emits_extends_per_entry() {
259		let src = "namespace Foo;\npublic class Base {}\npublic class Foo : Base, IBar {}\n";
260		let g = extract_default("F.cs", src, &make_anchor(), false);
261		let names: Vec<&[u8]> = g
262			.refs()
263			.filter(|r| r.kind == b"extends")
264			.map(|r| r.target.as_view().segments().last().unwrap().name)
265			.collect();
266		assert!(names.contains(&&b"Base"[..]));
267		assert!(names.contains(&&b"IBar"[..]));
268	}
269
270	#[test]
271	fn extract_generic_base_emits_extends_on_head_and_uses_type_on_arg() {
272		let src = "namespace Foo;\npublic class List<T> {}\npublic class Bar : List<int> {}\n";
273		let g = extract_default("F.cs", src, &make_anchor(), false);
274		assert!(g.refs().any(|r| r.kind == b"extends"
275			&& r.target.as_view().segments().last().unwrap().name == b"List"));
276	}
277
278	#[test]
279	fn extract_interface_base_emits_extends_per_entry() {
280		let src = "namespace Foo;\npublic interface IFoo : IBar, IBaz {}\n";
281		let g = extract_default("F.cs", src, &make_anchor(), false);
282		let count = g.refs().filter(|r| r.kind == b"extends").count();
283		assert_eq!(count, 2);
284	}
285
286	#[test]
287	fn extract_using_third_party_marks_imported() {
288		let g = extract_default("F.cs", "using Newtonsoft.Json;\n", &make_anchor(), false);
289		let r = g
290			.refs()
291			.find(|r| r.kind == b"imports_module")
292			.expect("imports_module ref");
293		assert_eq!(r.confidence, b"imported".to_vec());
294	}
295
296	#[test]
297	fn extract_using_alias_records_alias_attr() {
298		let g = extract_default("F.cs", "using IO = System.IO;\n", &make_anchor(), false);
299		let r = g
300			.refs()
301			.find(|r| r.kind == b"imports_module")
302			.expect("imports_module ref");
303		assert_eq!(r.alias, b"IO".to_vec());
304	}
305
306	#[test]
307	fn extract_global_using_emits_imports_module() {
308		let g = extract_default("F.cs", "global using System;\n", &make_anchor(), false);
309		assert!(
310			g.refs()
311				.any(|r| r.kind == b"imports_module" && r.confidence == b"external".to_vec())
312		);
313	}
314
315	#[test]
316	fn extract_using_static_emits_imports_module() {
317		let g = extract_default("F.cs", "using static System.Math;\n", &make_anchor(), false);
318		assert!(g.refs().any(|r| r.kind == b"imports_module"));
319	}
320
321	#[test]
322	fn extract_simple_invocation_to_unresolved_callee_uses_name_only() {
323		let src = "class B {\n    void M() { Helper(1, 2); }\n}\n";
324		let g = extract_default("F.cs", src, &make_anchor(), false);
325		let r = g
326			.refs()
327			.find(|r| {
328				r.kind == b"calls"
329					&& r.target.as_view().segments().last().unwrap().name == b"Helper"
330			})
331			.expect("calls Helper (name-only)");
332		assert_eq!(r.confidence, b"name_match".to_vec());
333	}
334
335	#[test]
336	fn extract_chained_member_call_receiver_hint_is_call() {
337		let src = "class B {\n    void M() { foo().bar(); }\n}\n";
338		let g = extract_default("F.cs", src, &make_anchor(), false);
339		let r = g
340			.refs()
341			.find(|r| {
342				r.kind == b"method_call"
343					&& r.target.as_view().segments().last().unwrap().name == b"bar"
344			})
345			.expect("method_call bar");
346		assert_eq!(r.receiver_hint, b"call".to_vec());
347	}
348
349	#[test]
350	fn extract_object_creation_unresolved_marks_name_match() {
351		let src = "class C {\n    void M() { var x = new Unknown(); }\n}\n";
352		let g = extract_default("F.cs", src, &make_anchor(), false);
353		let r = g
354			.refs()
355			.find(|r| r.kind == b"instantiates")
356			.expect("instantiates ref");
357		assert_eq!(r.confidence, b"name_match".to_vec());
358	}
359
360	#[test]
361	fn extract_class_attribute_emits_annotates() {
362		let src = "namespace Foo;\n[Serializable]\npublic class Bar {}\n";
363		let g = extract_default("F.cs", src, &make_anchor(), false);
364		let r = g
365			.refs()
366			.find(|r| r.kind == b"annotates")
367			.expect("annotates ref");
368		assert_eq!(
369			r.target.as_view().segments().last().unwrap().name,
370			b"Serializable"
371		);
372	}
373
374	#[test]
375	fn extract_method_attribute_emits_annotates() {
376		let src = "namespace Foo;\npublic class Bar {\n    [HttpGet] public void M() {}\n}\n";
377		let g = extract_default("F.cs", src, &make_anchor(), false);
378		let r = g
379			.refs()
380			.find(|r| r.kind == b"annotates")
381			.expect("annotates ref");
382		assert_eq!(
383			r.target.as_view().segments().last().unwrap().name,
384			b"HttpGet"
385		);
386	}
387
388	#[test]
389	fn extract_multiple_attribute_lists_each_emit_annotates() {
390		let src =
391			"namespace Foo;\npublic class Bar {\n    [Required] [Range(1,9)] public int N;\n}\n";
392		let g = extract_default("F.cs", src, &make_anchor(), false);
393		let names: Vec<&[u8]> = g
394			.refs()
395			.filter(|r| r.kind == b"annotates")
396			.map(|r| r.target.as_view().segments().last().unwrap().name)
397			.collect();
398		assert!(names.contains(&&b"Required"[..]));
399		assert!(names.contains(&&b"Range"[..]));
400	}
401
402	#[test]
403	fn extract_qualified_attribute_resolves_leaf_name() {
404		let src = "namespace Foo;\n[System.Serializable]\npublic class Bar {}\n";
405		let g = extract_default("F.cs", src, &make_anchor(), false);
406		assert!(g.refs().any(|r| r.kind == b"annotates"
407			&& r.target.as_view().segments().last().unwrap().name == b"Serializable"));
408	}
409
410	#[test]
411	fn extract_shallow_skips_param_and_local_defs() {
412		let src = "class B {\n    void M(int x) { int y = 1; var z = \"\"; }\n}\n";
413		let g = extract_default("F.cs", src, &make_anchor(), false);
414		assert!(
415			g.defs().all(|d| d.kind != b"param" && d.kind != b"local"),
416			"shallow extraction must not emit param/local defs"
417		);
418	}
419
420	#[test]
421	fn extract_deep_skips_blank_local() {
422		let src = "class B {\n    void M() { var _ = 1; var y = 2; }\n}\n";
423		let g = extract_default("F.cs", src, &make_anchor(), true);
424		let names: Vec<&[u8]> = g
425			.defs()
426			.filter(|d| d.kind == b"local")
427			.map(|d| d.moniker.as_view().segments().last().unwrap().name)
428			.collect();
429		assert_eq!(names, vec![&b"y"[..]]);
430	}
431}