1use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
4use tree_sitter::Node;
5
6pub struct FSharp;
8
9impl Language for FSharp {
10 fn name(&self) -> &'static str {
11 "F#"
12 }
13 fn extensions(&self) -> &'static [&'static str] {
14 &["fs", "fsi", "fsx"]
15 }
16 fn grammar_name(&self) -> &'static str {
17 "fsharp"
18 }
19
20 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
21 Some(self)
22 }
23
24 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
25 let mut doc_lines: Vec<String> = Vec::new();
26 let mut prev = node.prev_sibling();
27
28 while let Some(sibling) = prev {
29 if sibling.kind() == "line_comment" {
30 let text = &content[sibling.byte_range()];
31 if let Some(rest) = text.strip_prefix("///") {
32 let line = rest.strip_prefix(' ').unwrap_or(rest);
33 doc_lines.push(line.to_string());
34 } else {
35 break;
36 }
37 } else {
38 break;
39 }
40 prev = sibling.prev_sibling();
41 }
42
43 if doc_lines.is_empty() {
44 return None;
45 }
46
47 doc_lines.reverse();
48
49 let joined: String = doc_lines
51 .iter()
52 .map(|l| {
53 let l = l.trim();
54 if l.starts_with('<') && l.ends_with('>') {
56 ""
58 } else {
59 l
60 }
61 })
62 .filter(|l| !l.is_empty())
63 .collect::<Vec<&str>>()
64 .join(" ");
65
66 let trimmed = joined.trim().to_string();
67 if trimmed.is_empty() {
68 None
69 } else {
70 Some(trimmed)
71 }
72 }
73
74 fn build_signature(&self, node: &Node, content: &str) -> String {
75 let text = &content[node.byte_range()];
76 text.lines().next().unwrap_or(text).trim().to_string()
77 }
78
79 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
80 let text = &content[node.byte_range()];
81 let line = node.start_position().row + 1;
82
83 if let Some(rest) = text.strip_prefix("open ") {
84 let module = rest.trim().to_string();
85 return vec![Import {
86 module,
87 names: Vec::new(),
88 alias: None,
89 is_wildcard: true,
90 is_relative: false,
91 line,
92 }];
93 }
94
95 Vec::new()
96 }
97
98 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
99 format!("open {}", import.module)
101 }
102
103 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
104 let text = &content[node.byte_range()];
105 if text.contains("private ") {
106 Visibility::Private
107 } else if text.contains("internal ") {
108 Visibility::Protected } else {
110 Visibility::Public
111 }
112 }
113
114 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
115 let name = symbol.name.as_str();
116 match symbol.kind {
117 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
118 crate::SymbolKind::Module => name == "tests" || name == "test",
119 _ => false,
120 }
121 }
122
123 fn test_file_globs(&self) -> &'static [&'static str] {
124 &["**/*Test.fs", "**/*Tests.fs"]
125 }
126
127 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
128 node.child_by_field_name("body")
129 }
130
131 fn analyze_container_body(
132 &self,
133 body_node: &Node,
134 content: &str,
135 inner_indent: &str,
136 ) -> Option<ContainerBody> {
137 crate::body::analyze_end_body(body_node, content, inner_indent)
138 }
139
140 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
141 if let Some(n) = node
143 .child_by_field_name("name")
144 .or_else(|| node.child_by_field_name("identifier"))
145 {
146 return Some(&content[n.byte_range()]);
147 }
148
149 let kind = node.kind();
150 let mut cursor = node.walk();
151
152 match kind {
153 "function_or_value_defn" => {
155 for child in node.children(&mut cursor) {
156 if child.kind() == "function_declaration_left"
157 || child.kind() == "value_declaration_left"
158 {
159 let mut inner = child.walk();
160 for c in child.children(&mut inner) {
161 if c.kind() == "identifier" {
162 return Some(&content[c.byte_range()]);
163 }
164 }
165 }
166 }
167 None
168 }
169 "named_module" => {
171 for child in node.children(&mut cursor) {
172 if child.kind() == "long_identifier" {
173 let mut inner = child.walk();
174 for c in child.children(&mut inner) {
175 if c.kind() == "identifier" {
176 return Some(&content[c.byte_range()]);
177 }
178 }
179 }
180 }
181 None
182 }
183 "type_definition" => {
185 for child in node.children(&mut cursor) {
186 let ck = child.kind();
187 if ck.ends_with("_type_defn") || ck == "type_abbrev_defn" {
188 let mut inner = child.walk();
189 for c in child.children(&mut inner) {
190 if c.kind() == "type_name" {
191 let mut inner2 = c.walk();
192 for c2 in c.children(&mut inner2) {
193 if c2.kind() == "identifier" {
194 return Some(&content[c2.byte_range()]);
195 }
196 }
197 }
198 }
199 }
200 }
201 None
202 }
203 "member_defn" => {
205 for child in node.children(&mut cursor) {
206 if child.kind() == "method_or_prop_defn" {
207 let mut inner = child.walk();
208 for c in child.children(&mut inner) {
209 if c.kind() == "identifier" {
210 return Some(&content[c.byte_range()]);
211 }
212 }
213 }
214 }
215 None
216 }
217 _ => None,
218 }
219 }
220}
221
222impl LanguageSymbols for FSharp {}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use crate::validate_unused_kinds_audit;
228
229 #[test]
230 fn unused_node_kinds_audit() {
231 #[rustfmt::skip]
232 let documented_unused: &[&str] = &[
233 "access_modifier", "anon_record_expression", "anon_record_type",
234 "anon_type_defn", "array_expression", "atomic_type", "begin_end_expression",
235 "block_comment", "block_comment_content", "brace_expression",
236 "ce_expression", "class_as_reference", "class_inherits_decl",
237 "compound_type", "constrained_type", "declaration_expression",
238 "delegate_type_defn", "do_expression", "dot_expression", "elif_expression",
239 "enum_type_case", "enum_type_cases", "enum_type_defn",
240 "exception_definition", "flexible_type", "format_string",
241 "format_string_eval", "format_triple_quoted_string", "fun_expression", "function_expression", "function_type",
242 "generic_type", "identifier_pattern", "index_expression", "interface_implementation",
243 "interface_type_defn", "list_expression", "list_type", "literal_expression",
244 "long_identifier_or_op",
245 "module_abbrev", "module_defn", "mutate_expression", "object_expression",
246 "op_identifier", "paren_expression", "paren_type", "postfix_type",
247 "prefixed_expression", "preproc_else", "preproc_if", "range_expression",
248 "sequential_expression", "short_comp_expression", "simple_type",
249 "static_type", "trait_member_constraint", "tuple_expression",
250 "type_abbrev_defn", "type_argument", "type_argument_constraints",
251 "type_argument_defn", "type_arguments", "type_attribute", "type_attributes",
252 "type_check_pattern", "type_extension", "type_extension_elements", "typed_expression", "typed_pattern", "typecast_expression",
253 "types", "union_type_case", "union_type_cases", "union_type_field",
254 "union_type_fields", "value_declaration", "value_declaration_left",
255 "with_field_expression",
256 "union_type_defn",
258 "for_expression",
259 "application_expression",
260 "import_decl",
261 "while_expression",
262 "match_expression",
263 "record_type_defn",
264 "infix_expression",
265 "if_expression",
266 "try_expression",
267 ];
268 validate_unused_kinds_audit(&FSharp, documented_unused)
269 .expect("F# unused node kinds audit failed");
270 }
271}