Skip to main content

pydocstring/
syntax.rs

1//! Unified syntax tree types.
2//!
3//! This module defines the core tree data structures shared by all docstring
4//! styles.  Every parsed docstring is represented as a tree of [`SyntaxNode`]s
5//! (branches) and [`SyntaxToken`]s (leaves), each tagged with a [`SyntaxKind`].
6//!
7//! The [`Parsed`] struct owns the source text and the root node, and provides
8//! a convenience [`pretty_print`](Parsed::pretty_print) method for debugging.
9
10use core::fmt;
11use core::fmt::Write;
12
13use crate::text::TextRange;
14
15// =============================================================================
16// SyntaxKind
17// =============================================================================
18
19/// Node and token kinds for all docstring styles.
20///
21/// Google and NumPy variants coexist in a single enum, just as Biome puts
22/// `JsIfStatement` and `TsInterface` in one `SyntaxKind`.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24#[allow(non_camel_case_types)]
25pub enum SyntaxKind {
26    // ── Common tokens ──────────────────────────────────────────────────
27    /// Section name, parameter name, exception type name, etc.
28    NAME,
29    /// Type annotation.
30    TYPE,
31    /// `:` separator.
32    COLON,
33    /// Description text.
34    DESCRIPTION,
35    /// Opening bracket: `(`, `[`, `{`, or `<`.
36    OPEN_BRACKET,
37    /// Closing bracket: `)`, `]`, `}`, or `>`.
38    CLOSE_BRACKET,
39    /// `optional` marker.
40    OPTIONAL,
41    /// Free-text section body.
42    BODY_TEXT,
43    /// Summary line.
44    SUMMARY,
45    /// Extended summary paragraph.
46    EXTENDED_SUMMARY,
47    /// Stray line between sections.
48    STRAY_LINE,
49
50    // ── Google-specific tokens ─────────────────────────────────────────
51    /// Warning type (e.g. `UserWarning`).
52    WARNING_TYPE,
53
54    // ── NumPy-specific tokens ──────────────────────────────────────────
55    /// Section header underline (`----------`).
56    UNDERLINE,
57    /// RST directive marker (`..`).
58    DIRECTIVE_MARKER,
59    /// Keyword such as `deprecated`.
60    KEYWORD,
61    /// RST double colon (`::`).
62    DOUBLE_COLON,
63    /// Deprecation version string.
64    VERSION,
65    /// Return type (NumPy-style).
66    RETURN_TYPE,
67    /// `default` keyword.
68    DEFAULT_KEYWORD,
69    /// Default value separator (`=` or `:`).
70    DEFAULT_SEPARATOR,
71    /// Default value text.
72    DEFAULT_VALUE,
73    /// Reference number.
74    NUMBER,
75    /// Reference content text.
76    CONTENT,
77
78    // ── Google nodes ───────────────────────────────────────────────────
79    /// Root node for a Google-style docstring.
80    GOOGLE_DOCSTRING,
81    /// A complete Google section (header + body items).
82    GOOGLE_SECTION,
83    /// Section header (`Args:`, `Returns:`, etc.).
84    GOOGLE_SECTION_HEADER,
85    /// A single argument entry.
86    GOOGLE_ARG,
87    /// A single return value entry.
88    GOOGLE_RETURNS,
89    /// A single exception entry.
90    GOOGLE_EXCEPTION,
91    /// A single warning entry.
92    GOOGLE_WARNING,
93    /// A single "See Also" item.
94    GOOGLE_SEE_ALSO_ITEM,
95    /// A single attribute entry.
96    GOOGLE_ATTRIBUTE,
97    /// A single method entry.
98    GOOGLE_METHOD,
99
100    // ── NumPy nodes ────────────────────────────────────────────────────
101    /// Root node for a NumPy-style docstring.
102    NUMPY_DOCSTRING,
103    /// A complete NumPy section (header + body items).
104    NUMPY_SECTION,
105    /// Section header (name + underline).
106    NUMPY_SECTION_HEADER,
107    /// Deprecation directive block.
108    NUMPY_DEPRECATION,
109    /// A single parameter entry.
110    NUMPY_PARAMETER,
111    /// A single return value entry.
112    NUMPY_RETURNS,
113    /// A single exception entry.
114    NUMPY_EXCEPTION,
115    /// A single warning entry.
116    NUMPY_WARNING,
117    /// A single "See Also" item.
118    NUMPY_SEE_ALSO_ITEM,
119    /// A single reference entry.
120    NUMPY_REFERENCE,
121    /// A single attribute entry.
122    NUMPY_ATTRIBUTE,
123    /// A single method entry.
124    NUMPY_METHOD,
125}
126
127impl SyntaxKind {
128    /// Whether this kind represents a node (branch) rather than a token (leaf).
129    pub const fn is_node(self) -> bool {
130        matches!(
131            self,
132            Self::GOOGLE_DOCSTRING
133                | Self::GOOGLE_SECTION
134                | Self::GOOGLE_SECTION_HEADER
135                | Self::GOOGLE_ARG
136                | Self::GOOGLE_RETURNS
137                | Self::GOOGLE_EXCEPTION
138                | Self::GOOGLE_WARNING
139                | Self::GOOGLE_SEE_ALSO_ITEM
140                | Self::GOOGLE_ATTRIBUTE
141                | Self::GOOGLE_METHOD
142                | Self::NUMPY_DOCSTRING
143                | Self::NUMPY_SECTION
144                | Self::NUMPY_SECTION_HEADER
145                | Self::NUMPY_DEPRECATION
146                | Self::NUMPY_PARAMETER
147                | Self::NUMPY_RETURNS
148                | Self::NUMPY_EXCEPTION
149                | Self::NUMPY_WARNING
150                | Self::NUMPY_SEE_ALSO_ITEM
151                | Self::NUMPY_REFERENCE
152                | Self::NUMPY_ATTRIBUTE
153                | Self::NUMPY_METHOD
154        )
155    }
156
157    /// Whether this kind represents a token (leaf) rather than a node (branch).
158    pub const fn is_token(self) -> bool {
159        !self.is_node()
160    }
161
162    /// Display name for pretty-printing (e.g. `"GOOGLE_ARG"`, `"NAME"`).
163    pub const fn name(self) -> &'static str {
164        match self {
165            // Common tokens
166            Self::NAME => "NAME",
167            Self::TYPE => "TYPE",
168            Self::COLON => "COLON",
169            Self::DESCRIPTION => "DESCRIPTION",
170            Self::OPEN_BRACKET => "OPEN_BRACKET",
171            Self::CLOSE_BRACKET => "CLOSE_BRACKET",
172            Self::OPTIONAL => "OPTIONAL",
173            Self::BODY_TEXT => "BODY_TEXT",
174            Self::SUMMARY => "SUMMARY",
175            Self::EXTENDED_SUMMARY => "EXTENDED_SUMMARY",
176            Self::STRAY_LINE => "STRAY_LINE",
177            // Google tokens
178            Self::WARNING_TYPE => "WARNING_TYPE",
179            // NumPy tokens
180            Self::UNDERLINE => "UNDERLINE",
181            Self::DIRECTIVE_MARKER => "DIRECTIVE_MARKER",
182            Self::KEYWORD => "KEYWORD",
183            Self::DOUBLE_COLON => "DOUBLE_COLON",
184            Self::VERSION => "VERSION",
185            Self::RETURN_TYPE => "RETURN_TYPE",
186            Self::DEFAULT_KEYWORD => "DEFAULT_KEYWORD",
187            Self::DEFAULT_SEPARATOR => "DEFAULT_SEPARATOR",
188            Self::DEFAULT_VALUE => "DEFAULT_VALUE",
189            Self::NUMBER => "NUMBER",
190            Self::CONTENT => "CONTENT",
191            // Google nodes
192            Self::GOOGLE_DOCSTRING => "GOOGLE_DOCSTRING",
193            Self::GOOGLE_SECTION => "GOOGLE_SECTION",
194            Self::GOOGLE_SECTION_HEADER => "GOOGLE_SECTION_HEADER",
195            Self::GOOGLE_ARG => "GOOGLE_ARG",
196            Self::GOOGLE_RETURNS => "GOOGLE_RETURNS",
197            Self::GOOGLE_EXCEPTION => "GOOGLE_EXCEPTION",
198            Self::GOOGLE_WARNING => "GOOGLE_WARNING",
199            Self::GOOGLE_SEE_ALSO_ITEM => "GOOGLE_SEE_ALSO_ITEM",
200            Self::GOOGLE_ATTRIBUTE => "GOOGLE_ATTRIBUTE",
201            Self::GOOGLE_METHOD => "GOOGLE_METHOD",
202            // NumPy nodes
203            Self::NUMPY_DOCSTRING => "NUMPY_DOCSTRING",
204            Self::NUMPY_SECTION => "NUMPY_SECTION",
205            Self::NUMPY_SECTION_HEADER => "NUMPY_SECTION_HEADER",
206            Self::NUMPY_DEPRECATION => "NUMPY_DEPRECATION",
207            Self::NUMPY_PARAMETER => "NUMPY_PARAMETER",
208            Self::NUMPY_RETURNS => "NUMPY_RETURNS",
209            Self::NUMPY_EXCEPTION => "NUMPY_EXCEPTION",
210            Self::NUMPY_WARNING => "NUMPY_WARNING",
211            Self::NUMPY_SEE_ALSO_ITEM => "NUMPY_SEE_ALSO_ITEM",
212            Self::NUMPY_REFERENCE => "NUMPY_REFERENCE",
213            Self::NUMPY_ATTRIBUTE => "NUMPY_ATTRIBUTE",
214            Self::NUMPY_METHOD => "NUMPY_METHOD",
215        }
216    }
217}
218
219impl fmt::Display for SyntaxKind {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        f.write_str(self.name())
222    }
223}
224
225// =============================================================================
226// SyntaxNode / SyntaxToken / SyntaxElement
227// =============================================================================
228
229/// A branch node in the syntax tree.
230///
231/// Holds an ordered list of child [`SyntaxElement`]s (nodes or tokens).
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct SyntaxNode {
234    kind: SyntaxKind,
235    range: TextRange,
236    children: Vec<SyntaxElement>,
237}
238
239impl SyntaxNode {
240    /// Creates a new node with the given kind, range, and children.
241    pub fn new(kind: SyntaxKind, range: TextRange, children: Vec<SyntaxElement>) -> Self {
242        Self {
243            kind,
244            range,
245            children,
246        }
247    }
248
249    /// The kind of this node.
250    pub fn kind(&self) -> SyntaxKind {
251        self.kind
252    }
253
254    /// The source range of this node.
255    pub fn range(&self) -> &TextRange {
256        &self.range
257    }
258
259    /// The ordered child elements.
260    pub fn children(&self) -> &[SyntaxElement] {
261        &self.children
262    }
263
264    /// Mutable access to the ordered child elements.
265    pub fn children_mut(&mut self) -> &mut [SyntaxElement] {
266        &mut self.children
267    }
268
269    /// Append a child element.
270    pub fn push_child(&mut self, child: SyntaxElement) {
271        self.children.push(child);
272    }
273
274    /// Extend this node's range end to `end`.
275    pub fn extend_range_to(&mut self, end: crate::text::TextSize) {
276        self.range = TextRange::new(self.range.start(), end);
277    }
278
279    /// Find the first token child with the given kind.
280    pub fn find_token(&self, kind: SyntaxKind) -> Option<&SyntaxToken> {
281        self.children.iter().find_map(|c| match c {
282            SyntaxElement::Token(t) if t.kind() == kind => Some(t),
283            _ => None,
284        })
285    }
286
287    /// Return the first token child with the given kind.
288    ///
289    /// # Panics
290    ///
291    /// Panics if no such token exists.  This should only be used for tokens
292    /// that the parser guarantees to be present.
293    pub fn required_token(&self, kind: SyntaxKind) -> &SyntaxToken {
294        self.find_token(kind)
295            .unwrap_or_else(|| panic!("required token {:?} not found in {:?}", kind, self.kind))
296    }
297
298    /// Iterate over all token children with the given kind.
299    pub fn tokens(&self, kind: SyntaxKind) -> impl Iterator<Item = &SyntaxToken> {
300        self.children.iter().filter_map(move |c| match c {
301            SyntaxElement::Token(t) if t.kind() == kind => Some(t),
302            _ => None,
303        })
304    }
305
306    /// Find the first child node with the given kind.
307    pub fn find_node(&self, kind: SyntaxKind) -> Option<&SyntaxNode> {
308        self.children.iter().find_map(|c| match c {
309            SyntaxElement::Node(n) if n.kind() == kind => Some(n),
310            _ => None,
311        })
312    }
313
314    /// Iterate over all child nodes with the given kind.
315    pub fn nodes(&self, kind: SyntaxKind) -> impl Iterator<Item = &SyntaxNode> {
316        self.children.iter().filter_map(move |c| match c {
317            SyntaxElement::Node(n) if n.kind() == kind => Some(n),
318            _ => None,
319        })
320    }
321
322    /// Write a pretty-printed tree representation.
323    pub fn pretty_fmt(&self, src: &str, indent: usize, out: &mut String) {
324        for _ in 0..indent {
325            out.push_str("  ");
326        }
327        let _ = writeln!(out, "{}@{} {{", self.kind.name(), self.range);
328        for child in &self.children {
329            match child {
330                SyntaxElement::Node(n) => n.pretty_fmt(src, indent + 1, out),
331                SyntaxElement::Token(t) => t.pretty_fmt(src, indent + 1, out),
332            }
333        }
334        for _ in 0..indent {
335            out.push_str("  ");
336        }
337        out.push_str("}\n");
338    }
339}
340
341/// A leaf token in the syntax tree.
342///
343/// Represents a contiguous span of source text with a known kind.
344#[derive(Debug, Clone, PartialEq, Eq)]
345pub struct SyntaxToken {
346    kind: SyntaxKind,
347    range: TextRange,
348}
349
350impl SyntaxToken {
351    /// Creates a new token with the given kind and range.
352    pub fn new(kind: SyntaxKind, range: TextRange) -> Self {
353        Self { kind, range }
354    }
355
356    /// The kind of this token.
357    pub fn kind(&self) -> SyntaxKind {
358        self.kind
359    }
360
361    /// The source range of this token.
362    pub fn range(&self) -> &TextRange {
363        &self.range
364    }
365
366    /// Extract the corresponding text slice from source.
367    pub fn text<'a>(&self, source: &'a str) -> &'a str {
368        self.range.source_text(source)
369    }
370
371    /// Extend this token's range to include `other`.
372    pub fn extend_range(&mut self, other: TextRange) {
373        self.range.extend(other);
374    }
375
376    /// Write a pretty-printed token line.
377    pub fn pretty_fmt(&self, src: &str, indent: usize, out: &mut String) {
378        for _ in 0..indent {
379            out.push_str("  ");
380        }
381        let _ = writeln!(
382            out,
383            "{}: {:?}@{}",
384            self.kind.name(),
385            self.text(src),
386            self.range
387        );
388    }
389}
390
391/// A child element of a [`SyntaxNode`] — either a node or a token.
392#[derive(Debug, Clone, PartialEq, Eq)]
393pub enum SyntaxElement {
394    /// A branch node.
395    Node(SyntaxNode),
396    /// A leaf token.
397    Token(SyntaxToken),
398}
399
400impl SyntaxElement {
401    /// The source range of this element.
402    pub fn range(&self) -> &TextRange {
403        match self {
404            Self::Node(n) => n.range(),
405            Self::Token(t) => t.range(),
406        }
407    }
408
409    /// The kind of this element.
410    pub fn kind(&self) -> SyntaxKind {
411        match self {
412            Self::Node(n) => n.kind(),
413            Self::Token(t) => t.kind(),
414        }
415    }
416}
417
418// =============================================================================
419// Parsed
420// =============================================================================
421
422/// The result of parsing a docstring.
423///
424/// Owns the source text and the root [`SyntaxNode`].
425#[derive(Debug, Clone, PartialEq, Eq)]
426pub struct Parsed {
427    source: String,
428    root: SyntaxNode,
429}
430
431impl Parsed {
432    /// Creates a new `Parsed` from source text and root node.
433    pub fn new(source: String, root: SyntaxNode) -> Self {
434        Self { source, root }
435    }
436
437    /// The full source text.
438    pub fn source(&self) -> &str {
439        &self.source
440    }
441
442    /// The root node of the syntax tree.
443    pub fn root(&self) -> &SyntaxNode {
444        &self.root
445    }
446
447    /// Produce a Biome-style pretty-printed representation of the tree.
448    pub fn pretty_print(&self) -> String {
449        let mut out = String::new();
450        self.root.pretty_fmt(&self.source, 0, &mut out);
451        out
452    }
453}
454
455// =============================================================================
456// Visitor
457// =============================================================================
458
459/// Trait for visiting syntax tree nodes and tokens.
460///
461/// Implement this trait and pass it to [`walk`] for depth-first traversal.
462pub trait Visitor {
463    /// Called when entering a node (before visiting its children).
464    fn enter(&mut self, _node: &SyntaxNode) {}
465    /// Called when leaving a node (after visiting its children).
466    fn leave(&mut self, _node: &SyntaxNode) {}
467    /// Called for each token leaf.
468    fn visit_token(&mut self, _token: &SyntaxToken) {}
469}
470
471/// Walk the syntax tree depth-first, calling the visitor methods.
472pub fn walk(node: &SyntaxNode, visitor: &mut dyn Visitor) {
473    visitor.enter(node);
474    for child in node.children() {
475        match child {
476            SyntaxElement::Node(n) => walk(n, visitor),
477            SyntaxElement::Token(t) => visitor.visit_token(t),
478        }
479    }
480    visitor.leave(node);
481}
482
483// =============================================================================
484// Tests
485// =============================================================================
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490    use crate::text::{TextRange, TextSize};
491
492    #[test]
493    fn test_syntax_kind_name() {
494        assert_eq!(SyntaxKind::GOOGLE_ARG.name(), "GOOGLE_ARG");
495        assert_eq!(SyntaxKind::NAME.name(), "NAME");
496        assert_eq!(SyntaxKind::NUMPY_PARAMETER.name(), "NUMPY_PARAMETER");
497    }
498
499    #[test]
500    fn test_syntax_kind_is_node_is_token() {
501        assert!(SyntaxKind::GOOGLE_DOCSTRING.is_node());
502        assert!(!SyntaxKind::GOOGLE_DOCSTRING.is_token());
503        assert!(SyntaxKind::NAME.is_token());
504        assert!(!SyntaxKind::NAME.is_node());
505    }
506
507    #[test]
508    fn test_syntax_token_text() {
509        let source = "hello world";
510        let token = SyntaxToken::new(
511            SyntaxKind::NAME,
512            TextRange::new(TextSize::new(0), TextSize::new(5)),
513        );
514        assert_eq!(token.text(source), "hello");
515    }
516
517    #[test]
518    fn test_syntax_node_find_token() {
519        let node = SyntaxNode::new(
520            SyntaxKind::GOOGLE_ARG,
521            TextRange::new(TextSize::new(0), TextSize::new(10)),
522            vec![
523                SyntaxElement::Token(SyntaxToken::new(
524                    SyntaxKind::NAME,
525                    TextRange::new(TextSize::new(0), TextSize::new(3)),
526                )),
527                SyntaxElement::Token(SyntaxToken::new(
528                    SyntaxKind::COLON,
529                    TextRange::new(TextSize::new(3), TextSize::new(4)),
530                )),
531                SyntaxElement::Token(SyntaxToken::new(
532                    SyntaxKind::DESCRIPTION,
533                    TextRange::new(TextSize::new(5), TextSize::new(10)),
534                )),
535            ],
536        );
537
538        assert!(node.find_token(SyntaxKind::NAME).is_some());
539        assert!(node.find_token(SyntaxKind::COLON).is_some());
540        assert!(node.find_token(SyntaxKind::TYPE).is_none());
541        assert_eq!(node.tokens(SyntaxKind::NAME).count(), 1);
542    }
543
544    #[test]
545    fn test_syntax_node_find_node() {
546        let child = SyntaxNode::new(
547            SyntaxKind::GOOGLE_SECTION_HEADER,
548            TextRange::new(TextSize::new(0), TextSize::new(5)),
549            vec![SyntaxElement::Token(SyntaxToken::new(
550                SyntaxKind::NAME,
551                TextRange::new(TextSize::new(0), TextSize::new(4)),
552            ))],
553        );
554        let parent = SyntaxNode::new(
555            SyntaxKind::GOOGLE_SECTION,
556            TextRange::new(TextSize::new(0), TextSize::new(20)),
557            vec![SyntaxElement::Node(child)],
558        );
559
560        assert!(
561            parent
562                .find_node(SyntaxKind::GOOGLE_SECTION_HEADER)
563                .is_some()
564        );
565        assert!(parent.find_node(SyntaxKind::GOOGLE_ARG).is_none());
566        assert_eq!(parent.nodes(SyntaxKind::GOOGLE_SECTION_HEADER).count(), 1);
567    }
568
569    #[test]
570    fn test_pretty_print() {
571        let source = "Args:\n    x: int";
572        let root = SyntaxNode::new(
573            SyntaxKind::GOOGLE_DOCSTRING,
574            TextRange::new(TextSize::new(0), TextSize::new(source.len() as u32)),
575            vec![SyntaxElement::Node(SyntaxNode::new(
576                SyntaxKind::GOOGLE_SECTION,
577                TextRange::new(TextSize::new(0), TextSize::new(source.len() as u32)),
578                vec![
579                    SyntaxElement::Node(SyntaxNode::new(
580                        SyntaxKind::GOOGLE_SECTION_HEADER,
581                        TextRange::new(TextSize::new(0), TextSize::new(5)),
582                        vec![
583                            SyntaxElement::Token(SyntaxToken::new(
584                                SyntaxKind::NAME,
585                                TextRange::new(TextSize::new(0), TextSize::new(4)),
586                            )),
587                            SyntaxElement::Token(SyntaxToken::new(
588                                SyntaxKind::COLON,
589                                TextRange::new(TextSize::new(4), TextSize::new(5)),
590                            )),
591                        ],
592                    )),
593                    SyntaxElement::Node(SyntaxNode::new(
594                        SyntaxKind::GOOGLE_ARG,
595                        TextRange::new(TextSize::new(10), TextSize::new(source.len() as u32)),
596                        vec![
597                            SyntaxElement::Token(SyntaxToken::new(
598                                SyntaxKind::NAME,
599                                TextRange::new(TextSize::new(10), TextSize::new(11)),
600                            )),
601                            SyntaxElement::Token(SyntaxToken::new(
602                                SyntaxKind::COLON,
603                                TextRange::new(TextSize::new(11), TextSize::new(12)),
604                            )),
605                            SyntaxElement::Token(SyntaxToken::new(
606                                SyntaxKind::DESCRIPTION,
607                                TextRange::new(
608                                    TextSize::new(13),
609                                    TextSize::new(source.len() as u32),
610                                ),
611                            )),
612                        ],
613                    )),
614                ],
615            ))],
616        );
617
618        let parsed = Parsed::new(source.to_string(), root);
619        let output = parsed.pretty_print();
620
621        // Verify structure is present
622        assert!(output.contains("GOOGLE_DOCSTRING@"));
623        assert!(output.contains("GOOGLE_SECTION@"));
624        assert!(output.contains("GOOGLE_SECTION_HEADER@"));
625        assert!(output.contains("GOOGLE_ARG@"));
626        assert!(output.contains("NAME: \"Args\"@"));
627        assert!(output.contains("COLON: \":\"@"));
628        assert!(output.contains("NAME: \"x\"@"));
629        assert!(output.contains("DESCRIPTION: \"int\"@"));
630    }
631
632    #[test]
633    fn test_visitor_walk() {
634        let source = "hello";
635        let root = SyntaxNode::new(
636            SyntaxKind::GOOGLE_DOCSTRING,
637            TextRange::new(TextSize::new(0), TextSize::new(5)),
638            vec![SyntaxElement::Token(SyntaxToken::new(
639                SyntaxKind::SUMMARY,
640                TextRange::new(TextSize::new(0), TextSize::new(5)),
641            ))],
642        );
643
644        struct Counter {
645            nodes: usize,
646            tokens: usize,
647        }
648        impl Visitor for Counter {
649            fn enter(&mut self, _node: &SyntaxNode) {
650                self.nodes += 1;
651            }
652            fn visit_token(&mut self, _token: &SyntaxToken) {
653                self.tokens += 1;
654            }
655        }
656
657        let mut counter = Counter {
658            nodes: 0,
659            tokens: 0,
660        };
661        walk(&root, &mut counter);
662        assert_eq!(counter.nodes, 1);
663        assert_eq!(counter.tokens, 1);
664
665        // verify text extraction
666        let tok = root.required_token(SyntaxKind::SUMMARY);
667        assert_eq!(tok.text(source), "hello");
668    }
669}