Skip to main content

arity_parser/
namespace.rs

1//! A lossless, typed syntax surface for R package `NAMESPACE` files.
2//!
3//! R first parses a `NAMESPACE` file as R syntax, then interprets a deliberately
4//! small set of calls, blocks, assignments, and conditional expressions. This
5//! module mirrors that split: it reuses the lossless R CST and adds typed
6//! namespace entries without evaluating conditions or assigning meaning to a
7//! directive's arguments.
8//!
9//! ```
10//! use arity_parser::namespace::{self, DirectiveKind};
11//!
12//! let source = "export(foo)\nimportFrom(stats, predict)\n";
13//! let output = namespace::parse(source);
14//! assert!(output.diagnostics.is_empty());
15//! assert_eq!(
16//!     output
17//!         .document()
18//!         .directives()
19//!         .map(|directive| directive.kind())
20//!         .collect::<Vec<_>>(),
21//!     [DirectiveKind::Export, DirectiveKind::ImportFrom]
22//! );
23//! assert_eq!(namespace::reconstruct(source), source);
24//! ```
25
26use rowan::{SyntaxElement, SyntaxToken, TextRange};
27use smol_str::SmolStr;
28
29use crate::ast::{
30    Arg, AssignmentExpr, AstNode, BlockExpr, CallExpr, HasArgList, IfExpr, Root, token_name,
31};
32use crate::parser;
33use crate::syntax::{RLanguage, SyntaxKind, SyntaxNode};
34
35pub use crate::parser::ParseDiagnostic;
36
37/// A lossless `NAMESPACE` parse and its recoverable diagnostics.
38#[derive(Debug, Clone)]
39pub struct ParseOutput {
40    /// The ordinary R CST, preserved byte for byte.
41    pub cst: SyntaxNode,
42    /// R syntax and namespace-shape diagnostics, sorted by source position.
43    pub diagnostics: Vec<ParseDiagnostic>,
44}
45
46impl ParseOutput {
47    /// The typed `NAMESPACE` view of the parsed root.
48    pub fn document(&self) -> Document {
49        Document {
50            root: Root::cast(self.cst.clone()).expect("the R parser always roots the tree at ROOT"),
51        }
52    }
53}
54
55/// Parse a `NAMESPACE` buffer without evaluating it.
56///
57/// The underlying R parser remains total and lossless. Calls in namespace
58/// directive positions are retained even when unsupported; other expressions
59/// in those positions become [`Malformed`] entries. Namespace diagnostics do
60/// not replace or suppress ordinary R syntax diagnostics.
61pub fn parse(text: &str) -> ParseOutput {
62    let parsed = parser::parse(text);
63    let document = Document {
64        root: Root::cast(parsed.cst.clone()).expect("the R parser always roots the tree at ROOT"),
65    };
66    let mut diagnostics = parsed.diagnostics;
67
68    for entry in document.entries() {
69        match entry {
70            Entry::Directive(directive) if directive.kind() == DirectiveKind::Unsupported => {
71                let Some(name) = directive.name_token() else {
72                    continue;
73                };
74                push_if_uncovered(
75                    &mut diagnostics,
76                    "unsupported NAMESPACE directive",
77                    name.text_range(),
78                );
79            }
80            Entry::Malformed(malformed) => push_if_uncovered(
81                &mut diagnostics,
82                "expected a NAMESPACE directive",
83                malformed.text_range(),
84            ),
85            Entry::Directive(_) => {}
86        }
87    }
88
89    diagnostics.sort_by_key(|diagnostic| (diagnostic.start, diagnostic.end));
90    ParseOutput {
91        cst: parsed.cst,
92        diagnostics,
93    }
94}
95
96/// Round-trip `text` through the namespace parser. Always equal to `text`.
97pub fn reconstruct(text: &str) -> String {
98    parse(text)
99        .cst
100        .descendants_with_tokens()
101        .filter_map(|element| element.into_token())
102        .map(|token| token.text().to_string())
103        .collect()
104}
105
106fn push_if_uncovered(diagnostics: &mut Vec<ParseDiagnostic>, message: &str, range: TextRange) {
107    let start = usize::from(range.start());
108    let end = usize::from(range.end());
109    let overlaps_existing = diagnostics
110        .iter()
111        .any(|diagnostic| diagnostic.start < end && start < diagnostic.end);
112    if !overlaps_existing {
113        diagnostics.push(ParseDiagnostic {
114            message: message.to_string(),
115            start,
116            end,
117        });
118    }
119}
120
121/// A typed view of a complete `NAMESPACE` document.
122#[derive(Debug, Clone, PartialEq, Eq, Hash)]
123pub struct Document {
124    root: Root,
125}
126
127impl Document {
128    /// The underlying lossless R root, including all trivia.
129    pub fn syntax(&self) -> &SyntaxNode {
130        self.root.syntax()
131    }
132
133    /// Directive and malformed entries in source order.
134    ///
135    /// Braced groups, assignments, and both conditional branches are traversed
136    /// as namespace containers. Conditions themselves are never evaluated and
137    /// calls inside a condition or directive argument are not promoted.
138    pub fn entries(&self) -> impl Iterator<Item = Entry> + use<> {
139        let mut entries = Vec::new();
140        for element in self.root.syntax().children_with_tokens() {
141            collect_entry(element, None, &mut entries);
142        }
143        entries.into_iter()
144    }
145
146    /// Every directive in source order, including unsupported directives.
147    pub fn directives(&self) -> impl Iterator<Item = Directive> + use<> {
148        self.entries().filter_map(|entry| match entry {
149            Entry::Directive(directive) => Some(directive),
150            Entry::Malformed(_) => None,
151        })
152    }
153}
154
155/// One flattened namespace entry.
156#[derive(Debug, Clone, PartialEq, Eq, Hash)]
157pub enum Entry {
158    /// A named call in a directive position.
159    Directive(Directive),
160    /// R syntax that is not a valid namespace container or directive.
161    Malformed(Malformed),
162}
163
164/// A named call in a namespace directive position.
165#[derive(Debug, Clone, PartialEq, Eq, Hash)]
166pub struct Directive {
167    call: CallExpr,
168    range: TextRange,
169}
170
171impl Directive {
172    /// The recognized directive kind, or [`DirectiveKind::Unsupported`].
173    pub fn kind(&self) -> DirectiveKind {
174        self.name()
175            .as_deref()
176            .map(DirectiveKind::from_name)
177            .unwrap_or(DirectiveKind::Unsupported)
178    }
179
180    /// The directive name, with quotes removed when it was quoted.
181    pub fn name(&self) -> Option<SmolStr> {
182        self.name_token().as_ref().map(token_name)
183    }
184
185    /// The directive name token and its exact source range.
186    pub fn name_token(&self) -> Option<SyntaxToken<RLanguage>> {
187        direct_callee_token(&self.call)
188    }
189
190    /// The call node, including its arguments and internal trivia.
191    pub fn syntax(&self) -> &SyntaxNode {
192        self.call.syntax()
193    }
194
195    /// The full namespace expression's range.
196    ///
197    /// This equals the call range for an ordinary directive and includes the
198    /// assignment wrapper for `dll <- useDynLib(...)`.
199    pub fn text_range(&self) -> TextRange {
200        self.range
201    }
202
203    /// The directive call's range, excluding an optional assignment wrapper.
204    pub fn call_range(&self) -> TextRange {
205        self.call.syntax().text_range()
206    }
207
208    /// The opening `(`, if recovery produced one.
209    pub fn l_paren(&self) -> Option<SyntaxToken<RLanguage>> {
210        child_token(self.call.syntax(), SyntaxKind::LPAREN)
211    }
212
213    /// The closing `)`, if recovery produced one.
214    pub fn r_paren(&self) -> Option<SyntaxToken<RLanguage>> {
215        child_token(self.call.syntax(), SyntaxKind::RPAREN)
216    }
217
218    /// Arguments in source order, including empty or malformed slots.
219    pub fn arguments(&self) -> impl Iterator<Item = Argument> + '_ {
220        self.call.args().map(|argument| Argument { argument })
221    }
222}
223
224/// One directive argument, retaining its raw CST and exact source ranges.
225#[derive(Debug, Clone, PartialEq, Eq, Hash)]
226pub struct Argument {
227    argument: Arg,
228}
229
230impl Argument {
231    /// The underlying argument node, including internal trivia.
232    pub fn syntax(&self) -> &SyntaxNode {
233        self.argument.syntax()
234    }
235
236    /// The whole argument range, excluding its comma separator.
237    pub fn text_range(&self) -> TextRange {
238        self.argument.syntax().text_range()
239    }
240
241    /// The optional keyword name, unquoted.
242    pub fn name(&self) -> Option<SmolStr> {
243        self.argument.name()
244    }
245
246    /// The optional keyword name token and its exact source range.
247    pub fn name_token(&self) -> Option<SyntaxToken<RLanguage>> {
248        self.argument.name_token()
249    }
250
251    /// The argument's value syntax, whether a token atom or compound node.
252    pub fn value(&self) -> Option<SyntaxElement<RLanguage>> {
253        self.argument.value()
254    }
255
256    /// The exact range of [`Self::value`].
257    pub fn value_range(&self) -> Option<TextRange> {
258        self.value().map(|value| value.text_range())
259    }
260}
261
262/// An expression that occupies a directive position but is not a directive or
263/// supported namespace container.
264#[derive(Debug, Clone, PartialEq, Eq, Hash)]
265pub struct Malformed {
266    syntax: SyntaxElement<RLanguage>,
267    range: TextRange,
268}
269
270impl Malformed {
271    /// The retained R syntax.
272    pub fn syntax(&self) -> &SyntaxElement<RLanguage> {
273        &self.syntax
274    }
275
276    /// The invalid namespace expression's range.
277    pub fn text_range(&self) -> TextRange {
278        self.range
279    }
280}
281
282/// R's recognized namespace directive names.
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
284pub enum DirectiveKind {
285    Export,
286    ExportPattern,
287    ExportClassPattern,
288    ExportClass,
289    ExportClasses,
290    ExportMethods,
291    Import,
292    ImportFrom,
293    ImportClassFrom,
294    ImportClassesFrom,
295    ImportMethodsFrom,
296    UseDynLib,
297    S3Method,
298    /// A named directive call that this parser version does not support.
299    Unsupported,
300}
301
302impl DirectiveKind {
303    /// Classify a directive name exactly as R does, including capitalization.
304    pub fn from_name(name: &str) -> Self {
305        match name {
306            "export" => Self::Export,
307            "exportPattern" => Self::ExportPattern,
308            "exportClassPattern" => Self::ExportClassPattern,
309            "exportClass" => Self::ExportClass,
310            "exportClasses" => Self::ExportClasses,
311            "exportMethods" => Self::ExportMethods,
312            "import" => Self::Import,
313            "importFrom" => Self::ImportFrom,
314            "importClassFrom" => Self::ImportClassFrom,
315            "importClassesFrom" => Self::ImportClassesFrom,
316            "importMethodsFrom" => Self::ImportMethodsFrom,
317            "useDynLib" => Self::UseDynLib,
318            "S3method" => Self::S3Method,
319            _ => Self::Unsupported,
320        }
321    }
322}
323
324fn child_token(node: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxToken<RLanguage>> {
325    node.children_with_tokens()
326        .find_map(|element| match element {
327            SyntaxElement::Token(token) if token.kind() == kind => Some(token),
328            _ => None,
329        })
330}
331
332fn direct_callee_token(call: &CallExpr) -> Option<SyntaxToken<RLanguage>> {
333    match call.base()? {
334        SyntaxElement::Token(token)
335            if matches!(token.kind(), SyntaxKind::IDENT | SyntaxKind::STRING) =>
336        {
337            Some(token)
338        }
339        _ => None,
340    }
341}
342
343fn collect_entry(
344    element: SyntaxElement<RLanguage>,
345    enclosing_range: Option<TextRange>,
346    entries: &mut Vec<Entry>,
347) {
348    if is_namespace_trivia(&element) {
349        return;
350    }
351
352    let range = enclosing_range.unwrap_or_else(|| element.text_range());
353    let SyntaxElement::Node(node) = element.clone() else {
354        entries.push(Entry::Malformed(Malformed {
355            syntax: element,
356            range,
357        }));
358        return;
359    };
360
361    match node.kind() {
362        SyntaxKind::CALL_EXPR => {
363            let call = CallExpr::cast(node).expect("CALL_EXPR always casts");
364            if direct_callee_token(&call).is_some() {
365                entries.push(Entry::Directive(Directive { call, range }));
366            } else {
367                entries.push(Entry::Malformed(Malformed {
368                    syntax: element,
369                    range,
370                }));
371            }
372        }
373        SyntaxKind::BLOCK_EXPR => {
374            let block = BlockExpr::cast(node).expect("BLOCK_EXPR always casts");
375            for statement in block.statements() {
376                collect_entry(statement, enclosing_range, entries);
377            }
378        }
379        SyntaxKind::IF_EXPR => {
380            let conditional = IfExpr::cast(node).expect("IF_EXPR always casts");
381            if let Some(then_elements) = conditional.then_elements() {
382                for branch_element in then_elements {
383                    collect_entry(branch_element, enclosing_range, entries);
384                }
385            }
386            if let Some(else_elements) = conditional.else_elements() {
387                for branch_element in else_elements {
388                    collect_entry(branch_element, enclosing_range, entries);
389                }
390            }
391        }
392        SyntaxKind::ASSIGNMENT_EXPR => {
393            let assignment = AssignmentExpr::cast(node).expect("ASSIGNMENT_EXPR always casts");
394            if matches!(
395                assignment.op_kind(),
396                Some(SyntaxKind::ASSIGN_EQ | SyntaxKind::ASSIGN_LEFT)
397            ) && let Some(value) = assignment.value_element()
398            {
399                collect_entry(value, Some(range), entries);
400            } else {
401                entries.push(Entry::Malformed(Malformed {
402                    syntax: element,
403                    range,
404                }));
405            }
406        }
407        // A `#'` line is still a comment in a NAMESPACE file. The shared R
408        // parser gives it richer roxygen structure, which this typed view
409        // deliberately treats as trivia rather than rejecting.
410        SyntaxKind::ROXYGEN_BLOCK => {}
411        _ => entries.push(Entry::Malformed(Malformed {
412            syntax: element,
413            range,
414        })),
415    }
416}
417
418fn is_namespace_trivia(element: &SyntaxElement<RLanguage>) -> bool {
419    matches!(
420        element.kind(),
421        SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE | SyntaxKind::SEMICOLON | SyntaxKind::COMMENT
422    )
423}