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