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_dash_signs_keyword_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 fn get_operand_id<'a>(
262 node: &Node<'a>,
263 code: &'a [u8],
264 ancestors: Ancestors<'a, '_>,
265 ) -> &'a [u8] {
266 let (start, end) = (node.start_byte(), node.end_byte());
267 match node.kind_id().into() {
268 Perl::FileHandleOperator => {
269 let word = code[start + 1..end]
270 .iter()
271 .take_while(|byte| byte.is_ascii_alphanumeric() || **byte == b'_')
272 .count();
273 &code[start..=start + word]
274 }
275 Perl::And if perl_key_is_signed(node, ancestors) => b"-and",
276 Perl::Not if perl_key_is_signed(node, ancestors) => b"-not",
277 _ => &code[start..end],
278 }
279 }
280
281 /// Folds paired delimiters to one glyph, and names the two
282 /// pattern *operations* after their source spelling.
283 ///
284 /// Hand-written rather than `get_operator!(Perl)` because
285 /// `SubstitutionPatternS` and `TransliterationTrOrY` are named
286 /// nodes rather than punctuation tokens (#1314): the macro's
287 /// fallback renders a kind's own name, so `bca ops` would list
288 /// `substitution_pattern_s` and `transliteration_tr_or_y` among
289 /// the operators, which reads as a bug rather than as `s///`.
290 /// `tr///` covers `y///` too — one kind, one glyph, because they
291 /// are synonyms. `=>` is the named `fat_comma` node for the same
292 /// reason.
293 #[inline]
294 fn get_operator_id_as_str(id: u16) -> &'static str {
295 let typ = id.into();
296 match typ {
297 Perl::LPAREN => "()",
298 Perl::LBRACK => "[]",
299 Perl::LBRACE => "{}",
300 Perl::SubstitutionPatternS => "s///",
301 Perl::TransliterationTrOrY => "tr///",
302 Perl::FatComma => "=>",
303 _ => typ.into(),
304 }
305 }
306}
307
308/// The role of a token tree-sitter-perl 1.1.2 misparses inside an
309/// auto-quoted hash key, or `None` for any other token. Perl quotes a
310/// bareword before `=>` and alone in a hash subscript, keywords and a
311/// leading `-` included, and the grammar reads some of those keys as
312/// operators. Every arm is an upstream workaround; drop each with its
313/// issue once the pin carries the fix.
314fn auto_quoted_key_role<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> Option<TokenRole> {
315 use Perl as P;
316
317 let role = match node.kind_id().into() {
318 // FIXME(#1539 upstream): an auto-quoted `and` key (`$h{and}`,
319 // `(and => 1)`) lexes as the operator keyword. It is a
320 // string key, so it bills what `$h{or}` bills: the
321 // `identifier` operand the grammar gives every other
322 // bareword key.
323 P::And
324 if !ancestors
325 .parent(node)
326 .is_some_and(|parent| perl_and_is_operator(node, &parent)) =>
327 {
328 TokenRole::Operand
329 }
330 // FIXME(#1541 upstream): the same for an auto-quoted `not`
331 // key (`$h{not}`, `(not => 1)`); see `perl_not_is_key`.
332 P::Not
333 if ancestors
334 .parent(node)
335 .is_some_and(|parent| perl_not_is_key(node, &parent)) =>
336 {
337 TokenRole::Operand
338 }
339 // FIXME(#1545 upstream): a `-bareword` key (`$h{-foo}`,
340 // `(-text => 1)`) lexes as a file test on the rest of the
341 // word. It is one string key, so the test node bills it as
342 // one operand spelling the whole key, and the word inside
343 // bills nothing. A `-and` / `-not` key splits instead into
344 // a `-` and the keyword, which the arms above bill as the
345 // key; the `-` is part of it and bills nothing. See
346 // `perl_file_test_is_dash_key`.
347 P::FileHandleOperator if perl_file_test_is_dash_key(node, ancestors) => TokenRole::Operand,
348 P::Identifier
349 if ancestors
350 .iter(node)
351 .next()
352 .is_some_and(|(word, above)| perl_is_dash_key_word(&word, above)) =>
353 {
354 TokenRole::Unknown
355 }
356 P::DASH
357 if ancestors
358 .parent(node)
359 .is_some_and(|parent| perl_dash_signs_keyword_key(node, &parent)) =>
360 {
361 TokenRole::Unknown
362 }
363 _ => return None,
364 };
365 Some(role)
366}