Skip to main content

big_code_analysis_ast/lang_helpers/
elixir.rs

1//! Elixir: keyword `Call` identity (`def`, `defmodule`, `quote`, …).
2
3use crate::Elixir;
4use crate::node::{Ancestors, Node};
5
6/// Reads the text of the `target` field of an Elixir `Call` node.
7///
8/// Most of Elixir's control-flow constructs (`if`, `unless`, `for`,
9/// `while`, `case`, `cond`, `with`, `try`) and method-defining macros
10/// (`def`, `defp`, `defmacro`, …) parse as `Call` nodes whose `target`
11/// is an `Identifier` whose source text spells the keyword. The
12/// `Cyclomatic` and `Exit` impls already follow this pattern; this
13/// helper centralises the byte-text lookup so `Cognitive` and `Abc`
14/// can share it.
15///
16/// Returns `None` for Calls whose target is not a simple identifier
17/// (e.g. `Module.func(…)` parses as `RemoteCallWithParentheses` with
18/// the dotted name as target) or when the bytes are not valid UTF-8.
19#[inline]
20#[must_use]
21pub fn elixir_call_keyword<'a>(node: &'a Node<'a>, code: &'a [u8]) -> Option<&'a str> {
22    if node.kind_id() != Elixir::Call as u16 {
23        return None;
24    }
25    let target = node.child_by_field_name("target")?;
26    if target.kind_id() != Elixir::Identifier as u16 {
27        return None;
28    }
29    target.utf8_text(code)
30}
31
32/// Method-defining macros (`def`, `defp`, `defmacro`, `defmacrop`). The set
33/// is duplicated across checker, getter, and several metric impls
34/// because each consults it from a different trait surface; centralising
35/// the literal here keeps future additions (e.g. `defguard`) consistent.
36#[inline]
37#[must_use]
38pub fn elixir_is_method_macro(kw: &str) -> bool {
39    matches!(kw, "def" | "defp" | "defmacro" | "defmacrop")
40}
41
42/// Class-defining macro (`defmodule`). Paired with [`elixir_is_method_macro`]
43/// where a caller needs both ("any space-opening declaration").
44#[inline]
45#[must_use]
46pub fn elixir_is_class_macro(kw: &str) -> bool {
47    kw == "defmodule"
48}
49
50/// Returns true when `node` is lexically nested inside the `do_block` of a
51/// `quote do … end` Call (Elixir's metaprogramming template). A `def` /
52/// `defp` / `defmacro` / `defmacrop` inside `quote` does not define a
53/// method of any enclosing module — the syntax tree is a code template
54/// emitted later, when the surrounding macro is invoked. Treating those
55/// quoted Calls as methods inflates `Wmc` and disagrees with `Npm`'s
56/// direct-children classification (#310).
57///
58/// Walks the ancestor chain looking for a `quote` Call ancestor. Stops at
59/// the first match (true) or at the root (false). Each step is a single
60/// `child_by_field_name("target")` + identifier byte compare, so the cost
61/// is O(steps) when `ancestors` is known — with `Ancestors::unknown` each
62/// step additionally pays `Node::parent`'s O(depth) (#1084).
63#[inline]
64#[must_use]
65pub fn elixir_is_inside_quote_block<'a>(
66    node: &Node<'a>,
67    code: &[u8],
68    ancestors: Ancestors<'a, '_>,
69) -> bool {
70    ancestors
71        .iter(node)
72        .any(|(n, _)| elixir_call_keyword(&n, code) == Some("quote"))
73}
74
75/// Iterates the direct-child `Call` nodes inside the `do_block` of an
76/// Elixir Call (typically a `defmodule`). Used by `Npm` / `Npa` to scan
77/// a module body for method-defining macros / `defstruct` without
78/// descending into nested modules. Yields no items when the Call has
79/// no `do_block`.
80#[inline]
81pub fn elixir_do_block_call_children<'a>(
82    node: &'a Node<'a>,
83) -> impl Iterator<Item = Node<'a>> + 'a {
84    node.children()
85        .filter(|child| child.kind_id() == Elixir::DoBlock as u16)
86        .flat_map(|do_block| do_block.children())
87        .filter(|stmt| stmt.kind_id() == Elixir::Call as u16)
88}