use rowan::NodeOrToken;
use rowan::ast::AstNode as _;
use smol_str::SmolStr;
use crate::ast::{Arg, AstToken as _, BinaryExpr, CallExpr, Expr, HasArgList as _, Ident};
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken};
pub fn callee_name(call: &CallExpr) -> Option<SmolStr> {
call.callee_name()
}
pub fn call_named(node: &SyntaxNode, name: &str) -> Option<CallExpr> {
let call = CallExpr::cast(node.clone())?;
(callee_name(&call).as_deref() == Some(name)).then_some(call)
}
pub fn is_callee(root: &SyntaxNode, range: rowan::TextRange) -> bool {
let NodeOrToken::Token(token) = root.covering_element(range) else {
return false;
};
let Some(parent) = token.parent() else {
return false;
};
parent.kind() == SyntaxKind::CALL_EXPR
&& CallExpr::cast(parent)
.and_then(|call| call.callee_token())
.is_some_and(|callee| callee.text_range() == range)
}
pub struct ArgMatch {
pub name: Option<SmolStr>,
pub name_token: Option<SyntaxToken>,
pub value: Option<SyntaxElement>,
}
pub fn args(call: &CallExpr) -> Vec<ArgMatch> {
call.args().map(arg_match).collect()
}
pub fn nth_arg(call: &CallExpr, n: usize) -> Option<SyntaxElement> {
call.nth_positional(n)
}
pub fn named_arg(call: &CallExpr, name: &str) -> Option<SyntaxElement> {
call.named_arg(name)
}
fn arg_match(arg: Arg) -> ArgMatch {
ArgMatch {
name: arg.name(),
name_token: arg.name_token(),
value: arg.value(),
}
}
pub fn sole_positional(call: &CallExpr) -> Option<SyntaxElement> {
let mut valued = args(call).into_iter().filter(|a| a.value.is_some());
let only = valued.next()?;
if valued.next().is_some() || only.name.is_some() {
return None;
}
only.value
}
pub fn binary_parts(expr: &SyntaxNode) -> Option<(SyntaxElement, SyntaxToken, SyntaxElement)> {
BinaryExpr::cast(expr.clone())?.parts()
}
fn as_ident(el: &SyntaxElement) -> Option<Ident> {
el.as_token().cloned().and_then(Ident::cast)
}
pub fn is_true(el: &SyntaxElement) -> bool {
as_ident(el).is_some_and(|i| i.is_true())
}
pub fn is_false(el: &SyntaxElement) -> bool {
as_ident(el).is_some_and(|i| i.is_false())
}
pub fn is_bool_symbol(el: &SyntaxElement) -> bool {
as_ident(el).is_some_and(|i| i.is_bool_symbol())
}
pub fn is_na(el: &SyntaxElement) -> bool {
as_ident(el).is_some_and(|i| i.is_na())
}
pub fn is_null(el: &SyntaxElement) -> bool {
as_ident(el).is_some_and(|i| i.is_null())
}
pub fn is_nan(el: &SyntaxElement) -> bool {
as_ident(el).is_some_and(|i| i.is_nan())
}
pub fn string_literal(token: &SyntaxToken) -> Option<(char, &str)> {
if token.kind() != SyntaxKind::STRING {
return None;
}
let text = token.text();
let bytes = text.as_bytes();
if bytes.len() < 2 {
return None;
}
let quote = bytes[0];
if !matches!(quote, b'"' | b'\'') || bytes[bytes.len() - 1] != quote {
return None;
}
Some((quote as char, &text[1..text.len() - 1]))
}
pub fn is_plain_regex_literal(s: &str) -> bool {
!s.bytes().any(|b| {
matches!(
b,
b'.' | b'\\'
| b'|'
| b'('
| b')'
| b'['
| b']'
| b'{'
| b'}'
| b'^'
| b'$'
| b'*'
| b'+'
| b'?'
)
})
}
pub fn is_safe_splice_context(node: &SyntaxNode) -> bool {
let Some(parent) = node.parent() else {
return true;
};
match parent.kind() {
SyntaxKind::ROOT
| SyntaxKind::BLOCK_EXPR
| SyntaxKind::PAREN_EXPR
| SyntaxKind::ARG
| SyntaxKind::IF_EXPR
| SyntaxKind::WHILE_EXPR
| SyntaxKind::FOR_EXPR
| SyntaxKind::REPEAT_EXPR
| SyntaxKind::ASSIGNMENT_EXPR => true,
SyntaxKind::BINARY_EXPR => binary_parts(&parent).is_some_and(|(_, op, _)| {
matches!(
op.kind(),
SyntaxKind::AND
| SyntaxKind::AND2
| SyntaxKind::OR
| SyntaxKind::OR2
| SyntaxKind::TILDE
)
}),
SyntaxKind::UNARY_EXPR => parent
.children_with_tokens()
.find_map(|e| e.into_token())
.is_some_and(|t| t.kind() == SyntaxKind::BANG),
_ => false,
}
}
pub fn element_text(el: &SyntaxElement) -> String {
match el {
NodeOrToken::Token(t) => t.text().to_string(),
NodeOrToken::Node(n) => n.text().to_string(),
}
}
pub fn is_atom(el: &SyntaxElement) -> bool {
Expr::cast(el.clone()).is_some_and(|e| e.is_atom())
}
pub fn deletion_span(src: &str, range: rowan::TextRange) -> (usize, usize) {
let bytes = src.as_bytes();
let mut start = usize::from(range.start());
while start > 0 && matches!(bytes[start - 1], b' ' | b'\t') {
start -= 1;
}
let mut end = usize::from(range.end());
while end < bytes.len() && matches!(bytes[end], b' ' | b'\t') {
end += 1;
}
end = consume_newline(bytes, end);
loop {
let mut probe = end;
while probe < bytes.len() && matches!(bytes[probe], b' ' | b'\t') {
probe += 1;
}
let after_nl = consume_newline(bytes, probe);
if after_nl == probe {
break; }
end = after_nl;
}
if end == bytes.len() {
let mut prev = start;
while prev > 0 && matches!(bytes[prev - 1], b' ' | b'\t' | b'\n' | b'\r') {
prev -= 1;
}
start = if prev > 0 {
consume_newline(bytes, prev)
} else {
0
};
}
(start, end)
}
fn consume_newline(bytes: &[u8], i: usize) -> usize {
match bytes.get(i) {
Some(b'\n') => i + 1,
Some(b'\r') if bytes.get(i + 1) == Some(&b'\n') => i + 2,
_ => i,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
fn first_call(src: &str) -> CallExpr {
parse(src)
.cst
.descendants()
.find_map(CallExpr::cast)
.expect("a call")
}
fn first_binary(src: &str) -> SyntaxNode {
parse(src)
.cst
.descendants()
.find(|n| n.kind() == SyntaxKind::BINARY_EXPR)
.expect("a binary expr")
}
#[test]
fn callee_name_reads_simple_names() {
assert_eq!(callee_name(&first_call("foo(1)")).as_deref(), Some("foo"));
assert!(call_named(first_call("foo(1)").syntax(), "foo").is_some());
assert!(call_named(first_call("foo(1)").syntax(), "bar").is_none());
}
#[test]
fn callee_name_none_for_computed_callee() {
assert!(callee_name(&first_call("(g())(1)")).is_none());
}
#[test]
fn nth_and_named_args() {
let call = first_call("f(1, b = 2, 3)");
assert_eq!(element_text(&nth_arg(&call, 0).unwrap()), "1");
assert_eq!(element_text(&nth_arg(&call, 1).unwrap()), "3");
assert_eq!(element_text(&named_arg(&call, "b").unwrap()), "2");
assert!(named_arg(&call, "z").is_none());
}
#[test]
fn binary_parts_splits_comparison() {
let (lhs, op, rhs) = binary_parts(&first_binary("x == TRUE")).unwrap();
assert_eq!(element_text(&lhs), "x");
assert_eq!(op.kind(), SyntaxKind::EQUAL2);
assert!(is_true(&rhs));
}
#[test]
fn literal_classifiers() {
let (_, _, rhs) = binary_parts(&first_binary("x == FALSE")).unwrap();
assert!(is_false(&rhs));
let (_, _, rhs) = binary_parts(&first_binary("x == NA")).unwrap();
assert!(is_na(&rhs));
let (_, _, rhs) = binary_parts(&first_binary("x == NA_integer_")).unwrap();
assert!(is_na(&rhs));
let (_, _, rhs) = binary_parts(&first_binary("x == NULL")).unwrap();
assert!(is_null(&rhs));
let (lhs, _, _) = binary_parts(&first_binary("T == x")).unwrap();
assert!(is_bool_symbol(&lhs));
}
#[test]
fn string_literal_extracts_quote_and_inner() {
let call = first_call("grepl(\"^abc\", x)");
let tok = nth_arg(&call, 0).unwrap().into_token().unwrap();
assert_eq!(string_literal(&tok), Some(('"', "^abc")));
let call = first_call("grepl('a.b', x)");
let tok = nth_arg(&call, 0).unwrap().into_token().unwrap();
assert_eq!(string_literal(&tok), Some(('\'', "a.b")));
}
#[test]
fn plain_regex_literal_rejects_metacharacters() {
assert!(is_plain_regex_literal("abc"));
assert!(is_plain_regex_literal("hello world"));
assert!(!is_plain_regex_literal("a.b"));
assert!(!is_plain_regex_literal("a\\.b"));
assert!(!is_plain_regex_literal("^abc"));
assert!(!is_plain_regex_literal("a+b"));
}
#[test]
fn is_atom_guards_negation() {
let (lhs, _, _) = binary_parts(&first_binary("x == FALSE")).unwrap();
assert!(is_atom(&lhs));
let (lhs, _, _) = binary_parts(&first_binary("a > b == FALSE")).unwrap();
assert!(!is_atom(&lhs));
}
}