Skip to main content

normalize_languages/
csharp.rs

1//! C# language support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8/// C# language support.
9pub struct CSharp;
10
11impl Language for CSharp {
12    fn name(&self) -> &'static str {
13        "C#"
14    }
15    fn extensions(&self) -> &'static [&'static str] {
16        &["cs"]
17    }
18    fn grammar_name(&self) -> &'static str {
19        "c_sharp"
20    }
21
22    fn has_symbols(&self) -> bool {
23        true
24    }
25
26    fn container_kinds(&self) -> &'static [&'static str] {
27        &[
28            "class_declaration",
29            "struct_declaration",
30            "interface_declaration",
31            "enum_declaration",
32            "record_declaration",
33            "namespace_declaration",
34        ]
35    }
36
37    fn function_kinds(&self) -> &'static [&'static str] {
38        &[
39            "method_declaration",
40            "constructor_declaration",
41            "property_declaration",
42            "local_function_statement",
43            "lambda_expression",
44        ]
45    }
46
47    fn type_kinds(&self) -> &'static [&'static str] {
48        &[
49            "class_declaration",
50            "struct_declaration",
51            "interface_declaration",
52            "enum_declaration",
53            "record_declaration",
54            "delegate_declaration",
55        ]
56    }
57
58    fn import_kinds(&self) -> &'static [&'static str] {
59        &["using_directive"]
60    }
61
62    fn public_symbol_kinds(&self) -> &'static [&'static str] {
63        &[
64            "class_declaration",
65            "struct_declaration",
66            "interface_declaration",
67            "enum_declaration",
68            "record_declaration",
69            "method_declaration",
70            "property_declaration",
71        ]
72    }
73
74    fn visibility_mechanism(&self) -> VisibilityMechanism {
75        VisibilityMechanism::AccessModifier
76    }
77
78    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
79        if self.get_visibility(node, content) != Visibility::Public {
80            return Vec::new();
81        }
82
83        let name = match self.node_name(node, content) {
84            Some(n) => n.to_string(),
85            None => return Vec::new(),
86        };
87
88        let kind = match node.kind() {
89            "class_declaration" => SymbolKind::Class,
90            "struct_declaration" => SymbolKind::Struct,
91            "interface_declaration" => SymbolKind::Interface,
92            "enum_declaration" => SymbolKind::Enum,
93            "record_declaration" => SymbolKind::Class,
94            "method_declaration" | "constructor_declaration" => SymbolKind::Method,
95            "property_declaration" => SymbolKind::Variable,
96            _ => return Vec::new(),
97        };
98
99        vec![Export {
100            name,
101            kind,
102            line: node.start_position().row + 1,
103        }]
104    }
105
106    fn scope_creating_kinds(&self) -> &'static [&'static str] {
107        &[
108            "for_statement",
109            "foreach_statement",
110            "while_statement",
111            "do_statement",
112            "try_statement",
113            "catch_clause",
114            "switch_statement",
115            "using_statement",
116            "block",
117        ]
118    }
119
120    fn control_flow_kinds(&self) -> &'static [&'static str] {
121        &[
122            "if_statement",
123            "for_statement",
124            "foreach_statement",
125            "while_statement",
126            "do_statement",
127            "switch_statement",
128            "try_statement",
129            "return_statement",
130            "break_statement",
131            "continue_statement",
132            "throw_statement",
133            "yield_statement",
134        ]
135    }
136
137    fn complexity_nodes(&self) -> &'static [&'static str] {
138        &[
139            "if_statement",
140            "for_statement",
141            "foreach_statement",
142            "while_statement",
143            "do_statement",
144            "switch_section",
145            "catch_clause",
146            "conditional_expression",
147            "binary_expression",
148        ]
149    }
150
151    fn nesting_nodes(&self) -> &'static [&'static str] {
152        &[
153            "if_statement",
154            "for_statement",
155            "foreach_statement",
156            "while_statement",
157            "do_statement",
158            "switch_statement",
159            "try_statement",
160            "method_declaration",
161            "class_declaration",
162            "lambda_expression",
163        ]
164    }
165
166    fn signature_suffix(&self) -> &'static str {
167        " {}"
168    }
169
170    fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
171        let name = self.node_name(node, content)?;
172
173        let params = node
174            .child_by_field_name("parameters")
175            .map(|p| content[p.byte_range()].to_string())
176            .unwrap_or_else(|| "()".to_string());
177
178        let return_type = node
179            .child_by_field_name("type")
180            .or_else(|| node.child_by_field_name("returns"))
181            .map(|t| content[t.byte_range()].to_string());
182
183        let signature = match return_type {
184            Some(ret) => format!("{} {}{}", ret, name, params),
185            None => format!("{}{}", name, params),
186        };
187
188        // Check for override modifier
189        let is_override = {
190            let mut cursor = node.walk();
191            let children: Vec<_> = node.children(&mut cursor).collect();
192            children.iter().any(|child| {
193                child.kind() == "modifier" && child.child(0).map(|c| c.kind()) == Some("override")
194            })
195        };
196
197        Some(Symbol {
198            name: name.to_string(),
199            kind: if node.kind() == "property_declaration" {
200                SymbolKind::Variable
201            } else {
202                SymbolKind::Method
203            },
204            signature,
205            docstring: self.extract_docstring(node, content),
206            attributes: Vec::new(),
207            start_line: node.start_position().row + 1,
208            end_line: node.end_position().row + 1,
209            visibility: self.get_visibility(node, content),
210            children: Vec::new(),
211            is_interface_impl: is_override,
212            implements: Vec::new(),
213        })
214    }
215
216    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
217        let name = self.node_name(node, content)?;
218        let (kind, keyword) = match node.kind() {
219            "struct_declaration" => (SymbolKind::Struct, "struct"),
220            "interface_declaration" => (SymbolKind::Interface, "interface"),
221            "enum_declaration" => (SymbolKind::Enum, "enum"),
222            "record_declaration" => (SymbolKind::Class, "record"),
223            "namespace_declaration" => (SymbolKind::Module, "namespace"),
224            _ => (SymbolKind::Class, "class"),
225        };
226
227        Some(Symbol {
228            name: name.to_string(),
229            kind,
230            signature: format!("{} {}", keyword, name),
231            docstring: self.extract_docstring(node, content),
232            attributes: Vec::new(),
233            start_line: node.start_position().row + 1,
234            end_line: node.end_position().row + 1,
235            visibility: self.get_visibility(node, content),
236            children: Vec::new(),
237            is_interface_impl: false,
238            implements: Vec::new(),
239        })
240    }
241
242    fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
243        self.extract_container(node, content)
244    }
245
246    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
247        // Look for XML doc comments (/// or /** */)
248        let mut prev = node.prev_sibling();
249        let mut doc_lines = Vec::new();
250
251        while let Some(sibling) = prev {
252            let text = &content[sibling.byte_range()];
253            if sibling.kind() == "comment" {
254                if text.starts_with("///") {
255                    // Single-line XML doc comment
256                    let line = text.strip_prefix("///").unwrap_or(text).trim();
257                    // Strip XML tags for cleaner output
258                    let clean = strip_xml_tags(line);
259                    if !clean.is_empty() {
260                        doc_lines.insert(0, clean);
261                    }
262                } else if text.starts_with("/**") {
263                    // Multi-line doc comment
264                    let inner = text
265                        .strip_prefix("/**")
266                        .unwrap_or(text)
267                        .strip_suffix("*/")
268                        .unwrap_or(text);
269                    for line in inner.lines() {
270                        let clean = line.trim().strip_prefix("*").unwrap_or(line).trim();
271                        let clean = strip_xml_tags(clean);
272                        if !clean.is_empty() {
273                            doc_lines.push(clean);
274                        }
275                    }
276                    break;
277                } else {
278                    break;
279                }
280            } else {
281                break;
282            }
283            prev = sibling.prev_sibling();
284        }
285
286        if doc_lines.is_empty() {
287            None
288        } else {
289            Some(doc_lines.join(" "))
290        }
291    }
292
293    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
294        Vec::new()
295    }
296
297    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
298        if node.kind() != "using_directive" {
299            return Vec::new();
300        }
301
302        let line = node.start_position().row + 1;
303        let text = &content[node.byte_range()];
304
305        // Check for static using
306        let is_static = text.contains("static ");
307
308        // Get the namespace/type
309        let mut cursor = node.walk();
310        for child in node.children(&mut cursor) {
311            if child.kind() == "qualified_name" || child.kind() == "identifier" {
312                let module = content[child.byte_range()].to_string();
313                return vec![Import {
314                    module,
315                    names: Vec::new(),
316                    alias: if is_static {
317                        Some("static".to_string())
318                    } else {
319                        None
320                    },
321                    is_wildcard: false,
322                    is_relative: false,
323                    line,
324                }];
325            }
326        }
327
328        Vec::new()
329    }
330
331    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
332        // C#: using Namespace; or using Alias = Namespace;
333        if let Some(ref alias) = import.alias {
334            format!("using {} = {};", alias, import.module)
335        } else {
336            format!("using {};", import.module)
337        }
338    }
339
340    fn is_public(&self, node: &Node, content: &str) -> bool {
341        self.get_visibility(node, content) == Visibility::Public
342    }
343
344    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
345        let name = symbol.name.as_str();
346        match symbol.kind {
347            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
348            crate::SymbolKind::Module => name == "tests" || name == "test",
349            _ => false,
350        }
351    }
352
353    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
354        None
355    }
356
357    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
358        node.child_by_field_name("body")
359    }
360
361    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
362        false
363    }
364
365    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
366        node.child_by_field_name("name")
367            .map(|n| &content[n.byte_range()])
368    }
369
370    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
371        if path.extension()?.to_str()? != "cs" {
372            return None;
373        }
374        // C#: typically namespace matches folder structure
375        let stem = path.file_stem()?.to_str()?;
376        Some(stem.to_string())
377    }
378
379    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
380        // C# namespaces don't directly map to paths, but we can try
381        let path = module.replace('.', "/");
382        vec![format!("{}.cs", path), format!("src/{}.cs", path)]
383    }
384
385    fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
386        import_name.starts_with("System") || import_name.starts_with("Microsoft")
387    }
388
389    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
390        // .NET runtime assemblies are not easily indexable
391        None
392    }
393
394    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
395        let mut cursor = node.walk();
396        for child in node.children(&mut cursor) {
397            if child.kind() == "modifier" {
398                let mod_text = &content[child.byte_range()];
399                if mod_text == "private" {
400                    return Visibility::Private;
401                }
402                if mod_text == "protected" {
403                    return Visibility::Protected;
404                }
405                if mod_text == "internal" {
406                    return Visibility::Protected;
407                }
408                if mod_text == "public" {
409                    return Visibility::Public;
410                }
411            }
412        }
413        // C# default visibility depends on context, but for skeleton purposes treat as public
414        Visibility::Public
415    }
416
417    fn lang_key(&self) -> &'static str {
418        "csharp"
419    }
420
421    fn resolve_local_import(
422        &self,
423        import: &str,
424        _current_file: &Path,
425        project_root: &Path,
426    ) -> Option<PathBuf> {
427        // Try to find a file matching the namespace
428        let path_part = import.replace('.', "/");
429
430        {
431            let ext = &"cs";
432            let source_path = project_root.join(format!("{}.{}", path_part, ext));
433            if source_path.is_file() {
434                return Some(source_path);
435            }
436
437            // Try src/ prefix
438            let source_path = project_root
439                .join("src")
440                .join(format!("{}.{}", path_part, ext));
441            if source_path.is_file() {
442                return Some(source_path);
443            }
444        }
445
446        None
447    }
448
449    fn resolve_external_import(
450        &self,
451        _import_name: &str,
452        _project_root: &Path,
453    ) -> Option<ResolvedPackage> {
454        // NuGet package resolution would go here
455        None
456    }
457
458    fn get_version(&self, project_root: &Path) -> Option<String> {
459        // Try to find .NET version from global.json or .csproj
460        let global_json = project_root.join("global.json");
461        if global_json.is_file() {
462            if let Ok(content) = std::fs::read_to_string(&global_json) {
463                // Quick parse for "version": "X.Y.Z"
464                if let Some(idx) = content.find("\"version\"") {
465                    let rest = &content[idx..];
466                    if let Some(start) = rest.find(':') {
467                        let after_colon = rest[start + 1..].trim();
468                        if let Some(ver_start) = after_colon.find('"') {
469                            let ver_rest = &after_colon[ver_start + 1..];
470                            if let Some(ver_end) = ver_rest.find('"') {
471                                return Some(ver_rest[..ver_end].to_string());
472                            }
473                        }
474                    }
475                }
476            }
477        }
478        None
479    }
480
481    fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
482        // NuGet cache location
483        if let Ok(home) = std::env::var("HOME") {
484            let cache = PathBuf::from(home).join(".nuget").join("packages");
485            if cache.is_dir() {
486                return Some(cache);
487            }
488        }
489        if let Ok(home) = std::env::var("USERPROFILE") {
490            let cache = PathBuf::from(home).join(".nuget").join("packages");
491            if cache.is_dir() {
492                return Some(cache);
493            }
494        }
495        None
496    }
497
498    fn indexable_extensions(&self) -> &'static [&'static str] {
499        &["cs"]
500    }
501
502    fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
503        Vec::new() // NuGet sources would go here
504    }
505
506    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
507        use crate::traits::{has_extension, skip_dotfiles};
508        if skip_dotfiles(name) {
509            return true;
510        }
511        if is_dir && (name == "bin" || name == "obj" || name == "packages") {
512            return true;
513        }
514        !is_dir && !has_extension(name, self.indexable_extensions())
515    }
516
517    fn discover_packages(&self, _source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
518        Vec::new() // NuGet package discovery would go here
519    }
520
521    fn package_module_name(&self, entry_name: &str) -> String {
522        entry_name
523            .strip_suffix(".cs")
524            .unwrap_or(entry_name)
525            .to_string()
526    }
527
528    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
529        if path.is_file() {
530            return Some(path.to_path_buf());
531        }
532        None
533    }
534}
535
536/// Strip common XML doc comment tags for cleaner output
537fn strip_xml_tags(s: &str) -> String {
538    let mut result = s.to_string();
539    // Remove common tags
540    for tag in &[
541        "<summary>",
542        "</summary>",
543        "<param>",
544        "</param>",
545        "<returns>",
546        "</returns>",
547        "<remarks>",
548        "</remarks>",
549        "<example>",
550        "</example>",
551        "<c>",
552        "</c>",
553        "<see cref=\"",
554        "\"/>",
555        "<seealso cref=\"",
556    ] {
557        result = result.replace(tag, "");
558    }
559    // Handle self-closing see tags
560    while let Some(start) = result.find("<see ") {
561        if let Some(end) = result[start..].find("/>") {
562            result = format!("{}{}", &result[..start], &result[start + end + 2..]);
563        } else {
564            break;
565        }
566    }
567    result.trim().to_string()
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573    use crate::validate_unused_kinds_audit;
574
575    #[test]
576    fn unused_node_kinds_audit() {
577        #[rustfmt::skip]
578        let documented_unused: &[&str] = &[
579            // C# grammar uses "c_sharp" - check cross_check output for actual kinds
580            // This is a placeholder - run cross_check_node_kinds to get the full list
581        ];
582
583        // C# may need manual verification - skip for now if empty
584        if !documented_unused.is_empty() {
585            validate_unused_kinds_audit(&CSharp, documented_unused)
586                .expect("C# unused node kinds audit failed");
587        }
588    }
589}