rigortype 0.2.1 → 0.2.2
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 +557 -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 +532 -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.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/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,59 @@
|
|
|
1
|
+
# rigor-dry-types
|
|
2
|
+
|
|
3
|
+
The foundation plugin for the dry-rb family. It recognises the
|
|
4
|
+
canonical dry-types alias module —
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
module Types
|
|
8
|
+
include Dry.Types()
|
|
9
|
+
end
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
— and makes the alias names it generates resolvable to their
|
|
13
|
+
underlying Ruby classes, publishing them as a cross-plugin fact
|
|
14
|
+
(`:dry_type_aliases`) that [`rigor-dry-struct`](../07-plugins.md),
|
|
15
|
+
`rigor-dry-schema`, and `rigor-dry-validation` consume.
|
|
16
|
+
|
|
17
|
+
It ships bundled in `rigortype`. Activate it alongside the dry-rb
|
|
18
|
+
plugins that consume it:
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
plugins:
|
|
22
|
+
- rigor-dry-types
|
|
23
|
+
- rigor-dry-struct
|
|
24
|
+
# - rigor-dry-schema / rigor-dry-validation as needed
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## No diagnostics of its own
|
|
28
|
+
|
|
29
|
+
This plugin emits **no diagnostics** and has **no config keys** —
|
|
30
|
+
it only supplies type information to the other dry-rb plugins, so
|
|
31
|
+
its visible effect surfaces through them (a `Types::String`-typed
|
|
32
|
+
attribute resolving downstream, etc.).
|
|
33
|
+
|
|
34
|
+
## What it recognises
|
|
35
|
+
|
|
36
|
+
- **Canonical shortcuts** — `Types::String` / `Integer` / `Float` /
|
|
37
|
+
`Decimal` → `BigDecimal` / `Bool` / `Date` / `Hash` / `Array` /
|
|
38
|
+
`Any` → `Object`, and the rest.
|
|
39
|
+
- **Coercion-category namespaces** — the same names under
|
|
40
|
+
`Coercible::` / `Strict::` / `Params::` / `JSON::` (they share an
|
|
41
|
+
underlying class; the difference is runtime coercion).
|
|
42
|
+
- **Nested alias modules** — `module App; module Types; include
|
|
43
|
+
Dry.Types(); end; end` publishes `App::Types::String`, etc.
|
|
44
|
+
- **User-authored single-head compositions** — `Email =
|
|
45
|
+
String.constrained(format: …)`, `Status = Strict::Symbol`,
|
|
46
|
+
`PositiveInt = Integer.constrained(gt: 0).optional` publish under
|
|
47
|
+
the head's underlying class.
|
|
48
|
+
|
|
49
|
+
Unions (`String | Integer`), intersections, and transitive
|
|
50
|
+
references to other compositions (`ManagerEmail = Email`) are
|
|
51
|
+
skipped — there's no single underlying class to publish.
|
|
52
|
+
|
|
53
|
+
## Plugin internals
|
|
54
|
+
|
|
55
|
+
The `prepare(services)` scan, the published `:dry_type_aliases`
|
|
56
|
+
table, and the slice floor/ceiling are documented in the
|
|
57
|
+
[plugin's README](../../../plugins/rigor-dry-types/README.md). To
|
|
58
|
+
write a plugin, see [`examples/`](../../../examples/README.md) and
|
|
59
|
+
the [`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# rigor-dry-validation
|
|
2
|
+
|
|
3
|
+
Recognises `class T < Dry::Validation::Contract` subclasses,
|
|
4
|
+
publishes the set of contract class names as a cross-plugin fact
|
|
5
|
+
(`:dry_validation_contracts`), and ships an RBS overlay typing the
|
|
6
|
+
contract result API (`Contract#call → Result`, then `Result#success?`
|
|
7
|
+
/ `#failure?` / `#to_h` / `#errors` / `#[]`).
|
|
8
|
+
|
|
9
|
+
It ships bundled in `rigortype`. Activate it under `plugins:`:
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
plugins:
|
|
13
|
+
- rigor-dry-validation
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What it recognises
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
class NewUserContract < Dry::Validation::Contract
|
|
20
|
+
params do
|
|
21
|
+
required(:email).filled(:string)
|
|
22
|
+
required(:age).value(:integer)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The plugin recognises both the full-path
|
|
28
|
+
`Dry::Validation::Contract` and the lexical-`Dry` form
|
|
29
|
+
`Validation::Contract`, and publishes a sorted list of the
|
|
30
|
+
discovered contract names.
|
|
31
|
+
|
|
32
|
+
With the RBS overlay loaded (see below):
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
result = NewUserContract.new.call(input) # Dry::Validation::Result
|
|
36
|
+
result.success? # bool
|
|
37
|
+
result.to_h # Hash[Symbol, untyped]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## RBS overlay
|
|
41
|
+
|
|
42
|
+
The plugin bundles an RBS overlay (`sig/dry_validation.rbs`) that
|
|
43
|
+
types the result API above, and **contributes it automatically** —
|
|
44
|
+
the plugin's manifest declares `signature_paths: ["sig"]`
|
|
45
|
+
([ADR-25](../../adr/25-plugin-contributed-rbs.md)), so activating
|
|
46
|
+
`rigor-dry-validation` is enough; no project-side `signature_paths:`
|
|
47
|
+
wiring is required.
|
|
48
|
+
|
|
49
|
+
## No diagnostics, no config
|
|
50
|
+
|
|
51
|
+
The plugin publishes the contract list and the overlay; it emits no
|
|
52
|
+
diagnostics and takes no config keys. (Future slices add
|
|
53
|
+
`result.to_h` typing from a paired `rigor-dry-schema` `params` block
|
|
54
|
+
and per-contract `rule`-key diagnostics.)
|
|
55
|
+
|
|
56
|
+
## Plugin internals
|
|
57
|
+
|
|
58
|
+
The `prepare(services)` scan, the `:dry_validation_contracts` fact,
|
|
59
|
+
the RBS overlay, and the slice floor/ceiling are documented in the
|
|
60
|
+
[plugin's README](../../../plugins/rigor-dry-validation/README.md).
|
|
61
|
+
To write a plugin, see [`examples/`](../../../examples/README.md)
|
|
62
|
+
and the [`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# rigor-factorybot
|
|
2
|
+
|
|
3
|
+
Validates every `FactoryBot.create(:name, key: …)` / `.build(…)` /
|
|
4
|
+
`.build_stubbed(…)` / `.attributes_for(…)` / `*_list` call against
|
|
5
|
+
an index of your factory definitions: an unknown factory name or an
|
|
6
|
+
attribute key the factory doesn't declare is flagged (each with a
|
|
7
|
+
did-you-mean). When [`rigor-activerecord`](rigor-activerecord.md) is
|
|
8
|
+
also active, attribute keys are additionally cross-checked against
|
|
9
|
+
the model's columns. No FactoryBot runtime dependency.
|
|
10
|
+
|
|
11
|
+
It ships bundled in `rigortype`. Activate it under `plugins:`:
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
plugins:
|
|
15
|
+
- rigor-factorybot
|
|
16
|
+
# - rigor-activerecord # optional: enables the AR column cross-check
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What it checks
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# spec/factories/users.rb
|
|
23
|
+
FactoryBot.define do
|
|
24
|
+
factory :user do
|
|
25
|
+
name { "Alice" }
|
|
26
|
+
email { "alice@example.com" }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
FactoryBot.create(:user, name: "X") # ✓ info trace
|
|
31
|
+
FactoryBot.build(:post, headline: "Hi") # ✗ unknown-attribute (suggest :title)
|
|
32
|
+
FactoryBot.create(:usre) # ✗ unknown-factory (suggest :user)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Rule | Severity | Fires when |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `plugin.factorybot.factory-call` | info | the call resolved to a known factory; lists its declared attributes |
|
|
38
|
+
| `plugin.factorybot.unknown-factory` | error | the literal `:name` isn't in the factory index (with a did-you-mean) |
|
|
39
|
+
| `plugin.factorybot.unknown-attribute` | error | a keyword key isn't a declared attribute (with a did-you-mean); also checked against the model's columns when `:model_index` is available |
|
|
40
|
+
|
|
41
|
+
The legacy `FactoryGirl` constant is recognised the same way.
|
|
42
|
+
Recognised entry methods: `create` / `build` / `build_stubbed` /
|
|
43
|
+
`attributes_for` and the `*_list` variants. Inside a factory it
|
|
44
|
+
recognises `name { … }` (modern), `name "…"` (legacy positional),
|
|
45
|
+
and `add_attribute(:name) { … }`.
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
plugins:
|
|
51
|
+
- gem: rigor-factorybot
|
|
52
|
+
config:
|
|
53
|
+
factory_search_paths: ["spec/factories", "spec/factories.rb"] # default
|
|
54
|
+
# Minitest projects: ["test/factories"]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Limitations
|
|
58
|
+
|
|
59
|
+
- **Literal arguments only** — `FactoryBot.create(name)` with a
|
|
60
|
+
variable name passes through.
|
|
61
|
+
- **Traits / sequences / associations not collected yet** — an
|
|
62
|
+
attribute defined only inside `trait :admin do … end` can surface
|
|
63
|
+
a false `unknown-attribute` until the trait slice ships.
|
|
64
|
+
- **Explicit receiver only** — bare `create(:user)` (from
|
|
65
|
+
`include FactoryBot::Syntax::Methods`) is not recognised in this
|
|
66
|
+
slice; it needs receiver-type inference, which would otherwise
|
|
67
|
+
false-positive on every unrelated `create` call.
|
|
68
|
+
|
|
69
|
+
## Plugin internals
|
|
70
|
+
|
|
71
|
+
The factory discoverer / index, the cached producer, the
|
|
72
|
+
`:model_index` consumption, the demo, and the contract surfaces
|
|
73
|
+
this plugin exercises are in the
|
|
74
|
+
[plugin's README](../../../plugins/rigor-factorybot/README.md). To
|
|
75
|
+
write a plugin, see [`examples/`](../../../examples/README.md) and
|
|
76
|
+
the [`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# rigor-graphql
|
|
2
|
+
|
|
3
|
+
Recognises GraphQL-Ruby schema classes — `Schema::Object`,
|
|
4
|
+
`Schema::Enum`, `Schema::InputObject`, `Schema::Mutation` subclasses —
|
|
5
|
+
and walks their `field` / `value` / `argument` DSL declarations,
|
|
6
|
+
publishing the resulting type tables as
|
|
7
|
+
[ADR-9](../../adr/9-cross-plugin-api.md) cross-plugin facts that
|
|
8
|
+
downstream plugins can consume. graphql-ruby's `field` DSL is a pure
|
|
9
|
+
metadata recorder (it synthesises no Ruby methods), so Rigor's value
|
|
10
|
+
here is a static type table rather than method synthesis. It reads
|
|
11
|
+
source only, with no `graphql` runtime dependency.
|
|
12
|
+
|
|
13
|
+
It ships bundled in `rigortype`. Activate it under `plugins:`:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
plugins:
|
|
17
|
+
- rigor-graphql
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## What it infers
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
module Types
|
|
24
|
+
class User < GraphQL::Schema::Object
|
|
25
|
+
field :name, String, null: false
|
|
26
|
+
field :email, String, null: true
|
|
27
|
+
field :tags, [String], null: false # list-of form
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
publishes a `:graphql_type_table` fact:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
{
|
|
36
|
+
"Types::User" => {
|
|
37
|
+
"name" => { type: "String", nullable: false, list: false },
|
|
38
|
+
"email" => { type: "String", nullable: true, list: false },
|
|
39
|
+
"tags" => { type: "String", nullable: false, list: true }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
It publishes four independent facts from one project walk; consumers
|
|
45
|
+
that need only one (or none) are unaffected by the others:
|
|
46
|
+
|
|
47
|
+
| Fact | Source class | Shape |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `:graphql_type_table` | `Schema::Object` | `field` → `{type, nullable, list}` |
|
|
50
|
+
| `:graphql_enum_table` | `Schema::Enum` | `value "..."` → ordered value list |
|
|
51
|
+
| `:graphql_input_object_table` | `Schema::InputObject` | `argument` → `{type, required, list}` |
|
|
52
|
+
| `:graphql_mutation_table` | `Schema::Mutation` | `{arguments:, fields:}` combined |
|
|
53
|
+
|
|
54
|
+
Canonical GraphQL scalars map to Ruby classes (`String`→`String`,
|
|
55
|
+
`Integer`/`Int`→`Integer`, `Boolean`→`TrueClass`, `Float`→`Float`,
|
|
56
|
+
`ID`→`String`); user-defined types are recorded under their qualified
|
|
57
|
+
name. `null:` extracts to `nullable:` (defaulting to `true`, mirroring
|
|
58
|
+
graphql-ruby); `required:` defaults to `false`. The single-element
|
|
59
|
+
`[String]` list form is recognised.
|
|
60
|
+
|
|
61
|
+
## No diagnostics, no config
|
|
62
|
+
|
|
63
|
+
The plugin emits no diagnostics and has no configuration knobs — it
|
|
64
|
+
contributes the type tables above for other plugins to consume. It
|
|
65
|
+
walks every `paths:` entry's `.rb` files for the schema-class shapes.
|
|
66
|
+
|
|
67
|
+
## Limitations
|
|
68
|
+
|
|
69
|
+
- **No resolver-method type-check.** `field` declarations are recorded
|
|
70
|
+
as metadata; they are not yet cross-referenced against the Ruby
|
|
71
|
+
resolver methods that back them.
|
|
72
|
+
- **No `Schema.execute(...)` result typing.** Typing
|
|
73
|
+
`Schema.execute(query).to_h` against the queried fields is a future
|
|
74
|
+
plugin.
|
|
75
|
+
- **Constant-form types only.** The string form (`field :foo, "User"`)
|
|
76
|
+
and the `<Type>.array` / `<Type>!` sugar chains are not recognised
|
|
77
|
+
(the `[String]` bracket form is). Multi-element and empty list
|
|
78
|
+
literals are dropped.
|
|
79
|
+
- **Enum literal values only.** `value "ACTIVE"` registers; the
|
|
80
|
+
symbol-form (`value :ACTIVE`) and constant-form drop, and the
|
|
81
|
+
`value:` / `description:` kwargs ride along but stay out of the table.
|
|
82
|
+
- **No cache round-trip** — the walk re-runs each invocation.
|
|
83
|
+
|
|
84
|
+
## Plugin internals
|
|
85
|
+
|
|
86
|
+
The type scanner and the contract surfaces this plugin exercises are in
|
|
87
|
+
the [plugin's README](../../../plugins/rigor-graphql/README.md). To
|
|
88
|
+
write a plugin, see [`examples/`](../../../examples/README.md) and the
|
|
89
|
+
[`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# rigor-hanami
|
|
2
|
+
|
|
3
|
+
Enforces the **Hanami::Action protocol** for Hanami 3.x apps: every
|
|
4
|
+
class under `app/actions/**/*.rb` must define `#handle(request,
|
|
5
|
+
response)`, and inside those bodies Rigor types the two parameters as
|
|
6
|
+
`Hanami::Action::Request` / `Hanami::Action::Response` so misuse is
|
|
7
|
+
caught precisely. It is the reference production consumer of the
|
|
8
|
+
[ADR-28](../../adr/28-path-scoped-protocol-contracts.md) path-scoped
|
|
9
|
+
method-protocol contract — Rigor *provides* the parameter types and the
|
|
10
|
+
plugin *checks* the method shape. It reads source only, with no
|
|
11
|
+
`hanami` runtime dependency (it ships RBS stubs for the Request /
|
|
12
|
+
Response / Params surface).
|
|
13
|
+
|
|
14
|
+
It ships bundled in `rigortype`. Activate it under `plugins:`:
|
|
15
|
+
|
|
16
|
+
```yaml
|
|
17
|
+
plugins:
|
|
18
|
+
- rigor-hanami
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What it checks
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# app/actions/books/index.rb
|
|
25
|
+
module Bookshelf
|
|
26
|
+
module Actions
|
|
27
|
+
module Books
|
|
28
|
+
class Index < Bookshelf::Action
|
|
29
|
+
def handle(request, response)
|
|
30
|
+
response.status = 200 # response: Hanami::Action::Response
|
|
31
|
+
response.body = request.params[:q] # request: Hanami::Action::Request
|
|
32
|
+
request.no_such_method # error: call.undefined-method
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Rule | Severity | Fires when |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| `plugin.hanami.missing-handle-method` | error | a class in a matching file defines no `#handle` |
|
|
43
|
+
| `plugin.hanami.handle-arity-mismatch` | error | `#handle` is defined with a parameter count other than 2 |
|
|
44
|
+
|
|
45
|
+
Inside a conforming `#handle`, misuse of `request` / `response`
|
|
46
|
+
surfaces as the engine's own `call.undefined-method` — the same as if
|
|
47
|
+
the types had been declared in RBS. Return-type conformance is *not*
|
|
48
|
+
checked: `#handle` is void by contract (the response is mutated
|
|
49
|
+
in-place), so checking the return value would generate false positives
|
|
50
|
+
on every conditional branch. Parameter names are arbitrary (positions,
|
|
51
|
+
not names, are bound); only directly-defined `#handle` is checked, not
|
|
52
|
+
an inherited one.
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
plugins:
|
|
58
|
+
- gem: rigor-hanami
|
|
59
|
+
config:
|
|
60
|
+
action_path: "app/actions/**/*.rb" # default
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Override `action_path` for a custom slice layout, e.g.
|
|
64
|
+
`"slices/main/actions/**/*.rb"`. The override retargets both the
|
|
65
|
+
parameter-type provision and the `#handle` check.
|
|
66
|
+
|
|
67
|
+
## Limitations
|
|
68
|
+
|
|
69
|
+
- **Exact arity.** `#handle` must take exactly two parameters; optional
|
|
70
|
+
or keyword-only forms are flagged as `handle-arity-mismatch`.
|
|
71
|
+
- **Direct definition only.** A `#handle` inherited from a base class
|
|
72
|
+
(rather than defined on the class itself) is not detected.
|
|
73
|
+
- **Stubbed surface.** The bundled RBS covers the documented Request /
|
|
74
|
+
Response / Params methods; calls outside that surface resolve against
|
|
75
|
+
the stubs, not a live Hanami install.
|
|
76
|
+
|
|
77
|
+
## Plugin internals
|
|
78
|
+
|
|
79
|
+
The `ProtocolContract` declaration, the action checker, the RBS stubs,
|
|
80
|
+
and the ADR-28 provide-and-check split are in the
|
|
81
|
+
[plugin's README](../../../plugins/rigor-hanami/README.md). To write a
|
|
82
|
+
plugin, see [`examples/`](../../../examples/README.md) and the
|
|
83
|
+
[`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# rigor-mangrove
|
|
2
|
+
|
|
3
|
+
Sharpens [Mangrove](https://github.com/) `Result` / `Option`
|
|
4
|
+
unwrap-family return types from `untyped` to the concrete
|
|
5
|
+
type-argument carried at the call site, and synthesises the
|
|
6
|
+
subclasses an `Enum`'s `variants do … end` DSL generates at runtime
|
|
7
|
+
so they resolve statically. It layers on top of
|
|
8
|
+
[`rigor-sorbet`](rigor-sorbet.md) (which ingests Mangrove's `sig` /
|
|
9
|
+
RBI surface); this plugin adds the two precision steps sig-level
|
|
10
|
+
types can't reach. No runtime dependency.
|
|
11
|
+
|
|
12
|
+
It ships bundled in `rigortype`. Activate it under `plugins:`
|
|
13
|
+
(alongside `rigor-sorbet`, which supplies the carrier types):
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
plugins:
|
|
17
|
+
- rigor-sorbet
|
|
18
|
+
- rigor-mangrove
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What it infers — no diagnostics, no config
|
|
22
|
+
|
|
23
|
+
The plugin emits no diagnostics of its own and has no
|
|
24
|
+
configuration. It contributes types in two places; the engine's
|
|
25
|
+
ordinary method-existence check then catches typos on the
|
|
26
|
+
now-known types.
|
|
27
|
+
|
|
28
|
+
**Unwrap-family return types.** When the receiver is a Mangrove
|
|
29
|
+
carrier carrying a type argument, the unwrap return narrows to that
|
|
30
|
+
argument:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# token : Result::Ok[String, StandardError]
|
|
34
|
+
session.token.unwrap!.uppercaze # error: undefined method `uppercaze' for String
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Enum variant synthesis (ADR-36 Slice A).** The `variants` DSL
|
|
38
|
+
emits nested subclasses at runtime; the plugin synthesises them
|
|
39
|
+
statically — the variant constant resolves, `.new` dispatches, and
|
|
40
|
+
a typed `#inner` reader returns the declared payload type:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
class Shape
|
|
44
|
+
extend Mangrove::Enum
|
|
45
|
+
variants do
|
|
46
|
+
variant Circle, Float
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
Shape::Circle.new(1.5).inner.floor # ok — Float
|
|
51
|
+
Shape::Circle.new(1.5).inner.no_such_float_method # error: undefined for Float
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Limitations
|
|
55
|
+
|
|
56
|
+
- **No generic inference from constructors.** `Result::Ok.new("x")`
|
|
57
|
+
yields a carrier with no type argument, so unwrap there
|
|
58
|
+
contributes nothing (a conservative no-op, never a false
|
|
59
|
+
positive).
|
|
60
|
+
- **Downcast narrowing keeps no type args (ADR-36 Slice B,
|
|
61
|
+
deferred).** Narrowing a parent generic to a variant via `is_a?`
|
|
62
|
+
doesn't yet carry the type arguments through the inheritance edge.
|
|
63
|
+
- **Non-constant payload shapes degrade.** A variant whose payload
|
|
64
|
+
is a shape hash (`{ name: String }`) falls back to `Dynamic[top]`.
|
|
65
|
+
|
|
66
|
+
## Plugin internals
|
|
67
|
+
|
|
68
|
+
The carrier-generic instantiation, the `NestedClassTemplate`
|
|
69
|
+
variant synthesis, and the relationship to `rigor-sorbet` are in
|
|
70
|
+
the [plugin's README](../../../plugins/rigor-mangrove/README.md);
|
|
71
|
+
the synthesis tier is [ADR-36](../../adr/36-mangrove-enum-nested-class-emission.md).
|
|
72
|
+
To write a plugin, see [`examples/`](../../../examples/README.md)
|
|
73
|
+
and the [`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# rigor-minitest
|
|
2
|
+
|
|
3
|
+
Narrows local variables through **Minitest** and **Test::Unit**
|
|
4
|
+
assertions — and the Minitest/spec `_(x).must_*` / `.wont_*` matchers
|
|
5
|
+
layered on top. When the plugin recognises a supported assertion shape
|
|
6
|
+
it emits a `:local`-kind narrowing fact, so the rest of the test body
|
|
7
|
+
resolves against the asserted type. It also tells the engine that a
|
|
8
|
+
test framework's `setup` method initialises instance variables, which
|
|
9
|
+
suppresses spurious nil warnings on ivars read in the test body. It
|
|
10
|
+
reads source only, with no `minitest` runtime dependency.
|
|
11
|
+
|
|
12
|
+
It ships bundled in `rigortype`. Activate it under `plugins:`:
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
plugins:
|
|
16
|
+
- rigor-minitest
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What it infers
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
def test_user
|
|
23
|
+
user = build_user
|
|
24
|
+
assert_kind_of(User, user) # user narrowed to User
|
|
25
|
+
user.name.upcase # `.name` resolves on User
|
|
26
|
+
|
|
27
|
+
found = find_user(1)
|
|
28
|
+
refute_nil(found) # found narrowed away from nil
|
|
29
|
+
found.id # `.id` resolves cleanly
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "narrows the spec way" do
|
|
33
|
+
_(value).must_be_kind_of(String) # value narrowed to String
|
|
34
|
+
value.upcase
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`_(x)`, `value(x)`, and `expect(x)` are all accepted as the spec
|
|
39
|
+
wrapper. Test::Unit's `assert_not_kind_of` / `assert_not_nil` /
|
|
40
|
+
`assert_not_equal` / `assert_not_instance_of` share the recogniser with
|
|
41
|
+
their `refute_*` equivalents.
|
|
42
|
+
|
|
43
|
+
| Assertion / matcher | Effect on `x` |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `assert_kind_of(T, x)` / `assert_instance_of(T, x)` | narrow to `T` |
|
|
46
|
+
| `assert_nil(x)` | narrow to `Constant<nil>` |
|
|
47
|
+
| `assert_equal(literal, x)` | narrow to `Constant<literal>` |
|
|
48
|
+
| `assert_match(regex, x)` | narrow to `String` |
|
|
49
|
+
| `refute_kind_of` / `refute_instance_of` (+ `assert_not_*`) | narrow away from `T` |
|
|
50
|
+
| `refute_nil(x)` / `assert_not_nil(x)` | narrow away from nil |
|
|
51
|
+
| `refute_equal(literal, x)` / `assert_not_equal(...)` | narrow away from `Constant<literal>` |
|
|
52
|
+
| `_(x).must_be_kind_of(T)` / `must_be_a(T)` / `must_be_instance_of(T)` / `must_be_an_instance_of(T)` | narrow to `T` |
|
|
53
|
+
| `_(x).must_be_nil` | narrow to `Constant<nil>` |
|
|
54
|
+
| `_(x).must_equal(literal)` | narrow to `Constant<literal>` |
|
|
55
|
+
| `_(x).must_match(regex)` | narrow to `String` |
|
|
56
|
+
| `_(x).wont_be_kind_of(T)` / `wont_be_instance_of(T)` | narrow away from `T` |
|
|
57
|
+
| `_(x).wont_be_nil` | narrow away from nil |
|
|
58
|
+
| `_(x).wont_equal(literal)` | narrow away from `Constant<literal>` |
|
|
59
|
+
|
|
60
|
+
## No diagnostics, no config
|
|
61
|
+
|
|
62
|
+
The plugin emits no diagnostics and has no configuration knobs — it
|
|
63
|
+
only contributes narrowing facts to the engine. It walks every analysed
|
|
64
|
+
file; non-test files fall through untouched when no recognised shape
|
|
65
|
+
matches.
|
|
66
|
+
|
|
67
|
+
## Limitations
|
|
68
|
+
|
|
69
|
+
- **No block-shape matchers** — `assert_raises(T) { ... }`,
|
|
70
|
+
`assert_throws(:tag) { ... }`. Narrowing targets straight-line locals.
|
|
71
|
+
- **No predicate / respond-to matchers** — `assert_predicate(x, :foo?)`,
|
|
72
|
+
`assert_respond_to(x, :m)` need carriers Rigor doesn't model.
|
|
73
|
+
- **No `assert_in_delta` / `assert_operator`** — float-range / generic
|
|
74
|
+
operator narrowing is future work.
|
|
75
|
+
- **No legacy bare `x.must_be_kind_of(T)`** (Minitest < 6.0) — the
|
|
76
|
+
receiver *is* the value, so there's nothing to narrow against; migrate
|
|
77
|
+
to `_(x).must_*`.
|
|
78
|
+
|
|
79
|
+
## Plugin internals
|
|
80
|
+
|
|
81
|
+
The assertion recogniser and the contract surfaces this plugin
|
|
82
|
+
exercises — the ADR-37 `type_specifier` narrowing gate and the ADR-38
|
|
83
|
+
`additional_initializers` for `setup` — are in the
|
|
84
|
+
[plugin's README](../../../plugins/rigor-minitest/README.md). To write a
|
|
85
|
+
plugin, see [`examples/`](../../../examples/README.md) and the
|
|
86
|
+
[`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# rigor-pundit
|
|
2
|
+
|
|
3
|
+
Validates Pundit `authorize` / `policy` / `policy_scope` calls
|
|
4
|
+
against a statically-discovered policy index: the policy class must
|
|
5
|
+
exist, and an `authorize(record, :action)` action must correspond
|
|
6
|
+
to a defined `<action>?` predicate on the policy. It reads source
|
|
7
|
+
only — no Pundit runtime dependency.
|
|
8
|
+
|
|
9
|
+
It ships bundled in `rigortype`. Activate it under `plugins:`:
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
plugins:
|
|
13
|
+
- rigor-pundit
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What it checks
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# app/policies/post_policy.rb
|
|
20
|
+
class PostPolicy < ApplicationPolicy
|
|
21
|
+
def show?; true; end
|
|
22
|
+
def update?; true; end
|
|
23
|
+
def destroy?; true; end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
authorize(Post, :show) # info: resolves to PostPolicy#show?
|
|
27
|
+
authorize(Post, :destory) # error: PostPolicy#destory? is not defined (did you mean :destroy?)
|
|
28
|
+
authorize(Comment, :edit) # error: no policy class CommentPolicy (did you mean … ?)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Rule | Severity | Fires when |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| `plugin.pundit.policy-call` | info | an `authorize` / `policy` / `policy_scope` call resolved to a discovered policy |
|
|
34
|
+
| `plugin.pundit.unknown-policy-class` | error | the record maps to a `<Type>Policy` with no entry in the index (with a did-you-mean) |
|
|
35
|
+
| `plugin.pundit.unknown-policy-method` | error | the policy exists but the `:action` has no `<action>?` predicate (lists known predicates + a did-you-mean) |
|
|
36
|
+
| `plugin.pundit.load-error` | warning | policy discovery failed (parse/read error) — once per file |
|
|
37
|
+
|
|
38
|
+
The record maps to a policy by constant name or inferred
|
|
39
|
+
`Nominal[T]` (`Post` → `PostPolicy`); `:update` normalises to
|
|
40
|
+
`update?`. `authorize(record)` without an action validates only
|
|
41
|
+
the policy class (the action is controller-runtime-bound).
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
plugins:
|
|
47
|
+
- gem: rigor-pundit
|
|
48
|
+
config:
|
|
49
|
+
policy_search_paths: ["app/policies"] # default
|
|
50
|
+
policy_base_classes: ["ApplicationPolicy"] # default
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Limitations
|
|
54
|
+
|
|
55
|
+
- **Direct-superclass match only.** `class AdminPostPolicy <
|
|
56
|
+
AdminPolicy` (where `AdminPolicy < ApplicationPolicy`) isn't
|
|
57
|
+
discovered unless `AdminPolicy` is added to `policy_base_classes`.
|
|
58
|
+
- **Predicate methods only.** Non-`?` methods, and predicates built
|
|
59
|
+
with `define_method` or inherited from concerns, are out of
|
|
60
|
+
scope.
|
|
61
|
+
- **Untyped records pass through.** `authorize(local, :show)` is
|
|
62
|
+
not validated when `local` has no inferred `Nominal[T]`.
|
|
63
|
+
- **`Scope` policies** are validated for class existence, not for
|
|
64
|
+
`Scope#resolve`.
|
|
65
|
+
|
|
66
|
+
## Plugin internals
|
|
67
|
+
|
|
68
|
+
The policy discoverer / index and the contract surfaces this plugin
|
|
69
|
+
exercises are in the
|
|
70
|
+
[plugin's README](../../../plugins/rigor-pundit/README.md). To
|
|
71
|
+
write a plugin, see [`examples/`](../../../examples/README.md) and
|
|
72
|
+
the [`rigor-plugin-author`](../08-skills.md) skill.
|