1use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
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 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 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 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 let line = text.strip_prefix("///").unwrap_or(text).trim();
257 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 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 let is_static = text.contains("static ");
307
308 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 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 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 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 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 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 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 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 None
456 }
457
458 fn get_version(&self, project_root: &Path) -> Option<String> {
459 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 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 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() }
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() }
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
536fn strip_xml_tags(s: &str) -> String {
538 let mut result = s.to_string();
539 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 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 ];
582
583 if !documented_unused.is_empty() {
585 validate_unused_kinds_audit(&CSharp, documented_unused)
586 .expect("C# unused node kinds audit failed");
587 }
588 }
589}