1use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
4use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
5use std::path::Path;
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 as_symbols(&self) -> Option<&dyn LanguageSymbols> {
23 Some(self)
24 }
25
26 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
27 let mut doc_lines: Vec<String> = Vec::new();
28 let mut prev = node.prev_sibling();
29
30 while let Some(sibling) = prev {
31 if sibling.kind() == "line_comment" {
32 let text = &content[sibling.byte_range()];
33 if let Some(rest) = text.strip_prefix("///") {
34 let line = rest.strip_prefix(' ').unwrap_or(rest);
35 doc_lines.push(line.to_string());
36 } else {
37 break;
38 }
39 } else {
40 break;
41 }
42 prev = sibling.prev_sibling();
43 }
44
45 if doc_lines.is_empty() {
46 return None;
47 }
48
49 doc_lines.reverse();
50
51 let joined: String = doc_lines
53 .iter()
54 .map(|l| {
55 let l = l.trim();
56 if l.starts_with('<') && l.ends_with('>') {
58 ""
60 } else {
61 l
62 }
63 })
64 .filter(|l| !l.is_empty())
65 .collect::<Vec<&str>>()
66 .join(" ");
67
68 let trimmed = joined.trim().to_string();
69 if trimmed.is_empty() {
70 None
71 } else {
72 Some(trimmed)
73 }
74 }
75
76 fn build_signature(&self, node: &Node, content: &str) -> String {
77 let text = &content[node.byte_range()];
78 text.lines().next().unwrap_or(text).trim().to_string()
79 }
80
81 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
82 let text = &content[node.byte_range()];
83 let line = node.start_position().row + 1;
84
85 if let Some(rest) = text.strip_prefix("open ") {
86 let module = rest.trim().to_string();
87 return vec![Import {
88 module,
89 names: Vec::new(),
90 alias: None,
91 is_wildcard: true,
92 is_relative: false,
93 line,
94 }];
95 }
96
97 Vec::new()
98 }
99
100 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
101 format!("open {}", import.module)
103 }
104
105 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
106 let text = &content[node.byte_range()];
107 if text.contains("private ") {
108 Visibility::Private
109 } else if text.contains("internal ") {
110 Visibility::Protected } else {
112 Visibility::Public
113 }
114 }
115
116 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
117 let name = symbol.name.as_str();
118 match symbol.kind {
119 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
120 crate::SymbolKind::Module => name == "tests" || name == "test",
121 _ => false,
122 }
123 }
124
125 fn test_file_globs(&self) -> &'static [&'static str] {
126 &["**/*Test.fs", "**/*Tests.fs"]
127 }
128
129 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
130 node.child_by_field_name("body")
131 }
132
133 fn analyze_container_body(
134 &self,
135 body_node: &Node,
136 content: &str,
137 inner_indent: &str,
138 ) -> Option<ContainerBody> {
139 crate::body::analyze_end_body(body_node, content, inner_indent)
140 }
141
142 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
143 if let Some(n) = node
145 .child_by_field_name("name")
146 .or_else(|| node.child_by_field_name("identifier"))
147 {
148 return Some(&content[n.byte_range()]);
149 }
150
151 let kind = node.kind();
152 let mut cursor = node.walk();
153
154 match kind {
155 "function_or_value_defn" => {
157 for child in node.children(&mut cursor) {
158 if child.kind() == "function_declaration_left"
159 || child.kind() == "value_declaration_left"
160 {
161 let mut inner = child.walk();
162 for c in child.children(&mut inner) {
163 if c.kind() == "identifier" {
164 return Some(&content[c.byte_range()]);
165 }
166 }
167 }
168 }
169 None
170 }
171 "named_module" => {
173 for child in node.children(&mut cursor) {
174 if child.kind() == "long_identifier" {
175 let mut inner = child.walk();
176 for c in child.children(&mut inner) {
177 if c.kind() == "identifier" {
178 return Some(&content[c.byte_range()]);
179 }
180 }
181 }
182 }
183 None
184 }
185 "type_definition" => {
187 for child in node.children(&mut cursor) {
188 let ck = child.kind();
189 if ck.ends_with("_type_defn") || ck == "type_abbrev_defn" {
190 let mut inner = child.walk();
191 for c in child.children(&mut inner) {
192 if c.kind() == "type_name" {
193 let mut inner2 = c.walk();
194 for c2 in c.children(&mut inner2) {
195 if c2.kind() == "identifier" {
196 return Some(&content[c2.byte_range()]);
197 }
198 }
199 }
200 }
201 }
202 }
203 None
204 }
205 "member_defn" => {
207 for child in node.children(&mut cursor) {
208 if child.kind() == "method_or_prop_defn" {
209 let mut inner = child.walk();
210 for c in child.children(&mut inner) {
211 if c.kind() == "identifier" {
212 return Some(&content[c.byte_range()]);
213 }
214 }
215 }
216 }
217 None
218 }
219 _ => None,
220 }
221 }
222
223 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
224 static RESOLVER: FSharpModuleResolver = FSharpModuleResolver;
225 Some(&RESOLVER)
226 }
227}
228
229impl LanguageSymbols for FSharp {}
230
231pub struct FSharpModuleResolver;
239
240impl ModuleResolver for FSharpModuleResolver {
241 fn workspace_config(&self, root: &Path) -> ResolverConfig {
242 ResolverConfig {
243 workspace_root: root.to_path_buf(),
244 path_mappings: Vec::new(),
245 search_roots: vec![root.to_path_buf()],
246 }
247 }
248
249 fn module_of_file(&self, root: &Path, file: &Path, _cfg: &ResolverConfig) -> Vec<ModuleId> {
250 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
251 if ext != "fs" && ext != "fsi" && ext != "fsx" {
252 return Vec::new();
253 }
254 if let Ok(rel) = file.strip_prefix(root) {
255 let rel_str = rel
256 .to_str()
257 .unwrap_or("")
258 .trim_end_matches(".fsx")
259 .trim_end_matches(".fsi")
260 .trim_end_matches(".fs")
261 .replace(['/', '\\'], ".");
262 if !rel_str.is_empty() {
263 return vec![ModuleId {
264 canonical_path: rel_str,
265 }];
266 }
267 }
268 Vec::new()
269 }
270
271 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
272 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
273 if ext != "fs" && ext != "fsi" && ext != "fsx" {
274 return Resolution::NotApplicable;
275 }
276 let raw = spec.raw.strip_prefix("open ").unwrap_or(&spec.raw).trim();
278 let exported_name = raw.rsplit('.').next().unwrap_or(raw).to_string();
279 let path_part = raw.replace('.', "/");
280
281 for ext_try in &["fs", "fsi", "fsx"] {
282 let candidate = cfg
283 .workspace_root
284 .join(format!("{}.{}", path_part, ext_try));
285 if candidate.exists() {
286 return Resolution::Resolved(candidate, exported_name.clone());
287 }
288 }
289 if let Some(parent) = from_file.parent() {
291 let last = raw.rsplit('.').next().unwrap_or(raw);
292 for ext_try in &["fs", "fsi"] {
293 let candidate = parent.join(format!("{}.{}", last, ext_try));
294 if candidate.exists() {
295 return Resolution::Resolved(candidate, exported_name.clone());
296 }
297 }
298 }
299 Resolution::NotFound
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use crate::validate_unused_kinds_audit;
307
308 #[test]
309 fn unused_node_kinds_audit() {
310 #[rustfmt::skip]
311 let documented_unused: &[&str] = &[
312 "access_modifier", "anon_record_expression", "anon_record_type",
313 "anon_type_defn", "array_expression", "atomic_type", "begin_end_expression",
314 "block_comment", "block_comment_content", "brace_expression",
315 "ce_expression", "class_as_reference", "class_inherits_decl",
316 "compound_type", "constrained_type", "declaration_expression",
317 "delegate_type_defn", "do_expression", "dot_expression", "elif_expression",
318 "enum_type_case", "enum_type_cases", "enum_type_defn",
319 "exception_definition", "flexible_type", "format_string",
320 "format_string_eval", "format_triple_quoted_string", "fun_expression", "function_expression", "function_type",
321 "generic_type", "identifier_pattern", "index_expression", "interface_implementation",
322 "interface_type_defn", "list_expression", "list_type", "literal_expression",
323 "long_identifier_or_op",
324 "module_abbrev", "module_defn", "mutate_expression", "object_expression",
325 "op_identifier", "paren_expression", "paren_type", "postfix_type",
326 "prefixed_expression", "preproc_else", "preproc_if", "range_expression",
327 "sequential_expression", "short_comp_expression", "simple_type",
328 "static_type", "trait_member_constraint", "tuple_expression",
329 "type_abbrev_defn", "type_argument", "type_argument_constraints",
330 "type_argument_defn", "type_arguments", "type_attribute", "type_attributes",
331 "type_check_pattern", "type_extension", "type_extension_elements", "typed_expression", "typed_pattern", "typecast_expression",
332 "types", "union_type_case", "union_type_cases", "union_type_field",
333 "union_type_fields", "value_declaration", "value_declaration_left",
334 "with_field_expression",
335 "union_type_defn",
337 "for_expression",
338 "application_expression",
339 "import_decl",
340 "while_expression",
341 "match_expression",
342 "record_type_defn",
343 "infix_expression",
344 "if_expression",
345 "try_expression",
346 ];
347 validate_unused_kinds_audit(&FSharp, documented_unused)
348 .expect("F# unused node kinds audit failed");
349 }
350}