Skip to main content

normalize_languages/
r.rs

1//! R language support.
2
3use 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
12/// R language support.
13pub 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        // Extract package name from library(pkg) or require(pkg)
41        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        // R: library(package)
63        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        // roxygen2 comments start with #'
92        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
107// =============================================================================
108// R Module Resolver
109// =============================================================================
110
111/// Module resolver for R.
112///
113/// `source("./utils.R")` is a relative file load — resolved against the caller's
114/// directory. `library(pkg)` / `require(pkg)` are package calls — `NotFound`
115/// because they require the R package library to resolve.
116pub 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        // Relative paths: ./utils.R or ../shared/helpers.R
152        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            // Try adding .R extension if no extension present
159            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        // library(pkg) / require(pkg) — package calls, not resolvable here
170        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            // control flow — not extracted as symbols
186            "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}