badness-parser 0.2.0

Lossless CST parser, semantic model, and command-signature database for LaTeX and BibTeX
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
//! Typed [`AstNode`] wrappers over CST *nodes*, with positional/structural
//! accessors. These are a read-only typed view over the generic, greedy CST
//! (AGENTS.md decision #8): a `\section` and a `\newcommand` share the `COMMAND`
//! shape, so accessors are *positional* ([`Command::nth_group`]) and tolerate
//! greedily over-attached groups by construction. They expose structure only, never
//! command *meaning* (decision #2) — no signature-DB lookup lives here.

use rowan::{NodeOrToken, TextRange, TextSize};

use super::{AstNode, AstToken, child, child_token, children};
use crate::ast::tokens::ControlWord;
use crate::syntax::{SyntaxKind, SyntaxNode};

/// Declares a newtype wrapper over a `SyntaxNode` of exactly one `SyntaxKind`,
/// implementing [`AstNode`]. Only the *identity* (`can_cast`/`cast`/`syntax`) is
/// generated; every accessor is hand-written in a separate `impl` block. This is
/// ordinary in-tree Rust, not codegen — no build step, no generated artifacts.
macro_rules! ast_node {
    ($(#[$meta:meta])* $name:ident, $kind:ident) => {
        $(#[$meta])*
        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
        pub struct $name {
            syntax: SyntaxNode,
        }

        impl AstNode for $name {
            fn can_cast(kind: SyntaxKind) -> bool {
                kind == SyntaxKind::$kind
            }

            fn cast(syntax: SyntaxNode) -> Option<Self> {
                Self::can_cast(syntax.kind()).then_some(Self { syntax })
            }

            fn syntax(&self) -> &SyntaxNode {
                &self.syntax
            }
        }
    };
}

ast_node!(
    /// A control sequence with its greedily-attached argument groups.
    Command, COMMAND
);
ast_node!(
    /// A `{ … }` group — an argument, or a nested brace group.
    Group, GROUP
);
ast_node!(
    /// A `[ … ]` optional argument.
    Optional, OPTIONAL
);
ast_node!(
    /// The `{name}` group following `\begin` / `\end`.
    NameGroup, NAME_GROUP
);
ast_node!(
    /// A `\begin{name}` node.
    Begin, BEGIN
);
ast_node!(
    /// An `\end{name}` node.
    End, END
);
ast_node!(
    /// A `\begin{…} … \end{…}` environment.
    Environment, ENVIRONMENT
);
ast_node!(
    /// An `\if… … \else … \or … \fi` conditional the shape gate paired.
    Conditional, CONDITIONAL
);
ast_node!(
    /// One branch of a [`Conditional`]. The first holds the opener, its test, and
    /// the then-body; every later one opens with its `\else`/`\or` divider.
    ConditionalBranch, CONDITIONAL_BRANCH
);

impl Command {
    /// The leading `CONTROL_WORD` token, or `None` for a control symbol. The
    /// grammar bumps the control word as the command's first token.
    pub fn control_word(&self) -> Option<ControlWord> {
        self.syntax
            .children_with_tokens()
            .filter_map(NodeOrToken::into_token)
            .find_map(ControlWord::cast)
    }

    /// The control-word name (leading `\` stripped), or `None` for a control
    /// symbol.
    pub fn name(&self) -> Option<String> {
        self.control_word().map(|cw| cw.name())
    }

    /// The range of the leading `CONTROL_WORD` token (the `\foo` itself, backslash
    /// included), or `None` for a control symbol. Callers use this to underline just
    /// the control word rather than the whole node, which may carry greedily-attached
    /// argument groups.
    pub fn control_word_range(&self) -> Option<TextRange> {
        self.control_word().map(|cw| cw.range())
    }

    /// The `n`-th `GROUP` argument, if present. Filters `GROUP` only, so `OPTIONAL`
    /// arguments do *not* shift brace indexing (`\cmd[o]{a}` → `nth_group(0)` is
    /// `{a}`).
    pub fn nth_group(&self, n: usize) -> Option<Group> {
        self.groups().nth(n)
    }

    /// The `GROUP` argument nodes, in source order.
    pub fn groups(&self) -> impl Iterator<Item = Group> {
        children::<Group>(&self.syntax)
    }

    /// The `OPTIONAL` argument nodes, in source order.
    pub fn optionals(&self) -> impl Iterator<Item = Optional> {
        children::<Optional>(&self.syntax)
    }

    /// The literal text inside the `n`-th `GROUP` argument, braces dropped. Returns
    /// `None` when there is no `n`-th group or it holds non-token content (a nested
    /// command — not a flat literal). See [`Group::inner_text`].
    pub fn nth_group_text(&self, n: usize) -> Option<String> {
        self.nth_group(n)?.inner_text()
    }

    /// The byte range of the content *inside* the `n`-th `GROUP` argument together
    /// with that inner text — the location-aware counterpart to
    /// [`Command::nth_group_text`]. See [`Group::inner`].
    pub fn nth_group_inner(&self, n: usize) -> Option<(TextRange, String)> {
        self.nth_group(n)?.inner()
    }

    /// The byte range of this command spanning its control word through the end of
    /// its *first* `{…}` group — e.g. `\label{key}` up to the closing brace of
    /// `{key}`. Deliberately not [`SyntaxNode::text_range`], which the greedy parser
    /// may stretch over a *second* group it attached without knowing arity
    /// (`\label{a}\n{…}`; decision #8). Falls back to the full command range when the
    /// first group is absent.
    pub fn first_group_range(&self) -> TextRange {
        match self.nth_group(0) {
            Some(group) => TextRange::new(
                self.syntax.text_range().start(),
                group.syntax.text_range().end(),
            ),
            None => self.syntax.text_range(),
        }
    }
}

impl Group {
    /// The literal text inside this group, with the enclosing braces dropped.
    /// Concatenates the inner token text so content split across `WORD`/`.`/`/`/…
    /// tokens (e.g. `chapters/my_file`, `sec:intro`) reassembles. Returns `None` when
    /// the group holds non-token content (a nested command — not a flat literal) or a
    /// parameter token (`\ref{#1}`, `\eqref{##1}` — a macro-parameter template whose
    /// literal value exists only at expansion time).
    pub fn inner_text(&self) -> Option<String> {
        let mut text = String::new();
        for element in self.syntax.children_with_tokens() {
            match element {
                NodeOrToken::Token(token) => match token.kind() {
                    SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
                    SyntaxKind::HASH => return None,
                    _ => text.push_str(token.text()),
                },
                // A nested node (e.g. a COMMAND) means the argument isn't a flat
                // literal; treat the whole thing as unresolvable.
                NodeOrToken::Node(_) => return None,
            }
        }
        Some(text)
    }

    /// The byte range of the content *inside* this group (the span between the
    /// braces) together with that inner text — the location-aware counterpart to
    /// [`Group::inner_text`]. The inner range runs from the first inner token's start
    /// to the last inner token's end; an empty group (`{}`) yields a zero-width range
    /// just after the `{`. Returns `None` under the same conditions as
    /// [`Group::inner_text`].
    ///
    /// The text/range correspondence is exact: in the success path the group holds
    /// only flat tokens, so its inner bytes are contiguous and per-key sub-ranges can
    /// be sliced off the range by byte offset (used by the semantic builder to give
    /// each key in a `\cref{a,b}` its own precise span).
    pub fn inner(&self) -> Option<(TextRange, String)> {
        let mut text = String::new();
        let mut start: Option<TextSize> = None;
        let mut end: Option<TextSize> = None;
        // Fallback anchor for an empty group: the byte just after the opening brace.
        let mut after_l_brace = self.syntax.text_range().start();
        for element in self.syntax.children_with_tokens() {
            match element {
                NodeOrToken::Token(token) => match token.kind() {
                    SyntaxKind::L_BRACE => after_l_brace = token.text_range().end(),
                    SyntaxKind::R_BRACE => {}
                    SyntaxKind::HASH => return None,
                    _ => {
                        let range = token.text_range();
                        start.get_or_insert(range.start());
                        end = Some(range.end());
                        text.push_str(token.text());
                    }
                },
                // A nested node means the argument isn't a flat literal; treat the
                // whole thing as unresolvable, like `inner_text`.
                NodeOrToken::Node(_) => return None,
            }
        }
        let range = match (start, end) {
            (Some(start), Some(end)) => TextRange::new(start, end),
            _ => TextRange::empty(after_l_brace),
        };
        Some((range, text))
    }

    /// The raw inner source of this group with its outer braces dropped, but *all*
    /// interior text preserved — nested `{…}` braces included. Unlike
    /// [`Group::inner_text`], which bails on nested nodes, this reconstructs the
    /// verbatim content needed for an xparse argument spec like `{m O{0} m}` (whose
    /// `{0}` default parses as a nested `GROUP`). Trivia is kept verbatim; the caller
    /// tokenizes the result.
    pub fn inner_source(&self) -> String {
        inner_source_of(&self.syntax)
    }

    /// The single `COMMAND` child wrapped in this group, if any.
    pub fn command(&self) -> Option<Command> {
        child::<Command>(&self.syntax)
    }

    /// The control-word name (leading `\` stripped) of a single `COMMAND` wrapped in
    /// this group, as in a `\newcommand{\foo}` name group. Returns `None` unless the
    /// group's only relevant child is exactly one control word.
    pub fn command_name(&self) -> Option<String> {
        self.command()?.name()
    }
}

/// The shared body of [`Group::inner_source`], kept kind-agnostic so the
/// free-function shim can call it on any node — an xparse default like `O{0}` parses
/// its `{0}` as a nested group but a top-level default body may be an `OPTIONAL`
/// rather than a `GROUP`. Concatenates all descendant token text, then drops a single
/// leading `{` and trailing `}` if present (a bracket-delimited `OPTIONAL` keeps its
/// brackets, matching the pre-wrapper behavior).
pub(crate) fn inner_source_of(node: &SyntaxNode) -> String {
    let mut text = String::new();
    for element in node.descendants_with_tokens() {
        if let NodeOrToken::Token(token) = element {
            text.push_str(token.text());
        }
    }
    let inner = text.strip_prefix('{').unwrap_or(&text);
    inner.strip_suffix('}').unwrap_or(inner).to_string()
}

impl NameGroup {
    /// The environment name — the literal text of this `NAME_GROUP`, braces dropped.
    /// Returns `None` when it holds non-token content.
    pub fn text(&self) -> Option<String> {
        let mut text = String::new();
        for element in self.syntax.children_with_tokens() {
            match element {
                NodeOrToken::Token(token) => match token.kind() {
                    SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
                    _ => text.push_str(token.text()),
                },
                NodeOrToken::Node(_) => return None,
            }
        }
        Some(text)
    }

    /// The byte range of the name *inside* this `NAME_GROUP` (the span between the
    /// braces) — the location-aware counterpart to [`NameGroup::text`]. Returns
    /// `None` when it holds a nested node or the name is empty (`\begin{}`, nothing to
    /// highlight).
    pub fn range(&self) -> Option<TextRange> {
        let mut start: Option<TextSize> = None;
        let mut end: Option<TextSize> = None;
        for element in self.syntax.children_with_tokens() {
            match element {
                NodeOrToken::Token(token) => match token.kind() {
                    SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
                    _ => {
                        let range = token.text_range();
                        start.get_or_insert(range.start());
                        end = Some(range.end());
                    }
                },
                NodeOrToken::Node(_) => return None,
            }
        }
        Some(TextRange::new(start?, end?))
    }
}

/// The environment an alias-delimiter node names: its bare `CONTROL_WORD` with the
/// leading `\` stripped, when that word is not `keyword` (the spelled-out
/// `\begin`/`\end`, whose name lives in a `NAME_GROUP` instead).
fn alias_delimiter_name(node: &SyntaxNode, keyword: &str) -> Option<String> {
    let head = child_token::<ControlWord>(node)?;
    let text = head.syntax().text();
    (text != keyword)
        .then(|| text.strip_prefix('\\'))
        .flatten()
        .filter(|name| !name.is_empty())
        .map(str::to_owned)
}

impl Begin {
    /// The `{name}` group following `\begin`.
    pub fn name_group(&self) -> Option<NameGroup> {
        child::<NameGroup>(&self.syntax)
    }

    /// The environment name (braces dropped), or `None` for a malformed `\begin`.
    ///
    /// A `BEGIN` opened by an *environment alias* (`\bea`, issue #109) carries no
    /// `NAME_GROUP` at all — the whole node is the bare control word — so the name
    /// falls back to that word with its `\` stripped. Positional and meaning-free,
    /// per decision #10: it reads the name from wherever the tree puts it and looks
    /// nothing up. `Signatures::environment` is what maps `bea` on to the target's
    /// curated behavior.
    ///
    /// The fallback is guarded on the head *not* being `\begin`, so the malformed
    /// `\begin`-without-a-name path (which builds a `BEGIN` with no `NAME_GROUP`)
    /// keeps reporting `None` rather than suddenly claiming to be named `begin`.
    pub fn name(&self) -> Option<String> {
        match self.name_group() {
            Some(group) => group.text(),
            None => alias_delimiter_name(&self.syntax, "\\begin"),
        }
    }

    /// Whether this `BEGIN` is an *environment-alias* delimiter — a bare control
    /// word standing in for `\begin{X}` — rather than a spelled-out `\begin{X}`.
    ///
    /// Purely structural (no `NAME_GROUP`, head is not `\begin`), like every other
    /// accessor here. It exists because [`name`](Self::name) makes the two shapes
    /// indistinguishable by name, and the alias table describes the *command*, not
    /// the name: a literal `\begin{bea}` written in a file that also defines `\bea`
    /// as an alias is a different, unrelated environment and must not inherit the
    /// target's behavior. `Signatures::environment_at` is the consumer.
    pub fn is_alias(&self) -> bool {
        self.name_group().is_none() && alias_delimiter_name(&self.syntax, "\\begin").is_some()
    }

    /// The byte range of the environment name inside the `NAME_GROUP`.
    pub fn name_range(&self) -> Option<TextRange> {
        self.name_group()?.range()
    }
}

impl End {
    /// The `{name}` group following `\end`.
    pub fn name_group(&self) -> Option<NameGroup> {
        child::<NameGroup>(&self.syntax)
    }

    /// The environment name (braces dropped), or `None` for a malformed `\end`.
    pub fn name(&self) -> Option<String> {
        self.name_group()?.text()
    }

    /// The byte range of the environment name inside the `NAME_GROUP`.
    pub fn name_range(&self) -> Option<TextRange> {
        self.name_group()?.range()
    }
}

impl Environment {
    /// The `\begin{…}` node, replacing the raw `children().find(==BEGIN)` idiom.
    pub fn begin(&self) -> Option<Begin> {
        child::<Begin>(&self.syntax)
    }

    /// The `\end{…}` node.
    pub fn end(&self) -> Option<End> {
        child::<End>(&self.syntax)
    }

    /// The environment name, read from the `\begin` node.
    pub fn name(&self) -> Option<String> {
        self.begin()?.name()
    }
}

impl Conditional {
    /// The branches, in source order — at least one, since the grammar opens a
    /// branch before the opener.
    pub fn branches(&self) -> impl Iterator<Item = ConditionalBranch> {
        children::<ConditionalBranch>(&self.syntax)
    }

    /// The closing `\fi`, read *positionally* as the last child node rather than
    /// by matching the name: which control word closes a conditional is the
    /// grammar's call, and re-deciding it here would be the same meaning check
    /// twice (decision #10). `None` only if the gate's guarantee is ever broken,
    /// which callers must tolerate rather than assume away.
    pub fn closer(&self) -> Option<Command> {
        self.syntax.last_child().and_then(Command::cast)
    }
}

impl ConditionalBranch {
    /// The leading `\if…`/`\else`/`\or` control word of this branch, if it opens
    /// with one. Positional: the first child node, cast to a `COMMAND`.
    pub fn head(&self) -> Option<Command> {
        self.syntax.first_child().and_then(Command::cast)
    }
}