ripple_effect 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/.ripple-effect.yml.example +56 -0
- data/ARCHITECTURE.md +222 -0
- data/CHANGELOG.md +115 -0
- data/CODE_OF_CONDUCT.md +64 -0
- data/CONTRIBUTING.md +112 -0
- data/LICENSE.txt +21 -0
- data/README.md +305 -0
- data/SECURITY.md +73 -0
- data/docs/ANALYSIS_MODEL.md +275 -0
- data/docs/CLI.md +276 -0
- data/docs/CONFIGURATION.md +178 -0
- data/docs/DECISIONS.md +210 -0
- data/docs/PUBLIC_LAUNCH_CHECKLIST.md +105 -0
- data/docs/RELEASING.md +94 -0
- data/docs/TESTING.md +179 -0
- data/exe/ripple-effect +7 -0
- data/lib/ripple_effect/analyzer.rb +379 -0
- data/lib/ripple_effect/cache_store.rb +207 -0
- data/lib/ripple_effect/cli/application.rb +126 -0
- data/lib/ripple_effect/cli/command.rb +165 -0
- data/lib/ripple_effect/cli/diff_command.rb +76 -0
- data/lib/ripple_effect/cli/doctor_command.rb +106 -0
- data/lib/ripple_effect/cli/graph_command.rb +61 -0
- data/lib/ripple_effect/cli/inspect_command.rb +66 -0
- data/lib/ripple_effect/cli/tests_command.rb +109 -0
- data/lib/ripple_effect/cli/version_command.rb +46 -0
- data/lib/ripple_effect/confidence.rb +61 -0
- data/lib/ripple_effect/configuration.rb +264 -0
- data/lib/ripple_effect/diagnostic.rb +90 -0
- data/lib/ripple_effect/diff/changed_symbol_resolver.rb +292 -0
- data/lib/ripple_effect/diff/git.rb +175 -0
- data/lib/ripple_effect/diff/hunk.rb +80 -0
- data/lib/ripple_effect/edge.rb +114 -0
- data/lib/ripple_effect/error.rb +23 -0
- data/lib/ripple_effect/extractors/base.rb +292 -0
- data/lib/ripple_effect/extractors/rails_associations.rb +102 -0
- data/lib/ripple_effect/extractors/rails_callbacks.rb +144 -0
- data/lib/ripple_effect/extractors/rails_delegation.rb +121 -0
- data/lib/ripple_effect/extractors/rails_jobs.rb +131 -0
- data/lib/ripple_effect/extractors/rails_mailers.rb +120 -0
- data/lib/ripple_effect/extractors/rails_routes.rb +256 -0
- data/lib/ripple_effect/extractors/rails_views.rb +299 -0
- data/lib/ripple_effect/extractors/ruby_structure.rb +221 -0
- data/lib/ripple_effect/extractors/test_conventions.rb +135 -0
- data/lib/ripple_effect/formatters/dot.rb +69 -0
- data/lib/ripple_effect/formatters/json.rb +43 -0
- data/lib/ripple_effect/formatters/text.rb +197 -0
- data/lib/ripple_effect/graph.rb +199 -0
- data/lib/ripple_effect/node.rb +153 -0
- data/lib/ripple_effect/project.rb +264 -0
- data/lib/ripple_effect/result.rb +147 -0
- data/lib/ripple_effect/risk.rb +167 -0
- data/lib/ripple_effect/static_index/adapter.rb +84 -0
- data/lib/ripple_effect/static_index/rubydex_adapter.rb +356 -0
- data/lib/ripple_effect/traversal/impact_walker.rb +153 -0
- data/lib/ripple_effect/version.rb +11 -0
- data/lib/ripple_effect.rb +89 -0
- metadata +155 -0
data/README.md
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# RippleEffect
|
|
2
|
+
|
|
3
|
+
> Change-impact analysis for Ruby on Rails.
|
|
4
|
+
|
|
5
|
+
**RippleEffect maps the blast radius of Ruby on Rails changes.** It combines
|
|
6
|
+
Ruby-aware static indexing with Rails semantics: callbacks, associations, routes,
|
|
7
|
+
jobs, mailers, concerns, and test references: to explain what may be affected
|
|
8
|
+
before you refactor.
|
|
9
|
+
|
|
10
|
+
RippleEffect is deterministic and local. It does not boot your Rails application,
|
|
11
|
+
connect to your database, call an LLM, or upload your source code.
|
|
12
|
+
|
|
13
|
+
```console
|
|
14
|
+
$ ripple-effect inspect 'BillingService#charge'
|
|
15
|
+
|
|
16
|
+
BillingService#charge
|
|
17
|
+
app/services/billing_service.rb:2
|
|
18
|
+
|
|
19
|
+
Direct dependents
|
|
20
|
+
app/jobs/invoice_job.rb:2 InvoiceJob#perform
|
|
21
|
+
because: calls method (inference.unique_method_name) at app/jobs/invoice_job.rb:3
|
|
22
|
+
confidence: medium, depth: 1
|
|
23
|
+
app/services/checkout_service.rb:2 CheckoutService#call
|
|
24
|
+
because: calls method (inference.unique_method_name) at app/services/checkout_service.rb:3
|
|
25
|
+
confidence: medium, depth: 1
|
|
26
|
+
|
|
27
|
+
Rails semantic dependents
|
|
28
|
+
app/jobs/invoice_job.rb:1 InvoiceJob (job)
|
|
29
|
+
because: enqueues job (rails.job_perform) at app/jobs/invoice_job.rb:1
|
|
30
|
+
confidence: medium, depth: 2
|
|
31
|
+
config/routes.rb:5 POST /checkout
|
|
32
|
+
because: route dispatches to (rails.route_to_controller) at config/routes.rb:5
|
|
33
|
+
confidence: medium, depth: 3
|
|
34
|
+
|
|
35
|
+
Likely affected tests
|
|
36
|
+
spec/services/billing_service_spec.rb reference (medium)
|
|
37
|
+
spec/services/checkout_service_spec.rb reference (medium)
|
|
38
|
+
spec/jobs/invoice_job_spec.rb convention (medium)
|
|
39
|
+
|
|
40
|
+
Blast radius
|
|
41
|
+
direct nodes: 4
|
|
42
|
+
transitive nodes: 10
|
|
43
|
+
max depth: 3
|
|
44
|
+
risk: high
|
|
45
|
+
because:
|
|
46
|
+
4 direct dependents
|
|
47
|
+
10 transitive dependents
|
|
48
|
+
reachable from 2 routes
|
|
49
|
+
reachable from 1 background job
|
|
50
|
+
impact spans 3 architectural layers (controllers, jobs, services)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Why this exists
|
|
54
|
+
|
|
55
|
+
Rails hides its most important dependencies. Nothing in your source calls
|
|
56
|
+
`send_receipt`, an `after_commit` does. Nothing calls `InvoiceJob#perform`, a
|
|
57
|
+
`perform_later` two files away does. Nothing calls `OrdersController#create`, a
|
|
58
|
+
line in `config/routes.rb` does.
|
|
59
|
+
|
|
60
|
+
Grep cannot see any of that. RippleEffect can, and it shows you the evidence so
|
|
61
|
+
you can check its reasoning instead of trusting it.
|
|
62
|
+
|
|
63
|
+
## How it differs from tools you may already use
|
|
64
|
+
|
|
65
|
+
RippleEffect does not try to replace these:
|
|
66
|
+
|
|
67
|
+
- **Packwerk** enforces package boundaries. Its static analysis focuses on
|
|
68
|
+
constant references and intentionally ignores method calls. RippleEffect is not
|
|
69
|
+
a boundary enforcer; it answers "what could this change break?" at method level.
|
|
70
|
+
- **Enola** provides broad multi-language architecture intelligence. RippleEffect
|
|
71
|
+
is a Ruby gem and library first, focused narrowly on Rails source-symbol blast
|
|
72
|
+
radius with an embeddable API.
|
|
73
|
+
- **Regression-test-selection tools** (Crystalball, gitlab-crystalball, Kaisoku)
|
|
74
|
+
optimise which tests run, often using runtime traces. For RippleEffect, test
|
|
75
|
+
relevance is supporting evidence, not the product: the explainable graph is.
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
```console
|
|
80
|
+
$ gem install ripple_effect
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Or in a Gemfile:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
group :development do
|
|
87
|
+
gem "ripple_effect", require: false
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
RippleEffect does not depend on Rails and never loads your application.
|
|
92
|
+
|
|
93
|
+
## Quick start
|
|
94
|
+
|
|
95
|
+
```console
|
|
96
|
+
$ cd your-rails-app
|
|
97
|
+
$ ripple-effect doctor # check what RippleEffect can see
|
|
98
|
+
$ ripple-effect inspect 'BillingService#charge'
|
|
99
|
+
$ ripple-effect diff main # blast radius of your branch
|
|
100
|
+
$ bundle exec rspec $(ripple-effect tests main)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Commands
|
|
104
|
+
|
|
105
|
+
| Command | What it answers |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `ripple-effect inspect SYMBOL` | What may be affected by changing this symbol, and why |
|
|
108
|
+
| `ripple-effect diff BASE [HEAD]` | What may be affected by everything changed between two revisions |
|
|
109
|
+
| `ripple-effect tests BASE [HEAD]` | Which test files are most likely relevant |
|
|
110
|
+
| `ripple-effect graph SYMBOL` | The impacted subgraph, as DOT or JSON |
|
|
111
|
+
| `ripple-effect doctor` | Whether this project can be analysed, and what was found |
|
|
112
|
+
| `ripple-effect version` | The version |
|
|
113
|
+
|
|
114
|
+
Symbols may be written as `User`, `User#activate!`, `User.find`, `User::Profile`,
|
|
115
|
+
`OrdersController#create`, or `app/models/user.rb`.
|
|
116
|
+
|
|
117
|
+
Full details, flags and exit codes: [docs/CLI.md](docs/CLI.md).
|
|
118
|
+
|
|
119
|
+
### Machine-readable output
|
|
120
|
+
|
|
121
|
+
Every command accepts `--format json` and emits a versioned, deterministic
|
|
122
|
+
document: see [docs/CLI.md](docs/CLI.md#json-output) for the contract.
|
|
123
|
+
|
|
124
|
+
```console
|
|
125
|
+
$ ripple-effect diff main --format json | jq '.risk'
|
|
126
|
+
{
|
|
127
|
+
"level": "high",
|
|
128
|
+
"score": 15.0,
|
|
129
|
+
"reasons": [
|
|
130
|
+
"4 direct dependents",
|
|
131
|
+
"10 transitive dependents",
|
|
132
|
+
"reachable from 2 routes",
|
|
133
|
+
"reachable from 1 background job"
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### In CI
|
|
139
|
+
|
|
140
|
+
```console
|
|
141
|
+
$ ripple-effect diff origin/main --fail-on-risk critical
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`diff` exits non-zero **only** when a `--fail-on-risk` threshold you asked for is
|
|
145
|
+
met. Finding impact is not, by itself, a failure.
|
|
146
|
+
|
|
147
|
+
## Library API
|
|
148
|
+
|
|
149
|
+
The CLI is not the only product.
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
require "ripple_effect"
|
|
153
|
+
|
|
154
|
+
result = RippleEffect.analyze("BillingService#charge")
|
|
155
|
+
|
|
156
|
+
result.impacted_nodes.map(&:name) # => ["CheckoutService#call", ...]
|
|
157
|
+
result.test_files # => ["spec/services/billing_service_spec.rb", ...]
|
|
158
|
+
result.risk.level # => :high
|
|
159
|
+
result.risk.reasons # => ["2 direct dependents", ...]
|
|
160
|
+
result.to_h # => the JSON contract, as a Hash
|
|
161
|
+
|
|
162
|
+
RippleEffect.diff("main")
|
|
163
|
+
RippleEffect.tests_for("User#activate!")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
For more control:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
project = RippleEffect::Project.new(root: Dir.pwd)
|
|
170
|
+
analyzer = RippleEffect::Analyzer.new(project: project)
|
|
171
|
+
|
|
172
|
+
result = analyzer.inspect_symbol("BillingService#charge", depth: 2, min_confidence: :high)
|
|
173
|
+
result.impacts.each do |impact|
|
|
174
|
+
puts "#{impact.node.name} (#{impact.confidence}) via #{impact.reason.description}"
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Evidence and confidence
|
|
179
|
+
|
|
180
|
+
Every edge in the graph carries an evidence code and a confidence band. Nothing is
|
|
181
|
+
asserted without a reason you can go and look at.
|
|
182
|
+
|
|
183
|
+
| Band | Meaning | Example |
|
|
184
|
+
| --- | --- | --- |
|
|
185
|
+
| `high` | An explicit static reference, or a literal Rails DSL relationship | `rails.after_commit`, `rubydex.constant_reference` |
|
|
186
|
+
| `medium` | A target inferred from a strong convention | `inference.unique_method_name`, `rails.delegate` |
|
|
187
|
+
| `low` | A naming or path heuristic only | `convention.request_spec_path` |
|
|
188
|
+
|
|
189
|
+
By default RippleEffect traverses `high` and `medium` edges. Pass
|
|
190
|
+
`--include-low-confidence` to widen, or `--min-confidence high` to narrow.
|
|
191
|
+
|
|
192
|
+
A path is only as confident as its weakest link, and RippleEffect reports the
|
|
193
|
+
*shortest* chain of reasoning, because that is the one you can check fastest.
|
|
194
|
+
|
|
195
|
+
There are no percentages. Ruby is too dynamic for a number like "91% chance of
|
|
196
|
+
breakage" to mean anything, and a made-up number is worse than no number.
|
|
197
|
+
|
|
198
|
+
See [docs/ANALYSIS_MODEL.md](docs/ANALYSIS_MODEL.md) for every edge type and the
|
|
199
|
+
exact resolution rules.
|
|
200
|
+
|
|
201
|
+
## Safety and privacy
|
|
202
|
+
|
|
203
|
+
RippleEffect reads files. That is all.
|
|
204
|
+
|
|
205
|
+
- No network access, ever. No telemetry.
|
|
206
|
+
- Your application is never booted and your source is never `eval`ed.
|
|
207
|
+
- No database connection.
|
|
208
|
+
- Git is invoked with argument arrays, never a shell string.
|
|
209
|
+
- YAML config is loaded with safe loading: no aliases, no object deserialisation.
|
|
210
|
+
- The cache is JSON, never Marshal.
|
|
211
|
+
- Paths are normalised and confined to the project root; a symlink pointing
|
|
212
|
+
outside it is refused.
|
|
213
|
+
|
|
214
|
+
`ripple-effect doctor` restates these guarantees. See [SECURITY.md](SECURITY.md).
|
|
215
|
+
|
|
216
|
+
## Supported versions
|
|
217
|
+
|
|
218
|
+
- Ruby 3.2, 3.3, 3.4: the full suite is run against each
|
|
219
|
+
- Rails 7.0 – 8.1 (analysed statically; Rails is not a dependency)
|
|
220
|
+
|
|
221
|
+
### Validated against real applications
|
|
222
|
+
|
|
223
|
+
v0.1 was checked against six real Rails codebases: two private production
|
|
224
|
+
applications and four large open-source ones: covering Rails 7.0 through 8.1,
|
|
225
|
+
classic ERB apps, an API-only app, a Haml-based app, and an engine monorepo:
|
|
226
|
+
|
|
227
|
+
| Application | Ruby files | Templates | Graph | Cold |
|
|
228
|
+
| --- | --- | --- | --- | --- |
|
|
229
|
+
| lobsters | 258 | 121 erb | 1.4k nodes / 4.8k edges | 0.5s |
|
|
230
|
+
| rubygems.org | 1,044 | 173 erb | 5.7k nodes / 20.6k edges | 2.2s |
|
|
231
|
+
| Solidus (6 engines) | 1,810 | 299 erb | 8.9k nodes / 22.8k edges | 6.6s |
|
|
232
|
+
| Mastodon | 2,574 | 46 erb | 13.3k nodes / 47.7k edges | 4.4s |
|
|
233
|
+
| private app (Rails 7.0) | 521 | 688 erb | 5.0k nodes / 12.8k edges | 0.8s |
|
|
234
|
+
| private app (Rails 8.0) | 845 | 33 erb | 7.7k nodes / 21.1k edges | 1.2s |
|
|
235
|
+
|
|
236
|
+
None crashed, and none reported an analysis error. Warm runs are well under a
|
|
237
|
+
tenth of a second.
|
|
238
|
+
|
|
239
|
+
Dependents were ground-truthed against an exhaustive text search of each codebase
|
|
240
|
+
(`script/ground_truth.rb`). Across **18 heavily-referenced methods in those six
|
|
241
|
+
applications, there were zero false positives**, RippleEffect never claimed a
|
|
242
|
+
dependency that was not really there. Several were exact matches (151 of 151
|
|
243
|
+
files for one Mastodon method, 47 of 47 for one private-app helper).
|
|
244
|
+
|
|
245
|
+
Recall is the weaker side, by design: where a receiver is ambiguous RippleEffect
|
|
246
|
+
records a diagnostic instead of guessing, so it misses some real relationships.
|
|
247
|
+
See [Known limitations](#known-limitations).
|
|
248
|
+
|
|
249
|
+
That is a spot check, not a guarantee. Please report anything it gets wrong.
|
|
250
|
+
|
|
251
|
+
## Known limitations
|
|
252
|
+
|
|
253
|
+
RippleEffect prefers a false negative to a confident false positive. It will tell
|
|
254
|
+
you when it does not know.
|
|
255
|
+
|
|
256
|
+
- **Dynamic dispatch.** `send`, `public_send`, `method_missing` and metaprogrammed
|
|
257
|
+
methods are not resolved. Where a receiver cannot be resolved and one method
|
|
258
|
+
name matches uniquely, a `medium`-confidence edge is created; where several
|
|
259
|
+
match, a diagnostic is recorded and no edge is invented.
|
|
260
|
+
- **Polymorphic associations** have no static target. They are reported as a
|
|
261
|
+
diagnostic, not resolved.
|
|
262
|
+
- **Non-literal Rails DSL.** Callbacks, associations and routes built from
|
|
263
|
+
variables, constants or loops are not read.
|
|
264
|
+
- **Route DSL coverage** is conservative: explicit verbs with `to:`,
|
|
265
|
+
`root`, `resources`/`resource` with `only:`/`except:`/`controller:`, `namespace`
|
|
266
|
+
and `scope`. Constraints, `concern`, and `direct` are not modelled.
|
|
267
|
+
- **Test selection is a recommendation.** It is never permission to skip the rest
|
|
268
|
+
of your suite. When a global or boot-impact file changes, `tests` refuses to
|
|
269
|
+
print a narrowed list unless you pass `--allow-unsafe-focus`.
|
|
270
|
+
- **Recall is incomplete.** Where a call's receiver is ambiguous,
|
|
271
|
+
RippleEffect records a diagnostic and adds no edge, so some real dependencies
|
|
272
|
+
are missed. Run with `--verbose` to see exactly what was skipped.
|
|
273
|
+
- **ERB templates are indexed**, but a call with an explicit receiver inside one
|
|
274
|
+
(`<%= @order.total %>`) is not resolved: the receiver's type is unknown.
|
|
275
|
+
Non-ERB templates (Haml, Slim, Builder), JavaScript, and SQL are not indexed.
|
|
276
|
+
|
|
277
|
+
## Development
|
|
278
|
+
|
|
279
|
+
```console
|
|
280
|
+
$ bin/setup # or: bundle install
|
|
281
|
+
$ bundle exec rake # specs + RuboCop
|
|
282
|
+
$ bundle exec rspec
|
|
283
|
+
$ bundle exec rubocop
|
|
284
|
+
$ bundle exec rake smoke # build, install into a temp GEM_HOME, run the CLI
|
|
285
|
+
$ bundle exec rake benchmark
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md), [ARCHITECTURE.md](ARCHITECTURE.md) and
|
|
289
|
+
[docs/TESTING.md](docs/TESTING.md).
|
|
290
|
+
|
|
291
|
+
## Status
|
|
292
|
+
|
|
293
|
+
v0.1.0 is an early release. The analysis model and CLI are stable enough to use,
|
|
294
|
+
but expect rough edges on unusual codebases. Bug reports with a small
|
|
295
|
+
reproduction are very welcome.
|
|
296
|
+
|
|
297
|
+
## Acknowledgements
|
|
298
|
+
|
|
299
|
+
RippleEffect's Ruby declaration and reference index is powered by
|
|
300
|
+
[Rubydex](https://github.com/Shopify/rubydex) (MIT), and its Rails DSL parsing by
|
|
301
|
+
[Prism](https://github.com/ruby/prism).
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Threat model
|
|
4
|
+
|
|
5
|
+
RippleEffect reads files in your project and runs `git`. That is the whole of it.
|
|
6
|
+
|
|
7
|
+
It is designed to be safe to run against a codebase you do not fully trust, and to
|
|
8
|
+
be safe to run in CI.
|
|
9
|
+
|
|
10
|
+
### Guarantees
|
|
11
|
+
|
|
12
|
+
| Guarantee | How |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| **No network access** | Nothing in the gem opens a socket. Analysis is entirely local. |
|
|
15
|
+
| **No telemetry** | Nothing is collected, counted, or transmitted. |
|
|
16
|
+
| **Your application is never booted** | RippleEffect analyses source statically. It never loads your Rails environment or initializers. |
|
|
17
|
+
| **Your source is never evaluated** | No `eval`, `instance_eval`, or `Kernel#load` of analysed code. Parsing only. |
|
|
18
|
+
| **No database connection** | No adapter, connection, or query, ever. |
|
|
19
|
+
| **Safe subprocess invocation** | Git runs through `Open3.capture3` with argument arrays. A ref containing `; rm -rf /` is a ref that does not exist, not a command. There is a spec for this. |
|
|
20
|
+
| **Safe YAML** | Config is read with `YAML.safe_load_file`, no aliases and no permitted classes. A config file cannot construct objects. |
|
|
21
|
+
| **Safe cache** | The cache is JSON. Never Marshal, which would turn a corrupted cache file into arbitrary object construction. |
|
|
22
|
+
| **Path confinement** | Paths are normalised and symlinks resolved before comparison; anything resolving outside the project root is refused. |
|
|
23
|
+
|
|
24
|
+
`ripple-effect doctor` restates these at runtime.
|
|
25
|
+
|
|
26
|
+
### What RippleEffect writes
|
|
27
|
+
|
|
28
|
+
Only the cache, by default `tmp/ripple_effect/`, containing `manifest.json` and
|
|
29
|
+
`graph.json`. Disable it with `cache.enabled: false` or `--no-cache`.
|
|
30
|
+
|
|
31
|
+
The cache contains symbol names, file paths and line numbers from your project. It
|
|
32
|
+
should be treated with the same care as your source, and should not be committed.
|
|
33
|
+
|
|
34
|
+
### Residual risks
|
|
35
|
+
|
|
36
|
+
- **Subprocess:** RippleEffect executes the `git` on your `PATH`. A compromised
|
|
37
|
+
`git` is outside its control.
|
|
38
|
+
- **Dependencies:** `rubydex` (which includes a native extension) and `prism`.
|
|
39
|
+
- **Output:** analysis output contains your symbol names and paths. Redact before
|
|
40
|
+
sharing a report publicly.
|
|
41
|
+
|
|
42
|
+
## Supported versions
|
|
43
|
+
|
|
44
|
+
| Version | Supported |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| 0.1.x | Yes |
|
|
47
|
+
|
|
48
|
+
## Reporting a vulnerability
|
|
49
|
+
|
|
50
|
+
**Please do not open a public issue for a security problem.**
|
|
51
|
+
|
|
52
|
+
Report it privately through GitHub's private vulnerability reporting:
|
|
53
|
+
|
|
54
|
+
<https://github.com/iamzayn19/ripple-effect/security/advisories/new>
|
|
55
|
+
|
|
56
|
+
If private reporting is not enabled yet, email <iamzayn19@gmail.com> instead.
|
|
57
|
+
|
|
58
|
+
Please include:
|
|
59
|
+
|
|
60
|
+
- what the vulnerability allows an attacker to do,
|
|
61
|
+
- the affected version,
|
|
62
|
+
- reproduction steps or a proof of concept,
|
|
63
|
+
- any suggested fix.
|
|
64
|
+
|
|
65
|
+
### What to expect
|
|
66
|
+
|
|
67
|
+
- **Acknowledgement** within 3 working days.
|
|
68
|
+
- **An initial assessment** within 10 working days.
|
|
69
|
+
- **A fix or a plan** shared with you before public disclosure.
|
|
70
|
+
- **Credit** in the release notes, unless you prefer otherwise.
|
|
71
|
+
|
|
72
|
+
We ask that you give us a reasonable opportunity to fix an issue before disclosing
|
|
73
|
+
it publicly.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Analysis model
|
|
2
|
+
|
|
3
|
+
What RippleEffect claims, what it refuses to claim, and why.
|
|
4
|
+
|
|
5
|
+
## Node kinds
|
|
6
|
+
|
|
7
|
+
| Kind | What it represents |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `file` | A source file |
|
|
10
|
+
| `test_file` | A file under `spec/` or `test/` |
|
|
11
|
+
| `class` | A class declaration |
|
|
12
|
+
| `module` | A module declaration |
|
|
13
|
+
| `instance_method` | `Order#total` |
|
|
14
|
+
| `class_method` | `Order.recent` |
|
|
15
|
+
| `route` | One routing entry, e.g. `POST /checkout` |
|
|
16
|
+
| `callback` | One lifecycle callback declaration |
|
|
17
|
+
| `association` | Reserved for association nodes; v0.1 models associations as edges |
|
|
18
|
+
| `job` | An Active Job class, as a background entry point |
|
|
19
|
+
| `mailer_action` | One mailer action, e.g. `OrderMailer.receipt` |
|
|
20
|
+
| `view` | An ERB template or partial |
|
|
21
|
+
| `unknown` | Fallback; not emitted in v0.1 |
|
|
22
|
+
|
|
23
|
+
### Node IDs
|
|
24
|
+
|
|
25
|
+
Deterministic and readable, never random. The path is part of the ID so a class
|
|
26
|
+
reopened in two files does not collide.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
file:app/models/user.rb
|
|
30
|
+
class:app/models/order.rb:Order
|
|
31
|
+
method:app/services/billing_service.rb:BillingService#charge
|
|
32
|
+
route:config/routes.rb:5:route:POST:/checkout
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Canonical symbol names
|
|
36
|
+
|
|
37
|
+
| Kind | Canonical form | Also accepted |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| Instance method | `User#activate!` |, |
|
|
40
|
+
| Singleton method | `User.find` | `User::find` |
|
|
41
|
+
| Namespaced constant | `User::Profile` |, |
|
|
42
|
+
|
|
43
|
+
## Edge direction
|
|
44
|
+
|
|
45
|
+
One rule, never varied:
|
|
46
|
+
|
|
47
|
+
> `from_id` **depends on / invokes / references** `into_id`
|
|
48
|
+
|
|
49
|
+
Walking edges backwards from a changed node therefore finds its dependents.
|
|
50
|
+
|
|
51
|
+
## Edge types
|
|
52
|
+
|
|
53
|
+
| Type | Meaning | Typical evidence |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `constant_reference` | A constant is mentioned | `rubydex.constant_reference` |
|
|
56
|
+
| `method_call` | A method is called | `rubydex.method_reference`, `inference.unique_method_name` |
|
|
57
|
+
| `inheritance` | `class B < A` | `ruby.superclass` |
|
|
58
|
+
| `include` | `include M` | `ruby.include` |
|
|
59
|
+
| `prepend` | `prepend M` | `ruby.prepend` |
|
|
60
|
+
| `extend` | `extend M` | `ruby.extend` |
|
|
61
|
+
| `association` | An ActiveRecord association | `rails.belongs_to`, `rails.has_many`, … |
|
|
62
|
+
| `callback` | A lifecycle callback, or its condition | `rails.after_commit`, `rails.callback_condition`, … |
|
|
63
|
+
| `delegate` | `delegate ..., to:` | `rails.delegate` |
|
|
64
|
+
| `route_handler` | A route dispatches to a controller action | `rails.route_to_controller` |
|
|
65
|
+
| `job_enqueue` | A job is enqueued, or a job class owns its `#perform` | `rails.perform_later`, `rails.job_perform` |
|
|
66
|
+
| `mailer_delivery` | Mail is delivered, or a mailer owns its action | `rails.deliver_later`, `rails.mailer_action` |
|
|
67
|
+
| `test_convention` | A test covers code | `convention.rspec_path`, `convention.minitest_path`, `convention.request_spec_path` |
|
|
68
|
+
| `file_reference` | Structural containment, and template rendering | `ruby.file_declares`, `ruby.defines_method`, `rails.renders_template`, `rails.render_partial` |
|
|
69
|
+
|
|
70
|
+
### Structural edges
|
|
71
|
+
|
|
72
|
+
`file_reference` edges are graph plumbing: a file "depends on" the classes it
|
|
73
|
+
declares, and a class on the methods it defines. They exist so a file-level change
|
|
74
|
+
can reach its code, and so a method change reaches everything that depends on its
|
|
75
|
+
class.
|
|
76
|
+
|
|
77
|
+
Text output hides them, because reporting a file as a dependent of its own method
|
|
78
|
+
buries the real answer. JSON keeps them.
|
|
79
|
+
|
|
80
|
+
## Confidence
|
|
81
|
+
|
|
82
|
+
Three bands, no percentages. Ruby is too dynamic for a number like "91% chance of
|
|
83
|
+
breakage" to mean anything, and a fabricated number is worse than none.
|
|
84
|
+
|
|
85
|
+
| Band | Rule | Examples |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| `high` | An explicit static reference, or a literal Rails DSL relationship | `rails.after_commit`, `rubydex.constant_reference`, `ruby.superclass` |
|
|
88
|
+
| `medium` | A target inferred from a strong convention | `inference.unique_method_name`, `rails.delegate` |
|
|
89
|
+
| `low` | A naming or path heuristic only | `convention.request_spec_path` |
|
|
90
|
+
|
|
91
|
+
**A path is as confident as its weakest link.** Default traversal is `high` +
|
|
92
|
+
`medium`; `low` requires `--include-low-confidence`.
|
|
93
|
+
|
|
94
|
+
## Method call resolution
|
|
95
|
+
|
|
96
|
+
| Case | Example | Result |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| Receiver resolved by the index | `validate!` inside the same class | `high` edge |
|
|
99
|
+
| Explicit constant receiver | `InvoiceJob.perform_later(id)` | `high` edge |
|
|
100
|
+
| Unresolved receiver, one method in the project has that name | `billing.charge(order)` | `medium` edge |
|
|
101
|
+
| Unresolved receiver, several methods share the name | `x.run` where `A#run` and `B#run` exist | **No edge**, `unresolved_method_receiver` diagnostic |
|
|
102
|
+
| `send`, `public_send`, dynamic names | `public_send(name)` | **No edge** |
|
|
103
|
+
| `method_missing`, generated methods |, | **No edge** |
|
|
104
|
+
|
|
105
|
+
The fourth row is the heart of the design. Choosing between candidates would
|
|
106
|
+
produce a confident falsehood, and one of those costs more trust than many honest
|
|
107
|
+
"I don't know"s.
|
|
108
|
+
|
|
109
|
+
## Rails semantics
|
|
110
|
+
|
|
111
|
+
### Associations
|
|
112
|
+
|
|
113
|
+
Literal macros only: `belongs_to`, `has_one`, `has_many`,
|
|
114
|
+
`has_and_belongs_to_many`.
|
|
115
|
+
|
|
116
|
+
- Target resolved from an explicit `class_name:` when literal, otherwise from
|
|
117
|
+
Rails' own naming convention (`has_many :orders` → `Order`).
|
|
118
|
+
- Metadata records `macro`, `association_name`, `class_name`, `foreign_key`,
|
|
119
|
+
`through`.
|
|
120
|
+
- Scopes and blocks are **never executed**.
|
|
121
|
+
- **Polymorphic** targets have no static answer. They produce an
|
|
122
|
+
`unresolved_polymorphic_association` diagnostic, never an invented constant.
|
|
123
|
+
- A target that is not indexed produces `unresolved_association_target`.
|
|
124
|
+
|
|
125
|
+
### Callbacks
|
|
126
|
+
|
|
127
|
+
`before_validation`, `after_validation`, `before/around/after` × `save`, `create`,
|
|
128
|
+
`update`, `destroy`, plus `after_commit`, `after_rollback`, `after_touch`,
|
|
129
|
+
`after_initialize`, `after_find`.
|
|
130
|
+
|
|
131
|
+
Each declaration becomes a `callback` node, so the reason survives into the
|
|
132
|
+
explanation. The model reaches the callback; the callback reaches the method.
|
|
133
|
+
|
|
134
|
+
- Symbol and string names are both read, including several in one call.
|
|
135
|
+
- `if:` / `unless:` predicates get their own edge: they run on every save too.
|
|
136
|
+
- A block callback becomes an anonymous node anchored to its line range; the calls
|
|
137
|
+
inside it are indexed normally.
|
|
138
|
+
|
|
139
|
+
### Jobs
|
|
140
|
+
|
|
141
|
+
A class is a job when it inherits from `ApplicationJob` or `ActiveJob::Base`,
|
|
142
|
+
directly or through intermediates.
|
|
143
|
+
|
|
144
|
+
- `perform_later` and `perform_now` with a constant receiver link the caller to
|
|
145
|
+
`Job#perform` at `high` confidence.
|
|
146
|
+
- `Job.set(...).perform_later` is followed, because the constant is still visible.
|
|
147
|
+
- A non-constant receiver is ignored. No queue or adapter is ever loaded.
|
|
148
|
+
|
|
149
|
+
### Mailers
|
|
150
|
+
|
|
151
|
+
A class is a mailer when it inherits from `ApplicationMailer` or
|
|
152
|
+
`ActionMailer::Base`. Each instance method becomes a `mailer_action` node —
|
|
153
|
+
Action Mailer synthesises the class-level call, so it exists nowhere in the source.
|
|
154
|
+
|
|
155
|
+
`OrderMailer.receipt(order).deliver_later` links the sender to
|
|
156
|
+
`OrderMailer#receipt`.
|
|
157
|
+
|
|
158
|
+
### Routes
|
|
159
|
+
|
|
160
|
+
conservative. Supported:
|
|
161
|
+
|
|
162
|
+
- Explicit verbs with `to: "controller#action"`, and `root to:`
|
|
163
|
+
- `resources` / `resource`, honouring literal `only:`, `except:` and `controller:`
|
|
164
|
+
- `namespace` (prefixes both path and controller module)
|
|
165
|
+
- `scope` with a literal path or `module:`
|
|
166
|
+
- Nested resources inside a `resources` block
|
|
167
|
+
|
|
168
|
+
Not modelled in v0.1: constraints, `concern`, `direct`, `resolve`, and routes
|
|
169
|
+
built from variables or loops. A route whose controller is not indexed produces
|
|
170
|
+
`unresolved_route_controller`.
|
|
171
|
+
|
|
172
|
+
### Views
|
|
173
|
+
|
|
174
|
+
ERB templates are part of the graph. The Ruby is lifted out of the ERB tags and
|
|
175
|
+
parsed with Prism; a template is never rendered and its code is never executed.
|
|
176
|
+
|
|
177
|
+
Rails' own ERB handler: not stdlib `ERB`, is what understands
|
|
178
|
+
`<%= form_with do |f| %>`, so the tag contents are read directly rather than
|
|
179
|
+
compiled through `ERB#src`, which turns that form into a syntax error.
|
|
180
|
+
|
|
181
|
+
| Relationship | Evidence | Confidence |
|
|
182
|
+
| --- | --- | --- |
|
|
183
|
+
| A controller action renders its conventional template | `rails.renders_template` | `high` |
|
|
184
|
+
| A template renders a partial | `rails.render_partial` | `high` |
|
|
185
|
+
| A template references a constant | `view.constant_reference` | `high` |
|
|
186
|
+
| A template calls a uniquely-named helper or model method | `view.helper_call` | `medium` |
|
|
187
|
+
| `helper_method :foo` exposes a controller method to templates | `rails.helper_method` | `high` on the controller, `medium` via an included concern |
|
|
188
|
+
|
|
189
|
+
A call with an explicit receiver in a template (`@order.total`) is not resolved:
|
|
190
|
+
the receiver is a local or instance variable whose type is unknown.
|
|
191
|
+
|
|
192
|
+
### Delegation
|
|
193
|
+
|
|
194
|
+
`delegate :name, to: :account` names a **receiver**, not a class. RippleEffect
|
|
195
|
+
resolves it through an association of the same name on the same class, and
|
|
196
|
+
otherwise through the conventional class name: at `medium` confidence, because
|
|
197
|
+
it is an inference.
|
|
198
|
+
|
|
199
|
+
An unresolvable receiver produces `unresolved_delegate_target` and no edge.
|
|
200
|
+
|
|
201
|
+
## Test relevance
|
|
202
|
+
|
|
203
|
+
Ranked by *why* a test was reached:
|
|
204
|
+
|
|
205
|
+
| Rank | Reason | Meaning |
|
|
206
|
+
| --- | --- | --- |
|
|
207
|
+
| 1 | `reference` | The test actually references the changed code |
|
|
208
|
+
| 2 | `rails_semantic` | Reached through a Rails relationship |
|
|
209
|
+
| 3 | `convention` | It sits at the conventional path |
|
|
210
|
+
| 4 | `heuristic` | A looser path match, such as a request spec |
|
|
211
|
+
|
|
212
|
+
A changed test file always ranks first.
|
|
213
|
+
|
|
214
|
+
**Test selection is a recommendation, never permission to skip the rest of your
|
|
215
|
+
suite.** RippleEffect can prove a test is probably relevant; it cannot prove one is
|
|
216
|
+
irrelevant.
|
|
217
|
+
|
|
218
|
+
## Risk
|
|
219
|
+
|
|
220
|
+
Discrete bands with stated reasons: a ranking aid for reviewers, not a defect
|
|
221
|
+
prediction.
|
|
222
|
+
|
|
223
|
+
| Points | For |
|
|
224
|
+
| --- | --- |
|
|
225
|
+
| +1 each (max 8) | Direct dependents |
|
|
226
|
+
| +0.5 each (max 8) | Transitive dependents |
|
|
227
|
+
| +2 | Reachable from a route |
|
|
228
|
+
| +2 | Reachable from a background job |
|
|
229
|
+
| +2 | Reachable from a model callback |
|
|
230
|
+
| +1 | Reachable from a mailer |
|
|
231
|
+
| +3 | The change touches a class or module body, not just method bodies |
|
|
232
|
+
| +3 | A global or boot-impact file changed |
|
|
233
|
+
| +2 | Impact spans 3 or more architectural layers |
|
|
234
|
+
|
|
235
|
+
| Score | Band |
|
|
236
|
+
| --- | --- |
|
|
237
|
+
| `>= 18` | `critical` |
|
|
238
|
+
| `>= 10` | `high` |
|
|
239
|
+
| `>= 4` | `medium` |
|
|
240
|
+
| `< 4` | `low` |
|
|
241
|
+
|
|
242
|
+
File nodes are excluded from the dependent counts.
|
|
243
|
+
|
|
244
|
+
## Diagnostics
|
|
245
|
+
|
|
246
|
+
Non-fatal facts about the analysis. Match on `code`, not on message text: codes
|
|
247
|
+
are stable, messages are not.
|
|
248
|
+
|
|
249
|
+
| Code | Meaning |
|
|
250
|
+
| --- | --- |
|
|
251
|
+
| `unresolved_polymorphic_association` | A polymorphic `belongs_to` has no static target |
|
|
252
|
+
| `unresolved_association_target` | The association's target class is not indexed |
|
|
253
|
+
| `unresolved_delegate_target` | The delegation receiver could not be resolved |
|
|
254
|
+
| `unresolved_method_receiver` | A call site's receiver is ambiguous or unknown |
|
|
255
|
+
| `unresolved_route_controller` | A route points at a controller action that is not indexed |
|
|
256
|
+
| `unparsed_file` | A file could not be read or parsed; analysis continued |
|
|
257
|
+
| `index_note` | The indexer reported a lint finding about a file it parsed successfully |
|
|
258
|
+
| `unmapped_changed_lines` | Changed lines could not be mapped to a declaration |
|
|
259
|
+
| `global_file_changed` | A boot-impact file changed; impact may be broader than shown |
|
|
260
|
+
| `deleted_file` | A file was deleted; anything referencing it may break |
|
|
261
|
+
| `renamed_file` | A file was renamed |
|
|
262
|
+
| `cache_discarded` | The cache was unreadable and has been rebuilt |
|
|
263
|
+
| `dynamic_dispatch` | Reserved for dynamic dispatch reporting |
|
|
264
|
+
| `empty_index` | Files were indexed but no declarations found; results are not trustworthy |
|
|
265
|
+
|
|
266
|
+
Severities are `info`, `warning` and `error`. Text output shows warnings by
|
|
267
|
+
default and everything with `--verbose`; JSON always includes all of them.
|
|
268
|
+
|
|
269
|
+
## What RippleEffect does not do
|
|
270
|
+
|
|
271
|
+
- Execute your code, boot Rails, or connect to a database.
|
|
272
|
+
- Resolve dynamic dispatch or metaprogrammed methods.
|
|
273
|
+
- Analyse non-ERB templates (Haml, Slim, Builder), JavaScript, or SQL.
|
|
274
|
+
- Follow impact across repositories.
|
|
275
|
+
- Prove that unselected tests are unnecessary.
|