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