From 68e929b1d919659eac1496fcfcd12eaac57cb5f0 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 28 Oct 2026 10:52:53 +0000 Subject: [PATCH] Refactor formula for strict Clippy checks --- crates/formula/src/lexer.rs | 395 ++++++++++++++++++------------------ crates/formula/src/tests.rs | 317 +++++++++++++++-------------- 2 files changed, 363 insertions(+), 349 deletions(-) diff --git a/crates/formula/src/lexer.rs b/crates/formula/src/lexer.rs index 332cb6eeb..2799b5b6f 100644 --- a/crates/formula/src/lexer.rs +++ b/crates/formula/src/lexer.rs @@ -67,6 +67,198 @@ fn starts_range_end(c: Option) -> bool { c.is_some_and(|c| c.is_ascii_alphanumeric() || c == '$') } +fn byte_pos(src: &str, chars: &[(usize, char)], index: usize) -> usize { + chars.get(index).map(|c| c.0).unwrap_or(src.len()) +} + +fn char_at(chars: &[(usize, char)], index: usize) -> Option { + chars.get(index).map(|c| c.1) +} + +fn emit(src: &str, chars: &[(usize, char)], out: &mut Vec, tok: Tok, start: usize, end_index: usize) { + out.push(Token { tok, start, end: byte_pos(src, chars, end_index) }); +} + +fn quoted(src: &str, chars: &[(usize, char)], i: usize, start: usize, out: &mut Vec) -> Result { + let quote = char_at(chars, i).unwrap_or_default(); + let mut value = String::new(); + let mut j = i + 1; + loop { + match char_at(chars, j) { + None => { + let msg = if quote == '"' { "unterminated text" } else { "unterminated sheet name" }; + return Err(LexError { msg: msg.into(), pos: start }); + } + Some(c) if c == quote && char_at(chars, j + 1) == Some(quote) => { + value.push(quote); + j += 2; + } + Some(c) if c == quote => { + j += 1; + break; + } + Some(c) => { + value.push(c); + j += 1; + } + } + } + if quote == '"' { + emit(src, chars, out, Tok::Text(value), start, j); + return Ok(j); + } + if char_at(chars, j) != Some('!') { + return Err(LexError { msg: "expected ! after sheet name".into(), pos: byte_pos(src, chars, j) }); + } + let (first, last) = match value.split_once(':') { + Some((first, last)) => (first.to_string(), Some(last.to_string())), + None => (value, None), + }; + emit(src, chars, out, Tok::Sheet(first, last), start, j + 1); + Ok(j + 1) +} + +fn error_literal(src: &str, chars: &[(usize, char)], i: usize, start: usize, out: &mut Vec) -> Result { + let rest = src.get(start..).unwrap_or(""); + let error = CellError::ALL + .iter() + .copied() + .find(|error| rest.get(..error.as_str().len()).is_some_and(|text| text.eq_ignore_ascii_case(error.as_str()))) + .ok_or_else(|| LexError { msg: "unknown error literal".into(), pos: start })?; + let end = i + error.as_str().chars().count(); + emit(src, chars, out, Tok::Error(error), start, end); + Ok(end) +} + +fn structured_ref(src: &str, chars: &[(usize, char)], i: usize, start: usize, out: &mut Vec) -> Result { + let mut depth = 0; + let mut j = i; + loop { + match char_at(chars, j) { + None => return Err(LexError { msg: "unbalanced [".into(), pos: start }), + Some('\'') => j += 2, + Some('[') => { + depth += 1; + j += 1; + } + Some(']') => { + depth -= 1; + j += 1; + if depth == 0 { + break; + } + } + Some(_) => j += 1, + } + } + let inner = src.get(byte_pos(src, chars, i + 1)..byte_pos(src, chars, j.saturating_sub(1))).unwrap_or("").to_string(); + emit(src, chars, out, Tok::Struct(inner), start, j); + Ok(j) +} + +fn number(src: &str, chars: &[(usize, char)], locale: crate::FormulaLocale, i: usize, start: usize, out: &mut Vec) -> Result { + let ch = |index| char_at(chars, index); + let mut j = i; + while ch(j).is_some_and(|c| c.is_ascii_digit()) { + j += 1; + } + let is_row_ref = { + let mut k = j; + if ch(k) == Some('.') && ch(k + 1) == Some(':') { + k += 1; + } + ch(k) == Some(':') && { + k += 1; + if ch(k) == Some('.') { + k += 1; + } + if ch(k) == Some('$') { + k += 1; + } + ch(k).is_some_and(|c| c.is_ascii_digit()) + } + }; + if is_row_ref { + let word = chars.get(i..j).map(|s| s.iter().map(|c| c.1).collect()).unwrap_or_default(); + emit(src, chars, out, Tok::Word(word), start, j); + return Ok(j); + } + if ch(j) == Some(locale.decimal_separator()) { + j += 1; + while ch(j).is_some_and(|c| c.is_ascii_digit()) { + j += 1; + } + } + if matches!(ch(j), Some('e' | 'E')) { + let mut exponent = j + 1; + if matches!(ch(exponent), Some('+' | '-')) { + exponent += 1; + } + if ch(exponent).is_some_and(|c| c.is_ascii_digit()) { + while ch(exponent).is_some_and(|c| c.is_ascii_digit()) { + exponent += 1; + } + j = exponent; + } + } + let text = src.get(start..byte_pos(src, chars, j)).unwrap_or(""); + let value: f64 = + text.replace(locale.decimal_separator(), ".").parse().map_err(|_| LexError { msg: format!("bad number `{text}`"), pos: start })?; + if !value.is_finite() { + return Err(LexError { msg: format!("number too large `{text}`"), pos: start }); + } + emit(src, chars, out, Tok::Number(value), start, j); + Ok(j) +} + +fn word(src: &str, chars: &[(usize, char)], locale: crate::FormulaLocale, array_depth: usize, i: usize, start: usize, out: &mut Vec) -> usize { + let ch = |index| char_at(chars, index); + let mut j = i; + while ch(j).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && c == locale.array_column_separator())) { + j += 1; + } + let value: String = chars.get(i..j).map(|s| s.iter().map(|c| c.1).collect()).unwrap_or_default(); + if ch(j) == Some('!') { + emit(src, chars, out, Tok::Sheet(value, None), start, j + 1); + j + 1 + } else if ch(j) == Some(':') && sheet_range_end(chars, locale, array_depth, &value, j, out) { + let mut end = j + 1; + while ch(end).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && c == locale.array_column_separator())) { + end += 1; + } + let last: String = chars.get(j + 1..end).map(|s| s.iter().map(|c| c.1).collect()).unwrap_or_default(); + emit(src, chars, out, Tok::Sheet(value, Some(last)), start, end + 1); + end + 1 + } else if ch(j) == Some('(') { + emit(src, chars, out, Tok::Func(value), start, j + 1); + j + 1 + } else if value.eq_ignore_ascii_case("TRUE") && ch(j) != Some('[') { + emit(src, chars, out, Tok::Bool(true), start, j); + j + } else if value.eq_ignore_ascii_case("FALSE") && ch(j) != Some('[') { + emit(src, chars, out, Tok::Bool(false), start, j); + j + } else if value.len() > 1 && value.ends_with('.') && ch(j) == Some(':') && is_range_end(value.get(..value.len() - 1).unwrap_or("")) { + emit(src, chars, out, Tok::Word(value[..value.len() - 1].to_string()), start, j - 1); + j - 1 + } else { + emit(src, chars, out, Tok::Word(value), start, j); + j + } +} + +fn sheet_range_end(chars: &[(usize, char)], locale: crate::FormulaLocale, array_depth: usize, value: &str, colon: usize, out: &[Token]) -> bool { + let ch = |index| char_at(chars, index); + let mut end = colon + 1; + while ch(end).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && c == locale.array_column_separator())) { + end += 1; + } + let repeats_sheet = is_range_end(value) + && matches!(out.last(), Some(Token { tok: Tok::Sheet(previous, None), .. }) + if chars.get(colon + 1..end).is_some_and(|s| crate::same_name(&s.iter().map(|c| c.1).collect::(), previous))); + end > colon + 1 && ch(end) == Some('!') && !repeats_sheet +} + /// Tokenizes formula text (without the leading `=`). pub fn tokenize(src: &str) -> Result, LexError> { tokenize_locale(src, crate::FormulaLocale::En) @@ -94,107 +286,17 @@ pub fn tokenize_locale(src: &str, locale: crate::FormulaLocale) -> Result { - let mut s = String::new(); - let mut j = i + 1; - loop { - match ch(j) { - None => return Err(LexError { msg: "unterminated text".into(), pos: start }), - Some('"') if ch(j + 1) == Some('"') => { - s.push('"'); - j += 2; - } - Some('"') => { - j += 1; - break; - } - Some(x) => { - s.push(x); - j += 1; - } - } - } - push(&mut out, Tok::Text(s), j); - i = j; - } - '\'' => { - // Quoted sheet name: 'My Sheet'! or 'A:B'! - let mut s = String::new(); - let mut j = i + 1; - loop { - match ch(j) { - None => return Err(LexError { msg: "unterminated sheet name".into(), pos: start }), - Some('\'') if ch(j + 1) == Some('\'') => { - s.push('\''); - j += 2; - } - Some('\'') => { - j += 1; - break; - } - Some(x) => { - s.push(x); - j += 1; - } - } - } - if ch(j) != Some('!') { - return Err(LexError { msg: "expected ! after sheet name".into(), pos: pos_of(j) }); - } - let (a, b) = match s.split_once(':') { - Some((a, b)) => (a.to_string(), Some(b.to_string())), - None => (s, None), - }; - push(&mut out, Tok::Sheet(a, b), j + 1); - i = j + 1; - } + '"' | '\'' => i = quoted(src, &chars, i, start, &mut out)?, '#' if out.last().is_some_and(|t: &Token| t.end == start && matches!(t.tok, Tok::Word(_))) => { // Spill range operator right after a reference: `A1#`. push(&mut out, Tok::Op("#"), i + 1); i += 1; } '#' => { - // Error literal, or #-prefixed struct specifier (only inside [] which we lex as Struct). - let rest = src.get(start..).unwrap_or(""); - let mut found = None; - for e in CellError::ALL { - let t = e.as_str(); - if rest.len() >= t.len() && rest.get(..t.len()).is_some_and(|p| p.eq_ignore_ascii_case(t)) { - found = Some(e); - break; - } - } - let Some(e) = found else { return Err(LexError { msg: "unknown error literal".into(), pos: start }) }; - let len = e.as_str().chars().count(); - // `#REF!A1` style is not valid; `#REF!` is an error literal. - push(&mut out, Tok::Error(e), i + len); - i += len; + i = error_literal(src, &chars, i, start, &mut out)?; } '[' => { - // Structured reference body: balanced brackets. - let mut depth = 0; - let mut j = i; - loop { - match ch(j) { - None => return Err(LexError { msg: "unbalanced [".into(), pos: start }), - Some('\'') => j += 2, // escape char inside struct refs - Some('[') => { - depth += 1; - j += 1; - } - Some(']') => { - depth -= 1; - j += 1; - if depth == 0 { - break; - } - } - Some(_) => j += 1, - } - } - let inner = src.get(pos_of(i + 1)..pos_of(j.saturating_sub(1))).unwrap_or("").to_string(); - push(&mut out, Tok::Struct(inner), j); - i = j; + i = structured_ref(src, &chars, i, start, &mut out)?; } '.' if ch(i + 1) == Some(':') => { // Trim reference `A1.:E10` / `A1.:.E10` (the word before stopped at the dot). @@ -207,64 +309,7 @@ pub fn tokenize_locale(src: &str, locale: crate::FormulaLocale) -> Result { - // Number, unless it is a row range like 1:3 (handled by parser via Word) — numbers - // followed by ':' and digits are row refs: lex as Word. - let mut j = i; - while ch(j).is_some_and(|d| d.is_ascii_digit()) { - j += 1; - } - // Row ranges `1:3`, and the trim forms `1.:3`, `1:.3`, `1.:.3`. - let is_row_ref = { - let mut k = j; - if ch(k) == Some('.') && ch(k + 1) == Some(':') { - k += 1; - } - ch(k) == Some(':') && { - k += 1; - if ch(k) == Some('.') { - k += 1; - } - if ch(k) == Some('$') { - k += 1; - } - ch(k).is_some_and(|d| d.is_ascii_digit()) - } - }; - // A word like "1A" can't happen in valid formulas; numbers then. - if is_row_ref { - let w: String = chars.get(i..j).map(|s| s.iter().map(|c| c.1).collect()).unwrap_or_default(); - push(&mut out, Tok::Word(w), j); - i = j; - continue; - } - if ch(j) == Some(locale.decimal_separator()) { - j += 1; - while ch(j).is_some_and(|d| d.is_ascii_digit()) { - j += 1; - } - } - if matches!(ch(j), Some('e' | 'E')) { - let mut k = j + 1; - if matches!(ch(k), Some('+' | '-')) { - k += 1; - } - if ch(k).is_some_and(|d| d.is_ascii_digit()) { - while ch(k).is_some_and(|d| d.is_ascii_digit()) { - k += 1; - } - j = k; - } - } - let text = src.get(start..pos_of(j)).unwrap_or(""); - let v: f64 = text - .replace(locale.decimal_separator(), ".") - .parse() - .map_err(|_| LexError { msg: format!("bad number `{text}`"), pos: start })?; - if !v.is_finite() { - return Err(LexError { msg: format!("number too large `{text}`"), pos: start }); - } - push(&mut out, Tok::Number(v), j); - i = j; + i = number(src, &chars, locale, i, start, &mut out)?; } '(' => { push(&mut out, Tok::LParen, i + 1); @@ -328,53 +373,7 @@ pub fn tokenize_locale(src: &str, locale: crate::FormulaLocale) -> Result { - let mut j = i; - while ch(j).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && c == locale.array_column_separator())) { - j += 1; - } - let w: String = chars.get(i..j).map(|s| s.iter().map(|c| c.1).collect()).unwrap_or_default(); - if ch(j) == Some('!') { - push(&mut out, Tok::Sheet(w, None), j + 1); - i = j + 1; - } else if ch(j) == Some(':') && { - // Sheet1:Sheet3!A1 — look ahead for word then '!'. - let mut k = j + 1; - while ch(k).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && c == locale.array_column_separator())) { - k += 1; - } - // `Sheet1!A1:Sheet1!B2` is a range whose second end repeats the sheet, not a span. - let repeats_sheet = is_range_end(&w) - && matches!(out.last(), Some(Token { tok: Tok::Sheet(p, None), .. }) - if chars.get(j + 1..k).is_some_and(|s| crate::same_name(&s.iter().map(|c| c.1).collect::(), p))); - k > j + 1 && ch(k) == Some('!') && !repeats_sheet - } { - let mut k = j + 1; - while ch(k).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && c == locale.array_column_separator())) { - k += 1; - } - let w2: String = chars.get(j + 1..k).map(|s| s.iter().map(|c| c.1).collect()).unwrap_or_default(); - push(&mut out, Tok::Sheet(w, Some(w2)), k + 1); - i = k + 1; - } else if ch(j) == Some('(') { - push(&mut out, Tok::Func(w), j + 1); - i = j + 1; - } else if w.eq_ignore_ascii_case("TRUE") && ch(j) != Some('[') { - push(&mut out, Tok::Bool(true), j); - i = j; - } else if w.eq_ignore_ascii_case("FALSE") && ch(j) != Some('[') { - push(&mut out, Tok::Bool(false), j); - i = j; - } else if w.len() > 1 && w.ends_with('.') && ch(j) == Some(':') && is_range_end(w.get(..w.len() - 1).unwrap_or("")) { - // `A1.:E10`: the dot belongs to the trim operator, lexed next. - let w = w.get(..w.len() - 1).unwrap_or("").to_string(); - push(&mut out, Tok::Word(w), j - 1); - i = j - 1; - } else { - push(&mut out, Tok::Word(w), j); - i = j; - } - } + c if is_word_char(c) => i = word(src, &chars, locale, array_depth, i, start, &mut out), other => return Err(LexError { msg: format!("unexpected character `{other}`"), pos: start }), } } diff --git a/crates/formula/src/tests.rs b/crates/formula/src/tests.rs index 0f16116af..0fdc9ca38 100644 --- a/crates/formula/src/tests.rs +++ b/crates/formula/src/tests.rs @@ -2,7 +2,10 @@ use super::*; use crate::adjust::{Axis, Edit, adjust, rename_sheet, shift_relative}; fn rt(src: &str) -> String { - print(&parse(src).unwrap_or_else(|e| panic!("{src}: {e}"))) + match parse(src) { + Ok(e) => print(&e), + Err(e) => format!("{src}: {e}"), + } } #[test] @@ -87,11 +90,11 @@ fn normalises() { } #[test] -fn spill_operator() { +fn spill_operator() -> Result<(), Box> { for f in ["A1#", "Sheet1!$D$1#", "SUM(B2#)", "'My Sheet'!A1#*2", "ROWS(A1#)+1", "IF(A1,#N/A,#REF!)"] { assert_eq!(rt(f), f); } - assert!(matches!(parse("A1#").unwrap(), Expr::Unary(UnOp::Spill, _))); + assert!(matches!(parse("A1#")?, Expr::Unary(UnOp::Spill, _))); // Files spell it ANCHORARRAY. assert_eq!(rt("_xlfn.ANCHORARRAY(Sheet1!$D$1)"), "Sheet1!$D$1#"); assert_eq!(rt("SUM(ANCHORARRAY(B2))"), "SUM(B2#)"); @@ -99,35 +102,35 @@ fn spill_operator() { assert!(parse("A1:B2#").is_err()); assert!(parse("Name#").is_err()); // Deleting the anchor's sheet leaves #REF!. - assert_eq!(print(&crate::adjust::delete_sheet(parse("SUM(Data!A1#)").unwrap(), "Data", &[])), "SUM(#REF!)"); + assert_eq!(print(&crate::adjust::delete_sheet(parse("SUM(Data!A1#)")?, "Data", &[])), "SUM(#REF!)"); + Ok(()) } #[test] -fn delete_sheet_shrinks_3d_span() { +fn delete_sheet_shrinks_3d_span() -> Result<(), Box> { let order: Vec = ["Sheet1", "Sheet2", "Sheet3", "Results"].iter().map(|s| s.to_string()).collect(); - let del = |f: &str, n: &str| print(&crate::adjust::delete_sheet(parse(f).unwrap(), n, &order)); - assert_eq!(del("SUM(Sheet1:Sheet3!A1)", "Sheet1"), "SUM(Sheet2:Sheet3!A1)"); - assert_eq!(del("SUM(Sheet1:Sheet3!A1)", "Sheet3"), "SUM(Sheet1:Sheet2!A1)"); - assert_eq!(del("SUM(Sheet3:Sheet1!A1)", "Sheet1"), "SUM(Sheet3:Sheet2!A1)"); - assert_eq!(del("SUM(Sheet1:Sheet3!A1)", "Sheet2"), "SUM(Sheet1:Sheet3!A1)"); - assert_eq!(del("SUM(Sheet1:Sheet2!A1)", "Sheet1"), "SUM(Sheet2!A1)"); - assert_eq!(del("Sheet1!A1+Sheet3!A1", "Sheet1"), "#REF!+Sheet3!A1"); + let del = |f: &str, n: &str| parse(f).map(|e| print(&crate::adjust::delete_sheet(e, n, &order))); + assert_eq!(del("SUM(Sheet1:Sheet3!A1)", "Sheet1")?, "SUM(Sheet2:Sheet3!A1)"); + assert_eq!(del("SUM(Sheet1:Sheet3!A1)", "Sheet3")?, "SUM(Sheet1:Sheet2!A1)"); + assert_eq!(del("SUM(Sheet3:Sheet1!A1)", "Sheet1")?, "SUM(Sheet3:Sheet2!A1)"); + assert_eq!(del("SUM(Sheet1:Sheet3!A1)", "Sheet2")?, "SUM(Sheet1:Sheet3!A1)"); + assert_eq!(del("SUM(Sheet1:Sheet2!A1)", "Sheet1")?, "SUM(Sheet2!A1)"); + assert_eq!(del("Sheet1!A1+Sheet3!A1", "Sheet1")?, "#REF!+Sheet3!A1"); + Ok(()) } #[test] -fn precedence() { - let e = parse("1+2*3").unwrap(); +fn precedence() -> Result<(), Box> { + let e = parse("1+2*3")?; assert!(matches!(e, Expr::Binary(BinOp::Add, _, _))); - let e = parse("-2^2").unwrap(); + let e = parse("-2^2")?; assert!(matches!(e, Expr::Binary(BinOp::Pow, _, _))); - let e = parse("1&2=\"12\"").unwrap(); + let e = parse("1&2=\"12\"")?; assert!(matches!(e, Expr::Binary(BinOp::Eq, _, _))); - let e = parse("2^3^2").unwrap(); // left associative: (2^3)^2 - if let Expr::Binary(BinOp::Pow, l, _) = e { - assert!(matches!(*l, Expr::Binary(BinOp::Pow, _, _))); - } else { - panic!() - } + let e = parse("2^3^2")?; // left associative: (2^3)^2 + let Expr::Binary(BinOp::Pow, l, _) = e else { return Err("not a power".into()) }; + assert!(matches!(*l, Expr::Binary(BinOp::Pow, _, _))); + Ok(()) } #[test] @@ -149,122 +152,130 @@ fn no_panic_on_garbage() { } #[test] -fn references() { - let e = parse("SUM(A1:B2)+Sheet2!C3*D4").unwrap(); +fn references() -> Result<(), Box> { + let e = parse("SUM(A1:B2)+Sheet2!C3*D4")?; let refs: Vec = e.references().iter().map(|r| printer::reference_a1(r)).collect(); assert_eq!(refs, ["A1:B2", "Sheet2!C3", "D4"]); + Ok(()) } #[test] -fn copy_shift() { - let e = parse("A1+$A1+A$1+$A$1+SUM(B2:C3)").unwrap(); +fn copy_shift() -> Result<(), Box> { + let e = parse("A1+$A1+A$1+$A$1+SUM(B2:C3)")?; assert_eq!(print(&shift_relative(e, 2, 1)), "B3+$A3+B$1+$A$1+SUM(C4:D5)"); - let e = parse("A1").unwrap(); + let e = parse("A1")?; assert_eq!(print(&shift_relative(e, -1, 0)), "#REF!"); - let e = parse("SUM(A:A)").unwrap(); + let e = parse("SUM(A:A)")?; assert_eq!(print(&shift_relative(e, 5, 1)), "SUM(B:B)"); + Ok(()) } #[test] -fn insert_delete() { +fn insert_delete() -> Result<(), Box> { let ins = Edit::Insert { axis: Axis::Rows, at: 2, count: 2 }; - let e = parse("A1+A3+SUM(A2:A5)+Other!A3").unwrap(); + let e = parse("A1+A3+SUM(A2:A5)+Other!A3")?; assert_eq!(print(&adjust(e, "Sheet1", "Sheet1", &ins)), "A1+A5+SUM(A2:A7)+Other!A3"); let del = Edit::Delete { axis: Axis::Rows, at: 2, count: 2 }; // rows 3-4 - let e = parse("A1+A3+SUM(A2:A5)+A6").unwrap(); + let e = parse("A1+A3+SUM(A2:A5)+A6")?; assert_eq!(print(&adjust(e, "Sheet1", "Sheet1", &del)), "A1+#REF!+SUM(A2:A3)+A4"); let del = Edit::Delete { axis: Axis::Cols, at: 0, count: 1 }; - let e = parse("SUM(A1:C1)+B2").unwrap(); + let e = parse("SUM(A1:C1)+B2")?; assert_eq!(print(&adjust(e, "S", "S", &del)), "SUM(A1:B1)+A2"); - let e = parse("Sheet1!B2").unwrap(); + let e = parse("Sheet1!B2")?; assert_eq!(print(&adjust(e, "Other", "Sheet1", &del)), "Sheet1!A2"); - let e = parse("SUM(A:A)").unwrap(); + let e = parse("SUM(A:A)")?; assert_eq!(print(&adjust(e, "S", "S", &del)), "SUM(#REF!)"); + Ok(()) } #[test] -fn insert_delete_cells() { - let range = |a: &str| gridcraft_core::RangeRef::parse(a).unwrap(); - let adj = |f: &str, e: &Edit| print(&adjust(parse(f).unwrap(), "S", "S", e)); +fn insert_delete_cells() -> Result<(), Box> { + let range = |a: &str| gridcraft_core::RangeRef::parse(a); + let adj = |f: &str, e: &Edit| parse(f).map(|x| print(&adjust(x, "S", "S", e))); // Insert A3:A4, shifting down: column A moves, other columns and wider ranges don't. - let ins = Edit::InsertCells { axis: Axis::Rows, range: range("A3:A4") }; - assert_eq!(adj("SUM(A1:A5)+A2+A3+$A$9+B3+SUM(A1:B5)+SUM(A:A)", &ins), "SUM(A1:A7)+A2+A5+$A$11+B3+SUM(A1:B5)+SUM(A:A)"); + let ins = Edit::InsertCells { axis: Axis::Rows, range: range("A3:A4").ok_or("none")? }; + assert_eq!(adj("SUM(A1:A5)+A2+A3+$A$9+B3+SUM(A1:B5)+SUM(A:A)", &ins)?, "SUM(A1:A7)+A2+A5+$A$11+B3+SUM(A1:B5)+SUM(A:A)"); // Insert B2 shifting right. - let ins = Edit::InsertCells { axis: Axis::Cols, range: range("B2") }; - assert_eq!(adj("A2+B2+C2+B3+SUM(A2:C2)", &ins), "A2+C2+D2+B3+SUM(A2:D2)"); + let ins = Edit::InsertCells { axis: Axis::Cols, range: range("B2").ok_or("none")? }; + assert_eq!(adj("A2+B2+C2+B3+SUM(A2:C2)", &ins)?, "A2+C2+D2+B3+SUM(A2:D2)"); // Delete A3:A4 shifting up: references into it become #REF!, those below move up. - let del = Edit::DeleteCells { axis: Axis::Rows, range: range("A3:A4") }; - assert_eq!(adj("A2+A3+A4+A5+B5+SUM(A1:A10)+SUM(A3:A4)+SUM(A1:B10)", &del), "A2+#REF!+#REF!+A3+B5+SUM(A1:A8)+SUM(#REF!)+SUM(A1:B10)"); + let del = Edit::DeleteCells { axis: Axis::Rows, range: range("A3:A4").ok_or("none")? }; + assert_eq!(adj("A2+A3+A4+A5+B5+SUM(A1:A10)+SUM(A3:A4)+SUM(A1:B10)", &del)?, "A2+#REF!+#REF!+A3+B5+SUM(A1:A8)+SUM(#REF!)+SUM(A1:B10)"); // Delete B1:C2 shifting left. - let del = Edit::DeleteCells { axis: Axis::Cols, range: range("B1:C2") }; - assert_eq!(adj("A1+B1+D1+D2+D3+SUM(A1:E1)", &del), "A1+#REF!+B1+B2+D3+SUM(A1:C1)"); + let del = Edit::DeleteCells { axis: Axis::Cols, range: range("B1:C2").ok_or("none")? }; + assert_eq!(adj("A1+B1+D1+D2+D3+SUM(A1:E1)", &del)?, "A1+#REF!+B1+B2+D3+SUM(A1:C1)"); + Ok(()) } #[test] -fn moves_and_renames() { - let mv = Edit::Move { from: gridcraft_core::RangeRef::parse("A1:B2").unwrap(), to_row: 9, to_col: 3 }; - let e = parse("A1+B2+C3+SUM(A1:B2)").unwrap(); +fn moves_and_renames() -> Result<(), Box> { + let mv = Edit::Move { from: gridcraft_core::RangeRef::parse("A1:B2").ok_or("none")?, to_row: 9, to_col: 3 }; + let e = parse("A1+B2+C3+SUM(A1:B2)")?; assert_eq!(print(&adjust(e, "S", "S", &mv)), "D10+E11+C3+SUM(D10:E11)"); - let mv = Edit::Move { from: gridcraft_core::RangeRef::parse("B:C").unwrap(), to_row: 0, to_col: 4 }; - assert_eq!(print(&adjust(parse("SUM(B:B)+SUM($C:$C)+D1").unwrap(), "S", "S", &mv)), "SUM(E:E)+SUM($F:$F)+D1"); - let e = parse("Old!A1+'Old'!B2").unwrap(); + let mv = Edit::Move { from: gridcraft_core::RangeRef::parse("B:C").ok_or("none")?, to_row: 0, to_col: 4 }; + assert_eq!(print(&adjust(parse("SUM(B:B)+SUM($C:$C)+D1")?, "S", "S", &mv)), "SUM(E:E)+SUM($F:$F)+D1"); + let e = parse("Old!A1+'Old'!B2")?; assert_eq!(print(&rename_sheet(e, "Old", "New Name")), "'New Name'!A1+'New Name'!B2"); + Ok(()) } #[test] -fn rename_sheet_updates_qualified_names() { - let e = parse("Data!MyName+MyName+Other!MyName").unwrap(); +fn rename_sheet_updates_qualified_names() -> Result<(), Box> { + let e = parse("Data!MyName+MyName+Other!MyName")?; assert_eq!(print(&rename_sheet(e, "data", "Numbers")), "Numbers!MyName+MyName+Other!MyName"); - let e = parse("Data!MyName").unwrap(); + let e = parse("Data!MyName")?; assert_eq!(print(&rename_sheet(e, "Data", "New Name")), "'New Name'!MyName"); // Stored bare, like a parsed name, so the evaluator finds the sheet and a second rename still matches. - let e = rename_sheet(parse("Data!MyName").unwrap(), "Data", "It's"); + let e = rename_sheet(parse("Data!MyName")?, "Data", "It's"); assert_eq!(e, Expr::Name("It's!MyName".into())); assert_eq!(print(&rename_sheet(e, "It's", "Totals")), "Totals!MyName"); + Ok(()) } #[test] -fn moves_between_sheets() { - let mv = Edit::MoveToSheet { from: gridcraft_core::RangeRef::parse("A1:B2").unwrap(), to_sheet: "New Name".into(), to_row: 9, to_col: 3 }; +fn moves_between_sheets() -> Result<(), Box> { + let mv = Edit::MoveToSheet { from: gridcraft_core::RangeRef::parse("A1:B2").ok_or("none")?, to_sheet: "New Name".into(), to_row: 9, to_col: 3 }; for (host, input, expected) in [ ("Source", "A1+$B$2+C3+SUM(A1:B2)", "'New Name'!D10+'New Name'!$E$11+C3+SUM('New Name'!D10:E11)"), ("Other", "A1+sOuRcE!$B2+Other!B2", "A1+'New Name'!$E11+Other!B2"), ("Source", "SUM(A1:C3)+SUM(Source:Other!A1)", "SUM(A1:C3)+SUM(Source:Other!A1)"), ] { - assert_eq!(print(&adjust(parse(input).unwrap(), host, "Source", &mv)), expected); + assert_eq!(print(&adjust(parse(input)?, host, "Source", &mv)), expected); } let edge = Edit::MoveToSheet { - from: gridcraft_core::RangeRef::parse("A1:B2").unwrap(), + from: gridcraft_core::RangeRef::parse("A1:B2").ok_or("none")?, to_sheet: "New Name".into(), to_row: gridcraft_core::MAX_ROWS - 1, to_col: 3, }; - assert_eq!(print(&adjust(parse("B2").unwrap(), "Source", "Source", &edge)), "#REF!"); + assert_eq!(print(&adjust(parse("B2")?, "Source", "Source", &edge)), "#REF!"); for (from, input, row, col, expected) in [("A:A", "$A:$A", 0, 2, "Dest!$C:$C"), ("1:1", "$1:$1", 2, 0, "Dest!$3:$3")] { - let mv = Edit::MoveToSheet { from: gridcraft_core::RangeRef::parse(from).unwrap(), to_sheet: "Dest".into(), to_row: row, to_col: col }; - assert_eq!(print(&adjust(parse(input).unwrap(), "Source", "Source", &mv)), expected); + let mv = Edit::MoveToSheet { from: gridcraft_core::RangeRef::parse(from).ok_or("none")?, to_sheet: "Dest".into(), to_row: row, to_col: col }; + assert_eq!(print(&adjust(parse(input)?, "Source", "Source", &mv)), expected); } + Ok(()) } #[test] -fn r1c1() { - let e = parse("A1+$B$2+SUM(C:C)").unwrap(); +fn r1c1() -> Result<(), Box> { + let e = parse("A1+$B$2+SUM(C:C)")?; assert_eq!(print_r1c1(&e, gridcraft_core::CellRef::new(1, 1)), "R[-1]C[-1]+R2C2+SUM(C[1])"); + Ok(()) } #[test] -fn spans() { - let toks = lexer::tokenize("SUM(A1, B2)").unwrap(); - let a1 = toks.iter().find(|t| t.tok == lexer::Tok::Word("A1".into())).unwrap(); +fn spans() -> Result<(), Box> { + let toks = lexer::tokenize("SUM(A1, B2)")?; + let a1 = toks.iter().find(|t| t.tok == lexer::Tok::Word("A1".into())).ok_or("none")?; assert_eq!((a1.start, a1.end), (4, 6)); + Ok(()) } #[test] -fn unbracketed_this_row_column() { +fn unbracketed_this_row_column() -> Result<(), Box> { // Writers such as xlsxwriter spell `[@Units]` as `[[#This Row],Units]`. - let p = |f: &str| parse(f).unwrap_or_else(|e| panic!("{f}: {e}")); - let Expr::Binary(BinOp::Mul, a, _) = p("[[#This Row],Units]*[[#This Row],Price]") else { panic!() }; - let Expr::Struct(s) = *a else { panic!() }; + let Expr::Binary(BinOp::Mul, a, _) = parse("[[#This Row],Units]*[[#This Row],Price]")? else { return Err("not a product".into()) }; + let Expr::Struct(s) = *a else { return Err("not a struct".into()) }; assert_eq!((s.specifiers.as_slice(), s.col_start.as_deref()), ([StructItem::ThisRow].as_slice(), Some("Units"))); assert_eq!(rt("[[#This Row],Units]*[[#This Row],Price]"), "[@Units]*[@Price]"); assert_eq!(rt("Sales[[#This Row],Units]"), "Sales[@Units]"); @@ -284,24 +295,26 @@ fn unbracketed_this_row_column() { ] { assert!(parse(bad).is_err(), "{bad} should fail"); } + Ok(()) } #[test] -fn struct_column_keeps_edge_spaces() { +fn struct_column_keeps_edge_spaces() -> Result<(), Box> { // The spaces are part of the column name: `Table1[Qty ]` names `Qty `, never `Qty`. - let Expr::Struct(s) = parse("Table1[Qty ]").unwrap() else { panic!() }; + let Expr::Struct(s) = parse("Table1[Qty ]")? else { return Err("unexpected expression".into()) }; assert_eq!(s.col_start.as_deref(), Some("Qty ")); - let Expr::Struct(s) = parse("Table1[ Qty]").unwrap() else { panic!() }; + let Expr::Struct(s) = parse("Table1[ Qty]")? else { return Err("unexpected expression".into()) }; assert_eq!(s.col_start.as_deref(), Some(" Qty")); // Excel writes such a column in the double-bracket form. assert_eq!(rt("Table1[Qty ]"), "Table1[[Qty ]]"); for f in ["Table1[[Qty ]]", "Table1[[ Sum for Q2 ]]", "Table1[[#Totals],[Qty ]]", "Table1[@[Qty ]]", "Table1[Unit Price]"] { assert_eq!(rt(f), f); } + Ok(()) } #[test] -fn trim_references() { +fn trim_references() -> Result<(), Box> { for f in [ "SUM(A1:.E10)", "SUM(A1.:E10)", @@ -319,15 +332,15 @@ fn trim_references() { ] { assert_eq!(rt(f), f, "round trip of {f}"); } - let Expr::Call(_, args) = parse("SUM(A1:.E10)").unwrap() else { panic!() }; + let Expr::Call(_, args) = parse("SUM(A1:.E10)")? else { return Err("unexpected expression".into()) }; assert!(matches!(args.as_slice(), [Expr::Unary(UnOp::Trim(TrimMode::Trailing), r)] if matches!(**r, Expr::Ref(_)))); // Decimals, row ranges and dotted names keep their meaning. - assert_eq!(parse("1.5+.5").unwrap(), Expr::Binary(BinOp::Add, Box::new(Expr::Number(1.5)), Box::new(Expr::Number(0.5)))); + assert_eq!(parse("1.5+.5")?, Expr::Binary(BinOp::Add, Box::new(Expr::Number(1.5)), Box::new(Expr::Number(0.5)))); for f in ["SUM(1:5)", "Q1.2026!A1:B2", "my.name+1", "SUM(A1:B2)*1.5", "1.:5"] { assert_eq!(rt(f), f); } assert_eq!(rt("'Q1.2026'!A1:B2"), "Q1.2026!A1:B2"); - assert!(matches!(parse("Q1.2026!A1:B2").unwrap(), Expr::Ref(_))); + assert!(matches!(parse("Q1.2026!A1:B2")?, Expr::Ref(_))); // File call forms. assert_eq!(rt("SUM(_xlfn._TRO_TRAILING(A1:E10))"), "SUM(A1:.E10)"); assert_eq!(rt("_xlfn._TRO_LEADING(A:A)"), "A.:A"); @@ -338,77 +351,76 @@ fn trim_references() { assert!(parse(bad).is_err(), "{bad} should fail"); } // R1C1 keeps the operator. - assert_eq!(print_r1c1(&parse("SUM(A1:.B2)+ROWS(2.:2)").unwrap(), gridcraft_core::CellRef::new(0, 0)), "SUM(RC:.R[1]C[1])+ROWS(R[1].:R[1])"); + assert_eq!(print_r1c1(&parse("SUM(A1:.B2)+ROWS(2.:2)")?, gridcraft_core::CellRef::new(0, 0)), "SUM(RC:.R[1]C[1])+ROWS(R[1].:R[1])"); + Ok(()) } #[test] -fn trim_references_adjust() { +fn trim_references_adjust() -> Result<(), Box> { let ins = Edit::Insert { axis: Axis::Rows, at: 2, count: 2 }; - let e = parse("SUM(A1:.A5)+SUM(B:.B)+ROWS(2.:.4)").unwrap(); + let e = parse("SUM(A1:.A5)+SUM(B:.B)+ROWS(2.:.4)")?; assert_eq!(print(&adjust(e, "S", "S", &ins)), "SUM(A1:.A7)+SUM(B:.B)+ROWS(2.:.6)"); let del = Edit::Delete { axis: Axis::Cols, at: 0, count: 1 }; - let e = parse("SUM(A1.:C1)+SUM(A:.A)").unwrap(); + let e = parse("SUM(A1.:C1)+SUM(A:.A)")?; assert_eq!(print(&adjust(e, "S", "S", &del)), "SUM(A1.:B1)+SUM(#REF!)"); - assert_eq!(print(&shift_relative(parse("SUM(A1.:.B2)").unwrap(), 1, 1)), "SUM(B2.:.C3)"); - let refs: Vec = parse("SUM(Sheet2!A1:.B5)").unwrap().references().iter().map(|r| printer::reference_a1(r)).collect(); + assert_eq!(print(&shift_relative(parse("SUM(A1.:.B2)")?, 1, 1)), "SUM(B2.:.C3)"); + let refs: Vec = parse("SUM(Sheet2!A1:.B5)")?.references().iter().map(|r| printer::reference_a1(r)).collect(); assert_eq!(refs, ["Sheet2!A1:B5"]); + Ok(()) } #[test] -fn prints_long_flat_chain_without_deep_recursion() { +fn prints_long_flat_chain_without_deep_recursion() -> Result<(), Box> { // Small stack: a recursive printer overflows long before 20000 levels. - let handle = std::thread::Builder::new() - .stack_size(256 * 1024) - .spawn(|| { - let mut e = Expr::Number(1.0); - for _ in 0..20000 { - e = Expr::Binary(BinOp::Add, Box::new(e), Box::new(Expr::Number(1.0))); - } - let text = print(&e); - let r1c1 = print_r1c1(&e, gridcraft_core::CellRef { row: 0, col: 0 }); - // Dropping the deep tree recurses; leak it so only the printer is under test. - std::mem::forget(e); - (text, r1c1) - }) - .expect("spawn"); - let (text, r1c1) = handle.join().expect("printer must not overflow the stack"); + let handle = std::thread::Builder::new().stack_size(256 * 1024).spawn(|| { + let mut e = Expr::Number(1.0); + for _ in 0..20000 { + e = Expr::Binary(BinOp::Add, Box::new(e), Box::new(Expr::Number(1.0))); + } + let text = print(&e); + let r1c1 = print_r1c1(&e, gridcraft_core::CellRef { row: 0, col: 0 }); + // Dropping the deep tree recurses; leak it so only the printer is under test. + std::mem::forget(e); + (text, r1c1) + })?; + let (text, r1c1) = handle.join().map_err(|_| "thread panicked")?; assert_eq!(text, vec!["1"; 20001].join("+")); assert_eq!(r1c1, text); + Ok(()) } #[test] -fn hostile_formulas_are_errors_not_stack_overflows() { +fn hostile_formulas_are_errors_not_stack_overflows() -> Result<(), Box> { // A small stack: parsing must fail cleanly instead of building trees deeper than the nesting // limit, which dropping, printing and evaluating walk recursively. - let handle = std::thread::Builder::new() - .stack_size(512 * 1024) - .spawn(|| { - let percents = format!("1{}", "%".repeat(1_000)); - let invokes = format!("LAMBDA(x,x)(1){}", "(1)".repeat(1_000)); - let long_chain = format!("1{}", "+1".repeat(100_000)); - let huge_percents = format!("1{}", "%".repeat(1_000_000)); - (parse(&percents).is_err(), parse(&invokes).is_err(), parse(&long_chain).is_err(), parse(&huge_percents).is_err()) - }) - .expect("spawn"); - assert_eq!(handle.join().expect("parsing must not overflow the stack"), (true, true, true, true)); + let handle = std::thread::Builder::new().stack_size(512 * 1024).spawn(|| { + let percents = format!("1{}", "%".repeat(1_000)); + let invokes = format!("LAMBDA(x,x)(1){}", "(1)".repeat(1_000)); + let long_chain = format!("1{}", "+1".repeat(100_000)); + let huge_percents = format!("1{}", "%".repeat(1_000_000)); + (parse(&percents).is_err(), parse(&invokes).is_err(), parse(&long_chain).is_err(), parse(&huge_percents).is_err()) + })?; + assert_eq!(handle.join().map_err(|_| "thread panicked")?, (true, true, true, true)); // Within the limits these still parse. assert!(parse(&format!("1{}", "%".repeat(100))).is_ok()); assert!(parse("LAMBDA(x,x)(1)(2)").is_ok()); + Ok(()) } #[test] -fn formulas_longer_than_excel_allows_are_refused() { +fn formulas_longer_than_excel_allows_are_refused() -> Result<(), Box> { let at_limit = format!("1{}", "+1".repeat((crate::MAX_FORMULA_LEN - 1) / 2)); assert_eq!(at_limit.chars().count(), crate::MAX_FORMULA_LEN - 1); assert!(parse(&at_limit).is_ok()); assert!(parse(&format!("={at_limit}")).is_ok(), "the leading = does not count"); let over = format!("{at_limit}+1"); - let err = parse(&over).unwrap_err(); + let err = parse(&over).err().ok_or("none")?; assert!(err.msg.contains("8192"), "{}", err.msg); // Characters, not bytes: 8,000 two-byte characters in a text literal are within the limit. let text = format!("\"{}\"", "é".repeat(8_000)); assert!(text.len() > crate::MAX_FORMULA_LEN); assert!(parse(&text).is_ok()); + Ok(()) } #[test] @@ -424,41 +436,40 @@ fn sheet_names_excel_quotes_stay_quoted() { } #[test] -fn longest_flat_chains_clone_compare_walk_and_adjust_without_deep_recursion() { +fn longest_flat_chains_clone_compare_walk_and_adjust_without_deep_recursion() -> Result<(), Box> { // `=1+1+…` with 4,096 operands is 8,192 characters, Excel's formula limit, and nests 4,095 // levels on its left. On a 1 MiB stack (the Windows main thread) the derived, recursive // Clone overflowed when the engine stored such a formula (#916). - let handle = std::thread::Builder::new() - .stack_size(1 << 20) - .spawn(|| { - let plus = vec!["1"; 4096].join("+"); - let e = parse(&plus).expect("parse"); - let copy = e.clone(); - assert!(copy == e); - let other = parse(&format!("{}-1", vec!["1"; 4095].join("+"))).expect("parse"); - assert!(other != e); - let mut nodes = 0; - e.walk(&mut |_| nodes += 1); - assert_eq!(nodes, 2 * 4096 - 1); - assert_eq!(print(©.map(&mut |x| x)), plus); - // A chain of references: walk finds them in order, adjust rewrites every one. - let refs = (1..=1000).map(|r| format!("A{r}")).collect::>().join("+"); - let e = parse(&refs).expect("parse"); - let found: Vec = e.references().iter().map(|r| printer::reference_a1(r)).collect(); - assert_eq!(found.first().map(String::as_str), Some("A1")); - assert_eq!(found.last().map(String::as_str), Some("A1000")); - assert_eq!(found.len(), 1000); - let ins = Edit::Insert { axis: Axis::Rows, at: 0, count: 1 }; - let moved = print(&adjust(e.clone(), "S", "S", &ins)); - assert_eq!(moved, (2..=1001).map(|r| format!("A{r}")).collect::>().join("+")); - assert_eq!(print(&shift_relative(e, 0, 1)), (1..=1000).map(|r| format!("B{r}")).collect::>().join("+")); - }) - .expect("spawn"); - handle.join().expect("long chains must not overflow the stack"); + let handle = std::thread::Builder::new().stack_size(1 << 20).spawn(|| { + let plus = vec!["1"; 4096].join("+"); + let e = parse(&plus)?; + let copy = e.clone(); + assert!(copy == e); + let other = parse(&format!("{}-1", vec!["1"; 4095].join("+")))?; + assert!(other != e); + let mut nodes = 0; + e.walk(&mut |_| nodes += 1); + assert_eq!(nodes, 2 * 4096 - 1); + assert_eq!(print(©.map(&mut |x| x)), plus); + // A chain of references: walk finds them in order, adjust rewrites every one. + let refs = (1..=1000).map(|r| format!("A{r}")).collect::>().join("+"); + let e = parse(&refs)?; + let found: Vec = e.references().iter().map(|r| printer::reference_a1(r)).collect(); + assert_eq!(found.first().map(String::as_str), Some("A1")); + assert_eq!(found.last().map(String::as_str), Some("A1000")); + assert_eq!(found.len(), 1000); + let ins = Edit::Insert { axis: Axis::Rows, at: 0, count: 1 }; + let moved = print(&adjust(e.clone(), "S", "S", &ins)); + assert_eq!(moved, (2..=1001).map(|r| format!("A{r}")).collect::>().join("+")); + assert_eq!(print(&shift_relative(e, 0, 1)), (1..=1000).map(|r| format!("B{r}")).collect::>().join("+")); + Ok::<(), crate::ParseError>(()) + })?; + handle.join().map_err(|_| "thread panicked")??; + Ok(()) } #[test] -fn spanish_input_counts_left_operand_of_a_chain_at_the_same_depth() { +fn spanish_input_counts_left_operand_of_a_chain_at_the_same_depth() -> Result<(), Box> { // The leftmost operand of a flat chain is not nested, so `(((…1)))+1` is accepted whenever // `(((…1)))` is, in both the pre-check and `normalize`. let wrap = |n: usize| { @@ -469,17 +480,18 @@ fn spanish_input_counts_left_operand_of_a_chain_at_the_same_depth() { e }; let es = |e: &Expr| crate::print_input(e, FormulaLocale::Es, |_| false); - let deepest = (0..400).take_while(|&n| es(&wrap(n)).is_ok()).last().expect("depth 0 is accepted"); + let deepest = (0..400).take_while(|&n| es(&wrap(n)).is_ok()).last().ok_or("none")?; assert!(deepest > 1 && deepest < 400); let chain = Expr::Binary(BinOp::Add, Box::new(wrap(deepest)), Box::new(Expr::Number(1.0))); assert!(es(&chain).is_ok()); + Ok(()) } #[test] -fn walk_is_preorder_and_map_is_postorder_left_to_right() { +fn walk_is_preorder_and_map_is_postorder_left_to_right() -> Result<(), Box> { // `(A1+B1)+SUM(C1,D1)-E1`: a chain with a call on its right. walk visits parents before // children, map rewrites children (left to right) before their parent. - let e = parse("(A1+B1)+SUM(C1,D1)-E1").unwrap(); + let e = parse("(A1+B1)+SUM(C1,D1)-E1")?; let mut walked = Vec::new(); e.walk(&mut |n| walked.push(print(n))); assert_eq!(walked, ["(A1+B1)+SUM(C1,D1)-E1", "(A1+B1)+SUM(C1,D1)", "(A1+B1)", "A1+B1", "A1", "B1", "SUM(C1,D1)", "C1", "D1", "E1"]); @@ -490,19 +502,21 @@ fn walk_is_preorder_and_map_is_postorder_left_to_right() { }); assert_eq!(mapped, ["A1", "B1", "A1+B1", "(A1+B1)", "C1", "D1", "SUM(C1,D1)", "(A1+B1)+SUM(C1,D1)", "E1", "(A1+B1)+SUM(C1,D1)-E1"]); assert_eq!(print(&out), "(A1+B1)+SUM(C1,D1)-E1"); + Ok(()) } #[test] -fn missing_closing_parentheses_are_closed_and_errors_describe_the_typed_text() { +fn missing_closing_parentheses_are_closed_and_errors_describe_the_typed_text() -> Result<(), Box> { // #2030: `1+` reported "unexpected `)`", the error of the last retry with `)` appended. let entry = |src: &str| parse_closing_parens(src, parse); assert_eq!(entry("SUM(1,2").map(|e| print(&e)), Ok("SUM(1,2)".to_string())); assert_eq!(entry("((1+2").map(|e| print(&e)), Ok("((1+2))".to_string())); assert_eq!(entry("1+2").map(|e| print(&e)), Ok("1+2".to_string())); for src in ["1+", "--", "SUM(1,+", "1+*2", &format!("{}1", "(".repeat(9))] { - assert_eq!(entry(src).unwrap_err(), parse(src).unwrap_err(), "{src}"); + assert_eq!(entry(src).err().ok_or("none")?, parse(src).err().ok_or("none")?, "{src}"); } - assert_eq!(entry("1+").unwrap_err().msg, "unexpected end of formula"); + assert_eq!(entry("1+").err().ok_or("none")?.msg, "unexpected end of formula"); + Ok(()) } #[test] @@ -521,7 +535,7 @@ fn three_d_span_and_cell_start_still_lex_as_span() { /// References print the same through the appending printer for every reference shape. #[test] -fn reference_shapes_print_unchanged() { +fn reference_shapes_print_unchanged() -> Result<(), Box> { for text in [ "A1", "XFD1048576", @@ -536,10 +550,11 @@ fn reference_shapes_print_unchanged() { "'Sheet 1:Sheet3'!B2", "SUM(A1:.B3)+SUM(A.:.B)", ] { - let e = parse(text).unwrap(); + let e = parse(text)?; assert_eq!(print(&e), text); if let Expr::Ref(r) = &e { assert_eq!(printer::reference_a1(r), text); } } + Ok(()) }