From 4672b6da51b5cf89093e919f7fe1720c69dc4fd9 Mon Sep 17 00:00:00 2001 From: prxnvt <53838599+prxnvt@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:48:53 -0400 Subject: [PATCH] Model: an input name that reads as a cell reference is refused A macro could declare an input called Q1, AB12 or Sum1, and a step written with plain A1 text, as a recorded one is, would then resolve to the input's range instead of the cell. check_limits refuses such names; the docs say so. Regression test. --- crates/engine/src/macros/tests.rs | 14 ++++++++++++++ crates/model/src/macros.rs | 4 ++++ docs/macros.md | 5 ++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/macros/tests.rs b/crates/engine/src/macros/tests.rs index 77e9fb106..99235ec68 100644 --- a/crates/engine/src/macros/tests.rs +++ b/crates/engine/src/macros/tests.rs @@ -149,6 +149,20 @@ fn a_visible_name_that_looks_like_macro_storage_is_left_alone() { assert_eq!(wb.names, vec![name]); } +#[test] +fn input_names_that_read_as_cell_references_are_refused() { + // A recorded step holds plain A1 text; an input called `Q1` would take its place. + let with_input = |name: &str| json!({"name": "M", "inputs": [{"name": name}]}); + for name in ["Q1", "AB12", "Sum1", "a1"] { + let err = parse_macro(&with_input(name)).unwrap_err(); + assert!(err.contains("reads as a cell reference"), "{name}: {err}"); + } + for name in ["Amount", "Q", "Sales_Q1", "Total1a"] { + assert!(parse_macro(&with_input(name)).is_ok(), "{name}"); + } + assert!(parse_macro(&with_input("Row")).unwrap_err().contains("token of its own")); +} + #[test] fn a_hidden_macro_name_this_build_did_not_write_is_kept() { // `_gc_macro_index_0` is not a numbered item: it is neither read nor dropped on open, and a diff --git a/crates/model/src/macros.rs b/crates/model/src/macros.rs index 92afac61a..54f975b04 100644 --- a/crates/model/src/macros.rs +++ b/crates/model/src/macros.rs @@ -113,6 +113,10 @@ impl Macro { if ["Output", "Row", "Column", "Name"].iter().any(|r| r.eq_ignore_ascii_case(n)) { return Err(format!("'{n}' is a token of its own and can't name an input.")); } + // A step written with plain A1 text (a recorded one) must keep meaning the cell. + if gridcraft_core::RangeRef::parse(n).is_some() { + return Err(format!("'{n}' reads as a cell reference and can't name an input.")); + } if self.inputs.iter().take(k).any(|o| o.name.eq_ignore_ascii_case(n)) { return Err(format!("Two inputs are called '{n}'.")); } diff --git a/docs/macros.md b/docs/macros.md index 65160fad4..f98435bb5 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -39,7 +39,10 @@ version. - **Inputs**: `kind` (`cell` or `range`), `rows` and `cols` as `[min, max]` (0 for no maximum; rows exclude the header), `header`, `colTypes` (`number`, `text`, `date`, `bool`, `any`), `nonEmpty` and `defaultRef`. These declare what a range must look like for the macro to work - on it. This version stores and checks the declaration; it does not bind inputs or run macros. + on it. An input's name is letters, digits and `_`, starting with a letter, and is neither a + token of its own (`Output`, `Row`, `Column`, `Name`) nor something that reads as a cell + reference (`Q1`), so a step written with plain A1 text keeps meaning the cell. This version + stores and checks the declaration; it does not bind inputs or run macros. - **Limits**: 1,000 macros a workbook, 10,000 steps a macro, and Repeat and If nested 4 deep. ## Storage