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}