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 match node.kind_id().into() {
146 // Sigil delimiter punctuation. The delimiter choice is
147 // spelling, not semantics — `~r/abc/` is not two divisions
148 // and `~w(a b)` opens no call — so counting these tokens
149 // made `n1`/`N1` vary with the author's delimiter choice
150 // (#1256). The `Sigil` node itself is the operand (below)
151 // and `~` the single per-sigil operator; the delimiters are
152 // suppressed exactly when their parent is the sigil — the
153 // compound-leaf guard of grammar-dispatch §5, mirroring
154 // Bash's `SimpleExpansion` parent check. Verified via
155 // `bca dump` at the pinned grammar: `/ ( { [ < > |` are the
156 // only delimiter kinds surfacing as `Sigil` children that
157 // the operator arm below also matches; the quote delimiters
158 // (`"`, `'`, `"""`, `'''`) and the closers `) } ]` have no
159 // operator arm and need no guard. Same-kind tokens outside
160 // a sigil (division `a / b`, comparison `<`, pipe `|`) fall
161 // through to the operator arm and still count.
162 E::SLASH | E::LPAREN | E::LBRACE | E::LBRACK | E::LT | E::GT | E::PIPE
163 if ancestors.parent_has_kind(node, E::Sigil as u16) =>
164 {
165 TokenRole::Unknown
166 }
167 // Reserved-word keywords that have dedicated token kinds in
168 // the grammar — block delimiters, exception clauses, the
169 // `fn` keyword, and word-form logical / membership operators.
170 // (Macro-shaped keywords like `def`/`defp`/`if`/`case`/`cond`
171 // are NOT here: they surface as `Identifier` tokens in a
172 // `Call`'s `target` field and are counted as operands below.)
173 E::Do | E::End | E::End2 | E::Else | E::After | E::Catch | E::Rescue | E::Fn
174 | E::When | E::Not | E::Or | E::And | E::In | E::Notin
175 // Structural punctuation acting as operators. Only the
176 // *opening* delimiter counts (the pair folds to one glyph in
177 // `get_operator_id_as_str`); the former `RPAREN`/`RBRACE`/
178 // `RBRACK` arms double-counted every balanced pair, inflating
179 // n1/N1 (#695). `LTLT`/`GTGT` are the bitstring `<<`/`>>`
180 // delimiters — left unfolded and counted as the majority of
181 // languages count their shift-like glyphs. `LPAREN2`/`LBRACK2`
182 // are defensive — the runtime `public_symbol_map` collapses them
183 // to `LPAREN`/`LBRACK` before `kind_id()`, so they never fire
184 // (#768; see the Cpp impl note).
185 | E::LPAREN | E::LPAREN2 | E::LBRACE
186 | E::LBRACK | E::LBRACK2 | E::LTLT | E::GTGT
187 | E::COMMA | E::SEMI | E::COLON | E::COLONCOLON | E::DOT
188 | E::DOTDOT | E::DOTDOTDOT | E::PERCENT | E::AT
189 // Arithmetic / unary
190 | E::PLUS | E::DASH | E::STAR | E::STARSTAR | E::SLASH
191 // Comparison
192 | E::EQEQ | E::EQEQEQ | E::BANGEQ | E::BANGEQEQ
193 | E::LT | E::GT | E::LTEQ | E::GTEQ
194 // Logical
195 | E::AMPAMP | E::PIPEPIPE | E::BANG
196 // Bitwise / Erlang-band. `TILDE` is only reachable as the
197 // sigil marker in this grammar (Elixir has no unary `~`;
198 // the Erlang-style ops are `~~~` etc.) and is kept as the
199 // deliberate single per-sigil operator (#1256).
200 | E::AMP | E::PIPE | E::CARET | E::TILDE
201 | E::AMPAMPAMP | E::PIPEPIPEPIPE | E::CARETCARETCARET | E::TILDETILDETILDE
202 | E::LTLTLT | E::GTGTGT
203 // Assignment / match
204 | E::EQ
205 // Concat / list operations
206 | E::PLUSPLUS | E::DASHDASH | E::LTGT
207 | E::PLUSPLUSPLUS | E::DASHDASHDASH
208 // Pipe / capture / generator / stab arrow
209 | E::PIPEGT | E::LTPIPEGT | E::DASHGT | E::LTDASH
210 // Map pair / default arg / regex match / range step
211 | E::EQGT | E::BSLASHBSLASH | E::EQTILDE | E::SLASHSLASH
212 // Custom / less common Elixir operators
213 | E::LTTILDE | E::TILDEGT | E::LTTILDEGT | E::LTLTTILDE | E::TILDEGTGT
214 => TokenRole::Operator,
215
216 // String literals contribute exactly one operand each when
217 // they are inert. When they carry an `interpolation` child,
218 // the interpolated expressions are already walked and counted
219 // as operands in their own right; counting the wrapping
220 // literal as well would double-count the inner identifiers'
221 // contribution (issue #180).
222 //
223 // The `#{` marker itself contributes nothing. It was an
224 // operator via `HASHLBRACE` until #1314, which settled the
225 // rule across every interpolating language: an
226 // interpolation opener is spelling, not an operation, and
227 // the interpolated expression's own operators are counted
228 // either way. Elixir spells the marker with the *same*
229 // token as Ruby, so leaving it counted here would have made
230 // the two disagree on one construct — the divergence that
231 // rule exists to remove. Elixir Halstead operator counts
232 // drop for interpolated strings, charlists and sigils.
233 E::String | E::Charlist | E::Sigil => {
234 Self::string_operand_type(node, &[E::Interpolation as u16])
235 }
236
237 // Operands: identifiers and literals. Sigil names/modifiers
238 // (`~r`, the trailing `i`/`u` flags) stay as operands even
239 // for interpolated sigils — they are distinct tokens with
240 // their own text.
241 //
242 // `boolean: choice("true", "false")` and `nil: "nil"` each
243 // wrap a keyword leaf, so a literal reaches the walker
244 // twice; listing the wrapper and the leaf both inflated `N2`
245 // by one per literal while the text-keyed `n2` hid it
246 // (#1253). Only the wrappers count, mirroring Ruby's `Nil` /
247 // `Nil2` split. Unlike C#, the leaves need no parent guard:
248 // those two rules are the grammar's only producers of them,
249 // and the one other position accepting the reserved words —
250 // the right-hand side of a remote dot, `Foo.nil` — aliases
251 // them to `identifier`.
252 E::Identifier | E::Alias | E::OperatorIdentifier
253 | E::SigilName | E::SigilName2 | E::SigilModifiers
254 | E::Keyword | E::Keyword2 | E::QuotedKeyword
255 | E::Integer | E::Float | E::Char
256 | E::Atom | E::Atom2 | E::QuotedAtom
257 | E::Boolean | E::Nil
258 => TokenRole::Operand,
259
260 _ => TokenRole::Unknown,
261 }
262 }
263
264 get_operator!(Elixir);
265}