Engine: the name commands use the stash's own rule for what holds its data

The guard asked for a hidden name, but the stash reads a legacy _sc_<key>
name whether it is hidden or not, so a visible one could be removed or
replaced without force. stash::holds_item is the one rule (item_of), shared
by the guard; the test covers a visible legacy name.
This commit is contained in:
prxnvt
2026-10-24 01:41:03 -04:00
parent b09a5502e3
commit 4a294aefa9
2 changed files with 13 additions and 2 deletions
+6 -2
View File
@@ -392,7 +392,8 @@ fn rename_name_in_formulas(wb: &mut gridcraft_model::Workbook, old: &str, old_sc
/// name of an item's shape. A visible name of that shape is the person's own (see `stash`), and
/// the name commands may change it.
fn holds_gridcraft_data(wb: &Workbook, name: &str) -> bool {
wb.names.iter().any(|n| n.hidden && n.scope.is_none() && n.name.eq_ignore_ascii_case(name) && stash::item_key(&n.name).is_some())
// The stash's own rule, so the two never disagree: a legacy `_sc_` item is read visible or not.
stash::holds_item(wb, name)
}
fn refuse_stash_name(wb: &Workbook, p: &Json, name: &str) -> Result<()> {
@@ -787,6 +788,8 @@ mod tests {
let text = "m".repeat(9_000);
stash::set(wb, "macro_1", &text).unwrap();
wb.names.push(DefinedName { name: "_sc_watch".into(), scope: None, formula: "\"[]\"".into(), comment: String::new(), hidden: true });
// A legacy item is read whether or not it is hidden, so a visible one is data too.
wb.names.push(DefinedName { name: "_sc_old".into(), scope: None, formula: "\"[]\"".into(), comment: String::new(), hidden: false });
let names = |s: &Session| s.doc().unwrap().wb.names.clone();
let before = names(&s);
assert!(before.len() >= 3, "the item takes two chunks");
@@ -795,11 +798,12 @@ mod tests {
json!({"name": "_SC_Watch", "refersTo": "=1"}),
json!({"name": "Mine", "refersTo": "=1", "oldName": "_gc_macro_1_1"}),
json!({"name": "_gc_macro_1_0", "refersTo": "=1", "force": false}),
json!({"name": "Mine2", "refersTo": "=1", "oldName": "_SC_old"}),
] {
let err = s.execute("formulas.defineName", params.clone()).unwrap_err().to_string();
assert!(err.contains("force"), "{params}: {err}");
}
for name in ["_gc_macro_1_1", "_sc_watch"] {
for name in ["_gc_macro_1_1", "_sc_watch", "_sc_old"] {
let err = s.execute("formulas.deleteName", json!({"name": name})).unwrap_err().to_string();
assert!(err.contains("force"), "{name}: {err}");
}
+7
View File
@@ -215,6 +215,13 @@ pub fn keys(wb: &Workbook, prefix: &str) -> Vec<String> {
group(wb, prefix).into_iter().filter(|(_, parts)| parts.is_item()).map(|(k, _)| k).take(MAX_ITEMS).collect()
}
/// Whether the workbook holds an item under `name` (compared without case): a hidden workbook-scope
/// `_gc_<key>_<k>` name, or a workbook-scope legacy `_sc_<key>` name, which is read whether or not
/// it is hidden. A visible `_gc_` name is the person's own.
pub fn holds_item(wb: &Workbook, name: &str) -> bool {
wb.names.iter().any(|n| n.name.eq_ignore_ascii_case(name) && item_of(n).is_some())
}
/// The item a hidden name belongs to, lower-cased: `_gc_<key>_<k>` and `_sc_<key>` give `<key>`.
pub fn item_key(name: &str) -> Option<String> {
parse_name(name).map(|(k, _)| k.to_ascii_lowercase())