1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
//! `Getter` implementation for Elixir.
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
use super::*;
// Extracts the human-readable head name from the first non-`target`
// child of an Elixir `def` / `defp` / `defmacro` / `defmacrop` /
// `defmodule` Call. Handles three shapes:
// - `Arguments` wrapper: descend one level and recurse.
// - `Identifier` / `Alias` leaf: return its source text.
// - inner `Call` (e.g. `def foo(x, y)`): return the target identifier
// text of that inner Call.
// Returns `None` when the child does not match any of these shapes,
// allowing the caller to keep scanning siblings (notably the
// `do_block`, which is unconditionally present and never carries the
// name).
fn elixir_extract_head_name<'a>(node: &Node, code: &'a [u8]) -> Option<&'a str> {
use Elixir as E;
let text = |n: &Node| node_text(code, n);
match node.kind_id().into() {
E::Identifier | E::Alias => text(node),
E::Call => text(&node.child_by_field_name("target")?),
E::Arguments
| E::Arguments2
| E::Arguments3
| E::Arguments4
| E::Arguments5
| E::CallArgumentsWithTrailingSeparator => node
.children()
.find_map(|child| elixir_extract_head_name(&child, code)),
_ => None,
}
}
impl Getter for ElixirCode {
fn get_space_kind(node: &Node) -> SpaceKind {
use Elixir as E;
match node.kind_id().into() {
E::AnonymousFunction => SpaceKind::Function,
E::Source => SpaceKind::Unit,
_ => SpaceKind::Unknown,
}
}
// Source-aware classifier (#275). Elixir's `defmodule` /
// `def` / `defp` / `defmacro` / `defmacrop` are not distinct
// grammar productions — they all parse as `Call` nodes whose
// `target` Identifier text spells the keyword. The walker promotes
// these Calls to func spaces via `Checker::is_func_space_with_code`;
// this method labels the promoted space with the right `SpaceKind`
// so `Wmc` / `Npm` / `Npa` see a Class for `defmodule` and a
// Function for the method-defining macros.
fn get_space_kind_with_code(node: &Node, code: &[u8]) -> SpaceKind {
use crate::metrics::cognitive::{
elixir_call_keyword, elixir_is_class_macro, elixir_is_inside_quote_block,
elixir_is_method_macro,
};
let kind = Self::get_space_kind(node);
if kind != SpaceKind::Unknown {
return kind;
}
match elixir_call_keyword(node, code) {
Some(kw) if elixir_is_class_macro(kw) => SpaceKind::Class,
// Method-defining macros nested inside a `quote do … end`
// template are not real method declarations (#310).
Some(kw) if elixir_is_method_macro(kw) && !elixir_is_inside_quote_block(node, code) => {
SpaceKind::Function
}
_ => SpaceKind::Unknown,
}
}
// Source-aware name extraction for the macro-shaped declarations.
// `def foo(x, y) do … end` parses (with the tree-sitter-elixir
// grammar shipped here) as
// `Call { target: Identifier "def", Arguments { Call { target:
// Identifier "foo", Arguments { … } } }, DoBlock { … } }`
// i.e. the head Call is wrapped in an `Arguments` node, not a
// direct child. `defmodule Foo.Bar do … end` parses similarly with
// the `Alias` inside `Arguments`. We descend through one
// `Arguments` layer when present, then either:
// - return the `Identifier` / `Alias` text directly
// (`defmodule Foo`, `def foo` for an arity-zero head with no
// parentheses), or
// - return the inner head Call's `target` text
// (`def foo(x, y)`).
// Falls back to the trait default behaviour (`<anonymous>` for
// nodes without a `name` field) when the Call is not one we
// recognise.
fn get_func_space_name<'a>(node: &Node, code: &'a [u8]) -> Option<&'a str> {
use Elixir as E;
use crate::metrics::cognitive::{
elixir_call_keyword, elixir_is_class_macro, elixir_is_inside_quote_block,
elixir_is_method_macro,
};
// The Class kind always names its head; for method macros we
// additionally require the Call NOT to be inside a `quote`
// template, matching the func-space promotion rule (#310).
if node.kind_id() == E::Call as u16
&& let Some(kw) = elixir_call_keyword(node, code)
&& (elixir_is_class_macro(kw)
|| (elixir_is_method_macro(kw) && !elixir_is_inside_quote_block(node, code)))
{
let target_id = node.child_by_field_name("target").map(|t| t.id());
if let Some(name) = node
.children()
.filter(|child| Some(child.id()) != target_id)
.find_map(|child| elixir_extract_head_name(&child, code))
{
return Some(name);
}
}
if let Some(name) = node.child_by_field_name("name") {
return node_text(code, &name);
}
Some("<anonymous>")
}
fn get_op_type(node: &Node) -> HalsteadType {
use Elixir as E;
match node.kind_id().into() {
// Reserved-word keywords that have dedicated token kinds in
// the grammar — block delimiters, exception clauses, the
// `fn` keyword, and word-form logical / membership operators.
// (Macro-shaped keywords like `def`/`defp`/`if`/`case`/`cond`
// are NOT here: they surface as `Identifier` tokens in a
// `Call`'s `target` field and are counted as operands below.)
E::Do | E::End | E::End2 | E::Else | E::After | E::Catch | E::Rescue | E::Fn
| E::When | E::Not | E::Or | E::And | E::In | E::Notin
// Structural punctuation acting as operators. Only the
// *opening* delimiter counts (the pair folds to one glyph in
// `get_operator_id_as_str`); the former `RPAREN`/`RBRACE`/
// `RBRACK` arms double-counted every balanced pair, inflating
// n1/N1 (#695). `LTLT`/`GTGT` are the bitstring `<<`/`>>`
// delimiters — left unfolded and counted as the majority of
// languages count their shift-like glyphs. `LPAREN2`/`LBRACK2`
// are defensive — the runtime `public_symbol_map` collapses them
// to `LPAREN`/`LBRACK` before `kind_id()`, so they never fire
// (#768; see the Cpp impl note).
| E::LPAREN | E::LPAREN2 | E::LBRACE
| E::LBRACK | E::LBRACK2 | E::LTLT | E::GTGT
| E::COMMA | E::SEMI | E::COLON | E::COLONCOLON | E::DOT
| E::DOTDOT | E::DOTDOTDOT | E::PERCENT | E::HASHLBRACE | E::AT
// Arithmetic / unary
| E::PLUS | E::DASH | E::STAR | E::STARSTAR | E::SLASH
// Comparison
| E::EQEQ | E::EQEQEQ | E::BANGEQ | E::BANGEQEQ
| E::LT | E::GT | E::LTEQ | E::GTEQ
// Logical
| E::AMPAMP | E::PIPEPIPE | E::BANG
// Bitwise / Erlang-band
| E::AMP | E::PIPE | E::CARET | E::TILDE
| E::AMPAMPAMP | E::PIPEPIPEPIPE | E::CARETCARETCARET | E::TILDETILDETILDE
| E::LTLTLT | E::GTGTGT
// Assignment / match
| E::EQ
// Concat / list operations
| E::PLUSPLUS | E::DASHDASH | E::LTGT
| E::PLUSPLUSPLUS | E::DASHDASHDASH
// Pipe / capture / generator / stab arrow
| E::PIPEGT | E::LTPIPEGT | E::DASHGT | E::LTDASH
// Map pair / default arg / regex match / range step
| E::EQGT | E::BSLASHBSLASH | E::EQTILDE | E::SLASHSLASH
// Custom / less common Elixir operators
| E::LTTILDE | E::TILDEGT | E::LTTILDEGT | E::LTLTTILDE | E::TILDEGTGT
=> HalsteadType::Operator,
// String literals contribute exactly one operand each when
// they are inert. When they carry an `interpolation` child,
// the interpolated expressions are already walked and counted
// as operands in their own right; counting the wrapping
// literal as well would double-count the inner identifiers'
// contribution (issue #180). The interpolation markers
// `#{` / `}` are classified as operators via `HASHLBRACE` /
// `RBRACE`, so an interpolated literal still adds operator
// weight without inflating `N2`.
E::String | E::Charlist | E::Sigil => {
Self::string_operand_type(node, &[E::Interpolation as u16])
}
// Operands: identifiers and literals. Sigil names/modifiers
// (`~r`, the trailing `i`/`u` flags) stay as operands even
// for interpolated sigils — they are distinct tokens with
// their own text.
E::Identifier | E::Alias | E::OperatorIdentifier
| E::SigilName | E::SigilName2 | E::SigilModifiers
| E::Keyword | E::Keyword2 | E::QuotedKeyword
| E::Integer | E::Float | E::Char
| E::Atom | E::Atom2 | E::QuotedAtom
| E::Boolean | E::True | E::False
| E::Nil | E::Nil2
=> HalsteadType::Operand,
_ => HalsteadType::Unknown,
}
}
get_operator!(Elixir);
}