Skip to main content

badness_parser/ast/
nodes.rs

1//! Typed [`AstNode`] wrappers over CST *nodes*, with positional/structural
2//! accessors. These are a read-only typed view over the generic, greedy CST
3//! (AGENTS.md decision #8): a `\section` and a `\newcommand` share the `COMMAND`
4//! shape, so accessors are *positional* ([`Command::nth_group`]) and tolerate
5//! greedily over-attached groups by construction. They expose structure only, never
6//! command *meaning* (decision #2) — no signature-DB lookup lives here.
7
8use rowan::{NodeOrToken, TextRange, TextSize};
9
10use super::{AstNode, AstToken, child, child_token, children};
11use crate::ast::tokens::ControlWord;
12use crate::syntax::{SyntaxKind, SyntaxNode};
13
14/// Declares a newtype wrapper over a `SyntaxNode` of exactly one `SyntaxKind`,
15/// implementing [`AstNode`]. Only the *identity* (`can_cast`/`cast`/`syntax`) is
16/// generated; every accessor is hand-written in a separate `impl` block. This is
17/// ordinary in-tree Rust, not codegen — no build step, no generated artifacts.
18macro_rules! ast_node {
19    ($(#[$meta:meta])* $name:ident, $kind:ident) => {
20        $(#[$meta])*
21        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
22        pub struct $name {
23            syntax: SyntaxNode,
24        }
25
26        impl AstNode for $name {
27            fn can_cast(kind: SyntaxKind) -> bool {
28                kind == SyntaxKind::$kind
29            }
30
31            fn cast(syntax: SyntaxNode) -> Option<Self> {
32                Self::can_cast(syntax.kind()).then_some(Self { syntax })
33            }
34
35            fn syntax(&self) -> &SyntaxNode {
36                &self.syntax
37            }
38        }
39    };
40}
41
42ast_node!(
43    /// A control sequence with its greedily-attached argument groups.
44    Command, COMMAND
45);
46ast_node!(
47    /// A `{ … }` group — an argument, or a nested brace group.
48    Group, GROUP
49);
50ast_node!(
51    /// A `[ … ]` optional argument.
52    Optional, OPTIONAL
53);
54ast_node!(
55    /// The `{name}` group following `\begin` / `\end`.
56    NameGroup, NAME_GROUP
57);
58ast_node!(
59    /// A `\begin{name}` node.
60    Begin, BEGIN
61);
62ast_node!(
63    /// An `\end{name}` node.
64    End, END
65);
66ast_node!(
67    /// A `\begin{…} … \end{…}` environment.
68    Environment, ENVIRONMENT
69);
70ast_node!(
71    /// An `\if… … \else … \or … \fi` conditional the shape gate paired.
72    Conditional, CONDITIONAL
73);
74ast_node!(
75    /// One branch of a [`Conditional`]. The first holds the opener, its test, and
76    /// the then-body; every later one opens with its `\else`/`\or` divider.
77    ConditionalBranch, CONDITIONAL_BRANCH
78);
79
80impl Command {
81    /// The leading `CONTROL_WORD` token, or `None` for a control symbol. The
82    /// grammar bumps the control word as the command's first token.
83    pub fn control_word(&self) -> Option<ControlWord> {
84        self.syntax
85            .children_with_tokens()
86            .filter_map(NodeOrToken::into_token)
87            .find_map(ControlWord::cast)
88    }
89
90    /// The control-word name (leading `\` stripped), or `None` for a control
91    /// symbol.
92    pub fn name(&self) -> Option<String> {
93        self.control_word().map(|cw| cw.name())
94    }
95
96    /// The range of the leading `CONTROL_WORD` token (the `\foo` itself, backslash
97    /// included), or `None` for a control symbol. Callers use this to underline just
98    /// the control word rather than the whole node, which may carry greedily-attached
99    /// argument groups.
100    pub fn control_word_range(&self) -> Option<TextRange> {
101        self.control_word().map(|cw| cw.range())
102    }
103
104    /// The `n`-th `GROUP` argument, if present. Filters `GROUP` only, so `OPTIONAL`
105    /// arguments do *not* shift brace indexing (`\cmd[o]{a}` → `nth_group(0)` is
106    /// `{a}`).
107    pub fn nth_group(&self, n: usize) -> Option<Group> {
108        self.groups().nth(n)
109    }
110
111    /// The `GROUP` argument nodes, in source order.
112    pub fn groups(&self) -> impl Iterator<Item = Group> {
113        children::<Group>(&self.syntax)
114    }
115
116    /// The `OPTIONAL` argument nodes, in source order.
117    pub fn optionals(&self) -> impl Iterator<Item = Optional> {
118        children::<Optional>(&self.syntax)
119    }
120
121    /// The literal text inside the `n`-th `GROUP` argument, braces dropped. Returns
122    /// `None` when there is no `n`-th group or it holds non-token content (a nested
123    /// command — not a flat literal). See [`Group::inner_text`].
124    pub fn nth_group_text(&self, n: usize) -> Option<String> {
125        self.nth_group(n)?.inner_text()
126    }
127
128    /// The byte range of the content *inside* the `n`-th `GROUP` argument together
129    /// with that inner text — the location-aware counterpart to
130    /// [`Command::nth_group_text`]. See [`Group::inner`].
131    pub fn nth_group_inner(&self, n: usize) -> Option<(TextRange, String)> {
132        self.nth_group(n)?.inner()
133    }
134
135    /// The byte range of this command spanning its control word through the end of
136    /// its *first* `{…}` group — e.g. `\label{key}` up to the closing brace of
137    /// `{key}`. Deliberately not [`SyntaxNode::text_range`], which the greedy parser
138    /// may stretch over a *second* group it attached without knowing arity
139    /// (`\label{a}\n{…}`; decision #8). Falls back to the full command range when the
140    /// first group is absent.
141    pub fn first_group_range(&self) -> TextRange {
142        match self.nth_group(0) {
143            Some(group) => TextRange::new(
144                self.syntax.text_range().start(),
145                group.syntax.text_range().end(),
146            ),
147            None => self.syntax.text_range(),
148        }
149    }
150}
151
152impl Group {
153    /// The literal text inside this group, with the enclosing braces dropped.
154    /// Concatenates the inner token text so content split across `WORD`/`.`/`/`/…
155    /// tokens (e.g. `chapters/my_file`, `sec:intro`) reassembles. Returns `None` when
156    /// the group holds non-token content (a nested command — not a flat literal) or a
157    /// parameter token (`\ref{#1}`, `\eqref{##1}` — a macro-parameter template whose
158    /// literal value exists only at expansion time).
159    pub fn inner_text(&self) -> Option<String> {
160        let mut text = String::new();
161        for element in self.syntax.children_with_tokens() {
162            match element {
163                NodeOrToken::Token(token) => match token.kind() {
164                    SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
165                    SyntaxKind::HASH => return None,
166                    _ => text.push_str(token.text()),
167                },
168                // A nested node (e.g. a COMMAND) means the argument isn't a flat
169                // literal; treat the whole thing as unresolvable.
170                NodeOrToken::Node(_) => return None,
171            }
172        }
173        Some(text)
174    }
175
176    /// The byte range of the content *inside* this group (the span between the
177    /// braces) together with that inner text — the location-aware counterpart to
178    /// [`Group::inner_text`]. The inner range runs from the first inner token's start
179    /// to the last inner token's end; an empty group (`{}`) yields a zero-width range
180    /// just after the `{`. Returns `None` under the same conditions as
181    /// [`Group::inner_text`].
182    ///
183    /// The text/range correspondence is exact: in the success path the group holds
184    /// only flat tokens, so its inner bytes are contiguous and per-key sub-ranges can
185    /// be sliced off the range by byte offset (used by the semantic builder to give
186    /// each key in a `\cref{a,b}` its own precise span).
187    pub fn inner(&self) -> Option<(TextRange, String)> {
188        let mut text = String::new();
189        let mut start: Option<TextSize> = None;
190        let mut end: Option<TextSize> = None;
191        // Fallback anchor for an empty group: the byte just after the opening brace.
192        let mut after_l_brace = self.syntax.text_range().start();
193        for element in self.syntax.children_with_tokens() {
194            match element {
195                NodeOrToken::Token(token) => match token.kind() {
196                    SyntaxKind::L_BRACE => after_l_brace = token.text_range().end(),
197                    SyntaxKind::R_BRACE => {}
198                    SyntaxKind::HASH => return None,
199                    _ => {
200                        let range = token.text_range();
201                        start.get_or_insert(range.start());
202                        end = Some(range.end());
203                        text.push_str(token.text());
204                    }
205                },
206                // A nested node means the argument isn't a flat literal; treat the
207                // whole thing as unresolvable, like `inner_text`.
208                NodeOrToken::Node(_) => return None,
209            }
210        }
211        let range = match (start, end) {
212            (Some(start), Some(end)) => TextRange::new(start, end),
213            _ => TextRange::empty(after_l_brace),
214        };
215        Some((range, text))
216    }
217
218    /// The raw inner source of this group with its outer braces dropped, but *all*
219    /// interior text preserved — nested `{…}` braces included. Unlike
220    /// [`Group::inner_text`], which bails on nested nodes, this reconstructs the
221    /// verbatim content needed for an xparse argument spec like `{m O{0} m}` (whose
222    /// `{0}` default parses as a nested `GROUP`). Trivia is kept verbatim; the caller
223    /// tokenizes the result.
224    pub fn inner_source(&self) -> String {
225        inner_source_of(&self.syntax)
226    }
227
228    /// The single `COMMAND` child wrapped in this group, if any.
229    pub fn command(&self) -> Option<Command> {
230        child::<Command>(&self.syntax)
231    }
232
233    /// The control-word name (leading `\` stripped) of a single `COMMAND` wrapped in
234    /// this group, as in a `\newcommand{\foo}` name group. Returns `None` unless the
235    /// group's only relevant child is exactly one control word.
236    pub fn command_name(&self) -> Option<String> {
237        self.command()?.name()
238    }
239}
240
241/// The shared body of [`Group::inner_source`], kept kind-agnostic so the
242/// free-function shim can call it on any node — an xparse default like `O{0}` parses
243/// its `{0}` as a nested group but a top-level default body may be an `OPTIONAL`
244/// rather than a `GROUP`. Concatenates all descendant token text, then drops a single
245/// leading `{` and trailing `}` if present (a bracket-delimited `OPTIONAL` keeps its
246/// brackets, matching the pre-wrapper behavior).
247pub(crate) fn inner_source_of(node: &SyntaxNode) -> String {
248    let mut text = String::new();
249    for element in node.descendants_with_tokens() {
250        if let NodeOrToken::Token(token) = element {
251            text.push_str(token.text());
252        }
253    }
254    let inner = text.strip_prefix('{').unwrap_or(&text);
255    inner.strip_suffix('}').unwrap_or(inner).to_string()
256}
257
258impl NameGroup {
259    /// The environment name — the literal text of this `NAME_GROUP`, braces dropped.
260    /// Returns `None` when it holds non-token content.
261    pub fn text(&self) -> Option<String> {
262        let mut text = String::new();
263        for element in self.syntax.children_with_tokens() {
264            match element {
265                NodeOrToken::Token(token) => match token.kind() {
266                    SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
267                    _ => text.push_str(token.text()),
268                },
269                NodeOrToken::Node(_) => return None,
270            }
271        }
272        Some(text)
273    }
274
275    /// The byte range of the name *inside* this `NAME_GROUP` (the span between the
276    /// braces) — the location-aware counterpart to [`NameGroup::text`]. Returns
277    /// `None` when it holds a nested node or the name is empty (`\begin{}`, nothing to
278    /// highlight).
279    pub fn range(&self) -> Option<TextRange> {
280        let mut start: Option<TextSize> = None;
281        let mut end: Option<TextSize> = None;
282        for element in self.syntax.children_with_tokens() {
283            match element {
284                NodeOrToken::Token(token) => match token.kind() {
285                    SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
286                    _ => {
287                        let range = token.text_range();
288                        start.get_or_insert(range.start());
289                        end = Some(range.end());
290                    }
291                },
292                NodeOrToken::Node(_) => return None,
293            }
294        }
295        Some(TextRange::new(start?, end?))
296    }
297}
298
299/// The environment an alias-delimiter node names: its bare `CONTROL_WORD` with the
300/// leading `\` stripped, when that word is not `keyword` (the spelled-out
301/// `\begin`/`\end`, whose name lives in a `NAME_GROUP` instead).
302fn alias_delimiter_name(node: &SyntaxNode, keyword: &str) -> Option<String> {
303    let head = child_token::<ControlWord>(node)?;
304    let text = head.syntax().text();
305    (text != keyword)
306        .then(|| text.strip_prefix('\\'))
307        .flatten()
308        .filter(|name| !name.is_empty())
309        .map(str::to_owned)
310}
311
312impl Begin {
313    /// The `{name}` group following `\begin`.
314    pub fn name_group(&self) -> Option<NameGroup> {
315        child::<NameGroup>(&self.syntax)
316    }
317
318    /// The environment name (braces dropped), or `None` for a malformed `\begin`.
319    ///
320    /// A `BEGIN` opened by an *environment alias* (`\bea`, issue #109) carries no
321    /// `NAME_GROUP` at all — the whole node is the bare control word — so the name
322    /// falls back to that word with its `\` stripped. Positional and meaning-free,
323    /// per decision #10: it reads the name from wherever the tree puts it and looks
324    /// nothing up. `Signatures::environment` is what maps `bea` on to the target's
325    /// curated behavior.
326    ///
327    /// The fallback is guarded on the head *not* being `\begin`, so the malformed
328    /// `\begin`-without-a-name path (which builds a `BEGIN` with no `NAME_GROUP`)
329    /// keeps reporting `None` rather than suddenly claiming to be named `begin`.
330    pub fn name(&self) -> Option<String> {
331        match self.name_group() {
332            Some(group) => group.text(),
333            None => alias_delimiter_name(&self.syntax, "\\begin"),
334        }
335    }
336
337    /// Whether this `BEGIN` is an *environment-alias* delimiter — a bare control
338    /// word standing in for `\begin{X}` — rather than a spelled-out `\begin{X}`.
339    ///
340    /// Purely structural (no `NAME_GROUP`, head is not `\begin`), like every other
341    /// accessor here. It exists because [`name`](Self::name) makes the two shapes
342    /// indistinguishable by name, and the alias table describes the *command*, not
343    /// the name: a literal `\begin{bea}` written in a file that also defines `\bea`
344    /// as an alias is a different, unrelated environment and must not inherit the
345    /// target's behavior. `Signatures::environment_at` is the consumer.
346    pub fn is_alias(&self) -> bool {
347        self.name_group().is_none() && alias_delimiter_name(&self.syntax, "\\begin").is_some()
348    }
349
350    /// The byte range of the environment name inside the `NAME_GROUP`.
351    pub fn name_range(&self) -> Option<TextRange> {
352        self.name_group()?.range()
353    }
354}
355
356impl End {
357    /// The `{name}` group following `\end`.
358    pub fn name_group(&self) -> Option<NameGroup> {
359        child::<NameGroup>(&self.syntax)
360    }
361
362    /// The environment name (braces dropped), or `None` for a malformed `\end`.
363    pub fn name(&self) -> Option<String> {
364        self.name_group()?.text()
365    }
366
367    /// The byte range of the environment name inside the `NAME_GROUP`.
368    pub fn name_range(&self) -> Option<TextRange> {
369        self.name_group()?.range()
370    }
371}
372
373impl Environment {
374    /// The `\begin{…}` node, replacing the raw `children().find(==BEGIN)` idiom.
375    pub fn begin(&self) -> Option<Begin> {
376        child::<Begin>(&self.syntax)
377    }
378
379    /// The `\end{…}` node.
380    pub fn end(&self) -> Option<End> {
381        child::<End>(&self.syntax)
382    }
383
384    /// The environment name, read from the `\begin` node.
385    pub fn name(&self) -> Option<String> {
386        self.begin()?.name()
387    }
388}
389
390impl Conditional {
391    /// The branches, in source order — at least one, since the grammar opens a
392    /// branch before the opener.
393    pub fn branches(&self) -> impl Iterator<Item = ConditionalBranch> {
394        children::<ConditionalBranch>(&self.syntax)
395    }
396
397    /// The closing `\fi`, read *positionally* as the last child node rather than
398    /// by matching the name: which control word closes a conditional is the
399    /// grammar's call, and re-deciding it here would be the same meaning check
400    /// twice (decision #10). `None` only if the gate's guarantee is ever broken,
401    /// which callers must tolerate rather than assume away.
402    pub fn closer(&self) -> Option<Command> {
403        self.syntax.last_child().and_then(Command::cast)
404    }
405}
406
407impl ConditionalBranch {
408    /// The leading `\if…`/`\else`/`\or` control word of this branch, if it opens
409    /// with one. Positional: the first child node, cast to a `COMMAND`.
410    pub fn head(&self) -> Option<Command> {
411        self.syntax.first_child().and_then(Command::cast)
412    }
413}