use crate::Elixir;
use crate::node::{Ancestors, Node};
#[inline]
#[must_use]
pub fn elixir_call_keyword<'a>(node: &'a Node<'a>, code: &'a [u8]) -> Option<&'a str> {
if node.kind_id() != Elixir::Call as u16 {
return None;
}
let target = node.child_by_field_name("target")?;
if target.kind_id() != Elixir::Identifier as u16 {
return None;
}
target.utf8_text(code)
}
#[inline]
#[must_use]
pub fn elixir_is_method_macro(kw: &str) -> bool {
matches!(kw, "def" | "defp" | "defmacro" | "defmacrop")
}
#[inline]
#[must_use]
pub fn elixir_is_class_macro(kw: &str) -> bool {
kw == "defmodule"
}
#[inline]
#[must_use]
pub fn elixir_is_inside_quote_block<'a>(
node: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> bool {
ancestors
.iter(node)
.any(|(n, _)| elixir_call_keyword(&n, code) == Some("quote"))
}
#[inline]
pub fn elixir_do_block_call_children<'a>(
node: &'a Node<'a>,
) -> impl Iterator<Item = Node<'a>> + 'a {
node.children()
.filter(|child| child.kind_id() == Elixir::DoBlock as u16)
.flat_map(|do_block| do_block.children())
.filter(|stmt| stmt.kind_id() == Elixir::Call as u16)
}
#[must_use]
pub fn elixir_applying_operator<'a>(
op: &Node<'a>,
ancestors: Ancestors<'a, '_>,
) -> Option<Node<'a>> {
ancestors
.parent(op)
.filter(|parent| parent.kind() == "binary_operator")
}
#[must_use]
pub fn elixir_kernel_short_circuit_name<'a>(call: &Node<'a>, code: &[u8]) -> Option<Node<'a>> {
kernel_short_circuit_parts(call, code).map(|(name, _)| name)
}
fn kernel_short_circuit_parts<'a>(call: &Node<'a>, code: &[u8]) -> Option<(Node<'a>, Node<'a>)> {
if call.kind() != "call" {
return None;
}
let dot = call.child_by_field_name("target").filter(|target| {
target.kind() == "dot"
&& target.child_by_field_name("left").is_some_and(|receiver| {
matches!(
code.get(receiver.start_byte()..receiver.end_byte()),
Some(b"Kernel" | b"Elixir.Kernel")
)
})
})?;
let name = dot.child_by_field_name("right")?;
let name = if name.kind() == "operator_identifier" {
name.child(0)?
} else {
name
};
elixir_short_circuit_name(&name, code)
.and_then(|_| call.children().find(|child| child.kind() == "arguments"))
.map(|arguments| (name, arguments))
}
#[must_use]
pub fn elixir_short_circuit_kind(kind_id: u16) -> Option<Elixir> {
match kind_id.into() {
Elixir::AMPAMP | Elixir::And => Some(Elixir::AMPAMP),
Elixir::PIPEPIPE | Elixir::Or => Some(Elixir::PIPEPIPE),
_ => None,
}
}
#[must_use]
pub fn elixir_short_circuit_name(name: &Node, code: &[u8]) -> Option<Elixir> {
if name.kind_id() != Elixir::Identifier as u16 {
return elixir_short_circuit_kind(name.kind_id());
}
match code.get(name.start_byte()..name.end_byte())? {
b"and" => Some(Elixir::AMPAMP),
b"or" => Some(Elixir::PIPEPIPE),
_ => None,
}
}
pub struct ElixirRemoteCall<'tree, 'chain> {
pub call: Node<'tree>,
pub arguments: Node<'tree>,
pub ancestors: Ancestors<'tree, 'chain>,
}
#[must_use]
pub fn elixir_applying_remote_call<'a, 'c>(
op: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, 'c>,
) -> Option<ElixirRemoteCall<'a, 'c>> {
let mut up = ancestors.iter(op);
let (mut dot, _) = up.next()?;
if dot.kind() == "operator_identifier" {
dot = up.next()?.0;
}
if dot.kind() != "dot" {
return None;
}
let (call, call_ancestors) = up.next()?;
kernel_short_circuit_parts(&call, code)
.filter(|(name, _)| name.id() == op.id())
.map(|(_, arguments)| ElixirRemoteCall {
call,
arguments,
ancestors: call_ancestors,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ElixirCode;
use crate::node::Tree;
use crate::test_support::for_each_node_with_chain;
#[cfg(feature = "elixir")]
#[test]
fn short_circuit_name_requires_the_identifiers_source_span() {
let source = b"Kernel.and(a, b)";
let tree = Tree::new::<ElixirCode>(source);
let name = tree
.get_root()
.preorder()
.find(|node| {
node.kind_id() == Elixir::Identifier as u16 && node.utf8_text(source) == Some("and")
})
.expect("fixture contains the operator name");
assert_eq!(
elixir_short_circuit_name(&name, source),
Some(Elixir::AMPAMP)
);
assert_eq!(
elixir_short_circuit_name(&name, &source[..name.end_byte() - 1]),
None
);
}
#[cfg(feature = "elixir")]
#[test]
fn remote_call_lookup_rejects_a_root_and_keeps_its_argument_list() {
let source = b"Kernel.and(a, b)\nKernel.||(c, d)\n";
let mut recognized = Vec::new();
for_each_node_with_chain::<ElixirCode>(source, |node, chain| {
let known = elixir_applying_remote_call(node, source, Ancestors::known(chain));
let unknown = elixir_applying_remote_call(node, source, Ancestors::unknown());
assert_eq!(
known.as_ref().map(|r| (r.call.id(), r.arguments.id())),
unknown.map(|r| (r.call.id(), r.arguments.id()))
);
if chain.is_empty() {
assert!(known.is_none());
}
if let Some(remote) = known {
assert_eq!(remote.arguments.kind(), "arguments");
assert_eq!(
remote.ancestors.parent(&remote.call).map(|p| p.kind()),
Some("source")
);
recognized.push((
node.utf8_text(source)
.expect("operator is UTF-8")
.to_owned(),
remote
.arguments
.utf8_text(source)
.expect("arguments are UTF-8")
.to_owned(),
));
}
});
assert_eq!(
recognized,
[
("and".to_owned(), "(a, b)".to_owned()),
("||".to_owned(), "(c, d)".to_owned())
]
);
}
}