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}