use rowan::TextRange;
use crate::hir::types::{Expr, StringPart};
#[must_use]
pub fn expr_span(expr: &Expr) -> Option<TextRange> {
let mut span = None;
match expr {
Expr::Int(_) | Expr::Float(_) | Expr::Bool(_) | Expr::Null | Expr::Fragment(_) => {}
Expr::Path(p) | Expr::DivertTarget(p) => cover(&mut span, Some(p.range)),
Expr::ListLiteral(items) => {
for p in items {
cover(&mut span, Some(p.range));
}
}
Expr::String(s) => {
for part in &s.parts {
if let StringPart::Interpolation(inner) = part {
cover(&mut span, expr_span(inner));
}
}
}
Expr::Prefix(_, inner) | Expr::Postfix(inner, _) => cover(&mut span, expr_span(inner)),
Expr::Infix(ie) => return Some(ie.ptr.range),
Expr::Call(path, args) => {
cover(&mut span, Some(path.range));
for a in args {
cover(&mut span, expr_span(a));
}
}
Expr::ArrayLiteral(a) => {
cover(&mut span, Some(a.ptr.range));
for e in &a.elements {
cover(&mut span, expr_span(e));
}
}
Expr::MapLiteral(m) => {
cover(&mut span, Some(m.ptr.range));
for (k, v) in &m.entries {
cover(&mut span, expr_span(k));
cover(&mut span, expr_span(v));
}
}
Expr::Index(idx) => {
cover(&mut span, Some(idx.ptr.range));
cover(&mut span, expr_span(&idx.base));
cover(&mut span, expr_span(&idx.index));
}
Expr::Range(r) => {
cover(&mut span, Some(r.ptr.range));
cover(&mut span, expr_span(&r.start));
cover(&mut span, expr_span(&r.end));
}
Expr::StructLiteral(sl) => {
cover(&mut span, Some(sl.ptr.range));
for (_, v) in &sl.fields {
cover(&mut span, expr_span(v));
}
}
Expr::FieldAccess(fa) => {
cover(&mut span, Some(fa.ptr.range));
cover(&mut span, expr_span(&fa.base));
}
Expr::FnLiteral(fl) => {
cover(&mut span, Some(fl.ptr.range));
for a in &fl.args {
cover(&mut span, expr_span(a));
}
}
Expr::RefArg(ra) => {
cover(&mut span, Some(ra.ptr.range));
cover(&mut span, expr_span(&ra.operand));
}
Expr::Lambda(l) => cover(&mut span, Some(l.ptr.range)),
}
span
}
fn cover(span: &mut Option<TextRange>, next: Option<TextRange>) {
let Some(next) = next else {
return;
};
*span = Some(match *span {
Some(current) => current.cover(next),
None => next,
});
}
#[cfg(test)]
#[expect(
clippy::panic,
reason = "test-only assertions; see sibling test modules"
)]
mod tests {
use super::*;
use crate::FileId;
use crate::hir::types::{HirFile, Stmt};
fn first_logic_expr(src: &str) -> Expr {
let parse = brink_syntax_native::parse(src);
assert!(parse.errors().is_empty(), "{:?}", parse.errors());
let tree = parse.tree();
let (hir, _manifest, _diag): (HirFile, _, _) =
crate::hir::lower_native::lower(FileId(0), &tree);
let knot = hir.knots.first().expect("one flow");
for stmt in &knot.body.stmts {
if let Stmt::Content(c) = stmt {
for part in &c.parts {
if let crate::hir::types::ContentPart::Interpolation(e) = part {
return e.clone();
}
}
}
}
panic!("no inline expression found in {src}");
}
fn text(src: &str, range: TextRange) -> String {
src[usize::from(range.start())..usize::from(range.end())].to_string()
}
#[test]
fn scalar_literals_carry_no_span() {
assert_eq!(expr_span(&Expr::Int(5)), None);
assert_eq!(expr_span(&Expr::Bool(true)), None);
assert_eq!(expr_span(&Expr::Null), None);
}
#[test]
fn a_path_spans_itself() {
let src = "flow main() {\n {x}\n -> END\n}\n";
let e = first_logic_expr(src);
let span = expr_span(&e).expect("path has a range");
assert_eq!(text(src, span), "x");
}
#[test]
fn an_infix_spans_its_own_node() {
let src = "flow main() {\n {a or b}\n -> END\n}\n";
let e = first_logic_expr(src);
let span = expr_span(&e).expect("an infix node carries its own range");
assert_eq!(text(src, span), "a or b");
}
#[test]
fn a_chain_and_its_left_spine_have_distinct_spans() {
let src = "flow main() {\n {a or b or 99}\n -> END\n}\n";
let e = first_logic_expr(src);
let whole = expr_span(&e).expect("chain carries its own range");
assert_eq!(text(src, whole), "a or b or 99");
let Expr::Infix(root) = &e else {
panic!("expected a left-associative chain, got {e:?}");
};
let inner = expr_span(&root.lhs).expect("the left spine is an infix too");
assert_eq!(text(src, inner).trim_end(), "a or b");
assert_ne!(inner, whole, "root and spine must be separately keyable");
}
#[test]
fn a_trailing_scalar_literal_still_carries_no_span_of_its_own() {
let src = "flow main() {\n {a or b or 99}\n -> END\n}\n";
let e = first_logic_expr(src);
let Expr::Infix(root) = &e else {
panic!("expected a left-associative chain, got {e:?}");
};
assert_eq!(expr_span(&root.rhs), None);
}
#[test]
fn a_call_spans_its_callee_and_arguments() {
let src = "flow main() {\n {f(a, b)}\n -> END\n}\n";
let e = first_logic_expr(src);
let span = expr_span(&e).expect("call has a callee range");
assert_eq!(text(src, span), "f(a, b");
}
#[test]
fn an_all_literal_infix_is_keyed_by_the_operation_itself() {
let src = "flow main() {\n {5 or 9}\n -> END\n}\n";
let e = first_logic_expr(src);
let span = expr_span(&e).expect("the operation carries its own range");
assert_eq!(text(src, span), "5 or 9");
}
}