diff --git a/CHANGELOG.md b/CHANGELOG.md index f78c052..5240a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 best-effort non-Ferrum fallback in `Injector#evaluate_axe` is kept but is now documented as unsupported/untested ([#31](https://github.com/Guided-Rails/axe-cuprite/issues/31)). +- Documented that `AxeCuprite::Results` (and its nested wrappers) takes ownership + of the hash passed in and deep-freezes it **in place** — so a caller + constructing `Results.new(hash)` shouldn't pass or reuse a hash they intend to + mutate afterward. No behavior change; clarifies the existing read-only contract + in the `Results` doc comment and the README runner section + ([#33](https://github.com/Guided-Rails/axe-cuprite/issues/33)). ### Added - A unit test pinning the best-effort non-Ferrum fallback in diff --git a/README.md b/README.md index 427a854..8f9557c 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,13 @@ on top of your configured defaults. Only `violations` and `incomplete` are carried back across the CDP boundary — the full results object (with `passes`/`inapplicable`) can be huge. +`AxeCuprite::Results` is read-only: it **takes ownership of the hash it's given +and deep-freezes it in place** (this backs the read-only `raw`/`to_h` contract). +You normally get one straight from `Runner#run`, so this is transparent — but if +you construct `Results.new(hash)` yourself, don't pass or keep a reference to a +hash you intend to mutate afterward, or you'll hit a `FrozenError`. Dup it first +if you need a mutable copy. + ## Configuration ```ruby diff --git a/lib/axe/cuprite/results.rb b/lib/axe/cuprite/results.rb index ae6cae4..d4fb75f 100644 --- a/lib/axe/cuprite/results.rb +++ b/lib/axe/cuprite/results.rb @@ -26,6 +26,13 @@ def call(obj) # # The wrappers are read-only: `@raw` is deep-frozen at construction, so `raw` # and `to_h` expose the live underlying hash safely (callers cannot mutate it). + # + # Ownership note: `Results` (and the nested `Violation`/`Node`/`ContrastData` + # wrappers) **take ownership of the hash passed in and deep-freeze it in place** + # — they do not copy first. For the internal flow this is safe (the payload + # comes fresh off the CDP boundary), but if you construct `Results.new(hash)` + # yourself, don't pass — or hold onto — a hash you intend to mutate afterward, + # or you'll hit a `FrozenError`. Dup it first if you need a mutable copy. class Results attr_reader :raw