big_code_analysis_ast/getter/elixir.rs
1//! `Getter` implementation for Elixir.
2#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
3
4use super::*;
5
6// Extracts the human-readable head name from the first non-`target`
7// child of an Elixir `def` / `defp` / `defmacro` / `defmacrop` /
8// `defmodule` Call. Handles three shapes:
9// - `Arguments` wrapper: descend one level and recurse.
10// - `Identifier` / `Alias` leaf: return its source text.
11// - inner `Call` (e.g. `def foo(x, y)`): return the target identifier
12// text of that inner Call.
13// Returns `None` when the child does not match any of these shapes,
14// allowing the caller to keep scanning siblings (notably the
15// `do_block`, which is unconditionally present and never carries the
16// name).
17fn elixir_extract_head_name<'a>(node: &Node, code: &'a [u8]) -> Option<&'a str> {
18 use Elixir as E;
19
20 let text = |n: &Node| node_text(code, n);
21 match node.kind_id().into() {
22 E::Identifier | E::Alias => text(node),
23 E::Call => text(&node.child_by_field_name("target")?),
24 E::Arguments
25 | E::Arguments2
26 | E::Arguments3
27 | E::Arguments4
28 | E::Arguments5
29 | E::CallArgumentsWithTrailingSeparator => node
30 .children()
31 .find_map(|child| elixir_extract_head_name(&child, code)),
32 _ => None,
33 }
34}
35
36impl Getter for ElixirCode {
37 fn get_space_kind(node: &Node) -> SpaceKind {
38 use Elixir as E;
39
40 match node.kind_id().into() {
41 E::AnonymousFunction => SpaceKind::Function,
42 E::Source => SpaceKind::Unit,
43 _ => SpaceKind::Unknown,
44 }
45 }
46
47 // Source-aware classifier (#275). Elixir's `defmodule` /
48 // `def` / `defp` / `defmacro` / `defmacrop` are not distinct
49 // grammar productions — they all parse as `Call` nodes whose
50 // `target` Identifier text spells the keyword. The walker promotes
51 // these Calls to func spaces via `Checker::is_func_space_with_code`;
52 // this method labels the promoted space with the right `SpaceKind`
53 // so `Wmc` / `Npm` / `Npa` see a Class for `defmodule` and a
54 // Function for the method-defining macros.
55 fn get_space_kind_with_code<'a>(
56 node: &Node<'a>,
57 code: &[u8],
58 ancestors: Ancestors<'a, '_>,
59 ) -> SpaceKind {
60 use crate::lang_helpers::elixir::{
61 elixir_call_keyword, elixir_is_class_macro, elixir_is_inside_quote_block,
62 elixir_is_method_macro,
63 };
64 let kind = Self::get_space_kind(node);
65 if kind != SpaceKind::Unknown {
66 return kind;
67 }
68 match elixir_call_keyword(node, code) {
69 Some(kw) if elixir_is_class_macro(kw) => SpaceKind::Class,
70 // Method-defining macros nested inside a `quote do … end`
71 // template are not real method declarations (#310).
72 Some(kw)
73 if elixir_is_method_macro(kw)
74 && !elixir_is_inside_quote_block(node, code, ancestors) =>
75 {
76 SpaceKind::Function
77 }
78 _ => SpaceKind::Unknown,
79 }
80 }
81
82 // Source-aware name extraction for the macro-shaped declarations.
83 // `def foo(x, y) do … end` parses (with the tree-sitter-elixir
84 // grammar shipped here) as
85 // `Call { target: Identifier "def", Arguments { Call { target:
86 // Identifier "foo", Arguments { … } } }, DoBlock { … } }`
87 // i.e. the head Call is wrapped in an `Arguments` node, not a
88 // direct child. `defmodule Foo.Bar do … end` parses similarly with
89 // the `Alias` inside `Arguments`. We descend through one
90 // `Arguments` layer when present, then either:
91 // - return the `Identifier` / `Alias` text directly
92 // (`defmodule Foo`, `def foo` for an arity-zero head with no
93 // parentheses), or
94 // - return the inner head Call's `target` text
95 // (`def foo(x, y)`).
96 // Falls back to the trait default behaviour (`<anonymous>` for
97 // nodes without a `name` field) when the Call is not one we
98 // recognise.
99 fn get_func_space_name<'a, 'tree>(
100 node: &Node<'tree>,
101 code: &'a [u8],
102 ancestors: Ancestors<'tree, '_>,
103 ) -> Option<&'a str> {
104 use Elixir as E;
105
106 use crate::lang_helpers::elixir::{
107 elixir_call_keyword, elixir_is_class_macro, elixir_is_inside_quote_block,
108 elixir_is_method_macro,
109 };
110 // The Class kind always names its head; for method macros we
111 // additionally require the Call NOT to be inside a `quote`
112 // template, matching the func-space promotion rule (#310).
113 //
114 // The quote-block lookup reads the caller's chain rather than
115 // climbing with `Node::parent` (#1088). This fires once per
116 // *promoted* space rather than per node, so the win is smaller
117 // than at the walk's per-node sites — but `bca function` and the
118 // suppression scan reach it through `get_func_name` on every
119 // function node, where the climb was `O(depth)` apiece.
120 if node.kind_id() == E::Call as u16
121 && let Some(kw) = elixir_call_keyword(node, code)
122 && (elixir_is_class_macro(kw)
123 || (elixir_is_method_macro(kw)
124 && !elixir_is_inside_quote_block(node, code, ancestors)))
125 {
126 let target_id = node.child_by_field_name("target").map(|t| t.id());
127 if let Some(name) = node
128 .children()
129 .filter(|child| Some(child.id()) != target_id)
130 .find_map(|child| elixir_extract_head_name(&child, code))
131 {
132 return Some(name);
133 }
134 }
135
136 if let Some(name) = node.child_by_field_name("name") {
137 return node_text(code, &name);
138 }
139 Some("<anonymous>")
140 }
141
142 fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
143 use Elixir as E;
144
145 // A *named* operator (`&==/2`, `&and/2`, `Kernel.||(a, b)`) is one
146 // `operator_identifier` holding the operator token. The wrapper is
147 // the operand, as `foo` is in `&foo/2`, so the token inside it must
148 // not also bill an operator (grammar-dispatch §5, #1534). The
149 // wrapper is the keeper because it exists for every spelling: `..`
150 // is a childless `operator_identifier`, so keeping the leaf would
151 // lose it.
152 if ancestors.parent_has_kind(node, E::OperatorIdentifier as u16) {
153 return TokenRole::Unknown;
154 }
155
156 match node.kind_id().into() {
157 // Sigil delimiter punctuation. The delimiter choice is
158 // spelling, not semantics — `~r/abc/` is not two divisions
159 // and `~w(a b)` opens no call — so counting these tokens
160 // made `n1`/`N1` vary with the author's delimiter choice
161 // (#1256). The `Sigil` node itself is the operand (below)
162 // and `~` the single per-sigil operator; the delimiters are
163 // suppressed exactly when their parent is the sigil — the
164 // compound-leaf guard of grammar-dispatch §5, mirroring
165 // Bash's `SimpleExpansion` parent check. Verified via
166 // `bca dump` at the pinned grammar: `/ ( { [ < > |` are the
167 // only delimiter kinds surfacing as `Sigil` children that
168 // the operator arm below also matches; the quote delimiters
169 // (`"`, `'`, `"""`, `'''`) and the closers `) } ]` have no
170 // operator arm and need no guard. Same-kind tokens outside
171 // a sigil (division `a / b`, comparison `<`, pipe `|`) fall
172 // through to the operator arm and still count.
173 E::SLASH | E::LPAREN | E::LBRACE | E::LBRACK | E::LT | E::GT | E::PIPE
174 if ancestors.parent_has_kind(node, E::Sigil as u16) =>
175 {
176 TokenRole::Unknown
177 }
178 // Reserved-word keywords that have dedicated token kinds in
179 // the grammar — block delimiters, exception clauses, the
180 // `fn` keyword, and word-form logical / membership operators.
181 // (Macro-shaped keywords like `def`/`defp`/`if`/`case`/`cond`
182 // are NOT here: they surface as `Identifier` tokens in a
183 // `Call`'s `target` field and are counted as operands below.)
184 E::Do | E::End | E::End2 | E::Else | E::After | E::Catch | E::Rescue | E::Fn
185 | E::When | E::Not | E::Or | E::And | E::In | E::Notin
186 // Structural punctuation acting as operators. Only the
187 // *opening* delimiter counts (the pair folds to one glyph in
188 // `get_operator_id_as_str`); the former `RPAREN`/`RBRACE`/
189 // `RBRACK` arms double-counted every balanced pair, inflating
190 // n1/N1 (#695). `LTLT`/`GTGT` are the bitstring `<<`/`>>`
191 // delimiters — left unfolded and counted as the majority of
192 // languages count their shift-like glyphs. `LPAREN2`/`LBRACK2`
193 // are defensive — the runtime `public_symbol_map` collapses them
194 // to `LPAREN`/`LBRACK` before `kind_id()`, so they never fire
195 // (#768; see the Cpp impl note).
196 | E::LPAREN | E::LPAREN2 | E::LBRACE
197 | E::LBRACK | E::LBRACK2 | E::LTLT | E::GTGT
198 | E::COMMA | E::SEMI | E::COLON | E::COLONCOLON | E::DOT
199 | E::DOTDOT | E::DOTDOTDOT | E::PERCENT | E::AT
200 // Arithmetic / unary
201 | E::PLUS | E::DASH | E::STAR | E::STARSTAR | E::SLASH
202 // Comparison
203 | E::EQEQ | E::EQEQEQ | E::BANGEQ | E::BANGEQEQ
204 | E::LT | E::GT | E::LTEQ | E::GTEQ
205 // Logical
206 | E::AMPAMP | E::PIPEPIPE | E::BANG
207 // Bitwise / Erlang-band. `TILDE` is only reachable as the
208 // sigil marker in this grammar (Elixir has no unary `~`;
209 // the Erlang-style ops are `~~~` etc.) and is kept as the
210 // deliberate single per-sigil operator (#1256).
211 | E::AMP | E::PIPE | E::CARET | E::TILDE
212 | E::AMPAMPAMP | E::PIPEPIPEPIPE | E::CARETCARETCARET | E::TILDETILDETILDE
213 | E::LTLTLT | E::GTGTGT
214 // Assignment / match
215 | E::EQ
216 // Concat / list operations
217 | E::PLUSPLUS | E::DASHDASH | E::LTGT
218 | E::PLUSPLUSPLUS | E::DASHDASHDASH
219 // Pipe / capture / generator / stab arrow
220 | E::PIPEGT | E::LTPIPEGT | E::DASHGT | E::LTDASH
221 // Map pair / default arg / regex match / range step
222 | E::EQGT | E::BSLASHBSLASH | E::EQTILDE | E::SLASHSLASH
223 // Custom / less common Elixir operators
224 | E::LTTILDE | E::TILDEGT | E::LTTILDEGT | E::LTLTTILDE | E::TILDEGTGT
225 => TokenRole::Operator,
226
227 // String literals contribute exactly one operand each when
228 // they are inert. When they carry an `interpolation` child,
229 // the interpolated expressions are already walked and counted
230 // as operands in their own right; counting the wrapping
231 // literal as well would double-count the inner identifiers'
232 // contribution (issue #180).
233 //
234 // The `#{` marker itself contributes nothing. It was an
235 // operator via `HASHLBRACE` until #1314, which settled the
236 // rule across every interpolating language: an
237 // interpolation opener is spelling, not an operation, and
238 // the interpolated expression's own operators are counted
239 // either way. Elixir spells the marker with the *same*
240 // token as Ruby, so leaving it counted here would have made
241 // the two disagree on one construct — the divergence that
242 // rule exists to remove. Elixir Halstead operator counts
243 // drop for interpolated strings, charlists and sigils.
244 E::String | E::Charlist | E::Sigil => {
245 Self::string_operand_type(node, &[E::Interpolation as u16])
246 }
247
248 // Operands: identifiers and literals. Sigil names/modifiers
249 // (`~r`, the trailing `i`/`u` flags) stay as operands even
250 // for interpolated sigils — they are distinct tokens with
251 // their own text.
252 //
253 // `boolean: choice("true", "false")` and `nil: "nil"` each
254 // wrap a keyword leaf, so a literal reaches the walker
255 // twice; listing the wrapper and the leaf both inflated `N2`
256 // by one per literal while the text-keyed `n2` hid it
257 // (#1253). Only the wrappers count, mirroring Ruby's `Nil` /
258 // `Nil2` split. Unlike C#, the leaves need no parent guard:
259 // those two rules are the grammar's only producers of them,
260 // and the one other position accepting the reserved words —
261 // the right-hand side of a remote dot, `Foo.nil` — aliases
262 // them to `identifier`.
263 E::Identifier | E::Alias | E::OperatorIdentifier
264 | E::SigilName | E::SigilName2 | E::SigilModifiers
265 | E::Keyword | E::Keyword2 | E::QuotedKeyword
266 | E::Integer | E::Float | E::Char
267 | E::Atom | E::Atom2 | E::QuotedAtom
268 | E::Boolean | E::Nil
269 => TokenRole::Operand,
270
271 _ => TokenRole::Unknown,
272 }
273 }
274
275 get_operator!(Elixir);
276}