Skip to main content

normalize_languages/
csharp.rs

1//! C# language support.
2
3use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
4use tree_sitter::Node;
5
6/// C# language support.
7pub struct CSharp;
8
9impl Language for CSharp {
10    fn name(&self) -> &'static str {
11        "C#"
12    }
13    fn extensions(&self) -> &'static [&'static str] {
14        &["cs"]
15    }
16    fn grammar_name(&self) -> &'static str {
17        "c-sharp"
18    }
19
20    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
21        Some(self)
22    }
23
24    fn signature_suffix(&self) -> &'static str {
25        " {}"
26    }
27
28    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
29        let mut doc_lines: Vec<String> = Vec::new();
30        let mut prev = node.prev_sibling();
31
32        while let Some(sibling) = prev {
33            if sibling.kind() == "comment" {
34                let text = &content[sibling.byte_range()];
35                if text.starts_with("///") {
36                    let line = text.strip_prefix("///").unwrap_or("").trim();
37                    let line = strip_xml_tags(line);
38                    if !line.is_empty() {
39                        doc_lines.push(line);
40                    }
41                } else if text.starts_with("/**") {
42                    let lines: Vec<&str> = text
43                        .strip_prefix("/**")
44                        .unwrap_or(text)
45                        .strip_suffix("*/")
46                        .unwrap_or(text)
47                        .lines()
48                        .map(|l| l.trim().strip_prefix('*').unwrap_or(l).trim())
49                        .filter(|l| !l.is_empty())
50                        .collect();
51                    if !lines.is_empty() {
52                        return Some(lines.join(" "));
53                    }
54                    return None;
55                } else {
56                    break;
57                }
58            } else if sibling.kind() == "attribute_list" {
59                // Skip [Attribute] between doc comment and declaration
60            } else {
61                break;
62            }
63            prev = sibling.prev_sibling();
64        }
65
66        if doc_lines.is_empty() {
67            return None;
68        }
69
70        doc_lines.reverse();
71        let joined = doc_lines.join(" ").trim().to_string();
72        if joined.is_empty() {
73            None
74        } else {
75            Some(joined)
76        }
77    }
78
79    fn refine_kind(
80        &self,
81        node: &Node,
82        _content: &str,
83        tag_kind: crate::SymbolKind,
84    ) -> crate::SymbolKind {
85        match node.kind() {
86            "struct_declaration" => crate::SymbolKind::Struct,
87            "enum_declaration" => crate::SymbolKind::Enum,
88            "interface_declaration" => crate::SymbolKind::Interface,
89            "record_declaration" => crate::SymbolKind::Class,
90            _ => tag_kind,
91        }
92    }
93
94    fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
95        let mut implements = Vec::new();
96        let mut cursor = node.walk();
97        for child in node.children(&mut cursor) {
98            if child.kind() == "base_list" {
99                let mut bl = child.walk();
100                for t in child.children(&mut bl) {
101                    if t.kind() == "identifier" || t.kind() == "generic_name" {
102                        implements.push(content[t.byte_range()].to_string());
103                    }
104                }
105            }
106        }
107        crate::ImplementsInfo {
108            is_interface: false,
109            implements,
110        }
111    }
112
113    fn build_signature(&self, node: &Node, content: &str) -> String {
114        let name = match self.node_name(node, content) {
115            Some(n) => n,
116            None => {
117                return content[node.byte_range()]
118                    .lines()
119                    .next()
120                    .unwrap_or("")
121                    .trim()
122                    .to_string();
123            }
124        };
125        match node.kind() {
126            "method_declaration" | "constructor_declaration" | "property_declaration" => {
127                let params = node
128                    .child_by_field_name("parameters")
129                    .map(|p| content[p.byte_range()].to_string())
130                    .unwrap_or_default();
131                let return_type = node
132                    .child_by_field_name("type")
133                    .or_else(|| node.child_by_field_name("returns"))
134                    .map(|t| content[t.byte_range()].to_string());
135                match return_type {
136                    Some(ret) => format!("{} {}{}", ret, name, params),
137                    None => format!("{}{}", name, params),
138                }
139            }
140            "class_declaration" => format!("class {}", name),
141            "struct_declaration" => format!("struct {}", name),
142            "interface_declaration" => format!("interface {}", name),
143            "enum_declaration" => format!("enum {}", name),
144            "record_declaration" => format!("record {}", name),
145            "namespace_declaration" => format!("namespace {}", name),
146            _ => {
147                let text = &content[node.byte_range()];
148                text.lines().next().unwrap_or(text).trim().to_string()
149            }
150        }
151    }
152
153    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
154        if node.kind() != "using_directive" {
155            return Vec::new();
156        }
157
158        let line = node.start_position().row + 1;
159        let text = &content[node.byte_range()];
160
161        // Check for static using
162        let is_static = text.contains("static ");
163
164        // Get the namespace/type
165        let mut cursor = node.walk();
166        for child in node.children(&mut cursor) {
167            if child.kind() == "qualified_name" || child.kind() == "identifier" {
168                let module = content[child.byte_range()].to_string();
169                return vec![Import {
170                    module,
171                    names: Vec::new(),
172                    alias: if is_static {
173                        Some("static".to_string())
174                    } else {
175                        None
176                    },
177                    is_wildcard: false,
178                    is_relative: false,
179                    line,
180                }];
181            }
182        }
183
184        Vec::new()
185    }
186
187    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
188        // C#: using Namespace; or using Alias = Namespace;
189        if let Some(ref alias) = import.alias {
190            format!("using {} = {};", alias, import.module)
191        } else {
192            format!("using {};", import.module)
193        }
194    }
195
196    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
197        let name = symbol.name.as_str();
198        match symbol.kind {
199            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
200            crate::SymbolKind::Module => name == "tests" || name == "test",
201            _ => false,
202        }
203    }
204
205    fn test_file_globs(&self) -> &'static [&'static str] {
206        &["**/*Test.cs", "**/*Tests.cs"]
207    }
208
209    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
210        node.child_by_field_name("body")
211    }
212
213    fn analyze_container_body(
214        &self,
215        body_node: &Node,
216        content: &str,
217        inner_indent: &str,
218    ) -> Option<ContainerBody> {
219        crate::body::analyze_brace_body(body_node, content, inner_indent)
220    }
221
222    fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
223        let mut attrs = Vec::new();
224        let mut cursor = node.walk();
225        for child in node.children(&mut cursor) {
226            if child.kind() == "attribute_list" {
227                attrs.push(content[child.byte_range()].to_string());
228            }
229        }
230        attrs
231    }
232
233    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
234        let mut cursor = node.walk();
235        for child in node.children(&mut cursor) {
236            if child.kind() == "modifier" {
237                let mod_text = &content[child.byte_range()];
238                if mod_text == "private" {
239                    return Visibility::Private;
240                }
241                if mod_text == "protected" {
242                    return Visibility::Protected;
243                }
244                if mod_text == "internal" {
245                    return Visibility::Protected;
246                }
247                if mod_text == "public" {
248                    return Visibility::Public;
249                }
250            }
251        }
252        // C# default visibility depends on context, but for skeleton purposes treat as public
253        Visibility::Public
254    }
255}
256
257impl LanguageSymbols for CSharp {}
258
259/// Strip common XML doc comment tags.
260fn strip_xml_tags(s: &str) -> String {
261    let mut result = String::with_capacity(s.len());
262    let mut in_tag = false;
263    for ch in s.chars() {
264        if ch == '<' {
265            in_tag = true;
266        } else if ch == '>' {
267            in_tag = false;
268        } else if !in_tag {
269            result.push(ch);
270        }
271    }
272    result.trim().to_string()
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use crate::validate_unused_kinds_audit;
279
280    #[test]
281    fn unused_node_kinds_audit() {
282        #[rustfmt::skip]
283        let documented_unused: &[&str] = &[
284            // C# grammar uses "c_sharp" - check cross_check output for actual kinds
285            // This is a placeholder - run cross_check_node_kinds to get the full list
286        ];
287
288        // C# may need manual verification - skip for now if empty
289        if !documented_unused.is_empty() {
290            validate_unused_kinds_audit(&CSharp, documented_unused)
291                .expect("C# unused node kinds audit failed");
292        }
293    }
294}