rigortype 0.2.1 → 0.2.3
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 +4 -4
- data/README.md +41 -14
- data/docs/handbook/01-getting-started.md +311 -0
- data/docs/handbook/02-everyday-types.md +337 -0
- data/docs/handbook/03-narrowing.md +359 -0
- data/docs/handbook/04-tuples-and-shapes.md +321 -0
- data/docs/handbook/05-methods-and-blocks.md +339 -0
- data/docs/handbook/06-classes.md +305 -0
- data/docs/handbook/07-rbs-and-extended.md +427 -0
- data/docs/handbook/08-understanding-errors.md +373 -0
- data/docs/handbook/09-plugins.md +241 -0
- data/docs/handbook/10-sorbet.md +347 -0
- data/docs/handbook/11-sig-gen.md +312 -0
- data/docs/handbook/12-lightweight-hkt.md +333 -0
- data/docs/handbook/README.md +275 -0
- data/docs/handbook/appendix-elixir.md +370 -0
- data/docs/handbook/appendix-go.md +399 -0
- data/docs/handbook/appendix-java-csharp.md +470 -0
- data/docs/handbook/appendix-liskov.md +580 -0
- data/docs/handbook/appendix-mypy.md +370 -0
- data/docs/handbook/appendix-phpstan.md +338 -0
- data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
- data/docs/handbook/appendix-rust.md +446 -0
- data/docs/handbook/appendix-steep.md +336 -0
- data/docs/handbook/appendix-type-theory.md +1662 -0
- data/docs/handbook/appendix-typeprof.md +416 -0
- data/docs/handbook/appendix-typescript.md +332 -0
- data/docs/install.md +189 -0
- data/docs/llms.txt +72 -0
- data/docs/manual/01-installation.md +342 -0
- data/docs/manual/02-cli-reference.md +569 -0
- data/docs/manual/03-configuration.md +152 -0
- data/docs/manual/04-diagnostics.md +206 -0
- data/docs/manual/05-inspecting-types.md +109 -0
- data/docs/manual/06-baseline.md +104 -0
- data/docs/manual/07-plugins.md +92 -0
- data/docs/manual/08-skills.md +143 -0
- data/docs/manual/09-editor-integration.md +245 -0
- data/docs/manual/10-mcp-server.md +539 -0
- data/docs/manual/11-ci.md +274 -0
- data/docs/manual/12-caching.md +116 -0
- data/docs/manual/13-troubleshooting.md +120 -0
- data/docs/manual/14-rails-quickstart.md +332 -0
- data/docs/manual/15-type-protection-coverage.md +204 -0
- data/docs/manual/16-rbs-extended-annotations.md +190 -0
- data/docs/manual/17-driving-improvement.md +160 -0
- data/docs/manual/README.md +87 -0
- data/docs/manual/ci-templates/README.md +58 -0
- data/docs/manual/plugins/README.md +86 -0
- data/docs/manual/plugins/rigor-actioncable.md +78 -0
- data/docs/manual/plugins/rigor-actionmailer.md +74 -0
- data/docs/manual/plugins/rigor-actionpack.md +80 -0
- data/docs/manual/plugins/rigor-activejob.md +58 -0
- data/docs/manual/plugins/rigor-activerecord.md +102 -0
- data/docs/manual/plugins/rigor-activestorage.md +74 -0
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
- data/docs/manual/plugins/rigor-devise.md +70 -0
- data/docs/manual/plugins/rigor-dry-schema.md +56 -0
- data/docs/manual/plugins/rigor-dry-struct.md +60 -0
- data/docs/manual/plugins/rigor-dry-types.md +59 -0
- data/docs/manual/plugins/rigor-dry-validation.md +62 -0
- data/docs/manual/plugins/rigor-factorybot.md +76 -0
- data/docs/manual/plugins/rigor-graphql.md +89 -0
- data/docs/manual/plugins/rigor-hanami.md +83 -0
- data/docs/manual/plugins/rigor-mangrove.md +73 -0
- data/docs/manual/plugins/rigor-minitest.md +86 -0
- data/docs/manual/plugins/rigor-pundit.md +72 -0
- data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
- data/docs/manual/plugins/rigor-rails-routes.md +94 -0
- data/docs/manual/plugins/rigor-rails.md +44 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
- data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
- data/docs/manual/plugins/rigor-rspec.md +86 -0
- data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
- data/docs/manual/plugins/rigor-sidekiq.md +78 -0
- data/docs/manual/plugins/rigor-sinatra.md +61 -0
- data/docs/manual/plugins/rigor-sorbet.md +63 -0
- data/docs/manual/plugins/rigor-statesman.md +75 -0
- data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
- data/exe/rigor +1 -1
- data/lib/rigor/analysis/incremental_session.rb +4 -2
- data/lib/rigor/analysis/run_stats.rb +13 -1
- data/lib/rigor/analysis/runner.rb +54 -12
- data/lib/rigor/cli/check_command.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +248 -0
- data/lib/rigor/cli/skill_command.rb +103 -41
- data/lib/rigor/cli/skill_describe.rb +346 -0
- data/lib/rigor/cli/triage_command.rb +8 -2
- data/lib/rigor/cli/triage_renderer.rb +4 -0
- data/lib/rigor/cli.rb +25 -3
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +124 -32
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
- data/lib/rigor/inference/scope_indexer.rb +87 -89
- data/lib/rigor/plugin/isolation.rb +5 -5
- data/lib/rigor/plugin/loader.rb +4 -2
- data/lib/rigor/triage/catalogue.rb +16 -1
- data/lib/rigor/triage.rb +30 -7
- data/lib/rigor/version.rb +1 -1
- data/skills/rigor-ask/SKILL.md +172 -0
- data/skills/rigor-doctor/SKILL.md +87 -0
- data/skills/rigor-editor-setup/SKILL.md +114 -0
- data/skills/rigor-mcp-setup/SKILL.md +117 -0
- data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
- data/skills/rigor-next-steps/SKILL.md +113 -0
- data/skills/rigor-plugin-tune/SKILL.md +79 -0
- data/skills/rigor-protection-uplift/SKILL.md +133 -0
- data/skills/rigor-rbs-setup/SKILL.md +128 -0
- data/skills/rigor-upgrade/SKILL.md +79 -0
- metadata +90 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# rigor-actionmailer
|
|
2
|
+
|
|
3
|
+
Validates `Mailer.action(args).deliver_*` call sites for action
|
|
4
|
+
existence and argument arity, and flags mailer actions whose view
|
|
5
|
+
template is missing under `app/views/`. Actions inherited from
|
|
6
|
+
included concern modules are merged into the mailer's action set,
|
|
7
|
+
so a mailer that derives its actions from `include`d `Emails::*`
|
|
8
|
+
concerns still type-checks. No Rails runtime dependency.
|
|
9
|
+
|
|
10
|
+
It ships bundled in `rigortype`. Activate it under `plugins:`:
|
|
11
|
+
|
|
12
|
+
```yaml
|
|
13
|
+
plugins:
|
|
14
|
+
- rigor-actionmailer
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## What it checks
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
demo.rb:7:1: info: `UserMailer.welcome` matches mailer action (arity 1..2)
|
|
21
|
+
errors_demo.rb:7:1: error: `UserMailer.welcome` expects 1..2 argument(s), got 0
|
|
22
|
+
errors_demo.rb:15:1: error: `UserMailer.does_not_exist` is not a defined mailer action (known actions: digest, reset_password, welcome)
|
|
23
|
+
app/mailers/user_mailer.rb:14:7: warning: `UserMailer#digest` has no view template under `app/views/user_mailer/`
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
1. **Action existence** — `Mailer.unknown_action(...)` →
|
|
27
|
+
`unknown-action` (an unresolved `include` silences this rather
|
|
28
|
+
than guessing).
|
|
29
|
+
2. **Argument arity** — too few / too many positional args →
|
|
30
|
+
`wrong-arity`.
|
|
31
|
+
3. **View template existence** — each action needs at least one
|
|
32
|
+
`app/views/<mailer_underscore>/<action>.{html,text}.{erb,haml,slim}`;
|
|
33
|
+
a missing one → `missing-view`, anchored on the action's `def`.
|
|
34
|
+
|
|
35
|
+
Recognised call shapes: a direct action call
|
|
36
|
+
(`UserMailer.welcome(user)`), a `.with(...)` chain
|
|
37
|
+
(`UserMailer.with(user: u).welcome(user)`), and a trailing
|
|
38
|
+
`.deliver_now` / `.deliver_later` (accepted, not interpreted).
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
plugins:
|
|
44
|
+
- gem: rigor-actionmailer
|
|
45
|
+
config:
|
|
46
|
+
mailer_search_paths: ["app/mailers"] # default
|
|
47
|
+
mailer_base_classes: ["ApplicationMailer", "ActionMailer::Base"] # default
|
|
48
|
+
views_root: "app/views" # default
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Limitations
|
|
52
|
+
|
|
53
|
+
- **Direct-superclass match only.** `class CustomerMailer <
|
|
54
|
+
BaseMailer` where `BaseMailer < ApplicationMailer` is not
|
|
55
|
+
discovered unless `BaseMailer` is in `mailer_base_classes`.
|
|
56
|
+
(Actions from `include`d concern *modules* are merged; this is
|
|
57
|
+
about the superclass chain.)
|
|
58
|
+
- **Syntactic action list.** Actions are read from instance-side
|
|
59
|
+
`def`s; `define_method`, `initialize`, and `_`-prefixed names are
|
|
60
|
+
excluded.
|
|
61
|
+
- **Standard view filename pattern only**
|
|
62
|
+
(`<action>.{html,text}.{erb,haml,slim}`); custom engines / view
|
|
63
|
+
paths are out of scope.
|
|
64
|
+
- A brand-new view file does not invalidate the cached index until
|
|
65
|
+
something the mailer file touches changes (the read-tracking
|
|
66
|
+
trade-off).
|
|
67
|
+
|
|
68
|
+
## Plugin internals
|
|
69
|
+
|
|
70
|
+
The mailer/concern discoverer, the cached `:mailer_index` producer,
|
|
71
|
+
the demo, and the contract surfaces this plugin exercises are in
|
|
72
|
+
the [plugin's README](../../../plugins/rigor-actionmailer/README.md).
|
|
73
|
+
To write a plugin, see [`examples/`](../../../examples/README.md)
|
|
74
|
+
and the [`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# rigor-actionpack
|
|
2
|
+
|
|
3
|
+
Checks controller-side Action Pack code across four areas, by
|
|
4
|
+
consuming facts other Rails plugins publish (ADR-9):
|
|
5
|
+
|
|
6
|
+
- **Route-helper calls** — `redirect_to user_path(@user)` against
|
|
7
|
+
the `:helper_table` from [`rigor-rails-routes`](rigor-rails-routes.md).
|
|
8
|
+
- **Filter chains** — `before_action :name` against the
|
|
9
|
+
controller's (and its parents') defined methods.
|
|
10
|
+
- **Render targets** — `render :show` / `render partial:` against
|
|
11
|
+
the view templates under `view_search_paths`.
|
|
12
|
+
- **Strong parameters** — `params.require(:user).permit(:name, …)`
|
|
13
|
+
keys against the model's columns (via `:model_index` from
|
|
14
|
+
[`rigor-activerecord`](rigor-activerecord.md)).
|
|
15
|
+
|
|
16
|
+
It ships bundled in `rigortype`. Activate it under `plugins:`,
|
|
17
|
+
alongside the producers whose facts it consumes:
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
plugins:
|
|
21
|
+
- rigor-rails-routes # publishes :helper_table (optional)
|
|
22
|
+
- rigor-activerecord # publishes :model_index (optional)
|
|
23
|
+
- rigor-actionpack
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Both dependencies are declared `optional` — a project that omits a
|
|
27
|
+
producer still loads; the area that needed that fact degrades to a
|
|
28
|
+
no-op rather than erroring.
|
|
29
|
+
|
|
30
|
+
## What it checks
|
|
31
|
+
|
|
32
|
+
| Rule | Severity | Fires when |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `plugin.actionpack.helper-call` | info | a `*_path` / `*_url` call resolved against the helper table |
|
|
35
|
+
| `plugin.actionpack.unknown-helper` | error | the helper name is not in the table (with a did-you-mean) |
|
|
36
|
+
| `plugin.actionpack.wrong-helper-arity` | error | the call's positional-arg count ≠ the helper's recorded arity |
|
|
37
|
+
| `plugin.actionpack.filter-call` | info | a filter reference (`before_action :name`, `skip_around_action`, …) resolved to a defined method |
|
|
38
|
+
| `plugin.actionpack.unknown-filter-method` | error | a filter reference names a method not defined on the controller or a parent (with a did-you-mean) |
|
|
39
|
+
| `plugin.actionpack.render-target` | info | an explicit `render :symbol` / `"string"` / `partial:` resolved to a view template |
|
|
40
|
+
| `plugin.actionpack.missing-template` | error | an explicit `render` resolved to a view path that doesn't exist under any `view_search_paths` |
|
|
41
|
+
| `plugin.actionpack.permit-call` | info | a `params.require(:m).permit(:key, …)` chain resolved to a known model; keys matched against its columns |
|
|
42
|
+
| `plugin.actionpack.unknown-permit-key` | error | a literal `permit(:key)` isn't a column on the model (with a did-you-mean) |
|
|
43
|
+
|
|
44
|
+
Filter and render resolution honours nested-module controller
|
|
45
|
+
qualification (`module Admin; class WidgetsController` resolves
|
|
46
|
+
views under `admin/widgets/…`) and silences gem-shipped parent
|
|
47
|
+
classes it can't see.
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
plugins:
|
|
53
|
+
- gem: rigor-actionpack
|
|
54
|
+
config:
|
|
55
|
+
controller_search_paths: ["app/controllers"] # default
|
|
56
|
+
view_search_paths: ["app/views"] # default
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Limitations
|
|
60
|
+
|
|
61
|
+
- **Implicit-self helpers only.** `*_path` / `*_url` calls with an
|
|
62
|
+
explicit receiver (`Rails.application.routes.url_helpers.x_path`)
|
|
63
|
+
are passed through.
|
|
64
|
+
- **Path-based file filter.** Files under
|
|
65
|
+
`controller_search_paths` are checked regardless of class
|
|
66
|
+
hierarchy; a non-controller file placed there (rare) would be
|
|
67
|
+
scanned.
|
|
68
|
+
- **Coverage follows the upstream facts.** Helper validation only
|
|
69
|
+
knows what `rigor-rails-routes` published, and `permit`
|
|
70
|
+
validation only what `rigor-activerecord` published — enabling
|
|
71
|
+
those producers widens what this plugin can check.
|
|
72
|
+
|
|
73
|
+
## Plugin internals
|
|
74
|
+
|
|
75
|
+
The cross-plugin fact contract (`:helper_table` / `:model_index`),
|
|
76
|
+
the controller/view discovery producers, the demo, and the
|
|
77
|
+
contract surfaces this plugin exercises are in the
|
|
78
|
+
[plugin's README](../../../plugins/rigor-actionpack/README.md). To
|
|
79
|
+
write a plugin, see [`examples/`](../../../examples/README.md) and
|
|
80
|
+
the [`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# rigor-activejob
|
|
2
|
+
|
|
3
|
+
Validates `Job.perform_later(...)` / `.perform_now(...)` /
|
|
4
|
+
`.perform(...)` argument arity against the discovered `#perform`
|
|
5
|
+
definition. No Rails runtime dependency — the plugin reads project
|
|
6
|
+
source via Prism only.
|
|
7
|
+
|
|
8
|
+
It ships bundled in `rigortype`. Activate it under `plugins:`:
|
|
9
|
+
|
|
10
|
+
```yaml
|
|
11
|
+
plugins:
|
|
12
|
+
- rigor-activejob
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What it checks
|
|
16
|
+
|
|
17
|
+
Given a job whose `#perform` takes one required and one optional
|
|
18
|
+
argument (arity `1..2`):
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
demo.rb:6:1: info: `WelcomeEmailJob.perform_later` matches `#perform` (arity 1..2)
|
|
22
|
+
demo.rb:9:1: error: `WelcomeEmailJob.perform_later` expects 1..2 argument(s), got 0
|
|
23
|
+
demo.rb:12:1: error: `WelcomeEmailJob.perform_later` expects 1..2 argument(s), got 3
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
A `*rest` parameter yields an unbounded upper bound (`arity 0+`).
|
|
27
|
+
All three entry points — `perform_later` (async), `perform_now`
|
|
28
|
+
(sync), and bare `perform` — are validated against the same
|
|
29
|
+
`#perform` envelope.
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
plugins:
|
|
35
|
+
- gem: rigor-activejob
|
|
36
|
+
config:
|
|
37
|
+
job_search_paths: ["app/jobs"] # default
|
|
38
|
+
job_base_classes: ["ApplicationJob", "ActiveJob::Base"] # default
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Limitations
|
|
42
|
+
|
|
43
|
+
- **Direct-superclass match only.** `class WelcomeJob < BaseJob`
|
|
44
|
+
where `BaseJob < ApplicationJob` is not discovered unless you
|
|
45
|
+
add `BaseJob` to `job_base_classes`.
|
|
46
|
+
- **Syntactic arity.** `#perform` arity is read from the parameter
|
|
47
|
+
list; a `#perform` built with `define_method` is out of scope.
|
|
48
|
+
- **Positional arity only.** Required keyword arguments are
|
|
49
|
+
recorded by the discoverer but not yet validated at the call
|
|
50
|
+
site.
|
|
51
|
+
|
|
52
|
+
## Plugin internals
|
|
53
|
+
|
|
54
|
+
The job discoverer / index, the cached `:job_index` producer, the
|
|
55
|
+
demo, and the contract surfaces this plugin exercises are in the
|
|
56
|
+
[plugin's README](../../../plugins/rigor-activejob/README.md). To
|
|
57
|
+
write a plugin, see [`examples/`](../../../examples/README.md) and
|
|
58
|
+
the [`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# rigor-activerecord
|
|
2
|
+
|
|
3
|
+
Types ActiveRecord finder and relation calls against your
|
|
4
|
+
project's `db/schema.rb` and discovered model classes — so
|
|
5
|
+
`User.find(1)` is `User`, `User.where(emial: …)` is flagged as
|
|
6
|
+
an unknown column, and `user.posts` carries its element type
|
|
7
|
+
through the chain. The plugin reads source only; it never loads
|
|
8
|
+
`active_record`, so Rigor stays decoupled from Rails.
|
|
9
|
+
|
|
10
|
+
It ships bundled in `rigortype` — no separate install. Activate
|
|
11
|
+
it under `plugins:` in your config file:
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
plugins:
|
|
15
|
+
- rigor-activerecord
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What it checks
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
demo.rb:20:1: info: `User.find` returns User (table: `users`) [plugin.activerecord.model-call]
|
|
22
|
+
demo.rb:23:1: info: `User.where` (:admin) on table `users` [plugin.activerecord.model-call]
|
|
23
|
+
|
|
24
|
+
errors_demo.rb:13:1: error: `User.where(emial: ...)` references unknown column `emial` on table `users` (did you mean `:email`?) [plugin.activerecord.unknown-column]
|
|
25
|
+
errors_demo.rb:25:1: error: `User.find` expects at least 1 argument, got 0 [plugin.activerecord.wrong-arity]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Diagnostic | Severity | Rule |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| Recognised `Model.find` / `Model.find_by` / `Model.where` call | `:info` | `plugin.activerecord.model-call` |
|
|
31
|
+
| `Model.find_by(unknown: ...)` / `Model.where(unknown: ...)` | `:error` | `plugin.activerecord.unknown-column` |
|
|
32
|
+
| `Model.find` with 0 args | `:error` | `plugin.activerecord.wrong-arity` |
|
|
33
|
+
| `db/schema.rb` not readable | `:warning` | `plugin.activerecord.load-error` |
|
|
34
|
+
|
|
35
|
+
Did-you-mean suggestions use Levenshtein distance ≤ 3 against
|
|
36
|
+
the resolved table's column names.
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
plugins:
|
|
42
|
+
- gem: rigor-activerecord
|
|
43
|
+
config:
|
|
44
|
+
schema_file: "db/schema.rb" # default
|
|
45
|
+
model_search_paths: ["app/models"] # default
|
|
46
|
+
model_base_classes: ["ApplicationRecord", "ActiveRecord::Base"] # default
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
All three keys are optional. Tweak them when:
|
|
50
|
+
|
|
51
|
+
- the schema lives elsewhere (`schema_file: "shared/db/schema.rb"`);
|
|
52
|
+
- models are in a non-standard directory
|
|
53
|
+
(`model_search_paths: ["domain/models", "engines/billing/app/models"]`);
|
|
54
|
+
- the base class is custom
|
|
55
|
+
(`model_base_classes: ["DbRecord", "ApplicationRecord"]`).
|
|
56
|
+
|
|
57
|
+
## What it infers
|
|
58
|
+
|
|
59
|
+
The plugin contributes call-site types as well as diagnostics.
|
|
60
|
+
Class-side: `User.find(1)` → `User`, `User.find_by(...)` →
|
|
61
|
+
`User | nil`, `User.find_by!(...)` → non-nullable `User`.
|
|
62
|
+
Instance-side: a column read (`user.name`) narrows to the
|
|
63
|
+
column's value type, `user.admin?` to `bool`, and a singular
|
|
64
|
+
association (`post.user`) to the target model.
|
|
65
|
+
|
|
66
|
+
Relation-returning call sites — `User.where(...)`, `User.all`,
|
|
67
|
+
`User.order(...)`, a `has_many` / `has_and_belongs_to_many`
|
|
68
|
+
accessor (`user.posts`), and user-declared `scope`s
|
|
69
|
+
(`Post.published`) — narrow to `ActiveRecord::Relation[Model]`.
|
|
70
|
+
Chained query methods keep the element type, and iteration
|
|
71
|
+
(`user.posts.each { |p| ... }`) yields the model. A user-defined
|
|
72
|
+
scope invoked on a typed relation (`User.where(...).published`)
|
|
73
|
+
never surfaces a false `call.undefined-method`.
|
|
74
|
+
|
|
75
|
+
## Limitations
|
|
76
|
+
|
|
77
|
+
- **Direct-superclass match only.** `class Admin < User` where
|
|
78
|
+
`User < ApplicationRecord` is not discovered. Either add `User`
|
|
79
|
+
to `model_base_classes`, or list every concrete model
|
|
80
|
+
explicitly.
|
|
81
|
+
- **`db/schema.rb` only.** `db/structure.sql` (raw SQL dumps) is
|
|
82
|
+
not supported in this iteration.
|
|
83
|
+
- **Column reads, not setters.** The plugin types instance-side
|
|
84
|
+
column *reads* (`user.name`, `user.admin?`) and singular
|
|
85
|
+
associations, but not the `name=` setter or the dirty-tracking
|
|
86
|
+
family (`name_changed?`, `name_was`, …).
|
|
87
|
+
- **Project-custom inflections aren't read yet.** Model↔table
|
|
88
|
+
pluralization goes through the real ActiveSupport inflector
|
|
89
|
+
(so `Person → people`, `Mouse → mice` resolve), but rules you
|
|
90
|
+
declare in `config/initializers/inflections.rb` are not yet
|
|
91
|
+
ingested — a model relying on one needs `self.table_name`
|
|
92
|
+
(ADR-39 slice 3).
|
|
93
|
+
|
|
94
|
+
## Plugin internals
|
|
95
|
+
|
|
96
|
+
Architecture (the cached schema-parser → model-index → analyzer
|
|
97
|
+
chain), the source layout, how to run the demo, and the plugin
|
|
98
|
+
contract surfaces this plugin exercises are documented in the
|
|
99
|
+
[plugin's README](../../../plugins/rigor-activerecord/README.md).
|
|
100
|
+
To write a plugin of your own, see the
|
|
101
|
+
[`examples/`](../../../examples/README.md) walkthroughs and the
|
|
102
|
+
[`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# rigor-activestorage
|
|
2
|
+
|
|
3
|
+
Walks ActiveRecord model files for `has_one_attached` /
|
|
4
|
+
`has_many_attached` macros and types the attachment accessors they
|
|
5
|
+
generate, so navigating an attachment resolves through
|
|
6
|
+
ActiveStorage's own RBS surface instead of falling to the untyped
|
|
7
|
+
envelope. It reads source only — no Rails runtime dependency.
|
|
8
|
+
|
|
9
|
+
It ships bundled in `rigortype`. Activate it under `plugins:`:
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
plugins:
|
|
13
|
+
- rigor-activestorage
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What it infers
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
class User < ApplicationRecord
|
|
20
|
+
has_one_attached :avatar
|
|
21
|
+
has_many_attached :photos
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
user = User.find(1)
|
|
25
|
+
user.avatar # Nominal[ActiveStorage::Attached::One]
|
|
26
|
+
user.avatar.attached? # resolves through ActiveStorage's RBS
|
|
27
|
+
user.photos # Nominal[ActiveStorage::Attached::Many]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Macro | Accessor | Contributed type |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `has_one_attached :avatar` | `user.avatar` | `Nominal[ActiveStorage::Attached::One]` |
|
|
33
|
+
| `has_many_attached :photos` | `user.photos` | `Nominal[ActiveStorage::Attached::Many]` |
|
|
34
|
+
|
|
35
|
+
Setters (`user.avatar = …`) and attachment-name calls with
|
|
36
|
+
arguments decline — those are covered by ActiveStorage's own RBS.
|
|
37
|
+
|
|
38
|
+
## Diagnostics
|
|
39
|
+
|
|
40
|
+
| Rule | Severity | When |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| `plugin.activestorage.attachment-call` | info | a recognised `model.attachment_name` call surfaces; confirms the model → attachment mapping |
|
|
43
|
+
| `plugin.activestorage.load-error` | warning | discovery failed (e.g. the model directory is inaccessible under the IoBoundary trust policy) |
|
|
44
|
+
|
|
45
|
+
No `:error` diagnostics in this slice — the value is the
|
|
46
|
+
return-type contribution; an "unknown attachment name" rule is a
|
|
47
|
+
future slice.
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
plugins:
|
|
53
|
+
- gem: rigor-activestorage
|
|
54
|
+
config:
|
|
55
|
+
model_search_paths: ["app/models"] # default
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## With or without rigor-activerecord
|
|
59
|
+
|
|
60
|
+
The plugin discovers model files independently, so it works
|
|
61
|
+
stand-alone. When [`rigor-activerecord`](rigor-activerecord.md) is
|
|
62
|
+
also active the two coexist (each contributes its own per-call
|
|
63
|
+
return type and the contribution merger reconciles); the
|
|
64
|
+
`:model_index` dependency is declared `optional`, reserved for a
|
|
65
|
+
future slice that would restrict attachment recognition to
|
|
66
|
+
discovered AR classes.
|
|
67
|
+
|
|
68
|
+
## Plugin internals
|
|
69
|
+
|
|
70
|
+
The discovery pass, the `AttachmentIndex`, and the contract
|
|
71
|
+
surfaces this plugin exercises are in the
|
|
72
|
+
[plugin's README](../../../plugins/rigor-activestorage/README.md).
|
|
73
|
+
To write a plugin, see [`examples/`](../../../examples/README.md)
|
|
74
|
+
and the [`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# rigor-activesupport-core-ext
|
|
2
|
+
|
|
3
|
+
An opt-in **RBS bundle** for the ActiveSupport `core_ext` extensions
|
|
4
|
+
that real Rails code uses most — `Time.current`, `3.days`, `Array.wrap`,
|
|
5
|
+
`"x".squish`, `obj.blank?`, and the rest. It ships no analyzer and no
|
|
6
|
+
diagnostics: its whole job is to hand Rigor signatures for these
|
|
7
|
+
methods so they stop showing up as `call.undefined-method` false
|
|
8
|
+
positives. A four-project Rails survey found **64–90% of every
|
|
9
|
+
project's diagnostics** came from ActiveSupport extensions missing from
|
|
10
|
+
stdlib RBS — making this the single largest false-positive suppressor
|
|
11
|
+
for Rails apps, and the one to reach for first when Rigor floods a Rails
|
|
12
|
+
codebase with undefined-method noise.
|
|
13
|
+
|
|
14
|
+
It ships bundled in `rigortype`. Activate it under `plugins:`:
|
|
15
|
+
|
|
16
|
+
```yaml
|
|
17
|
+
plugins:
|
|
18
|
+
- rigor-activesupport-core-ext
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
That is the whole setup — Rigor resolves the bundled `sig/`
|
|
22
|
+
automatically ([ADR-25](../../adr/25-plugin-contributed-rbs.md)); no
|
|
23
|
+
path, no vendoring, no `signature_paths:` wiring.
|
|
24
|
+
|
|
25
|
+
> **You may not need this plugin.** As of
|
|
26
|
+
> [ADR-72](../../adr/72-gemfile-lock-gated-rbs-overlays.md), Rigor
|
|
27
|
+
> auto-loads a bundled core-ext RBS overlay whenever `activesupport` is
|
|
28
|
+
> in your `Gemfile.lock` but ships no RBS — so the most common
|
|
29
|
+
> ActiveSupport false positives are already suppressed with zero config.
|
|
30
|
+
> This plugin is the **opt-in, fuller twin** of that overlay (and the
|
|
31
|
+
> authoring home for the signatures); load it when you want the complete
|
|
32
|
+
> surface. When it is loaded, the auto overlay stands down so the two
|
|
33
|
+
> never double-declare.
|
|
34
|
+
|
|
35
|
+
## What it covers
|
|
36
|
+
|
|
37
|
+
Roughly the top ~40 selectors plus their close neighbours, across:
|
|
38
|
+
|
|
39
|
+
- **Object (universal)** — `#blank?`, `#present?`, `#presence`, `#try`,
|
|
40
|
+
`#try!`, `#acts_like?` (+ `NilClass` / `TrueClass` / `FalseClass`).
|
|
41
|
+
- **Integer / Float** — Duration multipliers (`#days`, `#hours`,
|
|
42
|
+
`#minutes`, …) and Bytes multipliers (`#megabytes`, `#gigabytes`, …).
|
|
43
|
+
- **String** — inflections (`#underscore`, `#camelize`, `#classify`,
|
|
44
|
+
`#constantize`, `#pluralize`, …), filters (`#squish`, `#truncate`),
|
|
45
|
+
`#html_safe`, `#starts_with?` / `#ends_with?`, conversions.
|
|
46
|
+
- **Time / Date / DateTime** — `.current`, `.zone`, `#yesterday`,
|
|
47
|
+
`#tomorrow`, `#beginning_of_*` / `#end_of_*`, `#ago`, `#since`.
|
|
48
|
+
- **Array** — `.wrap`, `#to_sentence`, `#in_groups_of`, `#second` …
|
|
49
|
+
`#fifth`, `#compact_blank`, `#exclude?`.
|
|
50
|
+
- **Hash** — `#symbolize_keys` / `#stringify_keys` (+ deep / bang),
|
|
51
|
+
`#deep_merge`, `#with_indifferent_access`, `#except!`.
|
|
52
|
+
- **Enumerable** — `#index_by`, `#index_with`, `#pluck`, `#exclude?`.
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
3.days # without the bundle: call.undefined-method Integer#days
|
|
56
|
+
" x ".squish # without the bundle: call.undefined-method String#squish
|
|
57
|
+
Time.current # without the bundle: call.undefined-method Time.current
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## No diagnostics, no config
|
|
61
|
+
|
|
62
|
+
The plugin is RBS-only — it emits no diagnostics and has no
|
|
63
|
+
configuration knobs. It contributes its signatures unconditionally when
|
|
64
|
+
listed under `plugins:`.
|
|
65
|
+
|
|
66
|
+
## Limitations
|
|
67
|
+
|
|
68
|
+
- **Conservative return types.** `Integer#days` really returns
|
|
69
|
+
`ActiveSupport::Duration`, but the bundle types it `untyped` because
|
|
70
|
+
the analysis environment usually lacks the Duration class — the goal
|
|
71
|
+
is to silence undefined-method, not to give precise returns. Likewise
|
|
72
|
+
`#html_safe` is typed `String` (not `SafeBuffer`) and `#try` / `#try!`
|
|
73
|
+
return `untyped`.
|
|
74
|
+
- **Project-private monkey-patches are not covered** — only real
|
|
75
|
+
ActiveSupport extensions. For your own core-class patches see the
|
|
76
|
+
`pre_eval:` mechanism ([ADR-17](../../adr/17-monkey-patch-pre-evaluation.md)).
|
|
77
|
+
- **Top ~40 selectors, not exhaustive.** ActiveSupport ships hundreds of
|
|
78
|
+
extensions; this covers the head of the real-world distribution.
|
|
79
|
+
|
|
80
|
+
## Plugin internals
|
|
81
|
+
|
|
82
|
+
The RBS layout, the per-class coverage, and the survey that picked the
|
|
83
|
+
selectors are in the
|
|
84
|
+
[plugin's README](../../../plugins/rigor-activesupport-core-ext/README.md).
|
|
85
|
+
To write a plugin, see [`examples/`](../../../examples/README.md) and the
|
|
86
|
+
[`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# rigor-devise
|
|
2
|
+
|
|
3
|
+
Teaches Rigor about the methods Devise mixes into a model from a
|
|
4
|
+
`devise :strategy, …` declaration, so cross-file calls to those
|
|
5
|
+
methods (`user.valid_password?("pw")`,
|
|
6
|
+
`user.send_reset_password_instructions`) resolve instead of
|
|
7
|
+
surfacing a false `call.undefined-method` — and resolve with their
|
|
8
|
+
real return types. It reads source only; no Devise runtime
|
|
9
|
+
dependency.
|
|
10
|
+
|
|
11
|
+
It ships bundled in `rigortype`. Activate it under `plugins:`:
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
plugins:
|
|
15
|
+
- rigor-devise
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What it does — no diagnostics, no config
|
|
19
|
+
|
|
20
|
+
`rigor-devise` is a macro-expansion plugin (ADR-16 Tier B): it
|
|
21
|
+
emits no diagnostics and has no configuration. From a declaration
|
|
22
|
+
like
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
class User < ApplicationRecord
|
|
26
|
+
devise :database_authenticatable, :recoverable
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
it synthesises the instance methods each named strategy module
|
|
31
|
+
contributes, attaching them to the declaring class so a call in
|
|
32
|
+
another file type-checks:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
user.valid_password?("pw") # bool
|
|
36
|
+
user.send_reset_password_instructions # (the module's RBS return)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Return types are the strategy module's **authored RBS return**
|
|
40
|
+
(via `origin_module:` provenance), not a widened `Dynamic[T]`.
|
|
41
|
+
Eleven strategies are recognised — `database_authenticatable`,
|
|
42
|
+
`recoverable`, `rememberable`, `registerable`, `trackable`,
|
|
43
|
+
`validatable`, `confirmable`, `lockable`, `timeoutable`,
|
|
44
|
+
`omniauthable`, `authenticatable` — plus the always-included
|
|
45
|
+
`Devise::Models::Authenticatable`. Strategies declared inside an
|
|
46
|
+
`ActiveSupport::Concern`'s `included do … end` are re-targeted onto
|
|
47
|
+
whatever class includes the concern.
|
|
48
|
+
|
|
49
|
+
## Limitations
|
|
50
|
+
|
|
51
|
+
- **Instance methods only.** Per-module `ClassMethods` (e.g.
|
|
52
|
+
`User.reset_password_by_token`) are not yet synthesised.
|
|
53
|
+
- **Controller helpers deferred.** `current_user` /
|
|
54
|
+
`authenticate_user!` / `user_signed_in?` come from
|
|
55
|
+
`devise_for :users` in the routes file (Tier C work), not the
|
|
56
|
+
model declaration, so they are not contributed yet.
|
|
57
|
+
- **Third-party strategies aren't scanned.** A strategy registered
|
|
58
|
+
via `Devise.add_module :foo` in an initializer is unknown to the
|
|
59
|
+
bundled strategy table.
|
|
60
|
+
|
|
61
|
+
## Plugin internals
|
|
62
|
+
|
|
63
|
+
The macro manifest (the trait registry mapping each strategy to its
|
|
64
|
+
module), the concern re-targeting walk, the demo, and the
|
|
65
|
+
contract surfaces this plugin exercises are in the
|
|
66
|
+
[plugin's README](../../../plugins/rigor-devise/README.md);
|
|
67
|
+
[handbook chapter 9](../../handbook/09-plugins.md) covers the Tier B
|
|
68
|
+
macro substrate generally. To write a plugin, see
|
|
69
|
+
[`examples/`](../../../examples/README.md) and the
|
|
70
|
+
[`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# rigor-dry-schema
|
|
2
|
+
|
|
3
|
+
Recognises dry-schema declarations and publishes a per-schema
|
|
4
|
+
typed-key table as a cross-plugin fact (`:dry_schema_table`) that
|
|
5
|
+
`rigor-dry-validation` consumes for typed-payload synthesis. Like
|
|
6
|
+
[`rigor-dry-types`](rigor-dry-types.md), it is a foundation plugin:
|
|
7
|
+
no diagnostics of its own, no config keys.
|
|
8
|
+
|
|
9
|
+
It ships bundled in `rigortype`. Activate it (pair with
|
|
10
|
+
`rigor-dry-types` to resolve `Types::*` aliases inside predicate
|
|
11
|
+
arguments):
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
plugins:
|
|
15
|
+
- rigor-dry-types # optional: resolves Types::Email etc.
|
|
16
|
+
- rigor-dry-schema
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What it recognises
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
NewUserSchema = Dry::Schema.Params do
|
|
23
|
+
required(:email).filled(:string)
|
|
24
|
+
required(:age).value(:integer)
|
|
25
|
+
required(:tags).each(:string)
|
|
26
|
+
optional(:nickname).maybe(:string)
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- `required` / `optional` keys, with the predicate's type symbol
|
|
31
|
+
mapped to a Ruby class (`:string` → `String`, `:integer` →
|
|
32
|
+
`Integer`, `:decimal` → `BigDecimal`, `:bool` → `TrueClass`, …).
|
|
33
|
+
- `each(:T)` marks the key as a **list** (`list: true`);
|
|
34
|
+
`filled` / `value` / `maybe` are scalar (`list: false`).
|
|
35
|
+
- `value(Types::Email)` resolves through the `:dry_type_aliases`
|
|
36
|
+
fact when `rigor-dry-types` is loaded; without it (or for an
|
|
37
|
+
unknown reference) the row drops from the table rather than
|
|
38
|
+
mislead downstream consumers.
|
|
39
|
+
- Top-level (`Foo = Dry::Schema.Params { … }`) and class-level
|
|
40
|
+
(`class Bar; SCHEMA = …; end` → `"Bar::SCHEMA"`) declarations,
|
|
41
|
+
across `.Params` / `.JSON` / `.define`.
|
|
42
|
+
|
|
43
|
+
## No diagnostics, no config
|
|
44
|
+
|
|
45
|
+
The plugin only publishes the schema table for other plugins to
|
|
46
|
+
consume; it surfaces no diagnostics and takes no config keys. (A
|
|
47
|
+
future slice adds `dry-schema.unknown-predicate` / `unknown-type`
|
|
48
|
+
info diagnostics and typed `result.to_h` synthesis.)
|
|
49
|
+
|
|
50
|
+
## Plugin internals
|
|
51
|
+
|
|
52
|
+
The `prepare(services)` scan, the published `:dry_schema_table`
|
|
53
|
+
shape, and the slice floor/ceiling are documented in the
|
|
54
|
+
[plugin's README](../../../plugins/rigor-dry-schema/README.md). To
|
|
55
|
+
write a plugin, see [`examples/`](../../../examples/README.md) and
|
|
56
|
+
the [`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# rigor-dry-struct
|
|
2
|
+
|
|
3
|
+
Recognises dry-struct's class-level `attribute :name, Type` /
|
|
4
|
+
`attribute? :name, Type` DSL on `Dry::Struct` subclasses and
|
|
5
|
+
synthesises the reader methods it generates — so a bare
|
|
6
|
+
`address.city` in another file resolves instead of falling through
|
|
7
|
+
to `call.undefined-method`.
|
|
8
|
+
|
|
9
|
+
It ships bundled in `rigortype`. Activate it — and pair it with
|
|
10
|
+
[`rigor-dry-types`](rigor-dry-types.md) for precise reader types:
|
|
11
|
+
|
|
12
|
+
```yaml
|
|
13
|
+
plugins:
|
|
14
|
+
- rigor-dry-struct
|
|
15
|
+
- rigor-dry-types # optional: resolves Types::String → String on the readers
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What it does
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
class Address < Dry::Struct
|
|
22
|
+
attribute :city, Types::String
|
|
23
|
+
attribute? :postcode, Types::String
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
address.city # resolves (synthesised reader)
|
|
27
|
+
address.postcode # resolves
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
When [`rigor-dry-types`](rigor-dry-types.md) is also active and the
|
|
31
|
+
project declares `module Types; include Dry.Types(); end`, the
|
|
32
|
+
reader's return type resolves through the attribute's type argument
|
|
33
|
+
(`attribute :city, Types::String` → `city` returns `String`). When
|
|
34
|
+
dry-types isn't loaded, or for a shape it can't resolve (a
|
|
35
|
+
`.constrained(...)` chain, an inline composition), the reader falls
|
|
36
|
+
back to `Dynamic[top]` — silently, no diagnostic.
|
|
37
|
+
|
|
38
|
+
## No diagnostics, no config
|
|
39
|
+
|
|
40
|
+
The plugin contributes synthesised methods, not diagnostics, and
|
|
41
|
+
has no config keys. It handles any class inheriting from
|
|
42
|
+
`Dry::Struct` (lexically, transitively, or through the RBS env when
|
|
43
|
+
the chain terminates upstream). Its visible effect is that
|
|
44
|
+
attribute-reader calls type-check.
|
|
45
|
+
|
|
46
|
+
## Limitations
|
|
47
|
+
|
|
48
|
+
- **Readers only.** The `schema` / `to_h` / `[:key]` / keyword-arg
|
|
49
|
+
`.new(name:)` shapes are not synthesised yet.
|
|
50
|
+
- **No nested-block form.** `attribute :details do … end` (which
|
|
51
|
+
mints a sibling `Dry::Struct` subclass) is deferred.
|
|
52
|
+
|
|
53
|
+
## Plugin internals
|
|
54
|
+
|
|
55
|
+
The declarative `HeredocTemplate` manifest, the
|
|
56
|
+
`returns_from_arg` precision path (ADR-18), and the
|
|
57
|
+
synthetic-method substrate it rides on are documented in the
|
|
58
|
+
[plugin's README](../../../plugins/rigor-dry-struct/README.md). To
|
|
59
|
+
write a plugin, see [`examples/`](../../../examples/README.md) and
|
|
60
|
+
the [`rigor-plugin-author`](../08-skills.md) skill.
|