upgrade_radar 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +537 -0
- data/exe/upgrade_radar +5 -0
- data/lib/upgrade_radar/audit_visitor.rb +121 -0
- data/lib/upgrade_radar/cli.rb +77 -0
- data/lib/upgrade_radar/configuration.rb +31 -0
- data/lib/upgrade_radar/effort_estimator.rb +63 -0
- data/lib/upgrade_radar/file_finder.rb +40 -0
- data/lib/upgrade_radar/finding.rb +43 -0
- data/lib/upgrade_radar/formatters/base.rb +20 -0
- data/lib/upgrade_radar/formatters/html_formatter.rb +20 -0
- data/lib/upgrade_radar/formatters/json_formatter.rb +26 -0
- data/lib/upgrade_radar/formatters/text_formatter.rb +96 -0
- data/lib/upgrade_radar/matchers/base.rb +32 -0
- data/lib/upgrade_radar/matchers/block_parameter_matcher.rb +19 -0
- data/lib/upgrade_radar/matchers/constant_reference_matcher.rb +43 -0
- data/lib/upgrade_radar/matchers/gemfile_matcher.rb +70 -0
- data/lib/upgrade_radar/matchers/method_call_matcher.rb +202 -0
- data/lib/upgrade_radar/matchers/string_mutation_matcher.rb +32 -0
- data/lib/upgrade_radar/result.rb +47 -0
- data/lib/upgrade_radar/rule.rb +52 -0
- data/lib/upgrade_radar/rule_registry.rb +43 -0
- data/lib/upgrade_radar/runtime/collector.rb +74 -0
- data/lib/upgrade_radar/runtime/middleware.rb +51 -0
- data/lib/upgrade_radar/runtime/rails_hook.rb +24 -0
- data/lib/upgrade_radar/runtime/ruby_warning_hook.rb +22 -0
- data/lib/upgrade_radar/scanner.rb +71 -0
- data/lib/upgrade_radar/templates/report.html.erb +125 -0
- data/lib/upgrade_radar/version.rb +5 -0
- data/lib/upgrade_radar/version_range.rb +38 -0
- data/lib/upgrade_radar.rb +33 -0
- data/rules/rails/rails_7_1.yml +17 -0
- data/rules/rails/rails_7_2.yml +49 -0
- data/rules/rails/rails_8_0.yml +81 -0
- data/rules/ruby/ruby_3_3.yml +91 -0
- data/rules/ruby/ruby_3_4.yml +84 -0
- metadata +113 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c739e32253aac169d3a9e3d6fac3576ded14e1403f0fa042266b2a4c372c4c8f
|
|
4
|
+
data.tar.gz: aabd8d308a91383957a4c02313cbba1805e99752dadba77c8f3bc03fb6d4d7c0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9f61aeac2f76d5ff07a401708507ca248dd93ee0090381a4492396105833dc0c9324889f73c883b4a342df6d52bcfd025307ab9dc9d7ee5850dde1d0a6b3045d
|
|
7
|
+
data.tar.gz: 2a865d6b51c715ac3539923772abf80826504d15c29996d2881e83d4c2d15c930af25c65371ac9bcd0bac5fb9193466af8e4d175f108a7abdea8e0d0d3892bd8
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-10-28)
|
|
4
|
+
|
|
5
|
+
- Initial release
|
|
6
|
+
- Static analysis scanner using Prism AST
|
|
7
|
+
- 19 built-in rules for Rails 7.1–8.0 and Ruby 3.3–3.4
|
|
8
|
+
- CLI with text, JSON, and HTML output formats
|
|
9
|
+
- Optional runtime deprecation capture
|
|
10
|
+
- Effort estimation
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Samarth Kulkarni
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
# Upgrade Radar
|
|
2
|
+
|
|
3
|
+
**Know your Ruby/Rails upgrade cost before you start.**
|
|
4
|
+
|
|
5
|
+
Upgrade Radar is a static analysis gem that scans your codebase for deprecations and breaking changes between Ruby/Rails versions. Run one command and get a full report — what will break, where, why, and how long it will take to fix — before you touch the version number.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
$ upgrade_radar scan --from rails-7.2 --to rails-8.0
|
|
9
|
+
|
|
10
|
+
Upgrade Radar v0.1.0
|
|
11
|
+
Scanning for: rails-7.2 → rails-8.0
|
|
12
|
+
Files scanned: 247
|
|
13
|
+
|
|
14
|
+
app/models/lead.rb
|
|
15
|
+
[BREAKS] Line 4: Positional hash argument to `enum` is deprecated in Rails 7.2 and removed in 8.0.
|
|
16
|
+
Source: enum status: { draft: 0, published: 1, archived: 2 }
|
|
17
|
+
Fix: enum :status, { active: 0, inactive: 1 }
|
|
18
|
+
Docs: https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#enumerations
|
|
19
|
+
|
|
20
|
+
app/models/profile.rb
|
|
21
|
+
[BREAKS] Line 5: `serialize` requires an explicit `coder:` keyword argument in Rails 7.1+.
|
|
22
|
+
Source: serialize :tags, Array
|
|
23
|
+
Fix: serialize :attr, coder: JSON
|
|
24
|
+
|
|
25
|
+
app/controllers/api_controller.rb
|
|
26
|
+
[BREAKS] Line 17: `params.require(:x).permit(...)` is deprecated in Rails 8.0.
|
|
27
|
+
Source: params.require(:user).permit(:name, :email)
|
|
28
|
+
Fix: params.expect(user: [:name, :email])
|
|
29
|
+
|
|
30
|
+
============================================================
|
|
31
|
+
UPGRADE SUMMARY
|
|
32
|
+
============================================================
|
|
33
|
+
Total findings: 14
|
|
34
|
+
Breaking: 11
|
|
35
|
+
Warnings: 3
|
|
36
|
+
Files affected: 8
|
|
37
|
+
Estimated effort: ~2.5 hours (small)
|
|
38
|
+
|
|
39
|
+
Scan completed in 0.03s
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Table of Contents
|
|
45
|
+
|
|
46
|
+
- [The Problem](#the-problem)
|
|
47
|
+
- [Installation](#installation)
|
|
48
|
+
- [Quick Start](#quick-start)
|
|
49
|
+
- [Commands](#commands)
|
|
50
|
+
- [Output Formats](#output-formats)
|
|
51
|
+
- [Configuration](#configuration)
|
|
52
|
+
- [Runtime Mode](#runtime-mode)
|
|
53
|
+
- [Built-in Rules](#built-in-rules)
|
|
54
|
+
- [Custom Rules](#custom-rules)
|
|
55
|
+
- [Architecture](#architecture)
|
|
56
|
+
- [Comparison with Existing Tools](#comparison-with-existing-tools)
|
|
57
|
+
- [Development](#development)
|
|
58
|
+
- [License](#license)
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## The Problem
|
|
63
|
+
|
|
64
|
+
Upgrading Ruby (say 3.3 → 3.4) or Rails (7.2 → 8.0) usually goes like this:
|
|
65
|
+
|
|
66
|
+
1. Bump the version
|
|
67
|
+
2. Run tests
|
|
68
|
+
3. Watch things break
|
|
69
|
+
4. Dig through changelogs to figure out why
|
|
70
|
+
5. Repeat for weeks
|
|
71
|
+
|
|
72
|
+
For a big app, you can't easily estimate the effort beforehand. You don't know if it's a 2-hour job or a 2-week project until you're in the middle of it.
|
|
73
|
+
|
|
74
|
+
**Upgrade Radar solves this.** Before upgrading, you run one command and get a complete report — every breaking change and deprecation in your codebase, with the exact file, line, fix, and documentation link. You know the cost before you start.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
### As a Gemfile dependency (recommended)
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# Gemfile
|
|
84
|
+
gem "upgrade_radar", group: :development
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Then run:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
bundle install
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Standalone installation
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
gem install upgrade_radar
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Requirements
|
|
100
|
+
|
|
101
|
+
- **Ruby >= 3.2.0** (uses the [Prism](https://github.com/ruby/prism) parser, Ruby's official AST parser since 3.3)
|
|
102
|
+
- No Rails dependency for static analysis — works on any Ruby project
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Quick Start
|
|
107
|
+
|
|
108
|
+
### Scan for Rails upgrade issues
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
upgrade_radar scan --from rails-7.2 --to rails-8.0
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Scan for Ruby upgrade issues
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
upgrade_radar scan --from ruby-3.3 --to ruby-3.4
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Get a JSON report for CI
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
upgrade_radar scan --from rails-7.2 --to rails-8.0 --format json --output report.json
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The exit code is **1** when breaking changes are found, **0** otherwise — so you can gate CI on it.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Commands
|
|
131
|
+
|
|
132
|
+
### `upgrade_radar scan`
|
|
133
|
+
|
|
134
|
+
The main command. Scans your codebase and reports findings.
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
Usage:
|
|
138
|
+
upgrade_radar scan --from FROM --to TO
|
|
139
|
+
|
|
140
|
+
Options:
|
|
141
|
+
--from Source version (e.g., rails-7.2, ruby-3.3) [required]
|
|
142
|
+
--to Target version (e.g., rails-8.0, ruby-3.4) [required]
|
|
143
|
+
--path Path to scan (default: current directory)
|
|
144
|
+
--format Output format: text, json, html (default: text)
|
|
145
|
+
--output Write report to file instead of stdout
|
|
146
|
+
--exclude Glob patterns to exclude (e.g., vendor/** tmp/**)
|
|
147
|
+
--config Path to config file
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Examples:**
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# Scan only the app directory
|
|
154
|
+
upgrade_radar scan --from rails-7.2 --to rails-8.0 --path app/
|
|
155
|
+
|
|
156
|
+
# Exclude vendor and generated files
|
|
157
|
+
upgrade_radar scan --from rails-7.2 --to rails-8.0 --exclude vendor/** db/schema.rb
|
|
158
|
+
|
|
159
|
+
# Generate an HTML report
|
|
160
|
+
upgrade_radar scan --from rails-7.2 --to rails-8.0 --format html --output upgrade_report.html
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### `upgrade_radar rules`
|
|
164
|
+
|
|
165
|
+
List all rules that apply for a version range, without scanning any files.
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
upgrade_radar rules --from rails-7.2 --to rails-8.0
|
|
169
|
+
upgrade_radar rules --from ruby-3.2 --to ruby-3.4 --format json
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `upgrade_radar version`
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
upgrade_radar version
|
|
176
|
+
# upgrade_radar 0.1.0
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Output Formats
|
|
182
|
+
|
|
183
|
+
### Text (default)
|
|
184
|
+
|
|
185
|
+
Colorized terminal output with `[BREAKS]` and `[WARN]` badges, grouped by file. Includes an effort summary at the bottom.
|
|
186
|
+
|
|
187
|
+
### JSON
|
|
188
|
+
|
|
189
|
+
Structured output for CI pipelines and tooling:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"metadata": {
|
|
194
|
+
"upgrade_radar_version": "0.1.0",
|
|
195
|
+
"from": "rails-7.2",
|
|
196
|
+
"to": "rails-8.0",
|
|
197
|
+
"scanned_at": "2026-10-27T15:45:28+0530",
|
|
198
|
+
"scan_duration_seconds": 0.012,
|
|
199
|
+
"files_scanned": 247
|
|
200
|
+
},
|
|
201
|
+
"summary": {
|
|
202
|
+
"total_findings": 14,
|
|
203
|
+
"breaking": 11,
|
|
204
|
+
"warnings": 3,
|
|
205
|
+
"files_affected": 8,
|
|
206
|
+
"total_effort_minutes": 150,
|
|
207
|
+
"total_effort_human": "~2.5 hours",
|
|
208
|
+
"effort_level": "small"
|
|
209
|
+
},
|
|
210
|
+
"findings": [
|
|
211
|
+
{
|
|
212
|
+
"id": "rails-7.2-enum-keyword-args",
|
|
213
|
+
"severity": "breaking",
|
|
214
|
+
"file": "app/models/lead.rb",
|
|
215
|
+
"line": 4,
|
|
216
|
+
"message": "Positional hash argument to `enum` is deprecated...",
|
|
217
|
+
"replacement": "enum :status, { active: 0, inactive: 1 }",
|
|
218
|
+
"docs": "https://guides.rubyonrails.org/...",
|
|
219
|
+
"effort_minutes": 5
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### HTML
|
|
226
|
+
|
|
227
|
+
A standalone, self-contained HTML report with dark mode support, severity badges, and effort summary cards. Suitable for sharing with the team.
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
upgrade_radar scan --from rails-7.2 --to rails-8.0 --format html --output report.html
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Configuration
|
|
236
|
+
|
|
237
|
+
Create `.upgrade_radar.yml` in your project root to set defaults:
|
|
238
|
+
|
|
239
|
+
```yaml
|
|
240
|
+
# .upgrade_radar.yml
|
|
241
|
+
exclude:
|
|
242
|
+
- vendor/**
|
|
243
|
+
- tmp/**
|
|
244
|
+
- log/**
|
|
245
|
+
- db/schema.rb
|
|
246
|
+
- node_modules/**
|
|
247
|
+
custom_rules_path: ./my_rules/
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The following paths are excluded by default: `vendor/**`, `node_modules/**`, `tmp/**`, `log/**`.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Runtime Mode
|
|
255
|
+
|
|
256
|
+
Some deprecations only surface at runtime — for example, Rails deprecation warnings emitted during test execution. Upgrade Radar provides optional hooks to capture these.
|
|
257
|
+
|
|
258
|
+
### RSpec Integration
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
# spec/spec_helper.rb
|
|
262
|
+
require "upgrade_radar/runtime/middleware"
|
|
263
|
+
|
|
264
|
+
UpgradeRadar::Runtime::Middleware.setup_for_rspec
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Minitest Integration
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
# test/test_helper.rb
|
|
271
|
+
require "upgrade_radar/runtime/middleware"
|
|
272
|
+
|
|
273
|
+
collector = UpgradeRadar::Runtime::Middleware.setup_for_minitest
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### What It Captures
|
|
277
|
+
|
|
278
|
+
- **Rails deprecations** via `ActiveSupport::Deprecation` (Rails 7.1+ deprecators API and legacy API)
|
|
279
|
+
- **Ruby deprecations** via `Warning[:deprecated]` (Ruby 3.0+)
|
|
280
|
+
|
|
281
|
+
Captured warnings are printed after the test suite with file/line information.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Built-in Rules
|
|
286
|
+
|
|
287
|
+
Upgrade Radar ships with **19 rules** covering the most impactful breaking changes in recent Rails and Ruby versions.
|
|
288
|
+
|
|
289
|
+
### Rails Rules (9)
|
|
290
|
+
|
|
291
|
+
| Rule ID | What It Detects | Introduced | Breaks |
|
|
292
|
+
|---------|-----------------|------------|--------|
|
|
293
|
+
| `rails-7.1-serialize-coder` | `serialize` without explicit `coder:` keyword | 7.1 | 8.0 |
|
|
294
|
+
| `rails-7.2-enum-keyword-args` | Old-style `enum status: {}` hash syntax | 7.2 | 8.0 |
|
|
295
|
+
| `rails-7.2-secrets-removed` | `Rails.application.secrets` usage | 7.2 | 7.2 |
|
|
296
|
+
| `rails-7.2-alias-attribute` | `alias_attribute` bypassing custom accessors | 7.2 | 7.2 |
|
|
297
|
+
| `rails-8.0-connection-pool` | `connection_pool.connection` (removed) | 8.0 | 8.0 |
|
|
298
|
+
| `rails-8.0-params-expect` | `params.require(:x).permit(...)` chain | 8.0 | 8.0 |
|
|
299
|
+
| `rails-8.0-proxy-object` | `ActiveSupport::ProxyObject` (removed) | 8.0 | 8.0 |
|
|
300
|
+
| `rails-8.0-form-with-nil` | `form_with model: nil` (invalid) | 8.0 | 8.0 |
|
|
301
|
+
| `rails-8.0-read-encrypted-secrets` | `config.read_encrypted_secrets` (removed) | 8.0 | 8.0 |
|
|
302
|
+
|
|
303
|
+
### Ruby Rules (10)
|
|
304
|
+
|
|
305
|
+
| Rule ID | What It Detects | Introduced | Breaks |
|
|
306
|
+
|---------|-----------------|------------|--------|
|
|
307
|
+
| `ruby-3.3-it-block-param` | Bare `it` in parameterless blocks | 3.3 | 3.4 |
|
|
308
|
+
| `ruby-3.3-default-gems-bundled` | `require 'csv'` etc. without Gemfile entry | 3.3 | 3.4 |
|
|
309
|
+
| `ruby-3.3-lambda-proc` | `lambda(&non_lambda_proc)` | 3.3 | 3.3 |
|
|
310
|
+
| `ruby-3.3-time-new-string` | `Time.new("2024-01-15")` partial strings | 3.3 | 3.3 |
|
|
311
|
+
| `ruby-3.3-io-pipe` | `IO.read("\| cmd")` pipe prefix | 3.3 | 3.3 |
|
|
312
|
+
| `ruby-3.4-kwargs-index` | `obj[1, key: val]` kwargs in `[]` | 3.4 | 3.4 |
|
|
313
|
+
| `ruby-3.4-range-size` | `Range#size` on non-iterable ranges (low confidence) | 3.4 | 3.4 |
|
|
314
|
+
| `ruby-3.4-socket-resolution-error` | `rescue SocketError` missing new subclass | 3.4 | 3.4 |
|
|
315
|
+
| `ruby-3.4-chilled-string` | `"literal".gsub!` string mutation | 3.4 | 3.4 |
|
|
316
|
+
| `ruby-3.4-hash-inspect` | `Hash#inspect` format change (low confidence) | 3.4 | 3.4 |
|
|
317
|
+
|
|
318
|
+
### Severity Levels
|
|
319
|
+
|
|
320
|
+
- **BREAKING** — Code will fail (raise an error, change behavior silently, or be removed) in the target version
|
|
321
|
+
- **WARNING** — Code is deprecated and emits warnings; will break in a future version
|
|
322
|
+
|
|
323
|
+
Severity is computed dynamically based on the `--to` version. A rule that is a warning in 7.2 becomes breaking in 8.0.
|
|
324
|
+
|
|
325
|
+
Rules marked `confidence: low` (`ruby-3.4-range-size`, `ruby-3.4-hash-inspect`) can't determine the receiver type statically, so they are always reported as **WARNING** and never cause a non-zero exit code on their own.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Custom Rules
|
|
330
|
+
|
|
331
|
+
You can author your own rules as YAML files. Point Upgrade Radar to them via config:
|
|
332
|
+
|
|
333
|
+
```yaml
|
|
334
|
+
# .upgrade_radar.yml
|
|
335
|
+
custom_rules_path: ./my_rules/
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Rule Schema
|
|
339
|
+
|
|
340
|
+
```yaml
|
|
341
|
+
# my_rules/custom.yml
|
|
342
|
+
- id: my-app-deprecated-helper # Unique rule identifier
|
|
343
|
+
framework: rails # "rails" or "ruby"
|
|
344
|
+
introduced_in: "8.0" # Version where this was deprecated/changed
|
|
345
|
+
breaking_in: "8.0" # Version where this breaks
|
|
346
|
+
pattern:
|
|
347
|
+
type: method_call # Pattern type (see below)
|
|
348
|
+
name: legacy_helper # Method name to match
|
|
349
|
+
receiver: null # null = implicit self
|
|
350
|
+
conditions: [] # Additional conditions
|
|
351
|
+
message: > # What to tell the developer
|
|
352
|
+
`legacy_helper` is removed. Use `modern_helper` instead.
|
|
353
|
+
replacement: "modern_helper" # Suggested fix
|
|
354
|
+
docs: "https://wiki.example.com/upgrade"
|
|
355
|
+
effort_minutes: 5 # Estimated fix time per occurrence
|
|
356
|
+
confidence: high # Optional; "low" = always reported as a warning
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Pattern Types
|
|
360
|
+
|
|
361
|
+
| Type | Matches | Use For |
|
|
362
|
+
|------|---------|---------|
|
|
363
|
+
| `method_call` | `Prism::CallNode` | Method invocations, chained calls |
|
|
364
|
+
| `constant_reference` | `Prism::ConstantPathNode` | Class/module references |
|
|
365
|
+
| `block_parameter` | `ItLocalVariableReadNode` | Bare `it` in blocks |
|
|
366
|
+
| `gemfile_dependency` | Gemfile analysis | Missing gem declarations |
|
|
367
|
+
| `string_mutation` | Literal string mutations | `"str".gsub!` patterns |
|
|
368
|
+
|
|
369
|
+
### Available Conditions (for `method_call` type)
|
|
370
|
+
|
|
371
|
+
| Condition | Example | Description |
|
|
372
|
+
|-----------|---------|-------------|
|
|
373
|
+
| `no_keyword` | `{no_keyword: coder}` | Keyword arg must NOT be present |
|
|
374
|
+
| `has_block_pass` | `{has_block_pass: true}` | Has `&block` argument |
|
|
375
|
+
| `first_arg_type` | `{first_arg_type: hash}` | First arg is hash/string/symbol/constant |
|
|
376
|
+
| `first_arg_starts_with` | `{first_arg_starts_with: "\|"}` | First string arg starts with prefix |
|
|
377
|
+
| `keyword_value` | `{keyword_value: {key: model, value: nil}}` | Keyword has specific value |
|
|
378
|
+
| `has_keywords` | `{has_keywords: true}` | Has any keyword arguments |
|
|
379
|
+
| `min_positional_args` | `{min_positional_args: 1}` | Minimum positional arg count |
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Architecture
|
|
384
|
+
|
|
385
|
+
### How It Works
|
|
386
|
+
|
|
387
|
+
```
|
|
388
|
+
┌──────────────┐ ┌───────────────┐ ┌───────────────┐
|
|
389
|
+
│ YAML Rules │────▶│ Rule Registry │────▶│ Active Rules │
|
|
390
|
+
│ (19 rules) │ │ (load+filter) │ │ (for version) │
|
|
391
|
+
└──────────────┘ └───────────────┘ └───────┬───────┘
|
|
392
|
+
│
|
|
393
|
+
┌──────────────┐ ┌───────────────┐ ┌───────▼───────┐
|
|
394
|
+
│ Ruby Files │────▶│ Prism Parser │────▶│ AuditVisitor │
|
|
395
|
+
│ (FileFinder)│ │ (AST per file)│ │ (1 walk/file) │
|
|
396
|
+
└──────────────┘ └───────────────┘ └───────┬───────┘
|
|
397
|
+
│
|
|
398
|
+
┌───────▼───────┐
|
|
399
|
+
│ Matchers │
|
|
400
|
+
│ (5 types) │
|
|
401
|
+
└───────┬───────┘
|
|
402
|
+
│
|
|
403
|
+
┌───────────────┐ ┌───────▼───────┐
|
|
404
|
+
│ Formatters │◀────│ Findings │
|
|
405
|
+
│ (text/json/ │ │ + Result │
|
|
406
|
+
│ html) │ └───────────────┘
|
|
407
|
+
└───────────────┘
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Key Design Decisions
|
|
411
|
+
|
|
412
|
+
1. **Single AuditVisitor (Commissioner pattern)** — One `Prism::Visitor` subclass walks each file's AST once and dispatches to all relevant matchers. Same pattern RuboCop uses. This is O(nodes x relevant_rules), not O(nodes x all_rules).
|
|
413
|
+
|
|
414
|
+
2. **YAML rules with typed matchers** — Rules are defined in accessible YAML. Each `pattern.type` maps to a Ruby matcher class. You don't need Ruby knowledge to author a rule.
|
|
415
|
+
|
|
416
|
+
3. **Dynamic severity** — A rule's severity depends on the target version. `serialize` without `coder:` is a warning in Rails 7.2 but breaking in 8.0.
|
|
417
|
+
|
|
418
|
+
4. **Version resolution via `Gem::Version`** — Built into Ruby, handles comparison correctly, no external dependency.
|
|
419
|
+
|
|
420
|
+
### Module Structure
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
UpgradeRadar
|
|
424
|
+
├── CLI # Thor CLI (scan, rules, version)
|
|
425
|
+
├── Scanner # Orchestrator
|
|
426
|
+
├── FileFinder # File discovery with excludes
|
|
427
|
+
├── Rule / RuleRegistry # YAML rule loading + version filtering
|
|
428
|
+
├── VersionRange # "rails-7.2" parsing
|
|
429
|
+
├── AuditVisitor # Prism::Visitor subclass
|
|
430
|
+
├── Matchers/
|
|
431
|
+
│ ├── MethodCallMatcher # 14 of 19 rules
|
|
432
|
+
│ ├── ConstantReferenceMatcher
|
|
433
|
+
│ ├── BlockParameterMatcher
|
|
434
|
+
│ ├── GemfileMatcher
|
|
435
|
+
│ └── StringMutationMatcher
|
|
436
|
+
├── Finding / Result # Data model
|
|
437
|
+
├── EffortEstimator # Time estimation
|
|
438
|
+
├── Formatters/
|
|
439
|
+
│ ├── TextFormatter # Colorized CLI
|
|
440
|
+
│ ├── JsonFormatter # CI-friendly
|
|
441
|
+
│ └── HtmlFormatter # Shareable report
|
|
442
|
+
└── Runtime/
|
|
443
|
+
├── Collector # Deduplicating warning collector
|
|
444
|
+
├── RailsHook # ActiveSupport::Deprecation hook
|
|
445
|
+
├── RubyWarningHook # Warning[:deprecated] hook
|
|
446
|
+
└── Middleware # RSpec/Minitest integration
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## Comparison with Existing Tools
|
|
452
|
+
|
|
453
|
+
| Tool | Static Analysis | Runtime Capture | Version-to-Version Report | Effort Estimate |
|
|
454
|
+
|------|:-:|:-:|:-:|:-:|
|
|
455
|
+
| **Upgrade Radar** | Yes | Yes | Yes | Yes |
|
|
456
|
+
| RuboCop + rubocop-rails | Partial (scattered cops) | No | No | No |
|
|
457
|
+
| next_rails (FastRuby.io) | No | Yes | No | No |
|
|
458
|
+
| `rails app:update` | No (config files only) | No | No | No |
|
|
459
|
+
| Deprecation Toolkit (Shopify) | No | Yes | No | No |
|
|
460
|
+
|
|
461
|
+
**The gap Upgrade Radar fills:** a single version-to-version report that combines static and runtime findings, tells you what will break before you upgrade, and estimates the effort so you can plan.
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## CI Integration
|
|
466
|
+
|
|
467
|
+
### GitHub Actions
|
|
468
|
+
|
|
469
|
+
```yaml
|
|
470
|
+
name: Upgrade Readiness Check
|
|
471
|
+
|
|
472
|
+
on:
|
|
473
|
+
schedule:
|
|
474
|
+
- cron: '0 9 * * 1' # Weekly on Monday
|
|
475
|
+
workflow_dispatch:
|
|
476
|
+
|
|
477
|
+
jobs:
|
|
478
|
+
upgrade-check:
|
|
479
|
+
runs-on: ubuntu-latest
|
|
480
|
+
steps:
|
|
481
|
+
- uses: actions/checkout@v4
|
|
482
|
+
- uses: ruby/setup-ruby@v1
|
|
483
|
+
with:
|
|
484
|
+
ruby-version: '3.3'
|
|
485
|
+
- run: gem install upgrade_radar
|
|
486
|
+
- name: Check Rails upgrade readiness
|
|
487
|
+
run: |
|
|
488
|
+
upgrade_radar scan \
|
|
489
|
+
--from rails-7.2 --to rails-8.0 \
|
|
490
|
+
--format json --output upgrade-report.json
|
|
491
|
+
- uses: actions/upload-artifact@v4
|
|
492
|
+
if: always()
|
|
493
|
+
with:
|
|
494
|
+
name: upgrade-report
|
|
495
|
+
path: upgrade-report.json
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Exit Codes
|
|
499
|
+
|
|
500
|
+
| Code | Meaning |
|
|
501
|
+
|------|---------|
|
|
502
|
+
| `0` | No breaking changes found (warnings only, or clean) |
|
|
503
|
+
| `1` | Breaking changes found |
|
|
504
|
+
| `2` | Invalid arguments or configuration error |
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## Development
|
|
509
|
+
|
|
510
|
+
```bash
|
|
511
|
+
git clone https://github.com/samrocks03/upgrade_radar.git
|
|
512
|
+
cd upgrade_radar
|
|
513
|
+
bundle install
|
|
514
|
+
bundle exec rspec # Run tests (78 specs)
|
|
515
|
+
bundle exec rake # Run tests (default task)
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Running Against Your Own App
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
cd /path/to/your/rails/app
|
|
522
|
+
upgrade_radar scan --from rails-7.2 --to rails-8.0
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Adding New Rules
|
|
526
|
+
|
|
527
|
+
1. Create or edit a YAML file in `rules/rails/` or `rules/ruby/`
|
|
528
|
+
2. Follow the schema documented in [Custom Rules](#custom-rules)
|
|
529
|
+
3. Add a fixture file in `spec/fixtures/` with the pattern
|
|
530
|
+
4. Add a test in `spec/audit_visitor_spec.rb`
|
|
531
|
+
5. Run `bundle exec rspec`
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## License
|
|
536
|
+
|
|
537
|
+
MIT License. See [LICENSE.txt](LICENSE.txt).
|
data/exe/upgrade_radar
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UpgradeRadar
|
|
4
|
+
class AuditVisitor < Prism::Visitor
|
|
5
|
+
attr_reader :findings
|
|
6
|
+
|
|
7
|
+
def initialize(filepath, rules, to_version)
|
|
8
|
+
super()
|
|
9
|
+
@filepath = filepath
|
|
10
|
+
@to_version = to_version
|
|
11
|
+
@findings = []
|
|
12
|
+
@context_stack = []
|
|
13
|
+
@in_parameterless_block = false
|
|
14
|
+
|
|
15
|
+
@call_node_rules = rules.select { |r| r.node_types.include?(:call_node) }
|
|
16
|
+
@constant_rules = rules.select { |r| r.node_types.include?(:constant_read_node) || r.node_types.include?(:constant_path_node) }
|
|
17
|
+
|
|
18
|
+
@matchers = build_matchers(rules)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def visit_call_node(node)
|
|
22
|
+
@call_node_rules.each do |rule|
|
|
23
|
+
matcher = @matchers[rule.id]
|
|
24
|
+
next unless matcher
|
|
25
|
+
|
|
26
|
+
matched = if rule.pattern["type"] == "block_parameter"
|
|
27
|
+
matcher.match?(node, in_parameterless_block: @in_parameterless_block)
|
|
28
|
+
else
|
|
29
|
+
matcher.match?(node)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if matched
|
|
33
|
+
@findings << Finding.new(
|
|
34
|
+
rule: rule,
|
|
35
|
+
filepath: @filepath,
|
|
36
|
+
line: node.location.start_line,
|
|
37
|
+
column: node.location.start_column,
|
|
38
|
+
source: node.location.slice,
|
|
39
|
+
severity: rule.severity_for(@to_version)
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def visit_constant_read_node(node)
|
|
47
|
+
check_constant_node(node)
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def visit_constant_path_node(node)
|
|
52
|
+
check_constant_node(node)
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def visit_it_local_variable_read_node(node)
|
|
57
|
+
@call_node_rules.each do |rule|
|
|
58
|
+
next unless rule.pattern["type"] == "block_parameter"
|
|
59
|
+
matcher = @matchers[rule.id]
|
|
60
|
+
next unless matcher
|
|
61
|
+
|
|
62
|
+
if matcher.match?(node, in_parameterless_block: @in_parameterless_block)
|
|
63
|
+
@findings << Finding.new(
|
|
64
|
+
rule: rule,
|
|
65
|
+
filepath: @filepath,
|
|
66
|
+
line: node.location.start_line,
|
|
67
|
+
column: node.location.start_column,
|
|
68
|
+
source: node.location.slice,
|
|
69
|
+
severity: rule.severity_for(@to_version)
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
super
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def visit_rescue_node(node)
|
|
77
|
+
@context_stack.push(:rescue)
|
|
78
|
+
super
|
|
79
|
+
@context_stack.pop
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def visit_block_node(node)
|
|
83
|
+
previous = @in_parameterless_block
|
|
84
|
+
@in_parameterless_block = node.parameters.nil? || node.parameters.is_a?(Prism::ItParametersNode)
|
|
85
|
+
super
|
|
86
|
+
@in_parameterless_block = previous
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def visit_lambda_node(node)
|
|
90
|
+
previous = @in_parameterless_block
|
|
91
|
+
@in_parameterless_block = node.parameters.nil? || node.parameters.is_a?(Prism::ItParametersNode)
|
|
92
|
+
super
|
|
93
|
+
@in_parameterless_block = previous
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def check_constant_node(node)
|
|
99
|
+
@constant_rules.each do |rule|
|
|
100
|
+
matcher = @matchers[rule.id]
|
|
101
|
+
next unless matcher
|
|
102
|
+
|
|
103
|
+
if matcher.match?(node, context: @context_stack.last)
|
|
104
|
+
@findings << Finding.new(
|
|
105
|
+
rule: rule,
|
|
106
|
+
filepath: @filepath,
|
|
107
|
+
line: node.location.start_line,
|
|
108
|
+
column: node.location.start_column,
|
|
109
|
+
source: node.location.slice,
|
|
110
|
+
severity: rule.severity_for(@to_version)
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def build_matchers(rules)
|
|
117
|
+
rules.reject { |r| r.pattern["type"] == "gemfile_dependency" }
|
|
118
|
+
.each_with_object({}) { |rule, hash| hash[rule.id] = Matchers::Base.for(rule.pattern) }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|