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::{ExtractionContext, KindSpec, LangExtractor, ParsedDocument};
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 <Lang as LangExtractor>::extract(uri, source, anchor, deep, _presets)
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 parse(_uri: &str, source: &str) -> ParsedDocument {
78 ParsedDocument::new(parse(source))
79 }
80
81 fn file_root(uri: &str, anchor: &Moniker) -> Option<Moniker> {
82 Some(canonicalize::compute_module_moniker(anchor, uri))
83 }
84
85 fn extract_parsed(
86 context: ExtractionContext<'_, Self::Presets>,
87 document: &ParsedDocument,
88 ) -> CodeGraph {
89 sdk_pipeline::extract(
90 context.uri,
91 context.source,
92 document,
93 context.anchor,
94 context.deep,
95 )
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::core::moniker::MonikerBuilder;
103 use crate::lang::assert_conformance;
104
105 fn make_anchor() -> Moniker {
106 MonikerBuilder::new().project(b"app").build()
107 }
108
109 fn extract_default(uri: &str, source: &str, anchor: &Moniker, deep: bool) -> CodeGraph {
110 let g = extract(uri, source, anchor, deep, &Presets::default());
111 assert_conformance::<super::Lang>(&g, anchor);
112 g
113 }
114
115 #[test]
116 fn parse_empty_returns_compilation_unit() {
117 let tree = parse("");
118 assert_eq!(tree.root_node().kind(), "compilation_unit");
119 }
120
121 #[test]
122 fn extract_struct_emits_struct_def() {
123 let src = "namespace Foo;\npublic struct Bar {}\n";
124 let g = extract_default("F.cs", src, &make_anchor(), false);
125 assert!(g.defs().any(|d| d.kind == b"struct"
126 && d.moniker.as_view().segments().last().unwrap().name == b"Bar"));
127 }
128
129 #[test]
130 fn extract_enum_emits_enum_def() {
131 let src = "namespace Foo;\npublic enum Color { Red, Green }\n";
132 let g = extract_default("F.cs", src, &make_anchor(), false);
133 let e = g.defs().find(|d| d.kind == b"enum").expect("enum def");
134 assert_eq!(
135 e.moniker.as_view().segments().last().unwrap().name,
136 b"Color"
137 );
138 }
139
140 #[test]
141 fn extract_enum_emits_enum_constants() {
142 let src = "namespace Foo;\npublic enum Color { Red, Green }\n";
143 let g = extract_default("F.cs", src, &make_anchor(), false);
144 let red = MonikerBuilder::new()
145 .project(b"app")
146 .segment(b"lang", b"cs")
147 .segment(b"module", b"F")
148 .segment(b"enum", b"Color")
149 .segment(b"enum_constant", b"Red")
150 .build();
151 let green = MonikerBuilder::new()
152 .project(b"app")
153 .segment(b"lang", b"cs")
154 .segment(b"module", b"F")
155 .segment(b"enum", b"Color")
156 .segment(b"enum_constant", b"Green")
157 .build();
158 assert!(g.contains(&red), "missing enum_constant:Red");
159 assert!(g.contains(&green), "missing enum_constant:Green");
160 }
161
162 #[test]
163 fn extract_top_level_type_default_visibility_is_internal() {
164 let src = "namespace Foo;\nclass Bar {}\n";
165 let g = extract_default("F.cs", src, &make_anchor(), false);
166 let bar = g.defs().find(|d| d.kind == b"class").expect("class def");
167 assert_eq!(
168 bar.visibility,
169 b"package".to_vec(),
170 "top-level C# class without modifier defaults to internal (= VIS_PACKAGE)"
171 );
172 }
173
174 #[test]
175 fn extract_block_namespace_descends_into_body() {
176 let src = "namespace Foo {\n public class Bar {}\n}\n";
177 let g = extract_default("F.cs", src, &make_anchor(), false);
178 assert!(g.defs().any(|d| d.kind == b"class"));
179 }
180
181 #[test]
182 fn extract_method_default_visibility_is_private() {
183 let src = "namespace Foo;\npublic class Bar {\n int Hidden() { return 0; }\n}\n";
184 let g = extract_default("F.cs", src, &make_anchor(), false);
185 let m = g.defs().find(|d| d.kind == b"method").expect("method def");
186 assert_eq!(m.visibility, b"private".to_vec());
187 }
188
189 #[test]
190 fn extract_method_params_modifier_emits_ellipsis() {
191 let src =
192 "namespace Foo;\npublic class Bar {\n public void Log(params object[] args) {}\n}\n";
193 let g = extract_default("F.cs", src, &make_anchor(), false);
194 let m = g.defs().find(|d| d.kind == b"method").expect("method def");
195 assert_eq!(
196 m.moniker.as_view().segments().last().unwrap().name,
197 b"Log(...)"
198 );
199 }
200
201 #[test]
202 fn extract_nested_class_attached_to_outer_class() {
203 let src = "namespace Foo;\npublic class Outer {\n public class Inner {}\n}\n";
204 let g = extract_default("F.cs", src, &make_anchor(), false);
205 let inner = MonikerBuilder::new()
206 .project(b"app")
207 .segment(b"lang", b"cs")
208 .segment(b"module", b"F")
209 .segment(b"class", b"Outer")
210 .segment(b"class", b"Inner")
211 .build();
212 assert!(g.contains(&inner));
213 }
214
215 #[test]
216 fn extract_expression_bodied_property_emits_property_def() {
217 let src = "namespace Foo;\npublic class Bar {\n public int N => 42;\n}\n";
218 let g = extract_default("F.cs", src, &make_anchor(), false);
219 assert!(g.defs().any(|d| d.kind == b"property"
220 && d.moniker.as_view().segments().last().unwrap().name == b"N"));
221 }
222
223 #[test]
224 fn extract_property_with_user_type_emits_uses_type() {
225 let src = "namespace Foo;\npublic class Other {}\npublic class Bar {\n public Other Item { get; set; }\n}\n";
226 let g = extract_default("F.cs", src, &make_anchor(), false);
227 assert!(g.refs().any(|r| r.kind == b"uses_type"
228 && r.target.as_view().segments().last().unwrap().name == b"Other"));
229 }
230
231 #[test]
232 fn extract_global_qualified_system_type_is_external() {
233 let src = "class B { global::System.Resources.ResourceManager Resources { get; } }";
234 let g = extract_default("F.cs", src, &make_anchor(), false);
235 let reference = g
236 .refs()
237 .find(|reference| reference.kind == b"uses_type")
238 .expect("uses_type reference");
239 assert_eq!(reference.confidence, b"external".to_vec());
240 let segments = reference.target.as_view().segments().collect::<Vec<_>>();
241 assert!(
242 segments
243 .iter()
244 .any(|segment| { segment.kind == b"sdk" && segment.name == b"cs" })
245 );
246 assert!(
247 segments
248 .iter()
249 .any(|segment| { segment.kind == b"path" && segment.name == b"System" })
250 );
251 assert!(
252 segments
253 .iter()
254 .any(|segment| { segment.kind == b"path" && segment.name == b"ResourceManager" })
255 );
256 }
257
258 #[test]
259 fn microsoft_namespace_is_not_implicitly_owned_by_the_sdk() {
260 let src = "using Microsoft.Extensions.Logging; class Service {}";
261 let g = extract_default("Service.cs", src, &make_anchor(), false);
262 let reference = g
263 .refs()
264 .find(|reference| reference.kind == b"imports_module")
265 .expect("using reference");
266 let first = reference.target.as_view().segments().next().unwrap();
267 assert_eq!(first.kind, b"external_pkg");
268 assert_eq!(first.name, b"Microsoft");
269 }
270
271 #[test]
272 fn extract_base_list_emits_extends_per_entry() {
273 let src = "namespace Foo;\npublic class Base {}\npublic class Foo : Base, IBar {}\n";
274 let g = extract_default("F.cs", src, &make_anchor(), false);
275 let names: Vec<&[u8]> = g
276 .refs()
277 .filter(|r| r.kind == b"extends")
278 .map(|r| r.target.as_view().segments().last().unwrap().name)
279 .collect();
280 assert!(names.contains(&&b"Base"[..]));
281 assert!(names.contains(&&b"IBar"[..]));
282 }
283
284 #[test]
285 fn extract_generic_base_emits_extends_on_head_and_uses_type_on_arg() {
286 let src = "namespace Foo;\npublic class List<T> {}\npublic class Bar : List<int> {}\n";
287 let g = extract_default("F.cs", src, &make_anchor(), false);
288 assert!(g.refs().any(|r| r.kind == b"extends"
289 && r.target.as_view().segments().last().unwrap().name == b"List"));
290 }
291
292 #[test]
293 fn extract_interface_base_emits_extends_per_entry() {
294 let src = "namespace Foo;\npublic interface IFoo : IBar, IBaz {}\n";
295 let g = extract_default("F.cs", src, &make_anchor(), false);
296 let count = g.refs().filter(|r| r.kind == b"extends").count();
297 assert_eq!(count, 2);
298 }
299
300 #[test]
301 fn extract_using_third_party_marks_imported() {
302 let g = extract_default("F.cs", "using Newtonsoft.Json;\n", &make_anchor(), false);
303 let r = g
304 .refs()
305 .find(|r| r.kind == b"imports_module")
306 .expect("imports_module ref");
307 assert_eq!(r.confidence, b"imported".to_vec());
308 }
309
310 #[test]
311 fn extract_system_prefixed_nuget_namespace_stays_manifest_owned() {
312 for namespace in [
313 "System.Reactive.Linq",
314 "System.CommandLine",
315 "System.IO.Abstractions",
316 "System.Linq.Dynamic.Core",
317 ] {
318 let source = format!("using {namespace};\n");
319 let g = extract_default("F.cs", &source, &make_anchor(), false);
320 let reference = g
321 .refs()
322 .find(|reference| reference.kind == b"imports_module")
323 .expect("imports_module ref");
324 assert_eq!(
325 reference.confidence,
326 b"imported".to_vec(),
327 "{namespace} must be resolved through the project manifest",
328 );
329 assert_eq!(
330 reference.target.as_view().segments().next().unwrap().kind,
331 b"external_pkg",
332 );
333 }
334 }
335
336 #[test]
337 fn extract_qualified_bcl_types_outside_the_short_catalog_use_sdk_targets() {
338 let source = concat!(
339 "class Service { ",
340 "System.IO.StreamReader reader; ",
341 "System.Net.Http.HttpClient client = new System.Net.Http.HttpClient(); ",
342 "}",
343 );
344 let g = extract_default("Service.cs", source, &make_anchor(), false);
345 for type_name in ["StreamReader", "HttpClient"] {
346 let reference = g
347 .refs()
348 .find(|reference| {
349 reference.kind == b"uses_type"
350 && reference
351 .target
352 .as_view()
353 .segments()
354 .last()
355 .is_some_and(|segment| segment.name == type_name.as_bytes())
356 })
357 .unwrap_or_else(|| panic!("missing uses_type for {type_name}"));
358 assert_eq!(
359 reference.target.as_view().segments().next().unwrap().kind,
360 b"sdk",
361 "{type_name} must be owned by the target framework",
362 );
363 assert_eq!(reference.confidence, b"external".to_vec());
364 }
365 }
366
367 #[test]
368 fn extract_using_alias_records_alias_attr() {
369 let g = extract_default("F.cs", "using IO = System.IO;\n", &make_anchor(), false);
370 let r = g
371 .refs()
372 .find(|r| r.kind == b"imports_module")
373 .expect("imports_module ref");
374 assert_eq!(r.alias, b"IO".to_vec());
375 }
376
377 #[test]
378 fn extract_global_using_emits_imports_module() {
379 let g = extract_default("F.cs", "global using System;\n", &make_anchor(), false);
380 assert!(
381 g.refs()
382 .any(|r| r.kind == b"imports_module" && r.confidence == b"external".to_vec())
383 );
384 }
385
386 #[test]
387 fn extract_using_static_emits_imports_module() {
388 let g = extract_default("F.cs", "using static System.Math;\n", &make_anchor(), false);
389 assert!(g.refs().any(|r| r.kind == b"imports_module"));
390 }
391
392 #[test]
393 fn extract_simple_invocation_to_unresolved_callee_uses_name_only() {
394 let src = "class B {\n void M() { Helper(1, 2); }\n}\n";
395 let g = extract_default("F.cs", src, &make_anchor(), false);
396 let r = g
397 .refs()
398 .find(|r| {
399 r.kind == b"calls"
400 && r.target.as_view().segments().last().unwrap().name == b"Helper"
401 })
402 .expect("calls Helper (name-only)");
403 assert_eq!(r.confidence, b"name_match".to_vec());
404 assert_eq!(r.call_name, b"Helper".to_vec());
405 assert_eq!(r.call_arity, Some(2));
406 }
407
408 #[test]
409 fn extract_chained_member_call_receiver_hint_is_call() {
410 let src = "class B {\n void M() { foo().bar(); }\n}\n";
411 let g = extract_default("F.cs", src, &make_anchor(), false);
412 let r = g
413 .refs()
414 .find(|r| {
415 r.kind == b"method_call"
416 && r.target.as_view().segments().last().unwrap().name == b"bar"
417 })
418 .expect("method_call bar");
419 assert_eq!(r.receiver_hint, b"call".to_vec());
420 assert_eq!(r.call_name, b"bar".to_vec());
421 assert_eq!(r.call_arity, Some(0));
422 }
423
424 #[test]
425 fn extract_callable_defs_expose_linkage_metadata() {
426 let src = "class B { int Sum(int left, int right) => left + right; }";
427 let g = extract_default("F.cs", src, &make_anchor(), false);
428 let def = g
429 .defs()
430 .find(|def| def.kind == b"method")
431 .expect("method definition");
432 assert_eq!(def.call_name, b"Sum".to_vec());
433 assert_eq!(def.call_arity, Some(2));
434 }
435
436 #[test]
437 fn extract_callable_arity_comes_from_syntax_not_type_commas() {
438 let src = "using System.Collections.Generic; class B { void One(Dictionary<string, int> value) {} void Many(params\nobject[] values) {} }";
439 let g = extract_default("F.cs", src, &make_anchor(), false);
440 let one = g
441 .defs()
442 .find(|def| def.call_name == b"One")
443 .expect("One definition");
444 let many = g
445 .defs()
446 .find(|def| def.call_name == b"Many")
447 .expect("Many definition");
448 assert_eq!(one.call_arity, Some(1));
449 assert_eq!(many.call_arity, None);
450 }
451
452 #[test]
453 fn extract_parameter_type_emits_typed_binding() {
454 let src = "class Worker {} class B { void Run(Worker worker) {} }";
455 let g = extract_default("F.cs", src, &make_anchor(), false);
456 let reference = g
457 .refs()
458 .find(|reference| reference.kind == b"typed_as")
459 .expect("typed_as reference");
460 assert_eq!(reference.alias, b"worker".to_vec());
461 assert_eq!(reference.confidence, b"resolved".to_vec());
462 }
463
464 #[test]
465 fn extract_field_and_inferred_local_types_emit_typed_bindings() {
466 let src =
467 "class Worker {} class B { Worker worker; void Run() { var local = new Worker(); } }";
468 let g = extract_default("F.cs", src, &make_anchor(), false);
469 let typed = g
470 .refs()
471 .filter(|reference| reference.kind == b"typed_as")
472 .collect::<Vec<_>>();
473 assert!(typed.iter().any(|reference| {
474 reference.alias.is_empty() && g.def_at(reference.source).kind == b"field"
475 }));
476 assert!(
477 typed
478 .iter()
479 .any(|reference| reference.alias == b"local".to_vec())
480 );
481 }
482
483 #[test]
484 fn extract_object_creation_unresolved_marks_name_match() {
485 let src = "class C {\n void M() { var x = new Unknown(); }\n}\n";
486 let g = extract_default("F.cs", src, &make_anchor(), false);
487 let r = g
488 .refs()
489 .find(|r| r.kind == b"instantiates")
490 .expect("instantiates ref");
491 assert_eq!(r.confidence, b"name_match".to_vec());
492 }
493
494 #[test]
495 fn extract_class_attribute_emits_annotates() {
496 let src = "namespace Foo;\n[Serializable]\npublic class Bar {}\n";
497 let g = extract_default("F.cs", src, &make_anchor(), false);
498 let r = g
499 .refs()
500 .find(|r| r.kind == b"annotates")
501 .expect("annotates ref");
502 assert_eq!(
503 r.target.as_view().segments().last().unwrap().name,
504 b"Serializable"
505 );
506 }
507
508 #[test]
509 fn extract_method_attribute_emits_annotates() {
510 let src = "namespace Foo;\npublic class Bar {\n [HttpGet] public void M() {}\n}\n";
511 let g = extract_default("F.cs", src, &make_anchor(), false);
512 let r = g
513 .refs()
514 .find(|r| r.kind == b"annotates")
515 .expect("annotates ref");
516 assert_eq!(
517 r.target.as_view().segments().last().unwrap().name,
518 b"HttpGet"
519 );
520 }
521
522 #[test]
523 fn extract_multiple_attribute_lists_each_emit_annotates() {
524 let src =
525 "namespace Foo;\npublic class Bar {\n [Required] [Range(1,9)] public int N;\n}\n";
526 let g = extract_default("F.cs", src, &make_anchor(), false);
527 let names: Vec<&[u8]> = g
528 .refs()
529 .filter(|r| r.kind == b"annotates")
530 .map(|r| r.target.as_view().segments().last().unwrap().name)
531 .collect();
532 assert!(names.contains(&&b"Required"[..]));
533 assert!(names.contains(&&b"Range"[..]));
534 }
535
536 #[test]
537 fn extract_qualified_attribute_resolves_leaf_name() {
538 let src = "namespace Foo;\n[System.Serializable]\npublic class Bar {}\n";
539 let g = extract_default("F.cs", src, &make_anchor(), false);
540 assert!(g.refs().any(|r| r.kind == b"annotates"
541 && r.target.as_view().segments().last().unwrap().name == b"Serializable"));
542 }
543
544 #[test]
545 fn extract_shallow_skips_param_and_local_defs() {
546 let src = "class B {\n void M(int x) { int y = 1; var z = \"\"; }\n}\n";
547 let g = extract_default("F.cs", src, &make_anchor(), false);
548 assert!(
549 g.defs().all(|d| d.kind != b"param" && d.kind != b"local"),
550 "shallow extraction must not emit param/local defs"
551 );
552 }
553
554 #[test]
555 fn extract_deep_skips_blank_local() {
556 let src = "class B {\n void M() { var _ = 1; var y = 2; }\n}\n";
557 let g = extract_default("F.cs", src, &make_anchor(), true);
558 let names: Vec<&[u8]> = g
559 .defs()
560 .filter(|d| d.kind == b"local")
561 .map(|d| d.moniker.as_view().segments().last().unwrap().name)
562 .collect();
563 assert_eq!(names, vec![&b"y"[..]]);
564 }
565}