Skip to main content

big_code_analysis_ast/getter/
perl.rs

1//! `Getter` implementation for Perl.
2#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
3
4use super::*;
5use crate::lang_helpers::perl::{
6    perl_and_is_operator, perl_bareword_key_sign, perl_dash_signs_key, perl_file_test_is_dash_key,
7    perl_is_dash_key_word, perl_key_is_signed, perl_not_is_key,
8};
9
10impl Getter for PerlCode {
11    fn get_space_kind(node: &Node) -> SpaceKind {
12        match node.kind_id().into() {
13            Perl::FunctionDefinition
14            | Perl::FunctionDefinitionWithoutSub
15            | Perl::AnonymousFunction => SpaceKind::Function,
16            Perl::SourceFile => SpaceKind::Unit,
17            _ => SpaceKind::Unknown,
18        }
19    }
20
21    fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
22        use Perl as P;
23
24        if let Some(role) = auto_quoted_key_role(node, ancestors) {
25            return role;
26        }
27        match node.kind_id().into() {
28            // Regex delimiter punctuation. The bare match form is the
29            // only one of Perl's five regex literals that spells its
30            // delimiters with an operator token kind: `bca dump` at the
31            // pinned grammar shows `/abc/` emitting two `SLASH` under
32            // `PatternMatcher`, while `m//`, `qr//`, `s///` and `tr///`
33            // use the dedicated `StartDelimiter` / `SeparatorDelimiter`
34            // / `EndDelimiter` kinds that no arm here classifies. So
35            // `$s =~ /abc/` fabricated two division operators (#1312,
36            // the Perl sibling of Elixir #1256). Suppress them exactly
37            // when their parent is the literal — the compound-leaf
38            // guard of grammar-dispatch §5 — which leaves the bare form
39            // contributing what its three synonyms already contribute
40            // (#1314 made that "one operand" rather than "nothing";
41            // see the pattern-value arm below).
42            P::SLASH
43                if ancestors.parent_has_kind(node, P::PatternMatcher as u16) =>
44            {
45                TokenRole::Unknown
46            }
47            // The bare form's pattern text is one operand, billed on the
48            // `PatternMatcher` wrapper, but the grammar spells four of its
49            // characters as anonymous tokens under `regex_pattern_content`:
50            // `[`, `]`, `\` and `#`. They are literal regex text, which
51            // `m//` and `qr//` emit no node for, so `/[a]/` billed a `[]`
52            // operator that `m/[a]/` does not (#1549). `#` is also in no
53            // operator arm, because its only other position is a comment's
54            // leading `#` under `comments`, and a comment is not code.
55            // `$#a` and the `#` delimiters of `qw#…#` / `s#a#b#` are
56            // distinct token kinds.
57            _ if ancestors.parent_has_kind(node, P::RegexPatternContent as u16) => {
58                TokenRole::Unknown
59            }
60            // Control-flow and declaration keywords. `Perl::Sub` is the
61            // `sub` keyword (token id 16); `Perl::SUB` is the `__SUB__`
62            // literal (token id 7) — that one is an operand, not an
63            // operator. Same split for `Package` (keyword) vs `PACKAGE`
64            // (`__PACKAGE__` literal).
65            P::If | P::Unless | P::Else | P::Elsif | P::While | P::Until | P::For
66            | P::Foreach | P::When | P::Continue | P::Next | P::Last | P::Redo | P::Goto
67            | P::Return | P::Sub | P::Package | P::My | P::Our
68            | P::Local | P::State | P::Use | P::No | P::Require | P::Bless | P::And | P::Or
69            | P::Xor | P::Not | P::Eq | P::Ne | P::Lt | P::Gt | P::Le | P::Ge | P::Cmp
70            // Punctuation acting as operators
71            | P::SEMI | P::COMMA | P::COLON | P::COLONCOLON | P::LBRACE | P::LBRACK
72            | P::LPAREN | P::DOT | P::DOTDOT | P::DOTDOTDOT | P::FatComma | P::DASHGT
73            | P::QMARK | P::BSLASH | P::DOLLAR | P::DOLLARHASH | P::AT | P::PERCENT
74            // Arithmetic / comparison / logical / bitwise / assignment operators
75            | P::EQ | P::PLUS | P::DASH | P::STAR | P::SLASH | P::STARSTAR | P::BANG
76            | P::TILDE | P::EQTILDE | P::BANGTILDE | P::EQEQ | P::BANGEQ | P::LT | P::GT
77            | P::LTEQ | P::GTEQ | P::AMPAMP | P::PIPEPIPE | P::SLASHSLASH | P::PIPE
78            | P::CARET | P::LTLT | P::GTGT | P::TILDETILDE | P::PLUSPLUS | P::DASHDASH
79            | P::PLUSEQ | P::DASHEQ | P::STAREQ | P::SLASHEQ | P::PERCENTEQ | P::STARSTAREQ
80            | P::AMPEQ | P::PIPEEQ | P::CARETEQ | P::LTLTEQ | P::GTGTEQ | P::AMPAMPEQ
81            | P::PIPEPIPEEQ | P::SLASHSLASHEQ | P::DOTEQ | P::XEQ
82            | P::LTEQGT | P::AMPDOTEQ | P::PIPEDOTEQ | P::CARETDOTEQ | P::Isa
83            // Substitution and transliteration. These are *operations
84            // applied to a target*, not pattern values, so they are
85            // operators while the three pattern spellings below are
86            // operands (#1314). `y///` is a synonym of `tr///` and
87            // shares `TransliterationTrOrY`, so the two fold to one
88            // entry, which is what synonyms should do.
89            //
90            // Their *literal* pattern and replacement text is
91            // invisible either way: `bca dump` shows `s/a/b/` emitting
92            // only the `s` keyword and its `start` / `separator` /
93            // `end` delimiters, with no content node, so classifying
94            // them as operators loses nothing that treating them as
95            // values would have kept. An *interpolated* `s/$x/$y/` does
96            // emit `Interpolation` children, and those count as
97            // operands under either choice — so the reason for the
98            // split is the semantic one above, not this. `get_operator_id_as_str` below renders them
99            // `s///` and `tr///` rather than as raw kind names.
100            | P::SubstitutionPatternS | P::TransliterationTrOrY
101                => TokenRole::Operator,
102            // `package_name`, `package_variable` and `typeglob` each
103            // spell a whole name — `Data::Dumper`, `$Foo::count`,
104            // `*STDOUT` — and are operands in their own right below, so
105            // an operand-classified node they *contain* is already
106            // billed by the wrapper. Before #1355 the wrapper and its
107            // contents both counted: `use strict;` scored N2 2 for one
108            // name, `require Data::Dumper;` n2 3 / N2 3 for one, and
109            // `our $Foo::count = 3;` n2 5 / N2 6 for two — the
110            // vocabulary even growing a bare `::` entry, because a
111            // `package_variable` spells its separator as a second,
112            // childless `package_name` beside the qualifier.
113            //
114            // Keyed on the parent alone rather than on a list of child
115            // kinds, because the invariant is positional: a wrapper's
116            // span contains its children's, so whatever sits directly
117            // inside one is already paid for. node-types.json admits ten
118            // (wrapper, child) pairings across eight operand kinds, all
119            // of them reachable — `package_name` ⊃ `typeglob` is
120            // `*Foo::glob`, `package_name` ⊃ `package_variable` the
121            // `$Foo::Bar::baz` nesting — and enumerating them here would
122            // be a coverage claim to re-derive on every grammar bump for
123            // no gain, the trade grammar-dispatch §1 recommends taking.
124            // `perl_name_wrappers_bill_the_name_once_1355` witnesses all
125            // ten and fails on an eleventh. Naming the three wrappers as
126            // *parents* also resolves arbitrary qualifier depth to the
127            // outermost, the way PHP handles `$$$x` (#1259).
128            //
129            // It sits below the operator arm on purpose: the `::` token
130            // inside a bareword `package_name` (`Data::Dumper`), and `*`
131            // and the opening `{` inside a `typeglob`, are matched there
132            // and keep their operator reading. A sigil-qualified name
133            // has no such token — its separator is the childless
134            // `package_name` above, which the guard drops with the rest
135            // of the wrapper — so `$Foo::count` bills no `::` where
136            // `Data::Dumper` bills one. That asymmetry is the grammar's:
137            // `main` billed that node as a bogus `::` operand and never
138            // as an operator, so n1/N1 are unchanged here. The typeglob
139            // `*` stays an operator because this getter already bills
140            // the `$` sigil on every scalar reference (`my $x` → n1
141            // {my, $, =, ;}), so the typeglob sigil follows the sigil
142            // convention rather than #1312's delimiter suppression; the
143            // cost is that it shares `Perl::STAR` with multiplication,
144            // so both fold into one n1 entry, and `*Foo` and `*{Foo}`
145            // differ by one `{}` operator. (The closing `}` has never
146            // been classified — `get_operator_id_as_str` folds the pair
147            // to one `{}` glyph — so the guard changes nothing for it.)
148            //
149            // Guarded, not deleted, per grammar-dispatch §6: these
150            // leaves are ordinary operands everywhere else (`my $x`,
151            // `foo()`, the `bar` of `Foo::bar()`, which is a *sibling*
152            // of the `package_name` rather than a child).
153            //
154            // Keeping the wrapper's whole span is the opposite call from
155            // PHP's `qualified_name` (#1293) and Groovy's
156            // `QualifiedName` (#1263 / #1352), which bill the leaves and
157            // drop the wrapper. Three reasons it goes the other way in
158            // Perl. `package_variable` and `typeglob` are sigil-bearing
159            // *variable references*, the shape #1259 settled by keeping
160            // the wrapper — the leaves-only reading detaches the sigil
161            // from the variable and glues it to the qualifier, billing
162            // `$Foo::Bar::baz` as `$Foo` + `Bar` + `baz`, three operands
163            // for one variable. The three kinds nest into each other, so
164            // they must be decided together; splitting `package_name`
165            // off would need a grandparent-aware guard. And the
166            // vocabulary-sharing argument of #1293 / #1352 — that
167            // `java.util.List` and `List` name the same class — has no
168            // Perl analogue, since a package is always spelled in full.
169            //
170            // `module_name` is not here, though #1355 expected it to
171            // be: it is the quoted `use 'Foo.pm'` form, and it holds
172            // nothing but its two quote tokens — not the `identifier`
173            // the issue thought it shared. It stays an operand, pinned
174            // by `perl_qualified_name_leaves_still_count_elsewhere_1355`.
175            _ if matches!(
176                ancestors.parent(node).map(|p| p.kind_id().into()),
177                Some(P::PackageName | P::PackageVariable | P::Typeglob)
178            ) =>
179            {
180                TokenRole::Unknown
181            }
182            // Operands: identifiers and literals. Non-interpolating
183            // string literals (`'…'`, `q{…}`) are leaf operands; the
184            // interpolating kinds (`"…"`, `qq{…}`, `` `…` ``, `qx{…}`)
185            // are handled separately below so their inner
186            // scalar/array/hash variables are not double-counted.
187            //
188            // `ListItem` is one element of a `qw(a b c)` list — a leaf
189            // the grammar emits under `word_list_qw` and nowhere else,
190            // so it cannot double-bill another construct. Until it was
191            // listed the whole `qw()` was invisible to Halstead: neither
192            // the elements nor the wrapper nor the `qw` keyword had an
193            // arm, so `my @a = qw(a b c)` billed `@a` alone while its
194            // synonym `("a", "b", "c")` billed four operands. The
195            // wrapper is gated below, the Ruby `%w[]` shape of #1353.
196            P::Identifier | P::ScalarVariable | P::ArrayVariable | P::HashVariable
197            | P::PackageVariable | P::SpecialScalarVariable | P::PackageName | P::ModuleName
198            | P::BarewordImport | P::Typeglob | P::FileHandle | P::ListItem
199            | P::Integer | P::FloatingPoint | P::ScientificNotation | P::Hexadecimal | P::Octal
200            | P::True | P::False | P::SpecialLiteral
201            | P::StringSingleQuoted | P::StringQQuoted
202            | P::FILE | P::LINE | P::SUB | P::PACKAGE
203                => TokenRole::Operand,
204            // Perl's interpolating string-like literals count as one
205            // operand when inert. When they carry an `Interpolation`
206            // child the inner scalar / array / hash variables are
207            // already walked and classified as operands; counting the
208            // wrapping literal too would double-count the inner
209            // variables' contribution to `N2` (issue #199, same
210            // pattern as #180 for Elixir/Bash, #183 for C#, #184 for
211            // PHP, #191 for Kotlin). `HeredocBodyStatement` is the
212            // visible body of `<<TAG ... TAG` heredocs (issue #287);
213            // it is interpolation-capable, so it joins the same
214            // dispatch — inert heredocs are one operand, interpolating
215            // heredocs let the inner variables carry the count.
216            // The pattern *values* — `/abc/`, `m/abc/`, `qr/abc/` —
217            // join the same dispatch (#1314). `m/abc/` is exactly
218            // `/abc/` and `qr/abc/` is the same pattern as a value, so
219            // all three must score alike; that equality is what
220            // `perl_every_pattern_value_spelling_scores_alike` protects
221            // and why #1312 declined to promote the bare form on its
222            // own. Promoting the three together closes the gap without
223            // reintroducing the spelling sensitivity: each contributes
224            // one operand, matching Ruby's `Regex` and Elixir's
225            // `Sigil`. `s///` and `tr///` are *not* here — they are
226            // operations applied to a target and sit in the operator
227            // arm above.
228            //
229            // Sharing the string dispatch is not incidental, which is
230            // why the arms are merged rather than duplicated: the bare
231            // form keeps an interpolated variable inside an
232            // unclassified `regex_pattern_content`, but `m/$x/` and
233            // `qr/$x/` emit a real `Interpolation` whose
234            // `scalar_variable` is already counted. A plain operand arm
235            // would score `/$x/` at one operand and `m/$x/` at two —
236            // the very divergence the equality above exists to prevent.
237            //
238            // `WordListQw` holds classified `ListItem` operands rather
239            // than interpolation, so the same guard gives it "one
240            // operand per element, or one for the empty `qw()`" — the
241            // rule #1353 settled for Ruby's `%w[]`. One list serves
242            // both groups because no string kind admits a `list_item`
243            // and `word_list_qw` admits no `interpolation`.
244            P::StringDoubleQuoted | P::StringQqQuoted | P::BacktickQuoted
245            | P::CommandQxQuoted | P::HeredocBodyStatement
246            | P::PatternMatcher | P::PatternMatcherM | P::RegexPatternQr | P::WordListQw => {
247                Self::string_operand_type(node, &[P::Interpolation as u16, P::ListItem as u16])
248            }
249            _ => TokenRole::Unknown,
250        }
251    }
252
253    /// Spells a `-bareword` key whole (#1545). A key that is exactly a
254    /// file test (`(-x => 1)`) has swallowed the `=>` and the value into
255    /// its node, so the key is the sign and the word after it; a
256    /// `-and` / `-not` key gets back the sign its `-` token stopped
257    /// billing, spelled the same however the two are spaced, since Perl
258    /// keys `(- not => 1)` as `"-not"` too. The sign is read off the tree
259    /// (`perl_key_is_signed`), not the byte before the key, which can
260    /// equally be an infix `-` or the end of a comment.
261    ///
262    /// Any other `-bareword` key (`(-name => 1)`, `$h{-name}`) bills its
263    /// word spelled from the sign (#1572). That is a source slice, so a
264    /// sign spaced off its word (`(- name => 1)`, rare) keeps the space,
265    /// and a comment between the two (`(- # c⏎ name => 1)`) is carried
266    /// along in it: one operand, distinct from `name`, but not merged
267    /// with `-name`, and listed by `bca ops` as written.
268    fn get_operand_id<'a>(
269        node: &Node<'a>,
270        code: &'a [u8],
271        ancestors: Ancestors<'a, '_>,
272    ) -> &'a [u8] {
273        let (start, end) = (node.start_byte(), node.end_byte());
274        match node.kind_id().into() {
275            Perl::FileHandleOperator => {
276                let word = code[start + 1..end]
277                    .iter()
278                    .take_while(|byte| byte.is_ascii_alphanumeric() || **byte == b'_')
279                    .count();
280                &code[start..=start + word]
281            }
282            Perl::And if perl_key_is_signed(node, ancestors) => b"-and",
283            Perl::Not if perl_key_is_signed(node, ancestors) => b"-not",
284            Perl::Identifier => perl_bareword_key_sign(node, ancestors)
285                .map_or(&code[start..end], |dash| &code[dash.start_byte()..end]),
286            _ => &code[start..end],
287        }
288    }
289
290    /// Folds paired delimiters to one glyph, and names the two
291    /// pattern *operations* after their source spelling.
292    ///
293    /// Hand-written rather than `get_operator!(Perl)` because
294    /// `SubstitutionPatternS` and `TransliterationTrOrY` are named
295    /// nodes rather than punctuation tokens (#1314): the macro's
296    /// fallback renders a kind's own name, so `bca ops` would list
297    /// `substitution_pattern_s` and `transliteration_tr_or_y` among
298    /// the operators, which reads as a bug rather than as `s///`.
299    /// `tr///` covers `y///` too — one kind, one glyph, because they
300    /// are synonyms. `=>` is the named `fat_comma` node for the same
301    /// reason.
302    #[inline]
303    fn get_operator_id_as_str(id: u16) -> &'static str {
304        let typ = id.into();
305        match typ {
306            Perl::LPAREN => "()",
307            Perl::LBRACK => "[]",
308            Perl::LBRACE => "{}",
309            Perl::SubstitutionPatternS => "s///",
310            Perl::TransliterationTrOrY => "tr///",
311            Perl::FatComma => "=>",
312            _ => typ.into(),
313        }
314    }
315}
316
317/// The role of a token tree-sitter-perl 1.1.2 misparses inside an
318/// auto-quoted hash key, or `None` for any other token. Perl quotes a
319/// bareword before `=>` and alone in a hash subscript, keywords and a
320/// leading `-` included, and the grammar reads some of those keys as
321/// operators. Every arm is an upstream workaround; drop each with its
322/// issue once the pin carries the fix.
323fn auto_quoted_key_role<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> Option<TokenRole> {
324    use Perl as P;
325
326    let role = match node.kind_id().into() {
327        // FIXME(#1539 upstream): an auto-quoted `and` key (`$h{and}`,
328        // `(and => 1)`) lexes as the operator keyword. It is a
329        // string key, so it bills what `$h{or}` bills: the
330        // `identifier` operand the grammar gives every other
331        // bareword key.
332        P::And
333            if !ancestors
334                .parent(node)
335                .is_some_and(|parent| perl_and_is_operator(node, &parent)) =>
336        {
337            TokenRole::Operand
338        }
339        // FIXME(#1541 upstream): the same for an auto-quoted `not`
340        // key (`$h{not}`, `(not => 1)`); see `perl_not_is_key`.
341        P::Not
342            if ancestors
343                .parent(node)
344                .is_some_and(|parent| perl_not_is_key(node, &parent)) =>
345        {
346            TokenRole::Operand
347        }
348        // FIXME(#1545 upstream): a `-bareword` key (`$h{-foo}`,
349        // `(-text => 1)`) lexes as a file test on the rest of the
350        // word. It is one string key, so the test node bills it as
351        // one operand spelling the whole key, and the word inside
352        // bills nothing. A `-and` / `-not` key splits instead into
353        // a `-` and the keyword, which the arms above bill as the
354        // key, and any other `-bareword` key (`(-name => 1)`) into a
355        // `-` and a bareword call, whose `identifier` bills the key
356        // (#1572, the local workaround for upstream #1571); in both
357        // the `-` is part of the key and bills nothing. See
358        // `perl_file_test_is_dash_key` and `perl_bareword_is_key`.
359        P::FileHandleOperator if perl_file_test_is_dash_key(node, ancestors) => TokenRole::Operand,
360        P::Identifier
361            if ancestors
362                .iter(node)
363                .next()
364                .is_some_and(|(word, above)| perl_is_dash_key_word(&word, above)) =>
365        {
366            TokenRole::Unknown
367        }
368        P::DASH if perl_dash_signs_key(node, ancestors) => TokenRole::Unknown,
369        _ => return None,
370    };
371    Some(role)
372}