Skip to main content

big_code_analysis_ast/lang_helpers/
elixir.rs

1//! Elixir: keyword `Call` identity (`def`, `defmodule`, `quote`, …),
2//! and applied versus named operators.
3
4use crate::Elixir;
5use crate::node::{Ancestors, Node};
6
7/// Reads the text of the `target` field of an Elixir `Call` node.
8///
9/// Most of Elixir's control-flow constructs (`if`, `unless`, `for`,
10/// `while`, `case`, `cond`, `with`, `try`) and method-defining macros
11/// (`def`, `defp`, `defmacro`, …) parse as `Call` nodes whose `target`
12/// is an `Identifier` whose source text spells the keyword. The
13/// `Cyclomatic` and `Exit` impls already follow this pattern; this
14/// helper centralises the byte-text lookup so `Cognitive` and `Abc`
15/// can share it.
16///
17/// Returns `None` for Calls whose target is not a simple identifier
18/// (e.g. `Module.func(…)` parses as `RemoteCallWithParentheses` with
19/// the dotted name as target) or when the bytes are not valid UTF-8.
20#[inline]
21#[must_use]
22pub fn elixir_call_keyword<'a>(node: &'a Node<'a>, code: &'a [u8]) -> Option<&'a str> {
23    if node.kind_id() != Elixir::Call as u16 {
24        return None;
25    }
26    let target = node.child_by_field_name("target")?;
27    if target.kind_id() != Elixir::Identifier as u16 {
28        return None;
29    }
30    target.utf8_text(code)
31}
32
33/// Method-defining macros (`def`, `defp`, `defmacro`, `defmacrop`). The set
34/// is duplicated across checker, getter, and several metric impls
35/// because each consults it from a different trait surface; centralising
36/// the literal here keeps future additions (e.g. `defguard`) consistent.
37#[inline]
38#[must_use]
39pub fn elixir_is_method_macro(kw: &str) -> bool {
40    matches!(kw, "def" | "defp" | "defmacro" | "defmacrop")
41}
42
43/// Class-defining macro (`defmodule`). Paired with [`elixir_is_method_macro`]
44/// where a caller needs both ("any space-opening declaration").
45#[inline]
46#[must_use]
47pub fn elixir_is_class_macro(kw: &str) -> bool {
48    kw == "defmodule"
49}
50
51/// Returns true when `node` is lexically nested inside the `do_block` of a
52/// `quote do … end` Call (Elixir's metaprogramming template). A `def` /
53/// `defp` / `defmacro` / `defmacrop` inside `quote` does not define a
54/// method of any enclosing module — the syntax tree is a code template
55/// emitted later, when the surrounding macro is invoked. Treating those
56/// quoted Calls as methods inflates `Wmc` and disagrees with `Npm`'s
57/// direct-children classification (#310).
58///
59/// Walks the ancestor chain looking for a `quote` Call ancestor. Stops at
60/// the first match (true) or at the root (false). Each step is a single
61/// `child_by_field_name("target")` + identifier byte compare, so the cost
62/// is O(steps) when `ancestors` is known — with `Ancestors::unknown` each
63/// step additionally pays `Node::parent`'s O(depth) (#1084).
64#[inline]
65#[must_use]
66pub fn elixir_is_inside_quote_block<'a>(
67    node: &Node<'a>,
68    code: &[u8],
69    ancestors: Ancestors<'a, '_>,
70) -> bool {
71    ancestors
72        .iter(node)
73        .any(|(n, _)| elixir_call_keyword(&n, code) == Some("quote"))
74}
75
76/// Iterates the direct-child `Call` nodes inside the `do_block` of an
77/// Elixir Call (typically a `defmodule`). Used by `Npm` / `Npa` to scan
78/// a module body for method-defining macros / `defstruct` without
79/// descending into nested modules. Yields no items when the Call has
80/// no `do_block`.
81#[inline]
82pub fn elixir_do_block_call_children<'a>(
83    node: &'a Node<'a>,
84) -> impl Iterator<Item = Node<'a>> + 'a {
85    node.children()
86        .filter(|child| child.kind_id() == Elixir::DoBlock as u16)
87        .flat_map(|do_block| do_block.children())
88        .filter(|stmt| stmt.kind_id() == Elixir::Call as u16)
89}
90
91/// The `binary_operator` that applies `op`, an operator token, or `None`
92/// when the token is not applied there.
93///
94/// An operator token also *names* the operator: `&==/2`, `&and/2` and
95/// `Kernel.||(a, b)` hold it inside an `operator_identifier`, and the
96/// single-token `atom` / `keyword` spell it too. None of those is a
97/// `binary_operator` with operands to compare or chain, and under error
98/// recovery the parent can be an `ERROR` with no operands, so every
99/// decision metric keys on this parent rather than on the token (#1531,
100/// #1534). A remote call that names the operator still *applies* it,
101/// though; see [`elixir_remote_call_applies`].
102///
103/// Matched by rule name rather than by `kind_id`: the grammar aliases
104/// `binary_operator` to three ids (`Elixir::BinaryOperator` through
105/// `BinaryOperator3`), and a name comparison stays correct when a bump
106/// adds a fourth (grammar-dispatch §1).
107#[must_use]
108pub fn elixir_applying_operator<'a>(
109    op: &Node<'a>,
110    ancestors: Ancestors<'a, '_>,
111) -> Option<Node<'a>> {
112    ancestors
113        .parent(op)
114        .filter(|parent| parent.kind() == "binary_operator")
115}
116
117/// Whether `op`, an operator token, is the name of a remote call on
118/// `Kernel` with arguments: `Kernel.||(a, b)`, or `x |> Kernel.||(y)`,
119/// which the pipe rewrites to it. Such a call expands the `Kernel` macro
120/// exactly as `a || b` does, so it short-circuits and decides as the
121/// operator does, where a capture (`&Kernel.||/2`, a call with no
122/// arguments) only names it. It has no `binary_operator`, so it carries no
123/// `left` / `right` operands either.
124///
125/// The receiver must spell `Kernel` (or `Elixir.Kernel`): a user module
126/// may define `||/2` as an ordinary function, and `Foo.||(a, b)` neither
127/// short-circuits nor decides.
128///
129/// Matched by rule name: `dot` and `arguments` are aliased to several
130/// ids each (grammar-dispatch §1).
131#[must_use]
132pub fn elixir_remote_call_applies<'a>(
133    op: &Node<'a>,
134    code: &[u8],
135    ancestors: Ancestors<'a, '_>,
136) -> bool {
137    let mut up = ancestors.iter(op).map(|(node, _)| node);
138    up.next()
139        .is_some_and(|name| name.kind() == "operator_identifier")
140        && up.next().is_some_and(|dot| {
141            dot.kind() == "dot"
142                && dot.child_by_field_name("left").is_some_and(|receiver| {
143                    matches!(
144                        code.get(receiver.start_byte()..receiver.end_byte()),
145                        Some(b"Kernel" | b"Elixir.Kernel")
146                    )
147                })
148        })
149        && up.next().is_some_and(|call| {
150            call.kind() == "call" && call.children().any(|child| child.kind() == "arguments")
151        })
152}