1use std::path::Path;
6
7use crate::types::{AcbResult, CodeUnitType, Language, Visibility};
8
9use super::treesitter::{get_node_text, node_to_span};
10use super::{LanguageParser, RawCodeUnit};
11
12pub struct CSharpParser;
14
15impl Default for CSharpParser {
16 fn default() -> Self {
17 Self::new()
18 }
19}
20
21impl CSharpParser {
22 pub fn new() -> Self {
24 Self
25 }
26
27 fn extract_from_node(
28 &self,
29 node: tree_sitter::Node,
30 source: &str,
31 file_path: &Path,
32 units: &mut Vec<RawCodeUnit>,
33 next_id: &mut u64,
34 parent_qname: &str,
35 ) {
36 let mut cursor = node.walk();
37 for child in node.children(&mut cursor) {
38 match child.kind() {
39 "class_declaration" => {
40 self.extract_type_decl(
41 child,
42 source,
43 file_path,
44 units,
45 next_id,
46 parent_qname,
47 CodeUnitType::Type,
48 );
49 }
50 "struct_declaration" => {
51 self.extract_type_decl(
52 child,
53 source,
54 file_path,
55 units,
56 next_id,
57 parent_qname,
58 CodeUnitType::Type,
59 );
60 }
61 "interface_declaration" => {
62 self.extract_type_decl(
63 child,
64 source,
65 file_path,
66 units,
67 next_id,
68 parent_qname,
69 CodeUnitType::Trait,
70 );
71 }
72 "enum_declaration" => {
73 self.extract_type_decl(
74 child,
75 source,
76 file_path,
77 units,
78 next_id,
79 parent_qname,
80 CodeUnitType::Type,
81 );
82 }
83 "record_declaration" => {
84 self.extract_type_decl(
85 child,
86 source,
87 file_path,
88 units,
89 next_id,
90 parent_qname,
91 CodeUnitType::Type,
92 );
93 }
94 "namespace_declaration" | "file_scoped_namespace_declaration" => {
95 self.extract_namespace(
96 child,
97 source,
98 file_path,
99 units,
100 next_id,
101 parent_qname,
102 );
103 }
104 "method_declaration" | "constructor_declaration" => {
105 if let Some(unit) =
106 self.extract_method(child, source, file_path, parent_qname, next_id)
107 {
108 units.push(unit);
109 }
110 }
111 "property_declaration" => {
112 if let Some(unit) =
113 self.extract_property(child, source, file_path, parent_qname, next_id)
114 {
115 units.push(unit);
116 }
117 }
118 "using_directive" => {
119 if let Some(unit) =
120 self.extract_using(child, source, file_path, parent_qname, next_id)
121 {
122 units.push(unit);
123 }
124 }
125 "declaration_list" => {
127 self.extract_from_node(
128 child,
129 source,
130 file_path,
131 units,
132 next_id,
133 parent_qname,
134 );
135 }
136 _ => {}
137 }
138 }
139 }
140
141 #[allow(clippy::too_many_arguments)]
142 fn extract_type_decl(
143 &self,
144 node: tree_sitter::Node,
145 source: &str,
146 file_path: &Path,
147 units: &mut Vec<RawCodeUnit>,
148 next_id: &mut u64,
149 parent_qname: &str,
150 unit_type: CodeUnitType,
151 ) {
152 let name = match node.child_by_field_name("name") {
153 Some(n) => get_node_text(n, source).to_string(),
154 None => return,
155 };
156 let qname = cs_qname(parent_qname, &name);
157 let span = node_to_span(node);
158 let vis = extract_cs_visibility(node, source);
159
160 let id = *next_id;
161 *next_id += 1;
162
163 let mut unit = RawCodeUnit::new(
164 unit_type,
165 Language::CSharp,
166 name,
167 file_path.to_path_buf(),
168 span,
169 );
170 unit.temp_id = id;
171 unit.qualified_name = qname.clone();
172 unit.visibility = vis;
173 units.push(unit);
174
175 if let Some(body) = node.child_by_field_name("body") {
177 self.extract_from_node(body, source, file_path, units, next_id, &qname);
178 }
179 }
180
181 fn extract_namespace(
182 &self,
183 node: tree_sitter::Node,
184 source: &str,
185 file_path: &Path,
186 units: &mut Vec<RawCodeUnit>,
187 next_id: &mut u64,
188 parent_qname: &str,
189 ) {
190 let name = match node.child_by_field_name("name") {
191 Some(n) => get_node_text(n, source).to_string(),
192 None => return,
193 };
194 let qname = cs_qname(parent_qname, &name);
195 let span = node_to_span(node);
196
197 let id = *next_id;
198 *next_id += 1;
199
200 let mut unit = RawCodeUnit::new(
201 CodeUnitType::Module,
202 Language::CSharp,
203 name,
204 file_path.to_path_buf(),
205 span,
206 );
207 unit.temp_id = id;
208 unit.qualified_name = qname.clone();
209 unit.visibility = Visibility::Public;
210 units.push(unit);
211
212 if let Some(body) = node.child_by_field_name("body") {
214 self.extract_from_node(body, source, file_path, units, next_id, &qname);
215 } else {
216 self.extract_from_node(node, source, file_path, units, next_id, &qname);
218 }
219 }
220
221 fn extract_method(
222 &self,
223 node: tree_sitter::Node,
224 source: &str,
225 file_path: &Path,
226 parent_qname: &str,
227 next_id: &mut u64,
228 ) -> Option<RawCodeUnit> {
229 let name_node = node.child_by_field_name("name")?;
230 let name = get_node_text(name_node, source).to_string();
231 let qname = cs_qname(parent_qname, &name);
232 let span = node_to_span(node);
233 let vis = extract_cs_visibility(node, source);
234
235 let id = *next_id;
236 *next_id += 1;
237
238 let is_test = has_cs_attribute(node, source, "Test")
239 || has_cs_attribute(node, source, "Fact")
240 || has_cs_attribute(node, source, "Theory")
241 || has_cs_attribute(node, source, "TestMethod");
242 let unit_type = if is_test {
243 CodeUnitType::Test
244 } else {
245 CodeUnitType::Function
246 };
247
248 let is_async = node_text_contains_modifier(node, source, "async");
249
250 let mut unit = RawCodeUnit::new(
251 unit_type,
252 Language::CSharp,
253 name,
254 file_path.to_path_buf(),
255 span,
256 );
257 unit.temp_id = id;
258 unit.qualified_name = qname;
259 unit.visibility = vis;
260 unit.is_async = is_async;
261
262 Some(unit)
263 }
264
265 fn extract_property(
266 &self,
267 node: tree_sitter::Node,
268 source: &str,
269 file_path: &Path,
270 parent_qname: &str,
271 next_id: &mut u64,
272 ) -> Option<RawCodeUnit> {
273 let name_node = node.child_by_field_name("name")?;
274 let name = get_node_text(name_node, source).to_string();
275 let qname = cs_qname(parent_qname, &name);
276 let span = node_to_span(node);
277 let vis = extract_cs_visibility(node, source);
278
279 let id = *next_id;
280 *next_id += 1;
281
282 let mut unit = RawCodeUnit::new(
283 CodeUnitType::Symbol,
284 Language::CSharp,
285 name,
286 file_path.to_path_buf(),
287 span,
288 );
289 unit.temp_id = id;
290 unit.qualified_name = qname;
291 unit.visibility = vis;
292
293 Some(unit)
294 }
295
296 fn extract_using(
297 &self,
298 node: tree_sitter::Node,
299 source: &str,
300 file_path: &Path,
301 parent_qname: &str,
302 next_id: &mut u64,
303 ) -> Option<RawCodeUnit> {
304 let text = get_node_text(node, source)
305 .trim_start_matches("using ")
306 .trim_start_matches("global ")
307 .trim_start_matches("static ")
308 .trim_end_matches(';')
309 .trim()
310 .to_string();
311 let span = node_to_span(node);
312
313 let id = *next_id;
314 *next_id += 1;
315
316 let mut unit = RawCodeUnit::new(
317 CodeUnitType::Import,
318 Language::CSharp,
319 text,
320 file_path.to_path_buf(),
321 span,
322 );
323 unit.temp_id = id;
324 unit.qualified_name = cs_qname(parent_qname, "using");
325
326 Some(unit)
327 }
328}
329
330impl LanguageParser for CSharpParser {
331 fn extract_units(
332 &self,
333 tree: &tree_sitter::Tree,
334 source: &str,
335 file_path: &Path,
336 ) -> AcbResult<Vec<RawCodeUnit>> {
337 let mut units = Vec::new();
338 let mut next_id = 0u64;
339
340 let module_name = file_path
341 .file_stem()
342 .and_then(|s| s.to_str())
343 .unwrap_or("unknown")
344 .to_string();
345
346 let root_span = node_to_span(tree.root_node());
347 let mut module_unit = RawCodeUnit::new(
348 CodeUnitType::Module,
349 Language::CSharp,
350 module_name.clone(),
351 file_path.to_path_buf(),
352 root_span,
353 );
354 module_unit.temp_id = next_id;
355 module_unit.qualified_name = module_name.clone();
356 next_id += 1;
357 units.push(module_unit);
358
359 self.extract_from_node(
360 tree.root_node(),
361 source,
362 file_path,
363 &mut units,
364 &mut next_id,
365 &module_name,
366 );
367
368 Ok(units)
369 }
370
371 fn is_test_file(&self, path: &Path, _source: &str) -> bool {
372 let name = path
373 .file_name()
374 .and_then(|n| n.to_str())
375 .unwrap_or("");
376 name.ends_with("Tests.cs")
377 || name.ends_with("Test.cs")
378 || name.starts_with("Test")
379 }
380}
381
382fn cs_qname(parent: &str, name: &str) -> String {
383 if parent.is_empty() {
384 name.to_string()
385 } else {
386 format!("{}.{}", parent, name)
387 }
388}
389
390fn extract_cs_visibility(node: tree_sitter::Node, source: &str) -> Visibility {
392 let mut cursor = node.walk();
393 for child in node.children(&mut cursor) {
394 if child.kind() == "modifier" {
395 let text = get_node_text(child, source);
396 match text {
397 "public" => return Visibility::Public,
398 "private" => return Visibility::Private,
399 "protected" | "internal" => return Visibility::Public,
400 _ => {}
401 }
402 }
403 }
404 Visibility::Private }
406
407fn has_cs_attribute(node: tree_sitter::Node, source: &str, attribute: &str) -> bool {
409 let mut cursor = node.walk();
410 for child in node.children(&mut cursor) {
411 if child.kind() == "attribute_list" {
412 let text = get_node_text(child, source);
413 if text.contains(attribute) {
414 return true;
415 }
416 }
417 }
418 false
419}
420
421fn node_text_contains_modifier(node: tree_sitter::Node, source: &str, keyword: &str) -> bool {
423 let mut cursor = node.walk();
424 for child in node.children(&mut cursor) {
425 if child.kind() == "modifier" && get_node_text(child, source) == keyword {
426 return true;
427 }
428 }
429 false
430}