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}