1use std::path::Path;
4
5use crate::docstring::extract_preceding_prefix_comments;
6use crate::{
7 Import, ImportSpec, Language, LanguageSymbols, ModuleId, ModuleResolver, Resolution,
8 ResolverConfig, Visibility,
9};
10use tree_sitter::Node;
11
12pub struct R;
14
15impl Language for R {
16 fn name(&self) -> &'static str {
17 "R"
18 }
19 fn extensions(&self) -> &'static [&'static str] {
20 &["r", "R", "rmd", "Rmd"]
21 }
22 fn grammar_name(&self) -> &'static str {
23 "r"
24 }
25
26 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
27 Some(self)
28 }
29
30 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
31 if node.kind() != "call" {
32 return Vec::new();
33 }
34
35 let text = &content[node.byte_range()];
36 if !text.starts_with("library(") && !text.starts_with("require(") {
37 return Vec::new();
38 }
39
40 let inner = text
42 .split('(')
43 .nth(1)
44 .and_then(|s| s.split(')').next())
45 .map(|s| s.trim().trim_matches('"').trim_matches('\'').to_string());
46
47 if let Some(module) = inner {
48 return vec![Import {
49 module,
50 names: Vec::new(),
51 alias: None,
52 is_wildcard: true,
53 is_relative: false,
54 line: node.start_position().row + 1,
55 }];
56 }
57
58 Vec::new()
59 }
60
61 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
62 format!("library({})", import.module)
64 }
65
66 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
67 if node
68 .child(0)
69 .is_none_or(|n| !content[n.byte_range()].starts_with('.'))
70 {
71 Visibility::Public
72 } else {
73 Visibility::Private
74 }
75 }
76
77 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
78 let name = symbol.name.as_str();
79 match symbol.kind {
80 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
81 crate::SymbolKind::Module => name == "tests" || name == "test",
82 _ => false,
83 }
84 }
85
86 fn test_file_globs(&self) -> &'static [&'static str] {
87 &["**/test-*.R", "**/test_*.R"]
88 }
89
90 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
91 extract_preceding_prefix_comments(node, content, "#'")
93 }
94
95 fn node_name<'a>(&self, _node: &Node, _content: &'a str) -> Option<&'a str> {
96 None
97 }
98
99 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
100 static RESOLVER: RModuleResolver = RModuleResolver;
101 Some(&RESOLVER)
102 }
103}
104
105impl LanguageSymbols for R {}
106
107pub struct RModuleResolver;
117
118impl ModuleResolver for RModuleResolver {
119 fn workspace_config(&self, root: &Path) -> ResolverConfig {
120 ResolverConfig {
121 workspace_root: root.to_path_buf(),
122 path_mappings: Vec::new(),
123 search_roots: vec![root.to_path_buf()],
124 }
125 }
126
127 fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
128 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
129 if ext != "R" && ext != "r" {
130 return Vec::new();
131 }
132
133 let rel = file.strip_prefix(&cfg.workspace_root).unwrap_or(file);
134 let path_str = rel.to_string_lossy().into_owned();
135 if path_str.is_empty() {
136 return Vec::new();
137 }
138 vec![ModuleId {
139 canonical_path: path_str,
140 }]
141 }
142
143 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
144 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
145 if ext != "R" && ext != "r" {
146 return Resolution::NotApplicable;
147 }
148
149 let raw = &spec.raw;
150
151 if raw.starts_with("./") || raw.starts_with("../") {
153 let base_dir = from_file.parent().unwrap_or(&cfg.workspace_root);
154 let candidate = base_dir.join(raw);
155 if candidate.exists() {
156 return Resolution::Resolved(candidate, String::new());
157 }
158 if candidate.extension().is_none() {
160 let mut with_ext = candidate.clone();
161 with_ext.set_extension("R");
162 if with_ext.exists() {
163 return Resolution::Resolved(with_ext, String::new());
164 }
165 }
166 return Resolution::NotFound;
167 }
168
169 Resolution::NotFound
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::validate_unused_kinds_audit;
178
179 #[test]
180 fn unused_node_kinds_audit() {
181 #[rustfmt::skip]
182 let documented_unused: &[&str] = &[
183 "extract_operator", "identifier",
184 "namespace_operator", "parenthesized_expression", "return", "unary_operator",
185 "braced_expression",
187 "if_statement",
188 "while_statement",
189 "function_definition",
190 "repeat_statement",
191 "for_statement",
192 ];
193 validate_unused_kinds_audit(&R, documented_unused)
194 .expect("R unused node kinds audit failed");
195 }
196}