Skip to main content

code_analyze_core/languages/
csharp.rs

1// SPDX-FileCopyrightText: 2026 code-analyze-mcp contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use tree_sitter::Node;
5
6/// Tree-sitter query for extracting C# elements (methods, constructors, classes,
7/// interfaces, records, structs, and enums).
8pub const ELEMENT_QUERY: &str = r"
9(method_declaration name: (identifier) @method_name) @function
10(constructor_declaration name: (identifier) @ctor_name) @function
11(class_declaration name: (identifier) @class_name) @class
12(interface_declaration name: (identifier) @interface_name) @class
13(record_declaration name: (identifier) @record_name) @class
14(struct_declaration name: (identifier) @struct_name) @class
15(enum_declaration name: (identifier) @enum_name) @class
16";
17
18/// Tree-sitter query for extracting C# method invocations.
19pub const CALL_QUERY: &str = r"
20(invocation_expression
21  function: (member_access_expression name: (identifier) @call))
22(invocation_expression
23  function: (identifier) @call)
24";
25
26/// Tree-sitter query for extracting C# type references (base types, generic args).
27pub const REFERENCE_QUERY: &str = r"
28(base_list (identifier) @type_ref)
29(base_list (generic_name (identifier) @type_ref))
30(type_argument_list (identifier) @type_ref)
31(type_parameter_list (type_parameter (identifier) @type_ref))
32";
33
34/// Tree-sitter query for extracting C# `using` directives.
35///
36/// In tree-sitter-c-sharp 0.23.1 all `using` forms (namespace, `using static`,
37/// and `using alias = ...`) are represented by a single `using_directive` node
38/// kind.  There are no separate `using_static_directive` or
39/// `using_alias_directive` node kinds, so one pattern captures everything.
40pub const IMPORT_QUERY: &str = r"
41(using_directive) @import_path
42";
43
44/// Extract base class and interface names from a C# class, interface, or record node.
45///
46/// The parser calls this with the class/interface/record declaration node itself.
47/// We locate the `base_list` child and extract each base type name.
48#[must_use]
49pub fn extract_inheritance(node: &Node, source: &str) -> Vec<String> {
50    let mut bases = Vec::new();
51
52    // base_list is an unnamed child of class_declaration/interface_declaration/record_declaration
53    for i in 0..node.child_count() {
54        if let Some(child) = node.child(u32::try_from(i).unwrap_or(u32::MAX))
55            && child.kind() == "base_list"
56        {
57            bases.extend(extract_base_list(&child, source));
58            break;
59        }
60    }
61
62    bases
63}
64
65/// Extract base type names from a `base_list` node.
66fn extract_base_list(node: &Node, source: &str) -> Vec<String> {
67    let mut bases = Vec::new();
68
69    for i in 0..node.named_child_count() {
70        if let Some(child) = node.named_child(u32::try_from(i).unwrap_or(u32::MAX)) {
71            match child.kind() {
72                "identifier" => {
73                    let end = child.end_byte();
74                    if end <= source.len() {
75                        bases.push(source[child.start_byte()..end].to_string());
76                    }
77                }
78                "generic_name" => {
79                    // First named child of generic_name is the identifier.
80                    if let Some(id) = child.named_child(0)
81                        && id.kind() == "identifier"
82                    {
83                        let end = id.end_byte();
84                        if end <= source.len() {
85                            bases.push(source[id.start_byte()..end].to_string());
86                        }
87                    }
88                }
89                _ => {}
90            }
91        }
92    }
93
94    bases
95}
96
97/// Return the method or constructor name when `node` is a `method_declaration`
98/// or `constructor_declaration` that is nested inside a class, interface, or
99/// record body.
100///
101/// This follows the same contract as the Rust, Go, and C++ handlers: return
102/// the **method name** (the `name` field of the declaration node), or `None`
103/// when the node is not a class-level method.
104#[must_use]
105pub fn find_method_for_receiver(
106    node: &Node,
107    source: &str,
108    _depth: Option<usize>,
109) -> Option<String> {
110    if node.kind() != "method_declaration" && node.kind() != "constructor_declaration" {
111        return None;
112    }
113
114    // Only return a name when the node is nested inside a type body.
115    let mut current = *node;
116    let mut in_type_body = false;
117    while let Some(parent) = current.parent() {
118        match parent.kind() {
119            "class_declaration"
120            | "interface_declaration"
121            | "record_declaration"
122            | "struct_declaration"
123            | "enum_declaration" => {
124                in_type_body = true;
125                break;
126            }
127            _ => {
128                current = parent;
129            }
130        }
131    }
132
133    if !in_type_body {
134        return None;
135    }
136
137    node.child_by_field_name("name").and_then(|n| {
138        let end = n.end_byte();
139        if end <= source.len() {
140            Some(source[n.start_byte()..end].to_string())
141        } else {
142            None
143        }
144    })
145}
146
147#[cfg(all(test, feature = "lang-csharp"))]
148mod tests {
149    use super::*;
150    use tree_sitter::Parser;
151
152    fn parse_csharp(src: &str) -> tree_sitter::Tree {
153        let mut parser = Parser::new();
154        parser
155            .set_language(&tree_sitter_c_sharp::LANGUAGE.into())
156            .expect("Error loading C# language");
157        parser.parse(src, None).expect("Failed to parse C#")
158    }
159
160    #[test]
161    fn test_csharp_method_in_class() {
162        // Arrange
163        let src = "class Foo { void Bar() { Baz(); } void Baz() {} }";
164        let tree = parse_csharp(src);
165        let root = tree.root_node();
166
167        // Act -- collect method names by reading the `name` field of each
168        // `method_declaration` node directly (testing name field extraction).
169        let mut methods: Vec<String> = Vec::new();
170        let mut stack = vec![root];
171        while let Some(node) = stack.pop() {
172            if node.kind() == "method_declaration" {
173                if let Some(name_node) = node.child_by_field_name("name") {
174                    methods.push(src[name_node.start_byte()..name_node.end_byte()].to_string());
175                }
176            }
177            for i in 0..node.child_count() {
178                if let Some(child) = node.child(u32::try_from(i).unwrap_or(u32::MAX)) {
179                    stack.push(child);
180                }
181            }
182        }
183        methods.sort();
184
185        // Assert
186        assert_eq!(methods, vec!["Bar", "Baz"]);
187    }
188
189    #[test]
190    fn test_csharp_constructor() {
191        // Arrange
192        let src = "class Foo { public Foo() {} }";
193        let tree = parse_csharp(src);
194        let root = tree.root_node();
195
196        // Act
197        let mut ctors: Vec<String> = Vec::new();
198        let mut stack = vec![root];
199        while let Some(node) = stack.pop() {
200            if node.kind() == "constructor_declaration" {
201                if let Some(name_node) = node.child_by_field_name("name") {
202                    ctors.push(src[name_node.start_byte()..name_node.end_byte()].to_string());
203                }
204            }
205            for i in 0..node.child_count() {
206                if let Some(child) = node.child(u32::try_from(i).unwrap_or(u32::MAX)) {
207                    stack.push(child);
208                }
209            }
210        }
211
212        // Assert
213        assert_eq!(ctors, vec!["Foo"]);
214    }
215
216    #[test]
217    fn test_csharp_interface() {
218        // Arrange
219        let src = "interface IBar { void Do(); }";
220        let tree = parse_csharp(src);
221        let root = tree.root_node();
222
223        // Act
224        let mut interfaces: Vec<String> = Vec::new();
225        let mut stack = vec![root];
226        while let Some(node) = stack.pop() {
227            if node.kind() == "interface_declaration" {
228                if let Some(name_node) = node.child_by_field_name("name") {
229                    interfaces.push(src[name_node.start_byte()..name_node.end_byte()].to_string());
230                }
231            }
232            for i in 0..node.child_count() {
233                if let Some(child) = node.child(u32::try_from(i).unwrap_or(u32::MAX)) {
234                    stack.push(child);
235                }
236            }
237        }
238
239        // Assert
240        assert_eq!(interfaces, vec!["IBar"]);
241    }
242
243    #[test]
244    fn test_csharp_using_directive() {
245        // Arrange
246        let src = "using System;";
247        let tree = parse_csharp(src);
248        let root = tree.root_node();
249
250        // Act
251        let mut imports: Vec<String> = Vec::new();
252        let mut stack = vec![root];
253        while let Some(node) = stack.pop() {
254            if node.kind() == "using_directive" {
255                imports.push(src[node.start_byte()..node.end_byte()].to_string());
256            }
257            for i in 0..node.child_count() {
258                if let Some(child) = node.child(u32::try_from(i).unwrap_or(u32::MAX)) {
259                    stack.push(child);
260                }
261            }
262        }
263
264        // Assert
265        assert_eq!(imports, vec!["using System;"]);
266    }
267
268    #[test]
269    fn test_csharp_async_method() {
270        // Arrange -- async modifier is a sibling of the return type; name field unchanged
271        let src = "class C { async Task Foo() { await Bar(); } Task Bar() { return Task.CompletedTask; } }";
272        let tree = parse_csharp(src);
273        let root = tree.root_node();
274
275        // Act
276        let mut methods: Vec<String> = Vec::new();
277        let mut stack = vec![root];
278        while let Some(node) = stack.pop() {
279            if node.kind() == "method_declaration" {
280                if let Some(name_node) = node.child_by_field_name("name") {
281                    methods.push(src[name_node.start_byte()..name_node.end_byte()].to_string());
282                }
283            }
284            for i in 0..node.child_count() {
285                if let Some(child) = node.child(u32::try_from(i).unwrap_or(u32::MAX)) {
286                    stack.push(child);
287                }
288            }
289        }
290
291        // Assert -- Foo must be extracted even with async modifier
292        assert!(methods.contains(&"Foo".to_string()));
293    }
294
295    #[test]
296    fn test_csharp_generic_class() {
297        // Arrange -- type_parameter_list is a child of class_declaration; class name unchanged
298        let src = "class Generic<T> { T value; }";
299        let tree = parse_csharp(src);
300        let root = tree.root_node();
301
302        // Act
303        let mut classes: Vec<String> = Vec::new();
304        let mut stack = vec![root];
305        while let Some(node) = stack.pop() {
306            if node.kind() == "class_declaration" {
307                if let Some(name_node) = node.child_by_field_name("name") {
308                    classes.push(src[name_node.start_byte()..name_node.end_byte()].to_string());
309                }
310            }
311            for i in 0..node.child_count() {
312                if let Some(child) = node.child(u32::try_from(i).unwrap_or(u32::MAX)) {
313                    stack.push(child);
314                }
315            }
316        }
317
318        // Assert -- generic name captured without type parameters, consistent with Go
319        assert_eq!(classes, vec!["Generic"]);
320    }
321
322    #[test]
323    fn test_csharp_inheritance_extraction() {
324        // Arrange
325        let src = "class Dog : Animal, ICanRun {}";
326        let tree = parse_csharp(src);
327        let root = tree.root_node();
328
329        // Act -- find base_list node under class_declaration
330        let mut base_list_node: Option<Node> = None;
331        let mut stack = vec![root];
332        while let Some(node) = stack.pop() {
333            if node.kind() == "base_list" {
334                base_list_node = Some(node);
335                break;
336            }
337            for i in 0..node.child_count() {
338                if let Some(child) = node.child(u32::try_from(i).unwrap_or(u32::MAX)) {
339                    stack.push(child);
340                }
341            }
342        }
343
344        // The parser passes the class_declaration node, not the base_list
345        let mut class_node: Option<Node> = None;
346        let mut stack2 = vec![root];
347        while let Some(node) = stack2.pop() {
348            if node.kind() == "class_declaration" {
349                class_node = Some(node);
350                break;
351            }
352            for i in 0..node.child_count() {
353                if let Some(child) = node.child(u32::try_from(i).unwrap_or(u32::MAX)) {
354                    stack2.push(child);
355                }
356            }
357        }
358        let class = class_node.expect("class_declaration not found");
359        let _ = base_list_node; // retained for context clarity
360        let bases = extract_inheritance(&class, src);
361
362        // Assert
363        assert_eq!(bases, vec!["Animal", "ICanRun"]);
364    }
365
366    #[test]
367    fn test_csharp_find_method_for_receiver() {
368        // Arrange
369        let src = "class MyClass { void MyMethod() {} }";
370        let tree = parse_csharp(src);
371        let root = tree.root_node();
372
373        // Act -- find method_declaration node and check it returns the method name
374        let mut method_node: Option<Node> = None;
375        let mut stack = vec![root];
376        while let Some(node) = stack.pop() {
377            if node.kind() == "method_declaration" {
378                method_node = Some(node);
379                break;
380            }
381            for i in 0..node.child_count() {
382                if let Some(child) = node.child(u32::try_from(i).unwrap_or(u32::MAX)) {
383                    stack.push(child);
384                }
385            }
386        }
387
388        let method = method_node.expect("method_declaration not found");
389        let name = find_method_for_receiver(&method, src, None);
390
391        // Assert -- returns the method name, not the enclosing type name
392        assert_eq!(name, Some("MyMethod".to_string()));
393    }
394}