From c16c833c58cf36fb6d31ac69d364aafebd22d02c Mon Sep 17 00:00:00 2001 From: Franco Jacquet <54250139+FrancoJacquet@users.noreply.github.com> Date: Wed, 4 Nov 2026 08:13:52 +0700 Subject: [PATCH] Check stroke.set params: a misnamed key is refused (#1442) Squashed from #5453 onto main. --- contributors/people.toml | 3 +++ crates/engine/src/cmd/params.rs | 1 - crates/engine/src/cmd/stroke.rs | 2 +- crates/engine/src/tests/params.rs | 21 +++++++++++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/contributors/people.toml b/contributors/people.toml index 0e8dae937..f4e508fdf 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -198,3 +198,6 @@ display_name = "Jonathan Cardoso" real_name = "Valentin Klein" display_name = "vangel76" sync_github_name = false + +[people.FrancoJacquet] +real_name = "Franco Jacquet" diff --git a/crates/engine/src/cmd/params.rs b/crates/engine/src/cmd/params.rs index 875721b92..0c0a81ed9 100644 --- a/crates/engine/src/cmd/params.rs +++ b/crates/engine/src/cmd/params.rs @@ -439,7 +439,6 @@ pub const UNCHECKED: &[&str] = &[ "shape.flare", "shape.polarGrid", "shape.rectangularGrid", - "stroke.set", "stroke.setAdvanced", "swatch.edit", "swatch.library.load", diff --git a/crates/engine/src/cmd/stroke.rs b/crates/engine/src/cmd/stroke.rs index 03b927cd3..3b1289a86 100644 --- a/crates/engine/src/cmd/stroke.rs +++ b/crates/engine/src/cmd/stroke.rs @@ -20,7 +20,7 @@ pub fn specs() -> Vec { "Stroke Options", ["Window", "Stroke"], None, - "{weight?: pt, cap?: butt|round|square, join?: miter|round|bevel, miterLimit?, align?: center|inside|outside, dash?: [d,g,…]|null (a 0 dash with a round or projecting cap draws dots or squares; a new pattern keeps the current offset and alignment), dashOffset? (exact dashes only), alignDashes?: bool (true: dashes fitted to corners and path ends, every run between them holding whole periods with a dash centred on each corner and end; false, the default for a new pattern: exact lengths), startArrow?, endArrow?: Arrow|ArrowOpen|Barbed|Concave|DoubleArrow|HalfArrowLeft|HalfArrowRight|Chevron|Feather|Swallowtail|Triangle|TriangleOpen|TriangleReverse|TriangleBar|Kite|Leaf|Drop|Circle|CircleOpen|HalfCircle|Oval|Target|Square|SquareOpen|Tag|TagOpen|Diamond|DiamondOpen|Hexagon|HexagonOpen|Star|Cross|Plus|Bar|DoubleBar|DotOnBar|Slash|DoubleSlash|Bracket|Fork|null (HalfArrowLeft/Right: one barb, left or right of the direction the head points), arrowAlign?: \"extend\" (tip past the end point, default)|\"tip\" (tip on the end point; the stroke is shortened), profile?: \"uniform\"|\"lens\"|\"taperStart\"|\"taperEnd\"|\"pinch\"|\"teardrop\"|\"wave\"|a saved profile's name (stroke.widthProfile.list), item?: stroke item index|null (omitted: the Appearance panel's active item if it is a stroke, else the top stroke, created when missing), ids?|id?, target?: \"object\"|\"contents\"} Without a stroke item, type takes weight, cap, join, miterLimit, align (around its glyph outlines; the type stays live) and the dash options as its characters' stroke (every run; text.setRangeStyle `strokeOptions` styles a range), and images and symbol instances (also in groups) are left alone. With nothing selected (and no ids) they set up the next object drawn (appearance.newArt; weight always does) → {objects: how many objects took the stroke options}", + "{weight?: pt, cap?: butt|round|square, join?: miter|round|bevel, miterLimit?, align?: center|inside|outside, dash?: [d,g,…]|null (a 0 dash with a round or projecting cap draws dots or squares; a new pattern keeps the current offset and alignment), dashOffset? (exact dashes only), alignDashes?: bool (true: dashes fitted to corners and path ends, every run between them holding whole periods with a dash centred on each corner and end; false, the default for a new pattern: exact lengths), startArrow?, endArrow?: Arrow|ArrowOpen|Barbed|Concave|DoubleArrow|HalfArrowLeft|HalfArrowRight|Chevron|Feather|Swallowtail|Triangle|TriangleOpen|TriangleReverse|TriangleBar|Kite|Leaf|Drop|Circle|CircleOpen|HalfCircle|Oval|Target|Square|SquareOpen|Tag|TagOpen|Diamond|DiamondOpen|Hexagon|HexagonOpen|Star|Cross|Plus|Bar|DoubleBar|DotOnBar|Slash|DoubleSlash|Bracket|Fork|null (HalfArrowLeft/Right: one barb, left or right of the direction the head points), arrowAlign?: \"extend\" (tip past the end point, default)|\"tip\" (tip on the end point; the stroke is shortened), profile?: \"uniform\"|\"lens\"|\"taperStart\"|\"taperEnd\"|\"pinch\"|\"teardrop\"|\"wave\"|a saved profile's name (stroke.widthProfile.list), widthPoints?, gradientMode? (as document.inspect reports them in strokeOptions, so those read back unchanged; ignored here: a custom profile is set with the Width tool, gradientMode with paint.editGradient), item?: stroke item index|null (omitted: the Appearance panel's active item if it is a stroke, else the top stroke, created when missing), ids?|id?, target?: \"object\"|\"contents\", aligned?: bool (index and item are rows of the first target's stack: each other target edits its equal one, as the Appearance panel does for a mixed selection)} Without a stroke item, type takes weight, cap, join, miterLimit, align (around its glyph outlines; the type stays live) and the dash options as its characters' stroke (every run; text.setRangeStyle `strokeOptions` styles a range), and images and symbol instances (also in groups) are left alone. With nothing selected (and no ids) they set up the next object drawn (appearance.newArt; weight always does) → {objects: how many objects took the stroke options}", has_doc, stroke_set ), diff --git a/crates/engine/src/tests/params.rs b/crates/engine/src/tests/params.rs index a1f8e00cc..e989597ae 100644 --- a/crates/engine/src/tests/params.rs +++ b/crates/engine/src/tests/params.rs @@ -74,6 +74,27 @@ fn an_unknown_key_is_named_with_the_nearest_accepted_one() { assert!(check_keys("x", "{a, …as file.new}", &json!({"b": 1})).is_err()); } +/// `stroke.set` names every key it reads (its own, `item`, and `ids`/`id`/`target`), so a +/// misnamed one is refused instead of answering `objects: 1` with nothing changed. +#[test] +fn stroke_set_refuses_an_unknown_key() { + assert!(!UNCHECKED.contains(&"stroke.set")); + let e = refusal("stroke.set", json!({"ids": [7], "width": 0.5})); + assert!(e.starts_with("unknown parameter `width`") && e.contains("weight"), "{e}"); + for ok in [ + json!({"ids": [7], "weight": 0.5}), + json!({"id": 7, "weight": 1, "cap": "round", "join": "bevel", "miterLimit": 4, "align": "inside"}), + json!({"dash": [6, 3], "dashOffset": 1, "alignDashes": true, "startArrow": "Arrow", "endArrow": null, "arrowAlign": "tip"}), + json!({"profile": "lens", "item": null, "target": "contents"}), + // `item_target` reads `aligned` for an explicit item. + json!({"item": 1, "aligned": true, "weight": 2}), + // What document.inspect reports in strokeOptions reads back unchanged. + json!({"widthPoints": null, "gradientMode": "within"}), + ] { + assert!(check_params("stroke.set", &ok).is_ok(), "{ok}"); + } +} + /// A command that takes the keys of one that isn't checked yet isn't checked either. #[test] fn a_reference_to_an_unchecked_command_is_unchecked() {