Skip to main content

normalize_languages/
fsharp.rs

1//! F# language support.
2
3use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
4use tree_sitter::Node;
5
6/// F# language support.
7pub 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        // Strip XML tags for a cleaner docstring
50        let joined: String = doc_lines
51            .iter()
52            .map(|l| {
53                let l = l.trim();
54                // Strip common XML doc tags like <summary>, </summary>, <param>, etc.
55                if l.starts_with('<') && l.ends_with('>') {
56                    // Pure tag line (e.g. <summary>), skip it
57                    ""
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        // F#: open Namespace
100        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 // Using Protected for internal
109        } 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        // Try standard field names first
142        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 > function_declaration_left > identifier (first child)
154            "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 > long_identifier > identifier (first)
170            "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 > *_type_defn > type_name > identifier
184            "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 > method_or_prop_defn > identifier (first)
204            "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            // covered by tags.scm
257            "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}