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 FSharp;
10
11impl Language for FSharp {
12 fn name(&self) -> &'static str {
13 "F#"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["fs", "fsi", "fsx"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "fsharp"
20 }
21
22 fn has_symbols(&self) -> bool {
23 true
24 }
25
26 fn container_kinds(&self) -> &'static [&'static str] {
27 &["module_defn", "type_definition"]
28 }
29
30 fn function_kinds(&self) -> &'static [&'static str] {
31 &["function_or_value_defn", "member_defn"]
32 }
33
34 fn type_kinds(&self) -> &'static [&'static str] {
35 &["type_definition", "record_type_defn", "union_type_defn"]
36 }
37
38 fn import_kinds(&self) -> &'static [&'static str] {
39 &["import_decl"]
40 }
41
42 fn public_symbol_kinds(&self) -> &'static [&'static str] {
43 &["function_or_value_defn", "type_definition", "module_defn"]
44 }
45
46 fn visibility_mechanism(&self) -> VisibilityMechanism {
47 VisibilityMechanism::AccessModifier }
49
50 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
51 if !self.is_public(node, content) {
52 return Vec::new();
53 }
54
55 let name = match self.node_name(node, content) {
56 Some(n) => n.to_string(),
57 None => return Vec::new(),
58 };
59
60 let kind = match node.kind() {
61 "function_or_value_defn" => SymbolKind::Function,
62 "member_defn" => SymbolKind::Method,
63 "type_definition" | "record_type_defn" => SymbolKind::Struct,
64 "union_type_defn" => SymbolKind::Enum,
65 "module_defn" => SymbolKind::Module,
66 _ => return Vec::new(),
67 };
68
69 vec![Export {
70 name,
71 kind,
72 line: node.start_position().row + 1,
73 }]
74 }
75
76 fn scope_creating_kinds(&self) -> &'static [&'static str] {
77 &[
78 "for_expression",
79 "while_expression",
80 "try_expression",
81 "match_expression",
82 ]
83 }
84
85 fn control_flow_kinds(&self) -> &'static [&'static str] {
86 &[
87 "if_expression",
88 "match_expression",
89 "for_expression",
90 "while_expression",
91 "try_expression",
92 "application_expression",
93 ]
94 }
95
96 fn complexity_nodes(&self) -> &'static [&'static str] {
97 &[
98 "if_expression",
99 "rule",
100 "for_expression",
101 "while_expression",
102 "try_expression",
103 "infix_expression",
104 ]
105 }
106
107 fn nesting_nodes(&self) -> &'static [&'static str] {
108 &[
109 "if_expression",
110 "match_expression",
111 "for_expression",
112 "while_expression",
113 "try_expression",
114 "function_or_value_defn",
115 "module_defn",
116 ]
117 }
118
119 fn signature_suffix(&self) -> &'static str {
120 ""
121 }
122
123 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
124 let name = self.node_name(node, content)?;
125
126 let text = &content[node.byte_range()];
128 let first_line = text.lines().next().unwrap_or(text);
129
130 let is_member = node.kind() == "member_defn";
131 let kind = if is_member {
132 SymbolKind::Method
133 } else {
134 SymbolKind::Function
135 };
136
137 Some(Symbol {
138 name: name.to_string(),
139 kind,
140 signature: first_line.trim().to_string(),
141 docstring: self.extract_docstring(node, content),
142 attributes: Vec::new(),
143 start_line: node.start_position().row + 1,
144 end_line: node.end_position().row + 1,
145 visibility: self.get_visibility(node, content),
146 children: Vec::new(),
147 is_interface_impl: false,
148 implements: Vec::new(),
149 })
150 }
151
152 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
153 let name = self.node_name(node, content)?;
154 let (kind, keyword) = match node.kind() {
155 "union_type_defn" => (SymbolKind::Enum, "type"),
156 "record_type_defn" => (SymbolKind::Struct, "type"),
157 "module_defn" => (SymbolKind::Module, "module"),
158 _ => (SymbolKind::Struct, "type"),
159 };
160
161 Some(Symbol {
162 name: name.to_string(),
163 kind,
164 signature: format!("{} {}", keyword, name),
165 docstring: self.extract_docstring(node, content),
166 attributes: Vec::new(),
167 start_line: node.start_position().row + 1,
168 end_line: node.end_position().row + 1,
169 visibility: self.get_visibility(node, content),
170 children: Vec::new(),
171 is_interface_impl: false,
172 implements: Vec::new(),
173 })
174 }
175
176 fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
177 self.extract_container(node, content)
178 }
179
180 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
181 let mut prev = node.prev_sibling();
183 let mut doc_lines = Vec::new();
184
185 while let Some(sibling) = prev {
186 let text = &content[sibling.byte_range()];
187 if text.starts_with("///") {
188 let line = text.strip_prefix("///").unwrap_or(text).trim();
189 let clean = line
191 .replace("<summary>", "")
192 .replace("</summary>", "")
193 .replace("<param name=\"", "")
194 .replace("</param>", "")
195 .replace("<returns>", "")
196 .replace("</returns>", "")
197 .trim()
198 .to_string();
199 if !clean.is_empty() {
200 doc_lines.push(clean);
201 }
202 prev = sibling.prev_sibling();
203 } else {
204 break;
205 }
206 }
207
208 if doc_lines.is_empty() {
209 return None;
210 }
211
212 doc_lines.reverse();
213 Some(doc_lines.join(" "))
214 }
215
216 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
217 Vec::new()
218 }
219
220 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
221 let text = &content[node.byte_range()];
222 let line = node.start_position().row + 1;
223
224 if let Some(rest) = text.strip_prefix("open ") {
225 let module = rest.trim().to_string();
226 return vec![Import {
227 module,
228 names: Vec::new(),
229 alias: None,
230 is_wildcard: true,
231 is_relative: false,
232 line,
233 }];
234 }
235
236 Vec::new()
237 }
238
239 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
240 format!("open {}", import.module)
242 }
243
244 fn is_public(&self, node: &Node, content: &str) -> bool {
245 let text = &content[node.byte_range()];
246 !text.contains("private ") && !text.contains("internal ")
248 }
249
250 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
251 let text = &content[node.byte_range()];
252 if text.contains("private ") {
253 Visibility::Private
254 } else if text.contains("internal ") {
255 Visibility::Protected } else {
257 Visibility::Public
258 }
259 }
260
261 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
262 let name = symbol.name.as_str();
263 match symbol.kind {
264 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
265 crate::SymbolKind::Module => name == "tests" || name == "test",
266 _ => false,
267 }
268 }
269
270 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
271 None
272 }
273
274 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
275 node.child_by_field_name("body")
276 }
277
278 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
279 false
280 }
281
282 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
283 node.child_by_field_name("name")
284 .or_else(|| node.child_by_field_name("identifier"))
285 .map(|n| &content[n.byte_range()])
286 }
287
288 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
289 let ext = path.extension()?.to_str()?;
290 if ext != "fs" && ext != "fsi" && ext != "fsx" {
291 return None;
292 }
293 let stem = path.file_stem()?.to_str()?;
294 Some(stem.to_string())
296 }
297
298 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
299 let parts: Vec<&str> = module.split('.').collect();
300 let file_name = parts.last().unwrap_or(&module);
301 vec![format!("{}.fs", file_name), format!("src/{}.fs", file_name)]
302 }
303
304 fn lang_key(&self) -> &'static str {
305 "fsharp"
306 }
307
308 fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
309 import_name.starts_with("System")
311 || import_name.starts_with("Microsoft.FSharp")
312 || import_name.starts_with("FSharp.")
313 }
314
315 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
316 None
317 }
318
319 fn resolve_local_import(
320 &self,
321 import: &str,
322 _current_file: &Path,
323 project_root: &Path,
324 ) -> Option<PathBuf> {
325 let parts: Vec<&str> = import.split('.').collect();
326 let file_name = parts.last()?;
327
328 let paths = [format!("{}.fs", file_name), format!("src/{}.fs", file_name)];
329
330 for p in &paths {
331 let full = project_root.join(p);
332 if full.is_file() {
333 return Some(full);
334 }
335 }
336
337 None
338 }
339
340 fn resolve_external_import(
341 &self,
342 _import_name: &str,
343 _project_root: &Path,
344 ) -> Option<ResolvedPackage> {
345 None
347 }
348
349 fn get_version(&self, project_root: &Path) -> Option<String> {
350 for entry in std::fs::read_dir(project_root).ok()? {
352 let entry = entry.ok()?;
353 let path = entry.path();
354 if path.extension().map_or(false, |e| e == "fsproj")
355 && let Ok(content) = std::fs::read_to_string(&path)
356 && let Some(start) = content.find("<Version>")
357 {
358 let rest = &content[start + 9..];
359 if let Some(end) = rest.find("</Version>") {
360 return Some(rest[..end].to_string());
361 }
362 }
363 }
364 None
365 }
366
367 fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
368 if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
370 let cache = PathBuf::from(home).join(".nuget/packages");
371 if cache.is_dir() {
372 return Some(cache);
373 }
374 }
375 None
376 }
377
378 fn indexable_extensions(&self) -> &'static [&'static str] {
379 &["fs"]
380 }
381 fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
382 Vec::new()
383 }
384
385 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
386 use crate::traits::{has_extension, skip_dotfiles};
387 if skip_dotfiles(name) {
388 return true;
389 }
390 if is_dir && (name == "bin" || name == "obj" || name == "packages") {
391 return true;
392 }
393 !is_dir && !has_extension(name, self.indexable_extensions())
394 }
395
396 fn discover_packages(&self, _source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
397 Vec::new()
398 }
399
400 fn package_module_name(&self, entry_name: &str) -> String {
401 entry_name
402 .strip_suffix(".fs")
403 .or_else(|| entry_name.strip_suffix(".fsi"))
404 .unwrap_or(entry_name)
405 .to_string()
406 }
407
408 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
409 if path.is_file() {
410 return Some(path.to_path_buf());
411 }
412 None
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use crate::validate_unused_kinds_audit;
420
421 #[test]
422 fn unused_node_kinds_audit() {
423 #[rustfmt::skip]
424 let documented_unused: &[&str] = &[
425 "access_modifier", "anon_record_expression", "anon_record_type",
426 "anon_type_defn", "array_expression", "atomic_type", "begin_end_expression",
427 "block_comment", "block_comment_content", "brace_expression",
428 "ce_expression", "class_as_reference", "class_inherits_decl",
429 "compound_type", "constrained_type", "declaration_expression",
430 "delegate_type_defn", "do_expression", "dot_expression", "elif_expression",
431 "enum_type_case", "enum_type_cases", "enum_type_defn",
432 "exception_definition", "flexible_type", "format_string",
433 "format_string_eval", "format_triple_quoted_string", "fun_expression",
434 "function_declaration_left", "function_expression", "function_type",
435 "generic_type", "identifier", "identifier_pattern", "index_expression", "interface_implementation",
436 "interface_type_defn", "list_expression", "list_type", "literal_expression",
437 "long_identifier", "long_identifier_or_op", "method_or_prop_defn",
438 "module_abbrev", "mutate_expression", "named_module", "object_expression",
439 "op_identifier", "paren_expression", "paren_type", "postfix_type",
440 "prefixed_expression", "preproc_else", "preproc_if", "range_expression",
441 "sequential_expression", "short_comp_expression", "simple_type",
442 "static_type", "trait_member_constraint", "tuple_expression",
443 "type_abbrev_defn", "type_argument", "type_argument_constraints",
444 "type_argument_defn", "type_arguments", "type_attribute", "type_attributes",
445 "type_check_pattern", "type_extension", "type_extension_elements",
446 "type_name", "typed_expression", "typed_pattern", "typecast_expression",
447 "types", "union_type_case", "union_type_cases", "union_type_field",
448 "union_type_fields", "value_declaration", "value_declaration_left",
449 "with_field_expression",
450 ];
451 validate_unused_kinds_audit(&FSharp, documented_unused)
452 .expect("F# unused node kinds audit failed");
453 }
454}