1use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
4use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
5use std::path::Path;
6use tree_sitter::Node;
7
8pub 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 as_symbols(&self) -> Option<&dyn LanguageSymbols> {
23 Some(self)
24 }
25
26 fn signature_suffix(&self) -> &'static str {
27 " {}"
28 }
29
30 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
31 let mut doc_lines: Vec<String> = Vec::new();
32 let mut prev = node.prev_sibling();
33
34 while let Some(sibling) = prev {
35 if sibling.kind() == "comment" {
36 let text = &content[sibling.byte_range()];
37 if text.starts_with("///") {
38 let line = text.strip_prefix("///").unwrap_or("").trim();
39 let line = strip_xml_tags(line);
40 if !line.is_empty() {
41 doc_lines.push(line);
42 }
43 } else if text.starts_with("/**") {
44 let lines: Vec<&str> = text
45 .strip_prefix("/**")
46 .unwrap_or(text)
47 .strip_suffix("*/")
48 .unwrap_or(text)
49 .lines()
50 .map(|l| l.trim().strip_prefix('*').unwrap_or(l).trim())
51 .filter(|l| !l.is_empty())
52 .collect();
53 if !lines.is_empty() {
54 return Some(lines.join(" "));
55 }
56 return None;
57 } else {
58 break;
59 }
60 } else if sibling.kind() == "attribute_list" {
61 } else {
63 break;
64 }
65 prev = sibling.prev_sibling();
66 }
67
68 if doc_lines.is_empty() {
69 return None;
70 }
71
72 doc_lines.reverse();
73 let joined = doc_lines.join(" ").trim().to_string();
74 if joined.is_empty() {
75 None
76 } else {
77 Some(joined)
78 }
79 }
80
81 fn refine_kind(
82 &self,
83 node: &Node,
84 _content: &str,
85 tag_kind: crate::SymbolKind,
86 ) -> crate::SymbolKind {
87 match node.kind() {
88 "struct_declaration" => crate::SymbolKind::Struct,
89 "enum_declaration" => crate::SymbolKind::Enum,
90 "interface_declaration" => crate::SymbolKind::Interface,
91 "record_declaration" => crate::SymbolKind::Class,
92 _ => tag_kind,
93 }
94 }
95
96 fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
97 let mut implements = Vec::new();
98 let mut cursor = node.walk();
99 for child in node.children(&mut cursor) {
100 if child.kind() == "base_list" {
101 let mut bl = child.walk();
102 for t in child.children(&mut bl) {
103 if t.kind() == "identifier" || t.kind() == "generic_name" {
104 implements.push(content[t.byte_range()].to_string());
105 }
106 }
107 }
108 }
109 crate::ImplementsInfo {
110 is_interface: false,
111 implements,
112 }
113 }
114
115 fn build_signature(&self, node: &Node, content: &str) -> String {
116 let name = match self.node_name(node, content) {
117 Some(n) => n,
118 None => {
119 return content[node.byte_range()]
120 .lines()
121 .next()
122 .unwrap_or("")
123 .trim()
124 .to_string();
125 }
126 };
127 match node.kind() {
128 "method_declaration" | "constructor_declaration" | "property_declaration" => {
129 let params = node
130 .child_by_field_name("parameters")
131 .map(|p| content[p.byte_range()].to_string())
132 .unwrap_or_default();
133 let return_type = node
134 .child_by_field_name("type")
135 .or_else(|| node.child_by_field_name("returns"))
136 .map(|t| content[t.byte_range()].to_string());
137 match return_type {
138 Some(ret) => format!("{} {}{}", ret, name, params),
139 None => format!("{}{}", name, params),
140 }
141 }
142 "class_declaration" => format!("class {}", name),
143 "struct_declaration" => format!("struct {}", name),
144 "interface_declaration" => format!("interface {}", name),
145 "enum_declaration" => format!("enum {}", name),
146 "record_declaration" => format!("record {}", name),
147 "namespace_declaration" => format!("namespace {}", name),
148 _ => {
149 let text = &content[node.byte_range()];
150 text.lines().next().unwrap_or(text).trim().to_string()
151 }
152 }
153 }
154
155 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
156 if node.kind() != "using_directive" {
157 return Vec::new();
158 }
159
160 let line = node.start_position().row + 1;
161 let text = &content[node.byte_range()];
162
163 let is_static = text.contains("static ");
165
166 let mut cursor = node.walk();
168 for child in node.children(&mut cursor) {
169 if child.kind() == "qualified_name" || child.kind() == "identifier" {
170 let module = content[child.byte_range()].to_string();
171 return vec![Import {
172 module,
173 names: Vec::new(),
174 alias: if is_static {
175 Some("static".to_string())
176 } else {
177 None
178 },
179 is_wildcard: false,
180 is_relative: false,
181 line,
182 }];
183 }
184 }
185
186 Vec::new()
187 }
188
189 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
190 if let Some(ref alias) = import.alias {
192 format!("using {} = {};", alias, import.module)
193 } else {
194 format!("using {};", import.module)
195 }
196 }
197
198 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
199 let name = symbol.name.as_str();
200 match symbol.kind {
201 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
202 crate::SymbolKind::Module => name == "tests" || name == "test",
203 _ => false,
204 }
205 }
206
207 fn test_file_globs(&self) -> &'static [&'static str] {
208 &["**/*Test.cs", "**/*Tests.cs"]
209 }
210
211 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
212 node.child_by_field_name("body")
213 }
214
215 fn analyze_container_body(
216 &self,
217 body_node: &Node,
218 content: &str,
219 inner_indent: &str,
220 ) -> Option<ContainerBody> {
221 crate::body::analyze_brace_body(body_node, content, inner_indent)
222 }
223
224 fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
225 let mut attrs = Vec::new();
226 let mut cursor = node.walk();
227 for child in node.children(&mut cursor) {
228 if child.kind() == "attribute_list" {
229 attrs.push(content[child.byte_range()].to_string());
230 }
231 }
232 attrs
233 }
234
235 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
236 let mut cursor = node.walk();
237 for child in node.children(&mut cursor) {
238 if child.kind() == "modifier" {
239 let mod_text = &content[child.byte_range()];
240 if mod_text == "private" {
241 return Visibility::Private;
242 }
243 if mod_text == "protected" {
244 return Visibility::Protected;
245 }
246 if mod_text == "internal" {
247 return Visibility::Protected;
248 }
249 if mod_text == "public" {
250 return Visibility::Public;
251 }
252 }
253 }
254 Visibility::Public
256 }
257
258 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
259 static RESOLVER: CSharpModuleResolver = CSharpModuleResolver;
260 Some(&RESOLVER)
261 }
262}
263
264impl LanguageSymbols for CSharp {}
265
266pub struct CSharpModuleResolver;
275
276impl ModuleResolver for CSharpModuleResolver {
277 fn workspace_config(&self, root: &Path) -> ResolverConfig {
278 ResolverConfig {
280 workspace_root: root.to_path_buf(),
281 path_mappings: Vec::new(),
282 search_roots: vec![root.to_path_buf()],
283 }
284 }
285
286 fn module_of_file(&self, root: &Path, file: &Path, _cfg: &ResolverConfig) -> Vec<ModuleId> {
287 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
288 if ext != "cs" {
289 return Vec::new();
290 }
291 if let Ok(rel) = file.strip_prefix(root) {
292 let rel_str = rel
293 .to_str()
294 .unwrap_or("")
295 .trim_end_matches(".cs")
296 .replace(['/', '\\'], ".");
297 if !rel_str.is_empty() {
298 return vec![ModuleId {
299 canonical_path: rel_str,
300 }];
301 }
302 }
303 Vec::new()
304 }
305
306 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
307 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
308 if ext != "cs" {
309 return Resolution::NotApplicable;
310 }
311 let raw = &spec.raw;
312 let exported_name = raw.rsplit('.').next().unwrap_or(raw).to_string();
313
314 let parts: Vec<&str> = raw.split('.').collect();
317 for skip in 0..parts.len() {
318 let path_part = parts[skip..].join("/");
319 let candidate = cfg.workspace_root.join(format!("{}.cs", path_part));
320 if candidate.exists() {
321 return Resolution::Resolved(candidate, exported_name);
322 }
323 }
324 Resolution::NotFound
325 }
326}
327
328fn strip_xml_tags(s: &str) -> String {
330 let mut result = String::with_capacity(s.len());
331 let mut in_tag = false;
332 for ch in s.chars() {
333 if ch == '<' {
334 in_tag = true;
335 } else if ch == '>' {
336 in_tag = false;
337 } else if !in_tag {
338 result.push(ch);
339 }
340 }
341 result.trim().to_string()
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use crate::validate_unused_kinds_audit;
348
349 #[test]
350 fn unused_node_kinds_audit() {
351 #[rustfmt::skip]
352 let documented_unused: &[&str] = &[
353 ];
356
357 if !documented_unused.is_empty() {
359 validate_unused_kinds_audit(&CSharp, documented_unused)
360 .expect("C# unused node kinds audit failed");
361 }
362 }
363}