1use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
4use crate::{ContainerBody, Import, Language, LanguageSymbols};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8pub struct ReScript;
10
11impl Language for ReScript {
12 fn name(&self) -> &'static str {
13 "ReScript"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["res", "resi"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "rescript"
20 }
21
22 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
23 Some(self)
24 }
25
26 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
27 if node.kind() != "open_statement" {
28 return Vec::new();
29 }
30
31 let text = &content[node.byte_range()];
32 vec![Import {
33 module: text.trim().to_string(),
34 names: Vec::new(),
35 alias: None,
36 is_wildcard: true,
37 is_relative: false,
38 line: node.start_position().row + 1,
39 }]
40 }
41
42 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
43 format!("open {}", import.module)
45 }
46
47 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
48 let name = symbol.name.as_str();
49 match symbol.kind {
50 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
51 crate::SymbolKind::Module => name == "tests" || name == "test",
52 _ => false,
53 }
54 }
55
56 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
57 node.child_by_field_name("body")
58 }
59
60 fn analyze_container_body(
61 &self,
62 body_node: &Node,
63 content: &str,
64 inner_indent: &str,
65 ) -> Option<ContainerBody> {
66 crate::body::analyze_brace_body(body_node, content, inner_indent)
67 }
68
69 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
70 static RESOLVER: ReScriptModuleResolver = ReScriptModuleResolver;
71 Some(&RESOLVER)
72 }
73}
74
75impl LanguageSymbols for ReScript {}
76
77pub struct ReScriptModuleResolver;
86
87impl ModuleResolver for ReScriptModuleResolver {
88 fn workspace_config(&self, root: &Path) -> ResolverConfig {
89 let mut search_roots: Vec<PathBuf> = Vec::new();
90
91 for config_name in &["bsconfig.json", "rescript.json"] {
93 let config_path = root.join(config_name);
94 if let Ok(content) = std::fs::read_to_string(&config_path)
95 && let Ok(json) = serde_json::from_str::<serde_json::Value>(&content)
96 && let Some(sources) = json.get("sources")
97 {
98 match sources {
99 serde_json::Value::String(s) => {
100 let dir = root.join(s);
101 if dir.is_dir() {
102 search_roots.push(dir);
103 }
104 }
105 serde_json::Value::Array(arr) => {
106 for item in arr {
107 let dir_str = item
108 .as_str()
109 .or_else(|| item.get("dir").and_then(|d| d.as_str()));
110 if let Some(dir_s) = dir_str {
111 let dir = root.join(dir_s);
112 if dir.is_dir() {
113 search_roots.push(dir);
114 }
115 }
116 }
117 }
118 _ => {}
119 }
120 break;
121 }
122 }
123
124 if search_roots.is_empty() {
125 for d in &["src", "lib"] {
127 let dir = root.join(d);
128 if dir.is_dir() {
129 search_roots.push(dir);
130 }
131 }
132 search_roots.push(root.to_path_buf());
133 }
134
135 ResolverConfig {
136 workspace_root: root.to_path_buf(),
137 path_mappings: Vec::new(),
138 search_roots,
139 }
140 }
141
142 fn module_of_file(&self, _root: &Path, file: &Path, _cfg: &ResolverConfig) -> Vec<ModuleId> {
143 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
144 if ext != "res" && ext != "resi" {
145 return Vec::new();
146 }
147 if let Some(stem) = file.file_stem().and_then(|s| s.to_str()) {
148 let module_name = {
150 let mut chars = stem.chars();
151 match chars.next() {
152 None => String::new(),
153 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
154 }
155 };
156 return vec![ModuleId {
157 canonical_path: module_name,
158 }];
159 }
160 Vec::new()
161 }
162
163 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
164 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
165 if ext != "res" && ext != "resi" {
166 return Resolution::NotApplicable;
167 }
168 let raw = &spec.raw;
169 let module_name = raw.strip_prefix("open ").unwrap_or(raw).trim();
171 if module_name.is_empty() {
172 return Resolution::NotFound;
173 }
174
175 for search_root in &cfg.search_roots {
176 for ext_try in &["res", "resi"] {
177 let candidate = search_root.join(format!("{}.{}", module_name, ext_try));
178 if candidate.exists() {
179 return Resolution::Resolved(candidate, module_name.to_string());
180 }
181 }
182 }
183 Resolution::NotFound
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use crate::validate_unused_kinds_audit;
191
192 #[test]
193 fn unused_node_kinds_audit() {
194 #[rustfmt::skip]
195 let documented_unused: &[&str] = &[
196 "try_expression", "ternary_expression", "while_expression", "for_expression",
198 "call_expression", "pipe_expression", "sequence_expression", "await_expression",
199 "coercion_expression", "lazy_expression", "assert_expression",
200 "parenthesized_expression", "unary_expression", "binary_expression",
201 "subscript_expression", "member_expression", "mutation_expression",
202 "extension_expression",
203 "type_identifier", "type_identifier_path", "unit_type", "generic_type",
205 "function_type", "polyvar_type", "polymorphic_type", "tuple_type",
206 "record_type", "record_type_field", "object_type", "variant_type",
207 "abstract_type", "type_arguments", "type_parameters", "type_constraint",
208 "type_annotation", "type_spread", "constrain_type",
209 "as_aliasing_type", "function_type_parameters",
210 "parenthesized_module_expression", "module_type_constraint", "module_type_annotation",
212 "module_type_of", "constrain_module", "module_identifier", "module_identifier_path",
213 "module_pack", "module_unpack",
214 "let_declaration", "exception_declaration", "variant_declaration",
216 "polyvar_declaration", "include_statement",
217 "jsx_expression", "jsx_identifier", "nested_jsx_identifier",
219 "exception_pattern", "polyvar_type_pattern",
221 "value_identifier_path", "variant_identifier",
223 "nested_variant_identifier", "polyvar_identifier", "property_identifier",
224 "extension_identifier", "decorator_identifier",
225 "else_clause", "else_if_clause",
227 "function", "expression_statement", "formal_parameters",
229 "if_expression",
231 "block",
232 "switch_expression",
233 "open_statement",
234 "switch_match",
235 ];
236 validate_unused_kinds_audit(&ReScript, documented_unused)
237 .expect("ReScript unused node kinds audit failed");
238 }
239}