Skip to main content

agentic_codebase/parse/
csharp.rs

1//! C# parsing using tree-sitter.
2//!
3//! Extracts classes, interfaces, structs, enums, methods, properties, namespaces, usings.
4
5use std::path::Path;
6
7use crate::types::{AcbResult, CodeUnitType, Language, Visibility};
8
9use super::treesitter::{get_node_text, node_to_span};
10use super::{LanguageParser, RawCodeUnit};
11
12/// C# language parser.
13pub struct CSharpParser;
14
15impl Default for CSharpParser {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl CSharpParser {
22    /// Create a new C# parser.
23    pub fn new() -> Self {
24        Self
25    }
26
27    fn extract_from_node(
28        &self,
29        node: tree_sitter::Node,
30        source: &str,
31        file_path: &Path,
32        units: &mut Vec<RawCodeUnit>,
33        next_id: &mut u64,
34        parent_qname: &str,
35    ) {
36        let mut cursor = node.walk();
37        for child in node.children(&mut cursor) {
38            match child.kind() {
39                "class_declaration" => {
40                    self.extract_type_decl(
41                        child,
42                        source,
43                        file_path,
44                        units,
45                        next_id,
46                        parent_qname,
47                        CodeUnitType::Type,
48                    );
49                }
50                "struct_declaration" => {
51                    self.extract_type_decl(
52                        child,
53                        source,
54                        file_path,
55                        units,
56                        next_id,
57                        parent_qname,
58                        CodeUnitType::Type,
59                    );
60                }
61                "interface_declaration" => {
62                    self.extract_type_decl(
63                        child,
64                        source,
65                        file_path,
66                        units,
67                        next_id,
68                        parent_qname,
69                        CodeUnitType::Trait,
70                    );
71                }
72                "enum_declaration" => {
73                    self.extract_type_decl(
74                        child,
75                        source,
76                        file_path,
77                        units,
78                        next_id,
79                        parent_qname,
80                        CodeUnitType::Type,
81                    );
82                }
83                "record_declaration" => {
84                    self.extract_type_decl(
85                        child,
86                        source,
87                        file_path,
88                        units,
89                        next_id,
90                        parent_qname,
91                        CodeUnitType::Type,
92                    );
93                }
94                "namespace_declaration" | "file_scoped_namespace_declaration" => {
95                    self.extract_namespace(child, source, file_path, units, next_id, parent_qname);
96                }
97                "method_declaration" | "constructor_declaration" => {
98                    if let Some(unit) =
99                        self.extract_method(child, source, file_path, parent_qname, next_id)
100                    {
101                        units.push(unit);
102                    }
103                }
104                "property_declaration" => {
105                    if let Some(unit) =
106                        self.extract_property(child, source, file_path, parent_qname, next_id)
107                    {
108                        units.push(unit);
109                    }
110                }
111                "using_directive" => {
112                    if let Some(unit) =
113                        self.extract_using(child, source, file_path, parent_qname, next_id)
114                    {
115                        units.push(unit);
116                    }
117                }
118                // Recurse into declaration_list (body of namespace/class)
119                "declaration_list" => {
120                    self.extract_from_node(child, source, file_path, units, next_id, parent_qname);
121                }
122                _ => {}
123            }
124        }
125    }
126
127    #[allow(clippy::too_many_arguments)]
128    fn extract_type_decl(
129        &self,
130        node: tree_sitter::Node,
131        source: &str,
132        file_path: &Path,
133        units: &mut Vec<RawCodeUnit>,
134        next_id: &mut u64,
135        parent_qname: &str,
136        unit_type: CodeUnitType,
137    ) {
138        let name = match node.child_by_field_name("name") {
139            Some(n) => get_node_text(n, source).to_string(),
140            None => return,
141        };
142        let qname = cs_qname(parent_qname, &name);
143        let span = node_to_span(node);
144        let vis = extract_cs_visibility(node, source);
145
146        let id = *next_id;
147        *next_id += 1;
148
149        let mut unit = RawCodeUnit::new(
150            unit_type,
151            Language::CSharp,
152            name,
153            file_path.to_path_buf(),
154            span,
155        );
156        unit.temp_id = id;
157        unit.qualified_name = qname.clone();
158        unit.visibility = vis;
159        units.push(unit);
160
161        // Recurse into body
162        if let Some(body) = node.child_by_field_name("body") {
163            self.extract_from_node(body, source, file_path, units, next_id, &qname);
164        }
165    }
166
167    fn extract_namespace(
168        &self,
169        node: tree_sitter::Node,
170        source: &str,
171        file_path: &Path,
172        units: &mut Vec<RawCodeUnit>,
173        next_id: &mut u64,
174        parent_qname: &str,
175    ) {
176        let name = match node.child_by_field_name("name") {
177            Some(n) => get_node_text(n, source).to_string(),
178            None => return,
179        };
180        let qname = cs_qname(parent_qname, &name);
181        let span = node_to_span(node);
182
183        let id = *next_id;
184        *next_id += 1;
185
186        let mut unit = RawCodeUnit::new(
187            CodeUnitType::Module,
188            Language::CSharp,
189            name,
190            file_path.to_path_buf(),
191            span,
192        );
193        unit.temp_id = id;
194        unit.qualified_name = qname.clone();
195        unit.visibility = Visibility::Public;
196        units.push(unit);
197
198        // Recurse into namespace body (or file-scoped namespace children)
199        if let Some(body) = node.child_by_field_name("body") {
200            self.extract_from_node(body, source, file_path, units, next_id, &qname);
201        } else {
202            // File-scoped namespace: children are siblings
203            self.extract_from_node(node, source, file_path, units, next_id, &qname);
204        }
205    }
206
207    fn extract_method(
208        &self,
209        node: tree_sitter::Node,
210        source: &str,
211        file_path: &Path,
212        parent_qname: &str,
213        next_id: &mut u64,
214    ) -> Option<RawCodeUnit> {
215        let name_node = node.child_by_field_name("name")?;
216        let name = get_node_text(name_node, source).to_string();
217        let qname = cs_qname(parent_qname, &name);
218        let span = node_to_span(node);
219        let vis = extract_cs_visibility(node, source);
220
221        let id = *next_id;
222        *next_id += 1;
223
224        let is_test = has_cs_attribute(node, source, "Test")
225            || has_cs_attribute(node, source, "Fact")
226            || has_cs_attribute(node, source, "Theory")
227            || has_cs_attribute(node, source, "TestMethod");
228        let unit_type = if is_test {
229            CodeUnitType::Test
230        } else {
231            CodeUnitType::Function
232        };
233
234        let is_async = node_text_contains_modifier(node, source, "async");
235
236        let mut unit = RawCodeUnit::new(
237            unit_type,
238            Language::CSharp,
239            name,
240            file_path.to_path_buf(),
241            span,
242        );
243        unit.temp_id = id;
244        unit.qualified_name = qname;
245        unit.visibility = vis;
246        unit.is_async = is_async;
247
248        Some(unit)
249    }
250
251    fn extract_property(
252        &self,
253        node: tree_sitter::Node,
254        source: &str,
255        file_path: &Path,
256        parent_qname: &str,
257        next_id: &mut u64,
258    ) -> Option<RawCodeUnit> {
259        let name_node = node.child_by_field_name("name")?;
260        let name = get_node_text(name_node, source).to_string();
261        let qname = cs_qname(parent_qname, &name);
262        let span = node_to_span(node);
263        let vis = extract_cs_visibility(node, source);
264
265        let id = *next_id;
266        *next_id += 1;
267
268        let mut unit = RawCodeUnit::new(
269            CodeUnitType::Symbol,
270            Language::CSharp,
271            name,
272            file_path.to_path_buf(),
273            span,
274        );
275        unit.temp_id = id;
276        unit.qualified_name = qname;
277        unit.visibility = vis;
278
279        Some(unit)
280    }
281
282    fn extract_using(
283        &self,
284        node: tree_sitter::Node,
285        source: &str,
286        file_path: &Path,
287        parent_qname: &str,
288        next_id: &mut u64,
289    ) -> Option<RawCodeUnit> {
290        let text = get_node_text(node, source)
291            .trim_start_matches("using ")
292            .trim_start_matches("global ")
293            .trim_start_matches("static ")
294            .trim_end_matches(';')
295            .trim()
296            .to_string();
297        let span = node_to_span(node);
298
299        let id = *next_id;
300        *next_id += 1;
301
302        let mut unit = RawCodeUnit::new(
303            CodeUnitType::Import,
304            Language::CSharp,
305            text,
306            file_path.to_path_buf(),
307            span,
308        );
309        unit.temp_id = id;
310        unit.qualified_name = cs_qname(parent_qname, "using");
311
312        Some(unit)
313    }
314}
315
316impl LanguageParser for CSharpParser {
317    fn extract_units(
318        &self,
319        tree: &tree_sitter::Tree,
320        source: &str,
321        file_path: &Path,
322    ) -> AcbResult<Vec<RawCodeUnit>> {
323        let mut units = Vec::new();
324        let mut next_id = 0u64;
325
326        let module_name = file_path
327            .file_stem()
328            .and_then(|s| s.to_str())
329            .unwrap_or("unknown")
330            .to_string();
331
332        let root_span = node_to_span(tree.root_node());
333        let mut module_unit = RawCodeUnit::new(
334            CodeUnitType::Module,
335            Language::CSharp,
336            module_name.clone(),
337            file_path.to_path_buf(),
338            root_span,
339        );
340        module_unit.temp_id = next_id;
341        module_unit.qualified_name = module_name.clone();
342        next_id += 1;
343        units.push(module_unit);
344
345        self.extract_from_node(
346            tree.root_node(),
347            source,
348            file_path,
349            &mut units,
350            &mut next_id,
351            &module_name,
352        );
353
354        Ok(units)
355    }
356
357    fn is_test_file(&self, path: &Path, _source: &str) -> bool {
358        let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
359        name.ends_with("Tests.cs") || name.ends_with("Test.cs") || name.starts_with("Test")
360    }
361}
362
363fn cs_qname(parent: &str, name: &str) -> String {
364    if parent.is_empty() {
365        name.to_string()
366    } else {
367        format!("{}.{}", parent, name)
368    }
369}
370
371/// Extract visibility from C# modifiers.
372fn extract_cs_visibility(node: tree_sitter::Node, source: &str) -> Visibility {
373    let mut cursor = node.walk();
374    for child in node.children(&mut cursor) {
375        if child.kind() == "modifier" {
376            let text = get_node_text(child, source);
377            match text {
378                "public" => return Visibility::Public,
379                "private" => return Visibility::Private,
380                "protected" | "internal" => return Visibility::Public,
381                _ => {}
382            }
383        }
384    }
385    Visibility::Private // C# default is private for members
386}
387
388/// Check if a member has a specific C# attribute.
389fn has_cs_attribute(node: tree_sitter::Node, source: &str, attribute: &str) -> bool {
390    let mut cursor = node.walk();
391    for child in node.children(&mut cursor) {
392        if child.kind() == "attribute_list" {
393            let text = get_node_text(child, source);
394            if text.contains(attribute) {
395                return true;
396            }
397        }
398    }
399    false
400}
401
402/// Check if a node's modifiers contain a specific keyword.
403fn node_text_contains_modifier(node: tree_sitter::Node, source: &str, keyword: &str) -> bool {
404    let mut cursor = node.walk();
405    for child in node.children(&mut cursor) {
406        if child.kind() == "modifier" && get_node_text(child, source) == keyword {
407            return true;
408        }
409    }
410    false
411}