Repository navigation
An each over a fresh String Array whose answer is read stores each mutated element back - #8020
Conversation
A String yielded out of an Array and mutated in place is refused ("not
yet shared by reference") when the Array is a call's result or a local
bound to one, since the parameter binds a copy and the mutation would
not reach the element. Some of those shapes can never read the element
back, and a copy gives CRuby's answer:
- a fresh String Array a String builtin answers (split, scan, lines,
chars) iterated where the iterator's answer is dropped, or is the
block's values (map/collect), or map!/collect!, which store the block's
values over the elements;
- map!/collect! over a local only ever bound to such an Array, with no
read of the local before the replacement (none can hand an element
out) and no loop around it.
Actionview's split_paragraphs (`...split(/\n\n+/).map! { |t| t.gsub!(..)
|| t }`) and rack's parse_http_accept_header (`parts = header.split(",");
parts.map! { |part| part.strip!; ... }`) stopped the actionpack app here.
An each whose answer is read, a select or find, and a local read before
or after an each still refuse (test/reject/string_split_*).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tated element back
`text.scan(re).each { |segment| segment.gsub!(..) }.join` (actionview's
highlight) mutates each element in place and then reads the Array each
answers. The block parameter binds a String Array's element as a copy,
so the mutation never reached the element, and the shape was refused.
Nothing else holds that Array or its Strings (a fresh split/scan/lines/
chars result), so storing the parameter's final value back over its
element is the same program: the call becomes map! with the parameter
appended to its block (`map! { |x| ...; x }`), which answers the Array
too. Only for a block with one plain parameter it mutates in place, that
runs to its end every time (no next, redo or retry) and never rebinds the
parameter; a break answers its value either way and leaves the Array
unread.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (7)
Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review. 📝 WalkthroughWalkthroughCompiler analysis now handles eligible in-place mutations of strings from fresh String arrays. The change adds conditions for rewriting iterator blocks and for allowing local ChangesFresh String Array Mutation
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Bug fix Merge Risk: ⚪ Minimal · up to The change lets the compiler accept a constrained set of in-place mutations to strings from freshly split or scanned arrays. Other cases are still refused, and new example and rejection tests cover both outcomes. No blocking issues remain. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change does not add an exposed service endpoint. An ownership gap remains: an iteration block can retain another reference to an element before mutating it, while the new rewrite preserves the array result without establishing equivalent behavior for that reference. Retained concerns
Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Hardening Proposals
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1⚔️ Resolve merge conflicts 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Spinel refused three common shapes where a String yielded from a fresh Array is mutated in place, reporting the String as "not yet shared by reference". In all three, CRuby's answer is reproduced by mutating a copy, or by writing the element back:
TextHelper#highlight:text.scan(/<[^>]*|[^<]+/).each do |segment| … segment.gsub!(…) … end.join(theeachanswer is read).split_paragraphs:text.to_str.gsub(…).split(/\n\n+/).map! { |t| t.gsub!(…) || t }.MediaType.parse_http_accept_header:parts = header.to_s.split(','); parts.map! { |part| part.strip!; … }.A "fresh Array" here is the block-less result of
split/scan/lines/charson a String, typed as a String Array: nothing else holds it or its Strings.A String mutated through a fresh Array nothing reads back is not refused (
promote_shared_stored_strings's twoshare_route_defersites):map/collectormap!/collect!.map!/collect!when all of these hold:map!;||=,+=or multiple-assignment write;map!ends;eachwith its answer used,select,find,each_with_objectand the rest stay refused.An
eachover a fresh String Array whose answer is read stores each mutated element back: an in-fixpoint desugar rewritesFRESH.each { |x| BODY }toFRESH.map! { |x| BODY; x }when the answer is used, the block has one plain parameter it mutates in place, and the block has nonext/redo/retryand never rebinds the parameter.map!also answers the Array, so it's the same program, and commit 1 accepts it.The test
test/fresh_string_array_element_mutation.rbcovers the three app shapes, plus:eachwith its answer dropped;map;eachwhose answer is printed;lines.each(&:chomp!).Its expected output comes from CRuby. Three new refusal tests in
make reject-testkeep the observable cases refused:a = s.split; a.each(&:strip!); p a;select;map!.The 801 corpus tests matching strbuf/shared/string/mutat/bang/strip/each/map/scan/split all pass, as do
make share-strings-testandmake reject-test.This textually conflicts with #8014, since both insert code just before
promote_shared_stored_strings; keep both blocks.🤖 Generated with Claude Code
Summary by CodeRabbit