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, 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);
70
71impl Command {
72 /// The leading `CONTROL_WORD` token, or `None` for a control symbol. The
73 /// grammar bumps the control word as the command's first token.
74 pub fn control_word(&self) -> Option<ControlWord> {
75 self.syntax
76 .children_with_tokens()
77 .filter_map(NodeOrToken::into_token)
78 .find_map(ControlWord::cast)
79 }
80
81 /// The control-word name (leading `\` stripped), or `None` for a control
82 /// symbol.
83 pub fn name(&self) -> Option<String> {
84 self.control_word().map(|cw| cw.name())
85 }
86
87 /// The range of the leading `CONTROL_WORD` token (the `\foo` itself, backslash
88 /// included), or `None` for a control symbol. Callers use this to underline just
89 /// the control word rather than the whole node, which may carry greedily-attached
90 /// argument groups.
91 pub fn control_word_range(&self) -> Option<TextRange> {
92 self.control_word().map(|cw| cw.range())
93 }
94
95 /// The `n`-th `GROUP` argument, if present. Filters `GROUP` only, so `OPTIONAL`
96 /// arguments do *not* shift brace indexing (`\cmd[o]{a}` → `nth_group(0)` is
97 /// `{a}`).
98 pub fn nth_group(&self, n: usize) -> Option<Group> {
99 self.groups().nth(n)
100 }
101
102 /// The `GROUP` argument nodes, in source order.
103 pub fn groups(&self) -> impl Iterator<Item = Group> {
104 children::<Group>(&self.syntax)
105 }
106
107 /// The `OPTIONAL` argument nodes, in source order.
108 pub fn optionals(&self) -> impl Iterator<Item = Optional> {
109 children::<Optional>(&self.syntax)
110 }
111
112 /// The literal text inside the `n`-th `GROUP` argument, braces dropped. Returns
113 /// `None` when there is no `n`-th group or it holds non-token content (a nested
114 /// command — not a flat literal). See [`Group::inner_text`].
115 pub fn nth_group_text(&self, n: usize) -> Option<String> {
116 self.nth_group(n)?.inner_text()
117 }
118
119 /// The byte range of the content *inside* the `n`-th `GROUP` argument together
120 /// with that inner text — the location-aware counterpart to
121 /// [`Command::nth_group_text`]. See [`Group::inner`].
122 pub fn nth_group_inner(&self, n: usize) -> Option<(TextRange, String)> {
123 self.nth_group(n)?.inner()
124 }
125
126 /// The byte range of this command spanning its control word through the end of
127 /// its *first* `{…}` group — e.g. `\label{key}` up to the closing brace of
128 /// `{key}`. Deliberately not [`SyntaxNode::text_range`], which the greedy parser
129 /// may stretch over a *second* group it attached without knowing arity
130 /// (`\label{a}\n{…}`; decision #8). Falls back to the full command range when the
131 /// first group is absent.
132 pub fn first_group_range(&self) -> TextRange {
133 match self.nth_group(0) {
134 Some(group) => TextRange::new(
135 self.syntax.text_range().start(),
136 group.syntax.text_range().end(),
137 ),
138 None => self.syntax.text_range(),
139 }
140 }
141}
142
143impl Group {
144 /// The literal text inside this group, with the enclosing braces dropped.
145 /// Concatenates the inner token text so content split across `WORD`/`.`/`/`/…
146 /// tokens (e.g. `chapters/my_file`, `sec:intro`) reassembles. Returns `None` when
147 /// the group holds non-token content (a nested command — not a flat literal) or a
148 /// parameter token (`\ref{#1}`, `\eqref{##1}` — a macro-parameter template whose
149 /// literal value exists only at expansion time).
150 pub fn inner_text(&self) -> Option<String> {
151 let mut text = String::new();
152 for element in self.syntax.children_with_tokens() {
153 match element {
154 NodeOrToken::Token(token) => match token.kind() {
155 SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
156 SyntaxKind::HASH => return None,
157 _ => text.push_str(token.text()),
158 },
159 // A nested node (e.g. a COMMAND) means the argument isn't a flat
160 // literal; treat the whole thing as unresolvable.
161 NodeOrToken::Node(_) => return None,
162 }
163 }
164 Some(text)
165 }
166
167 /// The byte range of the content *inside* this group (the span between the
168 /// braces) together with that inner text — the location-aware counterpart to
169 /// [`Group::inner_text`]. The inner range runs from the first inner token's start
170 /// to the last inner token's end; an empty group (`{}`) yields a zero-width range
171 /// just after the `{`. Returns `None` under the same conditions as
172 /// [`Group::inner_text`].
173 ///
174 /// The text/range correspondence is exact: in the success path the group holds
175 /// only flat tokens, so its inner bytes are contiguous and per-key sub-ranges can
176 /// be sliced off the range by byte offset (used by the semantic builder to give
177 /// each key in a `\cref{a,b}` its own precise span).
178 pub fn inner(&self) -> Option<(TextRange, String)> {
179 let mut text = String::new();
180 let mut start: Option<TextSize> = None;
181 let mut end: Option<TextSize> = None;
182 // Fallback anchor for an empty group: the byte just after the opening brace.
183 let mut after_l_brace = self.syntax.text_range().start();
184 for element in self.syntax.children_with_tokens() {
185 match element {
186 NodeOrToken::Token(token) => match token.kind() {
187 SyntaxKind::L_BRACE => after_l_brace = token.text_range().end(),
188 SyntaxKind::R_BRACE => {}
189 SyntaxKind::HASH => return None,
190 _ => {
191 let range = token.text_range();
192 start.get_or_insert(range.start());
193 end = Some(range.end());
194 text.push_str(token.text());
195 }
196 },
197 // A nested node means the argument isn't a flat literal; treat the
198 // whole thing as unresolvable, like `inner_text`.
199 NodeOrToken::Node(_) => return None,
200 }
201 }
202 let range = match (start, end) {
203 (Some(start), Some(end)) => TextRange::new(start, end),
204 _ => TextRange::empty(after_l_brace),
205 };
206 Some((range, text))
207 }
208
209 /// The raw inner source of this group with its outer braces dropped, but *all*
210 /// interior text preserved — nested `{…}` braces included. Unlike
211 /// [`Group::inner_text`], which bails on nested nodes, this reconstructs the
212 /// verbatim content needed for an xparse argument spec like `{m O{0} m}` (whose
213 /// `{0}` default parses as a nested `GROUP`). Trivia is kept verbatim; the caller
214 /// tokenizes the result.
215 pub fn inner_source(&self) -> String {
216 inner_source_of(&self.syntax)
217 }
218
219 /// The single `COMMAND` child wrapped in this group, if any.
220 pub fn command(&self) -> Option<Command> {
221 child::<Command>(&self.syntax)
222 }
223
224 /// The control-word name (leading `\` stripped) of a single `COMMAND` wrapped in
225 /// this group, as in a `\newcommand{\foo}` name group. Returns `None` unless the
226 /// group's only relevant child is exactly one control word.
227 pub fn command_name(&self) -> Option<String> {
228 self.command()?.name()
229 }
230}
231
232/// The shared body of [`Group::inner_source`], kept kind-agnostic so the
233/// free-function shim can call it on any node — an xparse default like `O{0}` parses
234/// its `{0}` as a nested group but a top-level default body may be an `OPTIONAL`
235/// rather than a `GROUP`. Concatenates all descendant token text, then drops a single
236/// leading `{` and trailing `}` if present (a bracket-delimited `OPTIONAL` keeps its
237/// brackets, matching the pre-wrapper behavior).
238pub(crate) fn inner_source_of(node: &SyntaxNode) -> String {
239 let mut text = String::new();
240 for element in node.descendants_with_tokens() {
241 if let NodeOrToken::Token(token) = element {
242 text.push_str(token.text());
243 }
244 }
245 let inner = text.strip_prefix('{').unwrap_or(&text);
246 inner.strip_suffix('}').unwrap_or(inner).to_string()
247}
248
249impl NameGroup {
250 /// The environment name — the literal text of this `NAME_GROUP`, braces dropped.
251 /// Returns `None` when it holds non-token content.
252 pub fn text(&self) -> Option<String> {
253 let mut text = String::new();
254 for element in self.syntax.children_with_tokens() {
255 match element {
256 NodeOrToken::Token(token) => match token.kind() {
257 SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
258 _ => text.push_str(token.text()),
259 },
260 NodeOrToken::Node(_) => return None,
261 }
262 }
263 Some(text)
264 }
265
266 /// The byte range of the name *inside* this `NAME_GROUP` (the span between the
267 /// braces) — the location-aware counterpart to [`NameGroup::text`]. Returns
268 /// `None` when it holds a nested node or the name is empty (`\begin{}`, nothing to
269 /// highlight).
270 pub fn range(&self) -> Option<TextRange> {
271 let mut start: Option<TextSize> = None;
272 let mut end: Option<TextSize> = None;
273 for element in self.syntax.children_with_tokens() {
274 match element {
275 NodeOrToken::Token(token) => match token.kind() {
276 SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
277 _ => {
278 let range = token.text_range();
279 start.get_or_insert(range.start());
280 end = Some(range.end());
281 }
282 },
283 NodeOrToken::Node(_) => return None,
284 }
285 }
286 Some(TextRange::new(start?, end?))
287 }
288}
289
290impl Begin {
291 /// The `{name}` group following `\begin`.
292 pub fn name_group(&self) -> Option<NameGroup> {
293 child::<NameGroup>(&self.syntax)
294 }
295
296 /// The environment name (braces dropped), or `None` for a malformed `\begin`.
297 pub fn name(&self) -> Option<String> {
298 self.name_group()?.text()
299 }
300
301 /// The byte range of the environment name inside the `NAME_GROUP`.
302 pub fn name_range(&self) -> Option<TextRange> {
303 self.name_group()?.range()
304 }
305}
306
307impl End {
308 /// The `{name}` group following `\end`.
309 pub fn name_group(&self) -> Option<NameGroup> {
310 child::<NameGroup>(&self.syntax)
311 }
312
313 /// The environment name (braces dropped), or `None` for a malformed `\end`.
314 pub fn name(&self) -> Option<String> {
315 self.name_group()?.text()
316 }
317
318 /// The byte range of the environment name inside the `NAME_GROUP`.
319 pub fn name_range(&self) -> Option<TextRange> {
320 self.name_group()?.range()
321 }
322}
323
324impl Environment {
325 /// The `\begin{…}` node, replacing the raw `children().find(==BEGIN)` idiom.
326 pub fn begin(&self) -> Option<Begin> {
327 child::<Begin>(&self.syntax)
328 }
329
330 /// The `\end{…}` node.
331 pub fn end(&self) -> Option<End> {
332 child::<End>(&self.syntax)
333 }
334
335 /// The environment name, read from the `\begin` node.
336 pub fn name(&self) -> Option<String> {
337 self.begin()?.name()
338 }
339}