use crate::ast::{Arg, AstNode, AstToken, CallExpr, Expr, HasArgList, Ident, KeywordArg};
use crate::syntax::{SyntaxKind, SyntaxNode};
pub fn call_expr(node: &SyntaxNode) -> Option<CallExpr> {
let call = CallExpr::cast(node.clone())?;
(!in_signature_position(node)).then_some(call)
}
pub fn call_named(node: &SyntaxNode, name: &str) -> Option<CallExpr> {
let call = call_expr(node)?;
(call.callee_ident()?.text() == name).then_some(call)
}
pub fn plain_call(node: &SyntaxNode, name: &str, arity: usize) -> Option<(CallExpr, Vec<Expr>)> {
let call = call_named(node, name)?;
let shape = CallShape::of(&call);
if !shape.is_plain(arity) {
return None;
}
Some((call, shape.positional))
}
pub struct CallShape {
pub positional: Vec<Expr>,
pub keywords: Vec<KeywordMatch>,
pub positional_open: bool,
pub keyword_open: bool,
pub do_block: bool,
}
pub struct KeywordMatch {
pub name: Ident,
pub value: Option<Expr>,
}
impl CallShape {
pub fn of(call: &CallExpr) -> Self {
let mut shape = CallShape {
positional: Vec::new(),
keywords: Vec::new(),
positional_open: false,
keyword_open: false,
do_block: has_do_block(call),
};
let Some(args) = call.arg_list() else {
shape.push_positional(lone_generator(call));
return shape;
};
for child in args.syntax().children() {
match child.kind() {
SyntaxKind::ARG => {
shape.push_positional(Arg::cast(child.clone()).and_then(|arg| arg.expr()));
}
SyntaxKind::GENERATOR => shape.push_positional(Expr::cast(child.clone())),
SyntaxKind::KEYWORD_ARG => shape.push_keyword(&child),
SyntaxKind::PARAMETERS => {
for param in child.children() {
match param.kind() {
SyntaxKind::KEYWORD_ARG => shape.push_keyword(¶m),
SyntaxKind::ARG => shape.push_shorthand(¶m),
_ => shape.keyword_open = true,
}
}
}
_ => shape.positional_open = true,
}
}
shape
}
pub fn is_plain(&self, arity: usize) -> bool {
self.positional.len() == arity
&& self.keywords.is_empty()
&& !self.positional_open
&& !self.keyword_open
&& !self.do_block
}
fn push_positional(&mut self, expr: Option<Expr>) {
match expr {
None | Some(Expr::SplatExpr(_)) => self.positional_open = true,
Some(expr) => self.positional.push(expr),
}
}
fn push_keyword(&mut self, node: &SyntaxNode) {
let name = KeywordArg::cast(node.clone())
.and_then(|kw| kw.name())
.and_then(|name| name.ident());
match name {
None => self.keyword_open = true,
Some(name) => {
let value = KeywordArg::cast(node.clone()).and_then(|kw| kw.value());
self.keywords.push(KeywordMatch { name, value });
}
}
}
fn push_shorthand(&mut self, node: &SyntaxNode) {
let name = Arg::cast(node.clone())
.and_then(|arg| arg.expr())
.and_then(|expr| expr.name_ident());
match name {
None => self.keyword_open = true,
Some(name) => self.keywords.push(KeywordMatch { name, value: None }),
}
}
}
fn lone_generator(call: &CallExpr) -> Option<Expr> {
call.syntax()
.children()
.skip(1)
.find(|child| child.kind() == SyntaxKind::GENERATOR)
.and_then(Expr::cast)
}
pub fn in_signature_position(node: &SyntaxNode) -> bool {
let mut current = node.clone();
loop {
let Some(parent) = current.parent() else {
return false;
};
match parent.kind() {
SyntaxKind::SIGNATURE => return true,
SyntaxKind::TYPE_ANNOTATION | SyntaxKind::WHERE_EXPR => current = parent,
SyntaxKind::ASSIGNMENT_EXPR => {
return parent
.children()
.next()
.is_some_and(|first| first == current);
}
_ => return false,
}
}
}
pub fn has_do_block(call: &CallExpr) -> bool {
call.syntax()
.parent()
.is_some_and(|parent| parent.kind() == SyntaxKind::DO_EXPR)
}
pub fn is_name(expr: &Expr, name: &str) -> bool {
expr.name_ident().is_some_and(|ident| ident.text() == name)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::BinaryExpr;
use crate::parser::parse;
fn call_node(src: &str) -> SyntaxNode {
parse(src)
.cst
.descendants()
.find(|n| n.kind() == SyntaxKind::CALL_EXPR)
.expect("a call")
}
fn call(src: &str) -> CallExpr {
CallExpr::cast(call_node(src)).expect("a call")
}
fn shape(src: &str) -> CallShape {
CallShape::of(&call(src))
}
fn texts(exprs: &[Expr]) -> Vec<String> {
exprs
.iter()
.map(|e| e.syntax().text().to_string())
.collect()
}
#[test]
fn call_named_matches_a_bare_callee() {
assert!(call_named(&call_node("length(x)\n"), "length").is_some());
assert!(call_named(&call_node("length(x)\n"), "size").is_none());
assert!(call_named(&call_node("Base.length(x)\n"), "length").is_none());
assert!(call_named(&call_node("length(x)\n").parent().unwrap(), "length").is_none());
}
#[test]
fn call_named_rejects_definition_signatures() {
assert!(call_named(&call_node("length(x) = 1\n"), "length").is_none());
assert!(call_named(&call_node("function length(x)\n 1\nend\n"), "length").is_none());
assert!(call_named(&call_node("length(x::T) where {T} = 1\n"), "length").is_none());
assert!(
call_named(
&call_node("function length(x)::Int\n 1\nend\n"),
"length"
)
.is_none()
);
assert!(call_named(&call_node("f() = length(x)\n"), "f").is_none());
}
#[test]
fn plain_call_wants_exactly_that_many_positional_arguments() {
let (call, args) = plain_call(&call_node("length(x)\n"), "length", 1).expect("a match");
assert_eq!(call.syntax().text().to_string(), "length(x)");
assert_eq!(texts(&args), ["x"]);
assert!(plain_call(&call_node("length(x)\n"), "length", 0).is_none());
assert!(plain_call(&call_node("length(x)\n"), "length", 2).is_none());
let (_, args) = plain_call(&call_node("occursin(p, s)\n"), "occursin", 2).expect("a match");
assert_eq!(texts(&args), ["p", "s"]);
}
#[test]
fn plain_call_rejects_anything_but_positional_arguments() {
assert!(plain_call(&call_node("f(x, by = g)\n"), "f", 1).is_none());
assert!(plain_call(&call_node("f(x; by = g)\n"), "f", 1).is_none());
assert!(plain_call(&call_node("f(x; by)\n"), "f", 1).is_none());
assert!(plain_call(&call_node("f(xs...)\n"), "f", 1).is_none());
assert!(plain_call(&call_node("f(x; kw...)\n"), "f", 1).is_none());
assert!(plain_call(&call_node("f(x) do y\n y\nend\n"), "f", 1).is_none());
}
#[test]
fn call_shape_splits_positional_and_keyword_arguments() {
let shape = shape("f(a, b, c = 1; d = 2, e)\n");
assert_eq!(shape.positional.len(), 2);
assert_eq!(texts(&shape.positional), ["a", "b"]);
let names: Vec<&str> = shape.keywords.iter().map(|k| k.name.text()).collect();
assert_eq!(names, ["c", "d", "e"]);
assert!(shape.keywords[2].value.is_none());
assert_eq!(
shape.keywords[0]
.value
.as_ref()
.map(|v| v.syntax().text().to_string()),
Some("1".to_string())
);
assert!(!shape.positional_open);
assert!(!shape.keyword_open);
assert!(!shape.do_block);
}
#[test]
fn call_shape_flags_splats_as_open() {
let splat = shape("f(a, xs...)\n");
assert!(splat.positional_open);
assert!(!splat.keyword_open);
assert_eq!(splat.positional.len(), 1);
let kw_splat = shape("f(a; kw...)\n");
assert!(!kw_splat.positional_open);
assert!(kw_splat.keyword_open);
assert!(kw_splat.keywords.is_empty());
}
#[test]
fn call_shape_counts_a_generator_as_one_positional_argument() {
let lone = shape("minimum(f(x) for x in xs)\n");
assert_eq!(texts(&lone.positional), ["(f(x) for x in xs)"]);
assert!(!lone.positional_open);
assert!(lone.is_plain(1));
let with_arg = shape("f(a, x for x in xs)\n");
assert_eq!(texts(&with_arg.positional), ["a", "x for x in xs"]);
assert!(with_arg.is_plain(2));
let with_kw = shape("sum(x for x in xs; init = 0)\n");
assert_eq!(texts(&with_kw.positional), ["x for x in xs"]);
assert_eq!(with_kw.keywords.len(), 1);
assert!(!with_kw.positional_open);
}
#[test]
fn call_shape_opens_the_count_on_an_unrecognized_entry() {
let broken = shape("f(x,,y)\n");
assert!(broken.positional_open);
assert!(!broken.is_plain(1));
}
#[test]
fn call_shape_handles_a_call_with_no_arguments() {
let shape = shape("f()\n");
assert!(shape.positional.is_empty());
assert!(shape.keywords.is_empty());
assert!(shape.is_plain(0));
}
#[test]
fn call_shape_sees_a_trailing_do_block() {
let with_do = shape("map(xs) do y\n y\nend\n");
assert!(with_do.do_block);
assert!(!with_do.is_plain(1));
assert!(!shape("map(f, xs)\n").do_block);
}
#[test]
fn is_plain_wants_the_arity_and_nothing_else() {
assert!(shape("f(a)\n").is_plain(1));
assert!(!shape("f(a)\n").is_plain(2));
assert!(!shape("f(a; b = 1)\n").is_plain(1));
assert!(!shape("f(a, xs...)\n").is_plain(1));
}
#[test]
fn in_signature_position_sees_through_annotations_and_where() {
assert!(in_signature_position(&call_node("f(x) = 1\n")));
assert!(in_signature_position(&call_node(
"function f(x)\n 1\nend\n"
)));
assert!(in_signature_position(&call_node("f(x)::Int = 1\n")));
assert!(in_signature_position(&call_node("f(x::T) where {T} = 1\n")));
assert!(!in_signature_position(&call_node("y = f(x)\n")));
assert!(!in_signature_position(&call_node("f(x)\n")));
}
#[test]
fn is_name_matches_only_bare_identifiers() {
let bin = parse("x == missing\n")
.cst
.descendants()
.find_map(BinaryExpr::cast)
.expect("a binary expr");
assert!(is_name(&bin.rhs().unwrap(), "missing"));
assert!(!is_name(&bin.rhs().unwrap(), "Missing"));
assert!(!is_name(&bin.lhs().unwrap(), "missing"));
let bin = parse("x == Base.missing\n")
.cst
.descendants()
.find_map(BinaryExpr::cast)
.expect("a binary expr");
assert!(!is_name(&bin.rhs().unwrap(), "missing"));
}
}