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_struct_emits_struct_def() {
128		let src = "namespace Foo;\npublic struct Bar {}\n";
129		let g = extract_default("F.cs", src, &make_anchor(), false);
130		assert!(g.defs().any(|d| d.kind == b"struct"
131			&& d.moniker.as_view().segments().last().unwrap().name == b"Bar"));
132	}
133
134	#[test]
135	fn extract_enum_emits_enum_def() {
136		let src = "namespace Foo;\npublic enum Color { Red, Green }\n";
137		let g = extract_default("F.cs", src, &make_anchor(), false);
138		let e = g.defs().find(|d| d.kind == b"enum").expect("enum def");
139		assert_eq!(
140			e.moniker.as_view().segments().last().unwrap().name,
141			b"Color"
142		);
143	}
144
145	#[test]
146	fn extract_top_level_type_default_visibility_is_internal() {
147		let src = "namespace Foo;\nclass Bar {}\n";
148		let g = extract_default("F.cs", src, &make_anchor(), false);
149		let bar = g.defs().find(|d| d.kind == b"class").expect("class def");
150		assert_eq!(
151			bar.visibility,
152			b"package".to_vec(),
153			"top-level C# class without modifier defaults to internal (= VIS_PACKAGE)"
154		);
155	}
156
157	#[test]
158	fn extract_block_namespace_descends_into_body() {
159		let src = "namespace Foo {\n    public class Bar {}\n}\n";
160		let g = extract_default("F.cs", src, &make_anchor(), false);
161		assert!(g.defs().any(|d| d.kind == b"class"));
162	}
163
164	#[test]
165	fn extract_method_default_visibility_is_private() {
166		let src = "namespace Foo;\npublic class Bar {\n    int Hidden() { return 0; }\n}\n";
167		let g = extract_default("F.cs", src, &make_anchor(), false);
168		let m = g.defs().find(|d| d.kind == b"method").expect("method def");
169		assert_eq!(m.visibility, b"private".to_vec());
170	}
171
172	#[test]
173	fn extract_method_params_modifier_emits_ellipsis() {
174		let src =
175			"namespace Foo;\npublic class Bar {\n    public void Log(params object[] args) {}\n}\n";
176		let g = extract_default("F.cs", src, &make_anchor(), false);
177		let m = g.defs().find(|d| d.kind == b"method").expect("method def");
178		assert_eq!(
179			m.moniker.as_view().segments().last().unwrap().name,
180			b"Log(...)"
181		);
182	}
183
184	#[test]
185	fn extract_nested_class_attached_to_outer_class() {
186		let src = "namespace Foo;\npublic class Outer {\n    public class Inner {}\n}\n";
187		let g = extract_default("F.cs", src, &make_anchor(), false);
188		let inner = MonikerBuilder::new()
189			.project(b"app")
190			.segment(b"lang", b"cs")
191			.segment(b"module", b"F")
192			.segment(b"class", b"Outer")
193			.segment(b"class", b"Inner")
194			.build();
195		assert!(g.contains(&inner));
196	}
197
198	#[test]
199	fn extract_expression_bodied_property_emits_property_def() {
200		let src = "namespace Foo;\npublic class Bar {\n    public int N => 42;\n}\n";
201		let g = extract_default("F.cs", src, &make_anchor(), false);
202		assert!(g.defs().any(|d| d.kind == b"property"
203			&& d.moniker.as_view().segments().last().unwrap().name == b"N"));
204	}
205
206	#[test]
207	fn extract_property_with_user_type_emits_uses_type() {
208		let src = "namespace Foo;\npublic class Other {}\npublic class Bar {\n    public Other Item { get; set; }\n}\n";
209		let g = extract_default("F.cs", src, &make_anchor(), false);
210		assert!(g.refs().any(|r| r.kind == b"uses_type"
211			&& r.target.as_view().segments().last().unwrap().name == b"Other"));
212	}
213
214	#[test]
215	fn extract_base_list_emits_extends_per_entry() {
216		let src = "namespace Foo;\npublic class Base {}\npublic class Foo : Base, IBar {}\n";
217		let g = extract_default("F.cs", src, &make_anchor(), false);
218		let names: Vec<&[u8]> = g
219			.refs()
220			.filter(|r| r.kind == b"extends")
221			.map(|r| r.target.as_view().segments().last().unwrap().name)
222			.collect();
223		assert!(names.contains(&&b"Base"[..]));
224		assert!(names.contains(&&b"IBar"[..]));
225	}
226
227	#[test]
228	fn extract_generic_base_emits_extends_on_head_and_uses_type_on_arg() {
229		let src = "namespace Foo;\npublic class List<T> {}\npublic class Bar : List<int> {}\n";
230		let g = extract_default("F.cs", src, &make_anchor(), false);
231		assert!(g.refs().any(|r| r.kind == b"extends"
232			&& r.target.as_view().segments().last().unwrap().name == b"List"));
233	}
234
235	#[test]
236	fn extract_interface_base_emits_extends_per_entry() {
237		let src = "namespace Foo;\npublic interface IFoo : IBar, IBaz {}\n";
238		let g = extract_default("F.cs", src, &make_anchor(), false);
239		let count = g.refs().filter(|r| r.kind == b"extends").count();
240		assert_eq!(count, 2);
241	}
242
243	#[test]
244	fn extract_using_third_party_marks_imported() {
245		let g = extract_default("F.cs", "using Newtonsoft.Json;\n", &make_anchor(), false);
246		let r = g
247			.refs()
248			.find(|r| r.kind == b"imports_module")
249			.expect("imports_module ref");
250		assert_eq!(r.confidence, b"imported".to_vec());
251	}
252
253	#[test]
254	fn extract_using_alias_records_alias_attr() {
255		let g = extract_default("F.cs", "using IO = System.IO;\n", &make_anchor(), false);
256		let r = g
257			.refs()
258			.find(|r| r.kind == b"imports_module")
259			.expect("imports_module ref");
260		assert_eq!(r.alias, b"IO".to_vec());
261	}
262
263	#[test]
264	fn extract_global_using_emits_imports_module() {
265		let g = extract_default("F.cs", "global using System;\n", &make_anchor(), false);
266		assert!(
267			g.refs()
268				.any(|r| r.kind == b"imports_module" && r.confidence == b"external".to_vec())
269		);
270	}
271
272	#[test]
273	fn extract_using_static_emits_imports_module() {
274		let g = extract_default("F.cs", "using static System.Math;\n", &make_anchor(), false);
275		assert!(g.refs().any(|r| r.kind == b"imports_module"));
276	}
277
278	#[test]
279	fn extract_simple_invocation_to_unresolved_callee_uses_name_only() {
280		let src = "class B {\n    void M() { Helper(1, 2); }\n}\n";
281		let g = extract_default("F.cs", src, &make_anchor(), false);
282		let r = g
283			.refs()
284			.find(|r| {
285				r.kind == b"calls"
286					&& r.target.as_view().segments().last().unwrap().name == b"Helper"
287			})
288			.expect("calls Helper (name-only)");
289		assert_eq!(r.confidence, b"name_match".to_vec());
290	}
291
292	#[test]
293	fn extract_chained_member_call_receiver_hint_is_call() {
294		let src = "class B {\n    void M() { foo().bar(); }\n}\n";
295		let g = extract_default("F.cs", src, &make_anchor(), false);
296		let r = g
297			.refs()
298			.find(|r| {
299				r.kind == b"method_call"
300					&& r.target.as_view().segments().last().unwrap().name == b"bar"
301			})
302			.expect("method_call bar");
303		assert_eq!(r.receiver_hint, b"call".to_vec());
304	}
305
306	#[test]
307	fn extract_object_creation_unresolved_marks_name_match() {
308		let src = "class C {\n    void M() { var x = new Unknown(); }\n}\n";
309		let g = extract_default("F.cs", src, &make_anchor(), false);
310		let r = g
311			.refs()
312			.find(|r| r.kind == b"instantiates")
313			.expect("instantiates ref");
314		assert_eq!(r.confidence, b"name_match".to_vec());
315	}
316
317	#[test]
318	fn extract_class_attribute_emits_annotates() {
319		let src = "namespace Foo;\n[Serializable]\npublic class Bar {}\n";
320		let g = extract_default("F.cs", src, &make_anchor(), false);
321		let r = g
322			.refs()
323			.find(|r| r.kind == b"annotates")
324			.expect("annotates ref");
325		assert_eq!(
326			r.target.as_view().segments().last().unwrap().name,
327			b"Serializable"
328		);
329	}
330
331	#[test]
332	fn extract_method_attribute_emits_annotates() {
333		let src = "namespace Foo;\npublic class Bar {\n    [HttpGet] public void M() {}\n}\n";
334		let g = extract_default("F.cs", src, &make_anchor(), false);
335		let r = g
336			.refs()
337			.find(|r| r.kind == b"annotates")
338			.expect("annotates ref");
339		assert_eq!(
340			r.target.as_view().segments().last().unwrap().name,
341			b"HttpGet"
342		);
343	}
344
345	#[test]
346	fn extract_multiple_attribute_lists_each_emit_annotates() {
347		let src =
348			"namespace Foo;\npublic class Bar {\n    [Required] [Range(1,9)] public int N;\n}\n";
349		let g = extract_default("F.cs", src, &make_anchor(), false);
350		let names: Vec<&[u8]> = g
351			.refs()
352			.filter(|r| r.kind == b"annotates")
353			.map(|r| r.target.as_view().segments().last().unwrap().name)
354			.collect();
355		assert!(names.contains(&&b"Required"[..]));
356		assert!(names.contains(&&b"Range"[..]));
357	}
358
359	#[test]
360	fn extract_qualified_attribute_resolves_leaf_name() {
361		let src = "namespace Foo;\n[System.Serializable]\npublic class Bar {}\n";
362		let g = extract_default("F.cs", src, &make_anchor(), false);
363		assert!(g.refs().any(|r| r.kind == b"annotates"
364			&& r.target.as_view().segments().last().unwrap().name == b"Serializable"));
365	}
366
367	#[test]
368	fn extract_shallow_skips_param_and_local_defs() {
369		let src = "class B {\n    void M(int x) { int y = 1; var z = \"\"; }\n}\n";
370		let g = extract_default("F.cs", src, &make_anchor(), false);
371		assert!(
372			g.defs().all(|d| d.kind != b"param" && d.kind != b"local"),
373			"shallow extraction must not emit param/local defs"
374		);
375	}
376
377	#[test]
378	fn extract_deep_skips_blank_local() {
379		let src = "class B {\n    void M() { var _ = 1; var y = 2; }\n}\n";
380		let g = extract_default("F.cs", src, &make_anchor(), true);
381		let names: Vec<&[u8]> = g
382			.defs()
383			.filter(|d| d.kind == b"local")
384			.map(|d| d.moniker.as_view().segments().last().unwrap().name)
385			.collect();
386		assert_eq!(names, vec![&b"y"[..]]);
387	}
388}