Skip to main content

arity_parser/parser/
core.rs

1use crate::parser::bracket_balancer::rebalance_brackets;
2use crate::parser::context::StatementTracker;
3use crate::parser::events::Event;
4use crate::parser::expr::parse_expr;
5use crate::parser::lexer::{TokKind, lex_with_md};
6use crate::parser::tree_builder::build_tree;
7use crate::syntax::SyntaxNode;
8
9pub use crate::parser::diagnostics::ParseDiagnostic;
10
11#[derive(Debug, Clone)]
12pub struct ParseOutput {
13    pub cst: SyntaxNode,
14    pub diagnostics: Vec<ParseDiagnostic>,
15}
16
17/// Options controlling a parse. Construct via [`Default`] and set the fields
18/// you need — the struct is `#[non_exhaustive]`, so new options can be added
19/// without a breaking change.
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
21#[non_exhaustive]
22pub struct ParseOptions {
23    /// The markdown mode of a roxygen block carrying no `@md`/`@noMd`
24    /// directive of its own (a block's directive always wins, last one in the
25    /// block winning — roxygen2's block-level toggle).
26    ///
27    /// Off means Rd-first, matching [`parse`] and roxygen2's loose-file
28    /// default. Since roxygen2 8.0.0 packages normally enable markdown
29    /// package-wide (`Config/roxygen2/markdown` in `DESCRIPTION`, or the older
30    /// `Roxygen: list(markdown = TRUE)` field) rather than writing per-block
31    /// `@md`, so a caller processing such a package should set this to `true`.
32    /// Discovering the setting from `DESCRIPTION` is the caller's job — this
33    /// crate never does file I/O beyond the text it is handed.
34    pub roxygen_markdown_default: bool,
35}
36
37impl ParseOptions {
38    /// Builder-style setter for [`roxygen_markdown_default`]: the struct is
39    /// `#[non_exhaustive]`, so outside this crate
40    /// `ParseOptions::default().with_roxygen_markdown_default(true)` is the way
41    /// to construct an options value with markdown on.
42    ///
43    /// [`roxygen_markdown_default`]: ParseOptions::roxygen_markdown_default
44    #[must_use]
45    pub fn with_roxygen_markdown_default(mut self, on: bool) -> Self {
46        self.roxygen_markdown_default = on;
47        self
48    }
49}
50
51pub fn parse(text: &str) -> ParseOutput {
52    parse_with_options(text, &ParseOptions::default())
53}
54
55/// [`parse`] with caller-supplied [`ParseOptions`]. `parse` itself is
56/// `parse_with_options` under default options.
57pub fn parse_with_options(text: &str, options: &ParseOptions) -> ParseOutput {
58    let md_default = options.roxygen_markdown_default;
59    let tokens = rebalance_brackets(lex_with_md(text, md_default));
60    let mut diagnostics = Vec::new();
61    let mut root_events = Vec::new();
62
63    let mut i = 0usize;
64    let mut statements = StatementTracker::default();
65    while i < tokens.len() {
66        if matches!(
67            tokens[i].kind,
68            TokKind::Whitespace | TokKind::Newline | TokKind::Semicolon
69        ) {
70            root_events.push(Event::Tok(i));
71            i += 1;
72            continue;
73        }
74
75        if tokens[i].kind == TokKind::RoxygenMarker {
76            i = crate::parser::roxygen::emit_roxygen_block(
77                &tokens,
78                i,
79                &mut root_events,
80                md_default,
81            );
82            continue;
83        }
84
85        let before = diagnostics.len();
86        if let Some(expr) = parse_expr(&tokens, i, 0, &mut diagnostics, md_default) {
87            // A comment is parsed as an atom but is not a statement: it needs no
88            // separator from the code it trails, and it must not become the
89            // "previous statement" for the next one's check.
90            if !tokens[expr.start].kind.is_comment_like() {
91                let recovered = diagnostics.len() > before;
92                statements.record(&tokens, expr.start, expr.end, recovered, &mut diagnostics);
93            }
94            root_events.extend(expr.events);
95            i = expr.end;
96        } else {
97            crate::parser::diagnostics::push_token_diagnostic(
98                &mut diagnostics,
99                &format!("unexpected '{}'", tokens[i].text),
100                &tokens[i],
101            );
102            root_events.push(Event::Tok(i));
103            i += 1;
104            statements.record_recovery(i);
105        }
106    }
107
108    crate::parser::diagnostics::refine_unicode_diagnostics(&tokens, &mut diagnostics);
109    let cst = build_tree(&tokens, &root_events);
110    ParseOutput { cst, diagnostics }
111}
112
113pub fn reconstruct(text: &str) -> String {
114    parse(text)
115        .cst
116        .descendants_with_tokens()
117        .filter_map(|el| el.into_token())
118        .map(|tok| tok.text().to_string())
119        .collect::<String>()
120}