From 49714d772c05ff4dfc53a1ced0ee56433e2219fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Mon, 25 May 2026 18:24:31 -0600 Subject: [PATCH 1/6] Add validates_format_of multiline anchors pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rails 4 raises ArgumentError: regular expression is using multiline anchors (^ or $), which may present a security risk whenever a validates_format_of regex uses ^ or $. The validator runs check_options_validity at model class-load, so the failure is boot-fatal. Fix is mechanical: replace ^...$ with \A...\z (absolute start/end of string). Ruby's regex engine accepts both forms on every Rails version, so the rewrite ships pre-bump without a NextRails.next? branch. The primary regex catches only the literal validates_format_of token. Two known blind spots documented in the explanation/fix: - Block-options form: validates :col, format: { with: /^.../ } or validates :col, format: /^.../. Same error, different AST. The fix field provides a secondary grep targeting this shape. - Method-call regex args: validates_format_of :col, with: regex_for(:something). The regex body is invisible to grep. Fix at the helper method's return value — one fix covers every caller. Classified high_priority / kind: breaking — boot-fatal. --- .../detection-scripts/patterns/rails-40-patterns.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml b/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml index a9047ac..6d072db 100644 --- a/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml +++ b/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml @@ -193,6 +193,16 @@ upgrade_findings: fix: "Delete the config.whiny_nils = true line entirely. No replacement — Ruby's own nil-method errors are sufficient. False-positive watch: matches in comments documenting the removal; verify the hit is an actual assignment, not a comment" variable_name: "WHINY_NILS" + - name: "validates_format_of with multiline anchors (^...$)" + kind: "breaking" + pattern: "validates_format_of.*with:\\s*/[^/]*\\^" + exclude: "" + search_paths: + - "app/models/" + explanation: "Rails 4 raises `ArgumentError: regular expression is using multiline anchors (^ or $), which may present a security risk` whenever a `validates_format_of` regex uses `^` / `$`. The validator runs check_options_validity at model class-load, so the error is boot-fatal. Note: the primary regex only catches the literal `validates_format_of` token. The block-options form `validates :col, format: { with: /^.../ }` and method-call regex args (`with: regex_for(:x)`) raise the same error but are not detected by this entry — see fix for secondary grep" + fix: "Replace `^...$` with `\\A...\\z` (absolute start/end of string). Ruby's regex engine accepts both forms on every Rails version, so the rewrite ships pre-bump without a NextRails.next? branch. Secondary grep for the block-options form: `grep -rnE \"validates\\s+:\\w+.*format:.*?/[^/]*\\^\" app/ --include=\"*.rb\"`. For method-call regex args, fix at the helper method's return value — one fix covers every caller" + variable_name: "VALIDATES_FORMAT_OF_ANCHORS" + medium_priority: - name: "rescue_action method" kind: "breaking" From 7e0fc9d09df69b02b7d1ec4abf7478749e800203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Mon, 25 May 2026 18:25:01 -0600 Subject: [PATCH 2/6] Add Regexp.new constant-bound multiline anchors pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to VALIDATES_FORMAT_OF_ANCHORS. The primary anchor grep looks for literal /^.../ regexes inline in validates_format_of calls; it does not see regex constants defined via Regexp.new: EMAIL = Regexp.new(%q{^[^@]+@[^@]+$}) validates_format_of :address, with: EMAIL The validator-call line has no anchors visible to grep, but Rails 4 still raises at model class-load when check_options_validity examines the constant's compiled regex. This entry has its own detection target — constant definitions, not validator calls — so it gets its own pattern and variable_name. Regex covers the three common construction forms: Regexp.new(%q{^...$}) Regexp.new("^...$") Regexp.new(%r{^...$}) Fix is the same rewrite as the primary entry but applied at the constant's definition. One fix covers every caller, so detection plus per-constant rewrite is a small fixed-cost effort. Per-match judgment is required because not every Regexp.new constant feeds a validator. String#match / String#scan callers operating on multi-line input may need ^ / $ for start/end-of-line semantics; the fix field calls this out and recommends a call-site grep before rewriting. Classified high_priority / kind: breaking — boot-fatal at model class-load. --- .../detection-scripts/patterns/rails-40-patterns.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml b/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml index 6d072db..f997eed 100644 --- a/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml +++ b/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml @@ -203,6 +203,17 @@ upgrade_findings: fix: "Replace `^...$` with `\\A...\\z` (absolute start/end of string). Ruby's regex engine accepts both forms on every Rails version, so the rewrite ships pre-bump without a NextRails.next? branch. Secondary grep for the block-options form: `grep -rnE \"validates\\s+:\\w+.*format:.*?/[^/]*\\^\" app/ --include=\"*.rb\"`. For method-call regex args, fix at the helper method's return value — one fix covers every caller" variable_name: "VALIDATES_FORMAT_OF_ANCHORS" + - name: "Regex constant built via Regexp.new with multiline anchors" + kind: "breaking" + pattern: "Regexp\\.new\\([^)]*[\\^\\$]" + exclude: "" + search_paths: + - "app/" + - "lib/" + explanation: "Companion to VALIDATES_FORMAT_OF_ANCHORS. A regex constant defined via `Regexp.new(%q{^...$})` (or `Regexp.new(\"^...$\")`) is invisible to the primary anchor grep, which looks for literal `/^.../` regexes inline. Apps commonly extract validation regexes into constants — `EMAIL = Regexp.new(%q{^[^@]+@[^@]+$})` — then reference them by name in `validates_format_of`. Rails 4 still raises at model class-load when check_options_validity examines the constant's compiled regex" + fix: "Same rewrite as the primary pattern: `\\A` for `^`, `\\z` for `$`, applied at the constant's definition. One fix covers every caller. Backwards-compatible — Ruby accepts `\\A` / `\\z` on every Rails version. Per-match judgment required: not every `Regexp.new` constant feeds a validator. `String#match` / `String#scan` callers operating on multi-line input may need `^` / `$` for start/end-of-line semantics. Inspect call sites (`grep -rnE \"\\b\\b\" app/ lib/`) and only rewrite when every consumer is OK with absolute-anchor semantics" + variable_name: "VALIDATES_FORMAT_OF_ANCHORS_CONSTANT_BOUND" + medium_priority: - name: "rescue_action method" kind: "breaking" From f4c819d473a1b9997efe80eb1e6d8edee4047e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Mon, 25 May 2026 18:25:36 -0600 Subject: [PATCH 3/6] Add accepts_nested_attributes_for without matching association pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rails 3.2 accepted accepts_nested_attributes_for :foo even when :foo was not a declared association — projects sometimes paired it with a hand-rolled reader (polymorphic lookup, memoized custom join) and Rails generated the foo_attributes= writer against whatever the override returned. Rails 4.0 looks up the reflection at class-load and raises ArgumentError: No association found for name 'foo' at boot. The protected_attributes bridge gem (commonly added during the 3.2 → 4.0 hop to keep attr_accessible alive) hits the same check earlier in its accessible_attributes setup, so the error can surface before AR's own reflection lookup runs. Boot-fatal — the offending class fails to load before any spec runs, taking the suite down. Fix is branched: add belongs_to :foo, polymorphic: true (or the matching has_one / has_many) under `if NextRails.next?`. Ruby method lookup resolves the explicit `def foo` first, so the hand-rolled override keeps winning while the reflection check passes on the next side. Why the gate: declaring unconditionally on the current side introduces a new auto-generated reader+writer on Rails 3.2 where neither existed. Even if `def foo` shadows the reader (placement-dependent), the new writer makes `record.foo = ...` silently succeed where it previously raised NoMethodError. Inheritance pitfall documented in the explanation/fix: the missing reflection may be a side effect of a parallel gem bump (e.g. paper_trail 2.x's top-level Version moves to PaperTrail::Version in 3.x — the inherited reflection disappears with no app code change). Check the gem's migration docs before patching the subclass. The primary regex catches all accepts_nested_attributes_for calls; most matches are benign (model declares both the macro and the matching association). Operator filters to the breaking ones by inspecting each model — the explanation calls this out so reviewers don't expect every hit to be actionable. Classified high_priority / kind: breaking. --- .../detection-scripts/patterns/rails-40-patterns.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml b/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml index f997eed..a789e2f 100644 --- a/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml +++ b/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml @@ -214,6 +214,17 @@ upgrade_findings: fix: "Same rewrite as the primary pattern: `\\A` for `^`, `\\z` for `$`, applied at the constant's definition. One fix covers every caller. Backwards-compatible — Ruby accepts `\\A` / `\\z` on every Rails version. Per-match judgment required: not every `Regexp.new` constant feeds a validator. `String#match` / `String#scan` callers operating on multi-line input may need `^` / `$` for start/end-of-line semantics. Inspect call sites (`grep -rnE \"\\b\\b\" app/ lib/`) and only rewrite when every consumer is OK with absolute-anchor semantics" variable_name: "VALIDATES_FORMAT_OF_ANCHORS_CONSTANT_BOUND" + - name: "accepts_nested_attributes_for without matching association" + kind: "breaking" + pattern: "accepts_nested_attributes_for\\b" + exclude: "" + search_paths: + - "app/models/" + - "lib/" + explanation: "Rails 3.2 accepted `accepts_nested_attributes_for :foo` even when `:foo` was not a declared association — projects sometimes paired it with a hand-rolled reader (polymorphic lookup, memoized custom join) and Rails generated the `foo_attributes=` writer against whatever the override returned. Rails 4.0 looks up the reflection at class-load and raises `ArgumentError: No association found for name 'foo'` at boot. The protected_attributes bridge gem (used during the 3.2 → 4.0 hop to keep attr_accessible alive) hits the same check earlier, so the error can surface even before AR's own reflection lookup runs. Boot-fatal — the class fails to load before any spec runs. Note: the primary regex catches ALL `accepts_nested_attributes_for` calls; most matches are benign (the model already declares the matching association). Operator filters to the breaking ones by inspecting each model" + fix: "Add `belongs_to :foo, polymorphic: true` (or the matching has_one / has_many) under `if NextRails.next?` so the auto-generated reader is registered with AR but does NOT replace the hand-rolled `def foo` override — Ruby method lookup resolves the explicit `def foo` first. Why the gate: declaring unconditionally on the current side introduces a new auto-generated reader+writer on Rails 3.2 where neither existed; even if `def foo` shadows the reader (placement-dependent), the new writer makes `record.foo = ...` silently succeed where it previously raised NoMethodError. Inheritance pitfall: the missing reflection may be a side effect of a parallel gem bump (e.g. paper_trail 2.x's top-level `Version` moves to `PaperTrail::Version` in 3.x — inherited reflection disappears with no app code change). Check the gem's migration docs before patching the subclass. After cutover, drop the gate" + variable_name: "ACCEPTS_NESTED_NO_REFLECTION" + medium_priority: - name: "rescue_action method" kind: "breaking" From fc1f1ead57cb020f5878a237f725211b008442f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Wed, 27 May 2026 14:32:45 -0600 Subject: [PATCH 4/6] Reframe accepts_nested_attributes_for pattern as gem-bump reflection loss The prior entry claimed Rails 4.0 newly raises on a missing association vs 3.2 silently allowing it. That is false: the ArgumentError raise in accepts_nested_attributes_for is byte-identical at v3.2.22.5 and v4.0.0 and predates both. A genuinely-missing association never booted on 3.2, so it is not a 3.2 to 4.0 regression. Reframe around the real trigger: an inherited association reflection from a gem-supplied base class disappears when that gem is bumped in Gemfile.next (paper_trail 2.x top-level Version to 3.x PaperTrail::Version). Document both remedies: root-cause re-point of the base class on the next side, and the per-model gated re-declaration. Strip app-specific detail. --- .../detection-scripts/patterns/rails-40-patterns.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml b/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml index a789e2f..7e50b5e 100644 --- a/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml +++ b/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml @@ -214,15 +214,15 @@ upgrade_findings: fix: "Same rewrite as the primary pattern: `\\A` for `^`, `\\z` for `$`, applied at the constant's definition. One fix covers every caller. Backwards-compatible — Ruby accepts `\\A` / `\\z` on every Rails version. Per-match judgment required: not every `Regexp.new` constant feeds a validator. `String#match` / `String#scan` callers operating on multi-line input may need `^` / `$` for start/end-of-line semantics. Inspect call sites (`grep -rnE \"\\b\\b\" app/ lib/`) and only rewrite when every consumer is OK with absolute-anchor semantics" variable_name: "VALIDATES_FORMAT_OF_ANCHORS_CONSTANT_BOUND" - - name: "accepts_nested_attributes_for without matching association" + - name: "accepts_nested_attributes_for reflection lost to a parallel gem bump" kind: "breaking" pattern: "accepts_nested_attributes_for\\b" exclude: "" search_paths: - "app/models/" - "lib/" - explanation: "Rails 3.2 accepted `accepts_nested_attributes_for :foo` even when `:foo` was not a declared association — projects sometimes paired it with a hand-rolled reader (polymorphic lookup, memoized custom join) and Rails generated the `foo_attributes=` writer against whatever the override returned. Rails 4.0 looks up the reflection at class-load and raises `ArgumentError: No association found for name 'foo'` at boot. The protected_attributes bridge gem (used during the 3.2 → 4.0 hop to keep attr_accessible alive) hits the same check earlier, so the error can surface even before AR's own reflection lookup runs. Boot-fatal — the class fails to load before any spec runs. Note: the primary regex catches ALL `accepts_nested_attributes_for` calls; most matches are benign (the model already declares the matching association). Operator filters to the breaking ones by inspecting each model" - fix: "Add `belongs_to :foo, polymorphic: true` (or the matching has_one / has_many) under `if NextRails.next?` so the auto-generated reader is registered with AR but does NOT replace the hand-rolled `def foo` override — Ruby method lookup resolves the explicit `def foo` first. Why the gate: declaring unconditionally on the current side introduces a new auto-generated reader+writer on Rails 3.2 where neither existed; even if `def foo` shadows the reader (placement-dependent), the new writer makes `record.foo = ...` silently succeed where it previously raised NoMethodError. Inheritance pitfall: the missing reflection may be a side effect of a parallel gem bump (e.g. paper_trail 2.x's top-level `Version` moves to `PaperTrail::Version` in 3.x — inherited reflection disappears with no app code change). Check the gem's migration docs before patching the subclass. After cutover, drop the gate" + explanation: "`accepts_nested_attributes_for :foo` raises `ArgumentError: No association found for name \\`foo'. Has it been defined yet?` at model class-load whenever `:foo` has no association reflection. This raise is not new in Rails 4.0 — it fires on any Rails version when the reflection is absent at class-load, so a genuinely-missing association is not a 3.2 → 4.0 regression (it would already have failed boot on 3.2). The way it surfaces as a NEW, next-side-only boot failure during a dual-boot hop: the reflection is provided by INHERITANCE from a gem-supplied base class, and a parallel gem bump in Gemfile.next silently removes it. Example: paper_trail 2.x defines a top-level `Version` carrying `belongs_to :item`; 3.x namespaces it to `PaperTrail::Version` and drops the top-level constant, so app models declared `< Version` that call `accepts_nested_attributes_for :item` lose the inherited reflection on the next side only. Operator filter — the primary regex matches ALL `accepts_nested_attributes_for` calls; a match is breaking ONLY when (a) the association is inherited from a gem-provided base class AND (b) that gem is being version-bumped in Gemfile.next. A same-file declared association is safe" + fix: "Restore the lost reflection on the next side; leave the current side untouched. Two ways. (1) ROOT-CAUSE, preferred when a gem namespace / inheritance move is the cause: re-point the inherited base class in ONE place under the gate, leaving every subclass and reference unchanged. For the paper_trail example: `if NextRails.next?\\n Version = PaperTrail::Version\\nend` in config/initializers/paper_trail.rb. (2) PER-MODEL, when the loss is per-model rather than a shared base class: re-declare the association on each affected model under the gate — `belongs_to :foo, polymorphic: true if NextRails.next?` (or the matching has_one / has_many). Why gate it: declaring unconditionally adds a duplicate auto-generated reader+writer on the current side, where the reflection still arrives via inheritance. A hand-rolled `def foo` override is preserved either way — Ruby resolves the explicit method before the generated reader. After cutover, drop the `NextRails.next?` gate" variable_name: "ACCEPTS_NESTED_NO_REFLECTION" medium_priority: From aac8272ff428ee1f4030b2888025fa70711a2a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Wed, 27 May 2026 14:44:59 -0600 Subject: [PATCH 5/6] Fix anchor-pattern false negatives and align error message VALIDATES_FORMAT_OF_ANCHORS only matched a leading caret, missing trailing-dollar-only regexes (with: /foo$/) that Rails 4 also rejects. Widen to [\^$]. VALIDATES_FORMAT_OF_ANCHORS_CONSTANT_BOUND truncated at the first ) so grouped regexes (Regexp.new("(a|b)$")) were missed; drop the [^)] cap. Quote the verbatim v4.0.0 ArgumentError message, note the check is new in 4.0, and document that only a leading ^ or trailing $ actually raises (char-class ^ is a false positive). Drop the stray NextRails.next? reference from the anchors fix - the rewrite is backwards-compatible and needs no gate. --- .../detection-scripts/patterns/rails-40-patterns.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml b/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml index 7e50b5e..047ca7a 100644 --- a/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml +++ b/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml @@ -195,22 +195,22 @@ upgrade_findings: - name: "validates_format_of with multiline anchors (^...$)" kind: "breaking" - pattern: "validates_format_of.*with:\\s*/[^/]*\\^" + pattern: "validates_format_of.*with:\\s*/[^/]*[\\^$]" exclude: "" search_paths: - "app/models/" - explanation: "Rails 4 raises `ArgumentError: regular expression is using multiline anchors (^ or $), which may present a security risk` whenever a `validates_format_of` regex uses `^` / `$`. The validator runs check_options_validity at model class-load, so the error is boot-fatal. Note: the primary regex only catches the literal `validates_format_of` token. The block-options form `validates :col, format: { with: /^.../ }` and method-call regex args (`with: regex_for(:x)`) raise the same error but are not detected by this entry — see fix for secondary grep" - fix: "Replace `^...$` with `\\A...\\z` (absolute start/end of string). Ruby's regex engine accepts both forms on every Rails version, so the rewrite ships pre-bump without a NextRails.next? branch. Secondary grep for the block-options form: `grep -rnE \"validates\\s+:\\w+.*format:.*?/[^/]*\\^\" app/ --include=\"*.rb\"`. For method-call regex args, fix at the helper method's return value — one fix covers every caller" + explanation: "Rails 4 raises `ArgumentError: The provided regular expression is using multiline anchors (^ or $), which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the :multiline => true option?` whenever a `validates_format_of` regex source STARTS with `^` or ENDS with `$` (unescaped). New in 4.0 — Rails 3.2 does not perform this check. The validator runs check_options_validity at model class-load, so the error is boot-fatal. Detection caveat: this grep flags any `^` or `$` inside the `with:` regex, but Rails only raises on a leading `^` or trailing `$`. A `^` inside a character class (e.g. `/[^@]+/`) or a `$` mid-pattern is a FALSE POSITIVE — confirm the anchor sits at the start/end before fixing. Out of scope for the primary regex: the block-options form `validates :col, format: { with: /^.../ }`, method-call regex args (`with: regex_for(:x)`), and positional / `without:` regexes — see fix for a secondary grep" + fix: "Replace a leading `^` with `\\A` and a trailing `$` with `\\z` (absolute start/end of string). Ruby's regex engine accepts both forms on every Rails version, so apply the rewrite directly — it is backwards-compatible and needs no version gate. Secondary grep for the block-options form: `grep -rnE \"validates\\s+:\\w+.*format:.*?/[^/]*[\\^$]\" app/ --include=\"*.rb\"`. For method-call regex args, fix at the helper method's return value — one fix covers every caller" variable_name: "VALIDATES_FORMAT_OF_ANCHORS" - name: "Regex constant built via Regexp.new with multiline anchors" kind: "breaking" - pattern: "Regexp\\.new\\([^)]*[\\^\\$]" + pattern: "Regexp\\.new\\(.*[\\^$]" exclude: "" search_paths: - "app/" - "lib/" - explanation: "Companion to VALIDATES_FORMAT_OF_ANCHORS. A regex constant defined via `Regexp.new(%q{^...$})` (or `Regexp.new(\"^...$\")`) is invisible to the primary anchor grep, which looks for literal `/^.../` regexes inline. Apps commonly extract validation regexes into constants — `EMAIL = Regexp.new(%q{^[^@]+@[^@]+$})` — then reference them by name in `validates_format_of`. Rails 4 still raises at model class-load when check_options_validity examines the constant's compiled regex" + explanation: "Companion to VALIDATES_FORMAT_OF_ANCHORS. A regex constant defined via `Regexp.new(%q{^...$})` (or `Regexp.new(\"^...$\")`) is invisible to the primary anchor grep, which looks for literal `/^.../` regexes inline. Apps commonly extract validation regexes into constants — `EMAIL = Regexp.new(%q{^[^@]+@[^@]+$})` — then reference them by name in `validates_format_of`. Rails 4 still raises at model class-load when check_options_validity examines the constant's compiled regex. Same leading/trailing rule as the primary pattern: Rails raises only when the compiled source starts with `^` or ends with `$`. This grep flags any `^` / `$` after `Regexp.new(` (including a `^` inside a character class), so per-match judgment is required" fix: "Same rewrite as the primary pattern: `\\A` for `^`, `\\z` for `$`, applied at the constant's definition. One fix covers every caller. Backwards-compatible — Ruby accepts `\\A` / `\\z` on every Rails version. Per-match judgment required: not every `Regexp.new` constant feeds a validator. `String#match` / `String#scan` callers operating on multi-line input may need `^` / `$` for start/end-of-line semantics. Inspect call sites (`grep -rnE \"\\b\\b\" app/ lib/`) and only rewrite when every consumer is OK with absolute-anchor semantics" variable_name: "VALIDATES_FORMAT_OF_ANCHORS_CONSTANT_BOUND" From 453ff10740bcb896cd4472cf0c5236615952c46a Mon Sep 17 00:00:00 2001 From: Ernesto Tagwerker Date: Sun, 12 Jul 2026 10:19:50 -0400 Subject: [PATCH 6/6] VALIDATES_FORMAT_OF_ANCHORS: also match hash-rocket :with => MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pattern only matched the 1.9 hash syntax `with: /.../`, missing the Ruby-1.8 hash-rocket `:with => /.../` that is common in Rails 3.2-era code — exactly the code being upgraded from. Widen the key matcher to `with(?::|\s*=>)` so both syntaxes flag. `without:` stays out of scope. Add fixtures: a hash-rocket match, a hash-rocket already-fixed no_match, and a without: no_match. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../patterns/rails-40-patterns.expectations.yml | 6 ++++++ .../detection-scripts/patterns/rails-40-patterns.yml | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/rails-upgrade/detection-scripts/patterns/rails-40-patterns.expectations.yml b/rails-upgrade/detection-scripts/patterns/rails-40-patterns.expectations.yml index ccf6b24..b610962 100644 --- a/rails-upgrade/detection-scripts/patterns/rails-40-patterns.expectations.yml +++ b/rails-upgrade/detection-scripts/patterns/rails-40-patterns.expectations.yml @@ -341,11 +341,17 @@ VALIDATES_FORMAT_OF_ANCHORS: - ' validates_format_of :email, with: /^[a-z]+$/' # trailing $ only still flags - ' validates_format_of :name, with: /foo$/' + # Ruby-1.8 hash-rocket syntax common in 3.2-era code + - ' validates_format_of :email, :with => /^[a-z]+$/' no_match: # already rewritten to \A / \z — the correct Rails 4 form - ' validates_format_of :email, with: /\A[a-z]+\z/' + # hash-rocket form, already rewritten — must not flag + - ' validates_format_of :email, :with => /\Afoo\z/' # block-options form is out of scope for the primary regex - ' validates :email, format: { with: /^x$/ }' + # without: is a different option, out of scope + - ' validates_format_of :x, without: /^foo$/' VALIDATES_FORMAT_OF_ANCHORS_CONSTANT_BOUND: # Pattern: "Regexp\\.new\\(.*[\\^$]" diff --git a/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml b/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml index e3a4dd3..c44defa 100644 --- a/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml +++ b/rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml @@ -195,11 +195,11 @@ upgrade_findings: - name: "validates_format_of with multiline anchors (^...$)" kind: "breaking" - pattern: "validates_format_of.*with:\\s*/[^/]*[\\^$]" + pattern: "validates_format_of.*\\bwith(?::|\\s*=>)\\s*/[^/]*[\\^$]" exclude: "" search_paths: - "app/models/" - explanation: "Rails 4 raises `ArgumentError: The provided regular expression is using multiline anchors (^ or $), which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the :multiline => true option?` whenever a `validates_format_of` regex source STARTS with `^` or ENDS with `$` (unescaped). New in 4.0 — Rails 3.2 does not perform this check. The validator runs check_options_validity at model class-load, so the error is boot-fatal. Detection caveat: this grep flags any `^` or `$` inside the `with:` regex, but Rails only raises on a leading `^` or trailing `$`. A `^` inside a character class (e.g. `/[^@]+/`) or a `$` mid-pattern is a FALSE POSITIVE — confirm the anchor sits at the start/end before fixing. Out of scope for the primary regex: the block-options form `validates :col, format: { with: /^.../ }`, method-call regex args (`with: regex_for(:x)`), and positional / `without:` regexes — see fix for a secondary grep" + explanation: "Rails 4 raises `ArgumentError: The provided regular expression is using multiline anchors (^ or $), which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the :multiline => true option?` whenever a `validates_format_of` regex source STARTS with `^` or ENDS with `$` (unescaped). New in 4.0 — Rails 3.2 does not perform this check. The validator runs check_options_validity at model class-load, so the error is boot-fatal. Matches both the 1.9 hash syntax `with: /.../` and the Ruby-1.8 hash-rocket `:with => /.../` still common in Rails 3.2-era code. Detection caveat: this grep flags any `^` or `$` inside the `with:` regex, but Rails only raises on a leading `^` or trailing `$`. A `^` inside a character class (e.g. `/[^@]+/`) or a `$` mid-pattern is a FALSE POSITIVE — confirm the anchor sits at the start/end before fixing. Out of scope for the primary regex: the block-options form `validates :col, format: { with: /^.../ }`, method-call regex args (`with: regex_for(:x)`), and positional / `without:` regexes — see fix for a secondary grep" fix: "Replace a leading `^` with `\\A` and a trailing `$` with `\\z` (absolute start/end of string). Ruby's regex engine accepts both forms on every Rails version, so apply the rewrite directly — it is backwards-compatible and needs no version gate. Secondary grep for the block-options form: `grep -rnE \"validates\\s+:\\w+.*format:.*?/[^/]*[\\^$]\" app/ --include=\"*.rb\"`. For method-call regex args, fix at the helper method's return value — one fix covers every caller" variable_name: "VALIDATES_FORMAT_OF_ANCHORS"