normalize_languages/
csharp.rs1use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
4use tree_sitter::Node;
5
6pub 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 } 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 let is_static = text.contains("static ");
163
164 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 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 Visibility::Public
254 }
255}
256
257impl LanguageSymbols for CSharp {}
258
259fn 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 ];
287
288 if !documented_unused.is_empty() {
290 validate_unused_kinds_audit(&CSharp, documented_unused)
291 .expect("C# unused node kinds audit failed");
292 }
293 }
294}