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