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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +10 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +537 -0
  5. data/exe/upgrade_radar +5 -0
  6. data/lib/upgrade_radar/audit_visitor.rb +121 -0
  7. data/lib/upgrade_radar/cli.rb +77 -0
  8. data/lib/upgrade_radar/configuration.rb +31 -0
  9. data/lib/upgrade_radar/effort_estimator.rb +63 -0
  10. data/lib/upgrade_radar/file_finder.rb +40 -0
  11. data/lib/upgrade_radar/finding.rb +43 -0
  12. data/lib/upgrade_radar/formatters/base.rb +20 -0
  13. data/lib/upgrade_radar/formatters/html_formatter.rb +20 -0
  14. data/lib/upgrade_radar/formatters/json_formatter.rb +26 -0
  15. data/lib/upgrade_radar/formatters/text_formatter.rb +96 -0
  16. data/lib/upgrade_radar/matchers/base.rb +32 -0
  17. data/lib/upgrade_radar/matchers/block_parameter_matcher.rb +19 -0
  18. data/lib/upgrade_radar/matchers/constant_reference_matcher.rb +43 -0
  19. data/lib/upgrade_radar/matchers/gemfile_matcher.rb +70 -0
  20. data/lib/upgrade_radar/matchers/method_call_matcher.rb +202 -0
  21. data/lib/upgrade_radar/matchers/string_mutation_matcher.rb +32 -0
  22. data/lib/upgrade_radar/result.rb +47 -0
  23. data/lib/upgrade_radar/rule.rb +52 -0
  24. data/lib/upgrade_radar/rule_registry.rb +43 -0
  25. data/lib/upgrade_radar/runtime/collector.rb +74 -0
  26. data/lib/upgrade_radar/runtime/middleware.rb +51 -0
  27. data/lib/upgrade_radar/runtime/rails_hook.rb +24 -0
  28. data/lib/upgrade_radar/runtime/ruby_warning_hook.rb +22 -0
  29. data/lib/upgrade_radar/scanner.rb +71 -0
  30. data/lib/upgrade_radar/templates/report.html.erb +125 -0
  31. data/lib/upgrade_radar/version.rb +5 -0
  32. data/lib/upgrade_radar/version_range.rb +38 -0
  33. data/lib/upgrade_radar.rb +33 -0
  34. data/rules/rails/rails_7_1.yml +17 -0
  35. data/rules/rails/rails_7_2.yml +49 -0
  36. data/rules/rails/rails_8_0.yml +81 -0
  37. data/rules/ruby/ruby_3_3.yml +91 -0
  38. data/rules/ruby/ruby_3_4.yml +84 -0
  39. 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,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "upgrade_radar"
5
+ UpgradeRadar::CLI.start(ARGV)
@@ -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