Skip to main content

normalize_languages/
r.rs

1//! R language support.
2
3use crate::docstring::extract_preceding_prefix_comments;
4use crate::{Import, Language, LanguageSymbols, Visibility};
5use tree_sitter::Node;
6
7/// R language support.
8pub struct R;
9
10impl Language for R {
11    fn name(&self) -> &'static str {
12        "R"
13    }
14    fn extensions(&self) -> &'static [&'static str] {
15        &["r", "R", "rmd", "Rmd"]
16    }
17    fn grammar_name(&self) -> &'static str {
18        "r"
19    }
20
21    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
22        Some(self)
23    }
24
25    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
26        if node.kind() != "call" {
27            return Vec::new();
28        }
29
30        let text = &content[node.byte_range()];
31        if !text.starts_with("library(") && !text.starts_with("require(") {
32            return Vec::new();
33        }
34
35        // Extract package name from library(pkg) or require(pkg)
36        let inner = text
37            .split('(')
38            .nth(1)
39            .and_then(|s| s.split(')').next())
40            .map(|s| s.trim().trim_matches('"').trim_matches('\'').to_string());
41
42        if let Some(module) = inner {
43            return vec![Import {
44                module,
45                names: Vec::new(),
46                alias: None,
47                is_wildcard: true,
48                is_relative: false,
49                line: node.start_position().row + 1,
50            }];
51        }
52
53        Vec::new()
54    }
55
56    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
57        // R: library(package)
58        format!("library({})", import.module)
59    }
60
61    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
62        if node
63            .child(0)
64            .is_none_or(|n| !content[n.byte_range()].starts_with('.'))
65        {
66            Visibility::Public
67        } else {
68            Visibility::Private
69        }
70    }
71
72    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
73        let name = symbol.name.as_str();
74        match symbol.kind {
75            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
76            crate::SymbolKind::Module => name == "tests" || name == "test",
77            _ => false,
78        }
79    }
80
81    fn test_file_globs(&self) -> &'static [&'static str] {
82        &["**/test-*.R", "**/test_*.R"]
83    }
84
85    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
86        // roxygen2 comments start with #'
87        extract_preceding_prefix_comments(node, content, "#'")
88    }
89
90    fn node_name<'a>(&self, _node: &Node, _content: &'a str) -> Option<&'a str> {
91        None
92    }
93}
94
95impl LanguageSymbols for R {}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::validate_unused_kinds_audit;
101
102    #[test]
103    fn unused_node_kinds_audit() {
104        #[rustfmt::skip]
105        let documented_unused: &[&str] = &[
106            "extract_operator", "identifier",
107            "namespace_operator", "parenthesized_expression", "return", "unary_operator",
108            // control flow — not extracted as symbols
109            "braced_expression",
110            "if_statement",
111            "while_statement",
112            "function_definition",
113            "repeat_statement",
114            "for_statement",
115        ];
116        validate_unused_kinds_audit(&R, documented_unused)
117            .expect("R unused node kinds audit failed");
118    }
119}