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(child, source, file_path, units, next_id, parent_qname);
96 }
97 "method_declaration" | "constructor_declaration" => {
98 if let Some(unit) =
99 self.extract_method(child, source, file_path, parent_qname, next_id)
100 {
101 units.push(unit);
102 }
103 }
104 "property_declaration" => {
105 if let Some(unit) =
106 self.extract_property(child, source, file_path, parent_qname, next_id)
107 {
108 units.push(unit);
109 }
110 }
111 "using_directive" => {
112 if let Some(unit) =
113 self.extract_using(child, source, file_path, parent_qname, next_id)
114 {
115 units.push(unit);
116 }
117 }
118 "declaration_list" => {
120 self.extract_from_node(child, source, file_path, units, next_id, parent_qname);
121 }
122 _ => {}
123 }
124 }
125 }
126
127 #[allow(clippy::too_many_arguments)]
128 fn extract_type_decl(
129 &self,
130 node: tree_sitter::Node,
131 source: &str,
132 file_path: &Path,
133 units: &mut Vec<RawCodeUnit>,
134 next_id: &mut u64,
135 parent_qname: &str,
136 unit_type: CodeUnitType,
137 ) {
138 let name = match node.child_by_field_name("name") {
139 Some(n) => get_node_text(n, source).to_string(),
140 None => return,
141 };
142 let qname = cs_qname(parent_qname, &name);
143 let span = node_to_span(node);
144 let vis = extract_cs_visibility(node, source);
145
146 let id = *next_id;
147 *next_id += 1;
148
149 let mut unit = RawCodeUnit::new(
150 unit_type,
151 Language::CSharp,
152 name,
153 file_path.to_path_buf(),
154 span,
155 );
156 unit.temp_id = id;
157 unit.qualified_name = qname.clone();
158 unit.visibility = vis;
159 units.push(unit);
160
161 if let Some(body) = node.child_by_field_name("body") {
163 self.extract_from_node(body, source, file_path, units, next_id, &qname);
164 }
165 }
166
167 fn extract_namespace(
168 &self,
169 node: tree_sitter::Node,
170 source: &str,
171 file_path: &Path,
172 units: &mut Vec<RawCodeUnit>,
173 next_id: &mut u64,
174 parent_qname: &str,
175 ) {
176 let name = match node.child_by_field_name("name") {
177 Some(n) => get_node_text(n, source).to_string(),
178 None => return,
179 };
180 let qname = cs_qname(parent_qname, &name);
181 let span = node_to_span(node);
182
183 let id = *next_id;
184 *next_id += 1;
185
186 let mut unit = RawCodeUnit::new(
187 CodeUnitType::Module,
188 Language::CSharp,
189 name,
190 file_path.to_path_buf(),
191 span,
192 );
193 unit.temp_id = id;
194 unit.qualified_name = qname.clone();
195 unit.visibility = Visibility::Public;
196 units.push(unit);
197
198 if let Some(body) = node.child_by_field_name("body") {
200 self.extract_from_node(body, source, file_path, units, next_id, &qname);
201 } else {
202 self.extract_from_node(node, source, file_path, units, next_id, &qname);
204 }
205 }
206
207 fn extract_method(
208 &self,
209 node: tree_sitter::Node,
210 source: &str,
211 file_path: &Path,
212 parent_qname: &str,
213 next_id: &mut u64,
214 ) -> Option<RawCodeUnit> {
215 let name_node = node.child_by_field_name("name")?;
216 let name = get_node_text(name_node, source).to_string();
217 let qname = cs_qname(parent_qname, &name);
218 let span = node_to_span(node);
219 let vis = extract_cs_visibility(node, source);
220
221 let id = *next_id;
222 *next_id += 1;
223
224 let is_test = has_cs_attribute(node, source, "Test")
225 || has_cs_attribute(node, source, "Fact")
226 || has_cs_attribute(node, source, "Theory")
227 || has_cs_attribute(node, source, "TestMethod");
228 let unit_type = if is_test {
229 CodeUnitType::Test
230 } else {
231 CodeUnitType::Function
232 };
233
234 let is_async = node_text_contains_modifier(node, source, "async");
235
236 let mut unit = RawCodeUnit::new(
237 unit_type,
238 Language::CSharp,
239 name,
240 file_path.to_path_buf(),
241 span,
242 );
243 unit.temp_id = id;
244 unit.qualified_name = qname;
245 unit.visibility = vis;
246 unit.is_async = is_async;
247
248 Some(unit)
249 }
250
251 fn extract_property(
252 &self,
253 node: tree_sitter::Node,
254 source: &str,
255 file_path: &Path,
256 parent_qname: &str,
257 next_id: &mut u64,
258 ) -> Option<RawCodeUnit> {
259 let name_node = node.child_by_field_name("name")?;
260 let name = get_node_text(name_node, source).to_string();
261 let qname = cs_qname(parent_qname, &name);
262 let span = node_to_span(node);
263 let vis = extract_cs_visibility(node, source);
264
265 let id = *next_id;
266 *next_id += 1;
267
268 let mut unit = RawCodeUnit::new(
269 CodeUnitType::Symbol,
270 Language::CSharp,
271 name,
272 file_path.to_path_buf(),
273 span,
274 );
275 unit.temp_id = id;
276 unit.qualified_name = qname;
277 unit.visibility = vis;
278
279 Some(unit)
280 }
281
282 fn extract_using(
283 &self,
284 node: tree_sitter::Node,
285 source: &str,
286 file_path: &Path,
287 parent_qname: &str,
288 next_id: &mut u64,
289 ) -> Option<RawCodeUnit> {
290 let text = get_node_text(node, source)
291 .trim_start_matches("using ")
292 .trim_start_matches("global ")
293 .trim_start_matches("static ")
294 .trim_end_matches(';')
295 .trim()
296 .to_string();
297 let span = node_to_span(node);
298
299 let id = *next_id;
300 *next_id += 1;
301
302 let mut unit = RawCodeUnit::new(
303 CodeUnitType::Import,
304 Language::CSharp,
305 text,
306 file_path.to_path_buf(),
307 span,
308 );
309 unit.temp_id = id;
310 unit.qualified_name = cs_qname(parent_qname, "using");
311
312 Some(unit)
313 }
314}
315
316impl LanguageParser for CSharpParser {
317 fn extract_units(
318 &self,
319 tree: &tree_sitter::Tree,
320 source: &str,
321 file_path: &Path,
322 ) -> AcbResult<Vec<RawCodeUnit>> {
323 let mut units = Vec::new();
324 let mut next_id = 0u64;
325
326 let module_name = file_path
327 .file_stem()
328 .and_then(|s| s.to_str())
329 .unwrap_or("unknown")
330 .to_string();
331
332 let root_span = node_to_span(tree.root_node());
333 let mut module_unit = RawCodeUnit::new(
334 CodeUnitType::Module,
335 Language::CSharp,
336 module_name.clone(),
337 file_path.to_path_buf(),
338 root_span,
339 );
340 module_unit.temp_id = next_id;
341 module_unit.qualified_name = module_name.clone();
342 next_id += 1;
343 units.push(module_unit);
344
345 self.extract_from_node(
346 tree.root_node(),
347 source,
348 file_path,
349 &mut units,
350 &mut next_id,
351 &module_name,
352 );
353
354 Ok(units)
355 }
356
357 fn is_test_file(&self, path: &Path, _source: &str) -> bool {
358 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
359 name.ends_with("Tests.cs") || name.ends_with("Test.cs") || name.starts_with("Test")
360 }
361}
362
363fn cs_qname(parent: &str, name: &str) -> String {
364 if parent.is_empty() {
365 name.to_string()
366 } else {
367 format!("{}.{}", parent, name)
368 }
369}
370
371fn extract_cs_visibility(node: tree_sitter::Node, source: &str) -> Visibility {
373 let mut cursor = node.walk();
374 for child in node.children(&mut cursor) {
375 if child.kind() == "modifier" {
376 let text = get_node_text(child, source);
377 match text {
378 "public" => return Visibility::Public,
379 "private" => return Visibility::Private,
380 "protected" | "internal" => return Visibility::Public,
381 _ => {}
382 }
383 }
384 }
385 Visibility::Private }
387
388fn has_cs_attribute(node: tree_sitter::Node, source: &str, attribute: &str) -> bool {
390 let mut cursor = node.walk();
391 for child in node.children(&mut cursor) {
392 if child.kind() == "attribute_list" {
393 let text = get_node_text(child, source);
394 if text.contains(attribute) {
395 return true;
396 }
397 }
398 }
399 false
400}
401
402fn node_text_contains_modifier(node: tree_sitter::Node, source: &str, keyword: &str) -> bool {
404 let mut cursor = node.walk();
405 for child in node.children(&mut cursor) {
406 if child.kind() == "modifier" && get_node_text(child, source) == keyword {
407 return true;
408 }
409 }
410 false
411}