big_code_analysis_ast/lang_helpers/perl.rs
1//! Perl: telling the `and` and `not` operators, the file tests, the
2//! unary minus and bareword calls from an auto-quoted key that the
3//! grammar lexes as one of them; and reading the POD in the data
4//! section after `__END__`, which the grammar leaves as one leaf.
5
6use crate::Perl;
7use crate::lang_helpers::doc_comment::pod_comment_region;
8use crate::node::{Ancestors, Node};
9
10/// One classified run of rows in a Perl data section, as reported by
11/// [`perl_data_section_rows`].
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum PerlDataSectionRows {
14 /// A POD block, `first..=last`. `documentation` is false for a block
15 /// that is nothing but `=begin comment` / `=for comment` regions.
16 Pod {
17 /// The block's opening command row.
18 first: usize,
19 /// Its `=cut` row, or its last non-blank row when no `=cut`
20 /// closes it.
21 last: usize,
22 /// Whether the block documents anything.
23 documentation: bool,
24 },
25 /// A non-blank row outside every POD block: the section's data.
26 Data(usize),
27}
28
29/// Classifies the rows of a `data_not_for_compiler` node — the text
30/// after `__END__` or `__DATA__`, which the grammar holds as one opaque
31/// leaf — in one pass over `section`, its bytes. `start_row` is the
32/// marker's row, where the node starts; the rest of that row is never
33/// reported, since the marker makes the row code and no line there
34/// starts with `=`.
35///
36/// A POD block is read as `Pod::Simple` reads one (`BlackBox.pm`, the
37/// parser `perldoc` runs): it opens on a line matching
38/// `^=[a-zA-Z][a-zA-Z0-9]*(\s|$)` other than `=cut`, so `=1` opens
39/// nothing, and closes on a line matching `^=cut` — its own opening line
40/// included, so `=cutting` is a block of one row — or at the end of the
41/// file. Blank rows outside a block are reported as nothing.
42pub fn perl_data_section_rows(
43 section: &[u8],
44 start_row: usize,
45 mut visit: impl FnMut(PerlDataSectionRows),
46) {
47 // bca: suppress(halstead) — one line-at-a-time POD state machine:
48 // open, close on the opening line itself, extend and data share each
49 // row's offsets, and splitting them would scatter Pod::Simple's
50 // fall-through across helpers that each re-derive the row.
51 // The open block's first row, the byte offset of that row, and its
52 // last non-blank row so far.
53 let mut block: Option<(usize, usize, usize)> = None;
54 let mut lines = section.split(|&byte| byte == b'\n');
55 let mut offset = lines.next().map_or(0, |marker_rest| marker_rest.len() + 1);
56 for (row, line) in (start_row.saturating_add(1)..).zip(lines) {
57 let end = offset + line.len();
58 let blank = line.trim_ascii().is_empty();
59 // `Pod::Simple` falls through from opening a block to its pod
60 // mode, whose `^=cut` test then reads the opening line too.
61 let opens = block.is_none() && opens_pod(line);
62 if opens {
63 block = Some((row, offset, row));
64 }
65 match block {
66 Some((first, start, _)) if line.starts_with(b"=cut") => {
67 visit(pod_rows(section, first, start..end, row));
68 block = None;
69 }
70 _ if opens || blank => {}
71 Some((first, start, _)) => block = Some((first, start, row)),
72 None => visit(PerlDataSectionRows::Data(row)),
73 }
74 offset = end + 1;
75 }
76 if let Some((first, start, last)) = block {
77 visit(pod_rows(section, first, start..section.len(), last));
78 }
79}
80
81fn pod_rows(
82 section: &[u8],
83 first: usize,
84 bytes: std::ops::Range<usize>,
85 last: usize,
86) -> PerlDataSectionRows {
87 PerlDataSectionRows::Pod {
88 first,
89 last,
90 documentation: !pod_comment_region(section.get(bytes).unwrap_or_default()),
91 }
92}
93
94/// `^=[a-zA-Z][a-zA-Z0-9]*(\s|$)`, `=cut` aside: a line that opens a
95/// POD block outside one. Perl's `\s` takes a vertical tab, which
96/// `is_ascii_whitespace` does not.
97fn opens_pod(line: &[u8]) -> bool {
98 let Some(rest) = line.strip_prefix(b"=") else {
99 return false;
100 };
101 let word = rest
102 .iter()
103 .position(|byte| !byte.is_ascii_alphanumeric())
104 .unwrap_or(rest.len());
105 let (command, after) = rest.split_at(word);
106 command.first().is_some_and(u8::is_ascii_alphabetic)
107 && command != b"cut"
108 && after
109 .first()
110 .is_none_or(|byte| byte.is_ascii_whitespace() || *byte == b'\x0b')
111}
112
113/// Whether `and`, a Perl `and` token whose parent is `parent`, is the
114/// low-precedence logical operator.
115///
116/// FIXME(#1539 upstream): tree-sitter-perl 1.1.2 lexes an auto-quoted
117/// bareword `and` as the keyword. Perl quotes a bareword before `=>`
118/// and alone inside a hash subscript, so `(and => 1)` and `$h{and}`
119/// hold a string key and no operator, yet the grammar emits two
120/// shapes that read as one:
121///
122/// - a subscript (`$h{and}`, `$r->{and}`, `$h{-and}`) recovers into an
123/// `ERROR` node holding the token;
124/// - a fat-comma key (`(and => 1)`, `{ and => 1 }`, `f(x => 1, and =>
125/// 2)`) becomes a `unary_expression` whose *first* child is the
126/// token — the kind a real `$a and $b` also gets, with `$a` first.
127///
128/// `and` is an infix operator, so in valid Perl the operator always has
129/// a left operand. That is the whole test: the parent is one of the two
130/// expression kinds and the token is not its first child, an `extra`
131/// aside.
132/// `or` and `xor` are unaffected — the grammar reads either as an
133/// identifier in both positions. Remove this, and every call site,
134/// once the pin carries the upstream fix.
135#[must_use]
136pub fn perl_and_is_operator(and: &Node, parent: &Node) -> bool {
137 matches!(
138 parent.kind_id().into(),
139 Perl::UnaryExpression | Perl::BinaryExpression
140 ) && first_non_extra(parent).is_some_and(|first| first.id() != and.id())
141}
142
143/// Whether `expr` is the `unary_expression` tree-sitter-perl builds
144/// around an auto-quoted `and` key (`(and => 1)`): its first child,
145/// comments aside, is the `and` token itself. See
146/// [`perl_and_is_operator`].
147#[must_use]
148pub fn perl_is_and_key_misparse(expr: &Node) -> bool {
149 first_non_extra(expr).is_some_and(|first| first.kind_id() == Perl::And as u16)
150}
151
152/// Whether `not`, a Perl `not` token whose parent is `parent`, is an
153/// auto-quoted hash key rather than the prefix operator.
154///
155/// FIXME(#1541 upstream): the `not` sibling of #1539 — see
156/// [`perl_and_is_operator`]. tree-sitter-perl 1.1.2 lexes a bareword
157/// `not` before `=>` or alone in a hash subscript as the keyword, and
158/// recovers into one of two shapes:
159///
160/// - a subscript (`$h{not}`, `$r->{not}`) gives an `ERROR` node holding
161/// the token;
162/// - a fat-comma key (`(not => 1)`, `{ not => 1 }`) gives the
163/// `unary_expression` a real `not 1` gets, with an `ERROR` wrapping
164/// the `=>` as the token's next sibling;
165/// - a fat-comma key after an earlier keyword key in the same list
166/// (`f(and => 1, not => 2)`) gives a `not` applied to a zero-width
167/// bareword the recovery invents, with the `=>` beside the whole
168/// `unary_expression`. A real `not` always has a written operand.
169///
170/// `and`'s test does not carry over: `not` is a prefix operator, so it
171/// is the first child of its expression whether it is a key or not. The
172/// recovery node is the only signal, so this reads it directly. A real
173/// `not` that lands inside an `ERROR` for an unrelated reason is billed
174/// as a key too; that takes input the grammar already failed to parse.
175/// `or` is unaffected — the grammar reads it as an identifier in both
176/// positions. Remove this, and every call site, once the pin carries the
177/// upstream fix.
178#[must_use]
179pub fn perl_not_is_key<'a>(not: &Node<'a>, parent: &Node<'a>) -> bool {
180 parent.is_error()
181 || parent
182 .children_after(not)
183 // The parser marks the `ERROR` it wraps a skipped `=>` in
184 // as an extra, so only comments are passed over here.
185 .find(|child| child.is_error() || !child.as_tree_sitter().is_extra())
186 .is_some_and(|next| {
187 (next.is_error() && next.is_child(Perl::FatComma as u16))
188 || next.start_byte() == next.end_byte()
189 })
190}
191
192/// Whether `test`, a Perl `file_handle_operator` whose ancestor chain is
193/// `ancestors`, is an auto-quoted `-bareword` key rather than a file
194/// test.
195///
196/// FIXME(#1545 upstream): tree-sitter-perl 1.1.2 lexes the leading `-X`
197/// of a dash-prefixed bareword key as a file test. Perl auto-quotes
198/// `-foo` before `=>` and alone in a hash subscript — the Tk, CGI and
199/// Getopt option-hash idiom — so `$h{-foo}` and `(-text => 1)` hold the
200/// strings `"-foo"` and `"-text"`, and no operator. The grammar emits
201/// two file-test shapes, which this recognises:
202///
203/// - `-` and a letter glued to more word characters (`-foo`) becomes a
204/// file test on the bareword `oo`. Perl never reads that as a file
205/// test, since one needs a non-word character after its letter, so
206/// only the position decides: the test is the whole `key` of a hash
207/// subscript, or the left of a `=>`. Elsewhere `-foo` is a negated
208/// call or a string, and stays as the grammar reads it.
209/// - a key that *is* a file test (`(-x => 1)`) swallows the `=>` and
210/// the value, wrapping the `=>` in an `ERROR`. A real file test
211/// cannot take a `=>` as its operand, so the `ERROR` decides — when
212/// the `=>` is on the test's own line. Perl quotes `-x` only when
213/// nothing but spaces and tabs separate it from the `=>`: across a
214/// newline or a comment (`(-f # c⏎ => 7)`) it stays the file test on
215/// `$_`, the list `(-f, 7)`, and the grammar recovers the same
216/// `ERROR` regardless. A comment runs to the end of its line, so the
217/// row alone tells the two apart. The `-and` / `-not` and `-foo`
218/// keys are quoted across either, as Perl does.
219///
220/// The subscript form of the second shape (`$h{-x}`) recovers into an
221/// `ERROR` that drops the `-x` token altogether, so nothing is left to
222/// bill. A key whose letter is no file-test letter (`-name`, `-height`)
223/// is a third shape, a `-` applied to the bareword, which
224/// [`perl_bareword_is_key`] covers. Remove this, and every call site,
225/// once the pin carries the upstream fix.
226#[must_use]
227pub(crate) fn perl_file_test_is_dash_key<'a>(
228 test: &Node<'a>,
229 ancestors: Ancestors<'a, '_>,
230) -> bool {
231 test.children()
232 // As in `perl_not_is_key`: the `ERROR` is an extra.
233 .find(|child| child.is_error() || !child.as_tree_sitter().is_extra())
234 .is_some_and(|operand| {
235 if operand.is_error() {
236 operand.is_child(Perl::FatComma as u16) && operand.start_row() == test.start_row()
237 } else {
238 is_glued_word(&operand, test)
239 && is_plain_bareword(&operand)
240 && ancestors
241 .iter(test)
242 .next()
243 .is_some_and(|(key, above)| is_key_position(&key, above))
244 }
245 })
246}
247
248/// Whether `node` is the word of a `-bareword` key that the enclosing
249/// file test bills whole, so it bills nothing of its own. `ancestors`
250/// is `node`'s chain.
251#[must_use]
252pub(crate) fn perl_is_dash_key_word<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
253 ancestors.iter(node).next().is_some_and(|(test, above)| {
254 test.kind_id() == Perl::FileHandleOperator as u16
255 && is_glued_word(node, &test)
256 && perl_file_test_is_dash_key(&test, above)
257 })
258}
259
260/// Whether `word`, a Perl `call_expression_with_bareword` whose
261/// ancestor chain is `ancestors`, is an auto-quoted hash key rather than
262/// a call.
263///
264/// FIXME(#1571 upstream): tree-sitter-perl 1.1.2 parses every bareword
265/// key as a call. Perl quotes a word of letters, digits and underscores
266/// before `=>` (perlop, "Comma Operator") and alone in a hash subscript
267/// (perldata, "Scalar value constructors"), a leading `-` included, so
268/// `(name => 1)`, `(-name => 1)`, `$h{-name}` and `$h{-size}` hold the
269/// strings `"name"`, `"-name"`, `"-name"` and `"-size"`, and call
270/// nothing. Three shapes reach here:
271///
272/// - the bareword itself before a `=>` (`$h{name}` is an `identifier`
273/// with no call around it, so the subscript form never arises);
274/// - the operand of a `-` whose `unary_expression` is the key —
275/// before a `=>`, or alone in a subscript;
276/// - the word of a `-bareword` key the grammar reads as a file test
277/// (`-size`), which `perl_is_dash_key_word` decides.
278///
279/// A package-qualified word (`Foo::Bar => 1`) is not quoted by `=>`
280/// and stays a call, as does a word with arguments (`name() => 1`),
281/// which the grammar wraps in a call of its own. Remove this, and every
282/// call site, once the pin carries the upstream fix.
283#[must_use]
284pub fn perl_bareword_is_key<'a>(word: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
285 if perl_is_dash_key_word(word, ancestors) {
286 return true;
287 }
288 if !is_plain_bareword(word) {
289 return false;
290 }
291 match ancestors.iter(word).next() {
292 Some((unary, above)) if dash_operator(&unary).is_some() => is_key_position(&unary, above),
293 _ => is_key_position(word, ancestors),
294 }
295}
296
297/// Whether `word`, a Perl `call_expression_with_bareword` whose ancestor
298/// chain is `ancestors`, is a call site of its own.
299///
300/// The grammar nests the callee of every call that takes arguments as a
301/// bareword call: `foo(1)`, `foo 1` and `sort { … } @x` each wrap a
302/// `call_expression_with_bareword` spelling `foo` or `sort`, so the
303/// wrapper is the call and the word inside it is its name. The word is
304/// kept for the spellings with no wrapper — `foo;`, `&foo;`, and the
305/// `foo(1` that error recovery leaves without one — so the wrapper
306/// cannot be dropped instead. Nor is an auto-quoted key a call
307/// ([`perl_bareword_is_key`]). ABC's branches and
308/// `Checker::is_call_with_code` both ask this, so the two count the same
309/// calls.
310#[must_use]
311pub fn perl_bareword_is_call<'a>(word: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
312 !ancestors.parent(word).is_some_and(|parent| {
313 matches!(
314 parent.kind_id().into(),
315 Perl::CallExpressionWithSpacedArgs
316 | Perl::CallExpressionWithSub
317 | Perl::CallExpressionWithArgsWithBrackets
318 | Perl::CallExpressionWithVariable
319 | Perl::CallExpressionRecursive
320 )
321 }) && !perl_bareword_is_key(word, ancestors)
322}
323
324/// Whether `dash`, a Perl `-` token whose ancestor chain is
325/// `ancestors`, is the sign of an auto-quoted key rather than a unary
326/// minus: a `-and` / `-not` key ([`perl_dash_signs_keyword_key`]) or a
327/// `-bareword` key ([`perl_bareword_is_key`]). A key's sign is part of
328/// the string, so it is no operator.
329#[must_use]
330pub(crate) fn perl_dash_signs_key<'a>(dash: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
331 perl_dash_signs_keyword_key(dash, ancestors)
332 || ancestors.iter(dash).next().is_some_and(|(parent, above)| {
333 parent.kind_id() == Perl::UnaryExpression as u16
334 && next_non_extra(dash, ancestors).is_some_and(|word| is_plain_bareword(&word))
335 && is_key_position(&parent, above)
336 })
337}
338
339/// The `-` that signs the `-bareword` key `identifier` spells, or
340/// `None` when `identifier`, a Perl `identifier` whose ancestor chain is
341/// `ancestors`, is no such key's word. The key bills as one operand
342/// spelled from the `-` to the word's end, so `(-name => 1)` bills
343/// `-name`, the spelling of its quoted twin's contents.
344#[must_use]
345pub(crate) fn perl_bareword_key_sign<'a>(
346 identifier: &Node<'a>,
347 ancestors: Ancestors<'a, '_>,
348) -> Option<Node<'a>> {
349 let mut up = ancestors.iter(identifier);
350 let (_, word_ancestors) = up
351 .next()
352 .filter(|(word, _)| word.kind_id() == Perl::CallExpressionWithBareword as u16)?;
353 let (unary, _) = up.next()?;
354 let dash = dash_operator(&unary)?;
355 // `word` and `dash` are siblings, so they share an ancestry.
356 perl_dash_signs_key(&dash, word_ancestors).then_some(dash)
357}
358
359/// Whether `dash`, a Perl `-` token whose ancestor chain is
360/// `ancestors`, is the sign of an auto-quoted `-and` or `-not` key
361/// (`$h{-not}`, `(-and => 1)`), which the grammar splits into the `-`
362/// and the keyword token. [`perl_and_is_operator`] and
363/// [`perl_not_is_key`] decide whether the keyword is a key, and a key's
364/// sign is part of the string whether or not a space separates the two.
365#[must_use]
366pub fn perl_dash_signs_keyword_key<'a>(dash: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
367 // A sign is a prefix `-`, whose parent is the subscript's `ERROR` or
368 // a `unary_expression`. An infix `-` is a real subtraction even
369 // before an auto-quoted key (`(1 - and => 2)` subtracts the string),
370 // and is by far the commonest `-`, so it is answered first.
371 let Some(parent) = ancestors
372 .parent(dash)
373 .filter(|parent| parent.kind_id() != Perl::BinaryExpression as u16)
374 else {
375 return false;
376 };
377 // `$h{-not}` keeps the keyword beside the `-`; `(-not => 1)` nests
378 // it as the first child of the `-`'s operand.
379 next_non_extra(dash, ancestors).is_some_and(|next| {
380 keyword_is_key(&next, &parent)
381 || first_non_extra(&next).is_some_and(|keyword| keyword_is_key(&keyword, &next))
382 })
383}
384
385/// Whether `key`, an `and` / `not` key token whose ancestor chain is
386/// `ancestors`, is signed by a `-` (`$h{-not}`, `(- and => 1)`), read off
387/// the tree as [`perl_dash_signs_keyword_key`] reads it: the `-` sits
388/// beside the key, or is the operator of the expression the key leads.
389/// The bytes cannot say it: an infix `-` (`$x - and => 2`) or a comment
390/// ending in one also precedes a key, and a comment can sit between a
391/// sign and its key.
392#[must_use]
393pub fn perl_key_is_signed<'a>(key: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
394 let mut up = ancestors.iter(key);
395 up.next().is_some_and(|(_, parent_ancestors)| {
396 let beside = ancestors
397 .siblings_before(key)
398 .find(|sibling| !sibling.as_tree_sitter().is_extra());
399 // A `-` beside the key shares its chain; the `operator` of the
400 // expression the key leads is a sibling of that expression.
401 if let Some(dash) = beside.filter(|node| node.kind_id() == Perl::DASH as u16) {
402 return perl_dash_signs_keyword_key(&dash, ancestors);
403 }
404 up.next().is_some_and(|(outer, _)| {
405 dash_operator(&outer)
406 .is_some_and(|dash| perl_dash_signs_keyword_key(&dash, parent_ancestors))
407 })
408 })
409}
410
411fn keyword_is_key(token: &Node, parent: &Node) -> bool {
412 match token.kind_id().into() {
413 Perl::And => !perl_and_is_operator(token, parent),
414 Perl::Not => perl_not_is_key(token, parent),
415 _ => false,
416 }
417}
418
419// The width of a file-test operator token (`-e`, `-x`, …): the grammar
420// lexes it as one hidden anonymous `/-[rwxo…]/` token, a `-` and one
421// letter, so no node carries its end byte.
422const FILE_TEST_TOKEN_LEN: usize = 2;
423
424// Whether `word` is the bareword the grammar split off a `-bareword`
425// key: a word glued to the file-test token starts right after it.
426fn is_glued_word(word: &Node, test: &Node) -> bool {
427 word.kind_id() == Perl::CallExpressionWithBareword as u16
428 && word.start_byte() == test.start_byte() + FILE_TEST_TOKEN_LEN
429}
430
431// Whether `key`, whose ancestor chain is `ancestors`, is a key where
432// Perl auto-quotes one: the whole of a hash subscript, which the grammar
433// may wrap in a `binary_expression` with no other child but comments,
434// or the last thing before a `=>`. Every auto-quoted key shape asks this
435// one question, of the node that spans its whole key.
436//
437// `=>` quotes the word immediately to its left whatever expression that
438// word closes (perlop, "Comma Operator"), so `(1 - name => 2)` subtracts
439// the string `"name"`. The grammar nests such a word as the last operand
440// of its expression, so the test climbs while the node is its parent's
441// last child: the sibling after the first ancestor that has one is the
442// token after the word. A key that stopped being the whole of its node
443// on the way up is no subscript key (`$h{1 - name}` is not quoted).
444fn is_key_position<'a>(key: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
445 let (mut node, mut node_ancestors) = (*key, ancestors);
446 let mut whole = true;
447 for (parent, above) in ancestors.iter(key) {
448 if whole
449 && matches!(
450 parent.kind_id().into(),
451 Perl::HashAccessVariableSimple | Perl::HashAccessVariable
452 )
453 {
454 return true;
455 }
456 // As in `perl_not_is_key`, only comments are passed over: the
457 // grammar wraps a stray `,` (`(name, => 1)`) in an `ERROR` it
458 // marks as an extra, and that `,`, not the word, is then the
459 // token the `=>` quotes, which is nothing. A `-foo, =>` file-test
460 // key stops here too and reads as the grammar does (a file test
461 // on `oo`), as every `-bareword` outside key position does; Perl
462 // itself reads one as a string or a negated call by whether a sub
463 // of that name is visible, which no single file says.
464 if let Some(next) = node_ancestors
465 .siblings_after(&node)
466 .find(|sibling| sibling.is_error() || !sibling.as_tree_sitter().is_extra())
467 {
468 return next.kind_id() == Perl::FatComma as u16;
469 }
470 whole &= parent.kind_id() == Perl::BinaryExpression as u16 && non_extra_count(&parent) == 1;
471 (node, node_ancestors) = (parent, above);
472 }
473 false
474}
475
476// Whether `word` is a call of a bare word and nothing else: one
477// `identifier`, no package qualifier, no arguments. Only such a word is
478// auto-quoted.
479fn is_plain_bareword(word: &Node) -> bool {
480 if word.kind_id() != Perl::CallExpressionWithBareword as u16 {
481 return false;
482 }
483 // Every call's name is one of these, so the common shape — one
484 // child, no comment inside the call — is read off two fields rather
485 // than through a cursor over the children.
486 match word.child_count() {
487 1 => word
488 .child(0)
489 .is_some_and(|child| child.kind_id() == Perl::Identifier as u16),
490 _ => word
491 .children()
492 .filter(|child| !child.as_tree_sitter().is_extra())
493 .map(|child| child.kind_id())
494 .eq([Perl::Identifier as u16]),
495 }
496}
497
498// The `-` of `node` when it is a unary minus — the `unary_expression` a
499// `-bareword` key whose letter is no file-test letter parses as — and
500// `None` for any other node, a subtraction's `binary_expression`
501// included.
502fn dash_operator<'a>(node: &Node<'a>) -> Option<Node<'a>> {
503 if node.kind_id() != Perl::UnaryExpression as u16 {
504 return None;
505 }
506 node.child_by_field_name("operator")
507 .filter(|op| op.kind_id() == Perl::DASH as u16)
508}
509
510fn non_extra_count(node: &Node) -> usize {
511 node.children()
512 .filter(|child| !child.as_tree_sitter().is_extra())
513 .count()
514}
515
516// The sibling after `node`, whose ancestor chain is `ancestors`,
517// comments aside. Every key of a wide `(-a => 1, -b => 2, …)` list or
518// a flat `ERROR` asks, so it is found without a scan from the parent's
519// start (#1545, #1584).
520fn next_non_extra<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> Option<Node<'a>> {
521 ancestors
522 .siblings_after(node)
523 .find(|sibling| !sibling.as_tree_sitter().is_extra())
524}
525
526fn first_non_extra<'a>(node: &Node<'a>) -> Option<Node<'a>> {
527 node.children()
528 .find(|child| !child.as_tree_sitter().is_extra())
529}