Skip to main content

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}