rigortype 0.1.11 → 0.1.13
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/lib/rigor/analysis/check_rules.rb +96 -3
- data/lib/rigor/analysis/erb_template_detector.rb +38 -0
- data/lib/rigor/analysis/runner.rb +6 -1
- data/lib/rigor/analysis/worker_session.rb +6 -1
- data/lib/rigor/cli/plugins_command.rb +308 -0
- data/lib/rigor/cli/plugins_renderer.rb +173 -0
- data/lib/rigor/cli/skill_command.rb +170 -0
- data/lib/rigor/cli.rb +37 -1
- data/lib/rigor/configuration/severity_profile.rb +3 -0
- data/lib/rigor/inference/block_parameter_binder.rb +35 -0
- data/lib/rigor/inference/expression_typer.rb +69 -30
- data/lib/rigor/inference/indexed_narrowing.rb +187 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +24 -0
- data/lib/rigor/inference/method_dispatcher.rb +23 -0
- data/lib/rigor/inference/mutation_widening.rb +285 -0
- data/lib/rigor/inference/narrowing.rb +72 -4
- data/lib/rigor/inference/scope_indexer.rb +409 -12
- data/lib/rigor/inference/statement_evaluator.rb +256 -4
- data/lib/rigor/scope.rb +195 -4
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +22 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +94 -6
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +11 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +7 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +135 -11
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +94 -43
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +138 -35
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +17 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +10 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +13 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +6 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +83 -7
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +4 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +16 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +81 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +11 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +194 -5
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +264 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +100 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +175 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +64 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1107 -59
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +81 -4
- data/sig/rigor/scope.rbs +23 -0
- data/skills/rigor-baseline-reduce/SKILL.md +100 -0
- data/skills/rigor-baseline-reduce/references/01-classify.md +107 -0
- data/skills/rigor-baseline-reduce/references/02-fix-or-suppress.md +133 -0
- data/skills/rigor-plugin-author/SKILL.md +95 -0
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +195 -0
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +155 -0
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +163 -0
- data/skills/rigor-project-init/SKILL.md +129 -0
- data/skills/rigor-project-init/references/01-detect.md +101 -0
- data/skills/rigor-project-init/references/02-configure.md +185 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +168 -0
- data/skills/rigor-project-init/references/04-sig-uplift.md +171 -0
- metadata +22 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# 01 — Package and scaffold
|
|
2
|
+
|
|
3
|
+
Covers **Phase 1**. Output: a directory tree, a plugin class
|
|
4
|
+
skeleton that registers itself, and an `.rigor.yml` that activates
|
|
5
|
+
it.
|
|
6
|
+
|
|
7
|
+
## Naming
|
|
8
|
+
|
|
9
|
+
- **Plugin id** — kebab-case, starts with a letter, matches
|
|
10
|
+
`/\A[a-z][a-z0-9._-]*\z/`. Descriptive: `units`, `myapp-dsl`,
|
|
11
|
+
`legacy-macros`.
|
|
12
|
+
- **Require name** — Rigor activates a plugin by `require`-ing the
|
|
13
|
+
name in the `.rigor.yml` `plugins:` entry. The convention is
|
|
14
|
+
`rigor-<id>`, and the file that name resolves to must, when
|
|
15
|
+
loaded, call `Rigor::Plugin.register`.
|
|
16
|
+
- **Ruby class** — CamelCase under `Rigor::Plugin`, e.g.
|
|
17
|
+
`Rigor::Plugin::MyappDsl`.
|
|
18
|
+
|
|
19
|
+
## Standalone gem layout
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
rigor-<id>/ # its own repository
|
|
23
|
+
├── README.md
|
|
24
|
+
├── rigor-<id>.gemspec
|
|
25
|
+
├── Gemfile # gem "rigortype" (dev); gemspec
|
|
26
|
+
├── lib/
|
|
27
|
+
│ ├── rigor-<id>.rb # require name → require_relative the class
|
|
28
|
+
│ └── rigor/plugin/
|
|
29
|
+
│ └── <id>.rb # the plugin class + Rigor::Plugin.register
|
|
30
|
+
│ └── <id>/ # only if it needs helpers
|
|
31
|
+
│ └── analyzer.rb # AST walker extracted out of the class
|
|
32
|
+
├── sig/ # optional — RBS for the DSL (Phase 2)
|
|
33
|
+
└── spec/ or test/ # fixture tests (Phase 3)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Gemspec template
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# rigor-<id>.gemspec
|
|
40
|
+
# frozen_string_literal: true
|
|
41
|
+
|
|
42
|
+
Gem::Specification.new do |spec|
|
|
43
|
+
spec.name = "rigor-<id>"
|
|
44
|
+
spec.version = "0.1.0"
|
|
45
|
+
spec.authors = ["Your Name"]
|
|
46
|
+
spec.summary = "Rigor plugin: <one line>."
|
|
47
|
+
spec.description = "<two sentences naming the DSL / API the plugin types>."
|
|
48
|
+
spec.license = "MIT" # your choice
|
|
49
|
+
spec.required_ruby_version = ">= 3.2"
|
|
50
|
+
|
|
51
|
+
spec.files = Dir.glob(%w[README.md lib/**/*.rb sig/**/*.rbs])
|
|
52
|
+
spec.require_paths = ["lib"]
|
|
53
|
+
|
|
54
|
+
spec.add_dependency "prism", ">= 1.0", "< 2.0"
|
|
55
|
+
# Pin tightly — the plugin contract is pre-1.0 (see SKILL.md).
|
|
56
|
+
spec.add_dependency "rigortype", ">= 0.1.0", "< 0.2.0"
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`prism` is Rigor's parser; a plugin walks `Prism::Node`s, so depend
|
|
61
|
+
on it directly.
|
|
62
|
+
|
|
63
|
+
## Project-private layout
|
|
64
|
+
|
|
65
|
+
A project-private plugin lives inside the application repo and is
|
|
66
|
+
never published. Two ways to make `require "rigor-<id>"` succeed:
|
|
67
|
+
|
|
68
|
+
### Recommended — a path gem
|
|
69
|
+
|
|
70
|
+
Keep the plugin in a subdirectory with its own gemspec, and reference
|
|
71
|
+
it from the app's `Gemfile` by path:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
your-app/
|
|
75
|
+
├── Gemfile
|
|
76
|
+
├── rigor-plugin/ # the plugin, unpublished
|
|
77
|
+
│ ├── rigor-myapp.gemspec
|
|
78
|
+
│ └── lib/
|
|
79
|
+
│ ├── rigor-myapp.rb
|
|
80
|
+
│ └── rigor/plugin/myapp.rb
|
|
81
|
+
└── .rigor.yml
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
# your-app/Gemfile
|
|
86
|
+
gem "rigortype", "~> 0.1.0"
|
|
87
|
+
gem "rigor-myapp", path: "rigor-plugin"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`bundle install` then puts `rigor-myapp` on the load path, so Rigor's
|
|
91
|
+
`require "rigor-myapp"` resolves. This keeps the plugin versioned,
|
|
92
|
+
testable, and trivially promotable to a real gem later.
|
|
93
|
+
|
|
94
|
+
### Simplest — a bare file on the load path
|
|
95
|
+
|
|
96
|
+
If you do not want a gemspec at all, drop `rigor-myapp.rb` somewhere
|
|
97
|
+
and run `rigor` with that directory on `RUBYLIB`:
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
your-app/rigor-ext/rigor-myapp.rb # requires the plugin class
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
RUBYLIB=rigor-ext rigor check
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Workable, but the `RUBYLIB` has to be set on every invocation (CI
|
|
108
|
+
included). Prefer the path gem unless the plugin is a throwaway.
|
|
109
|
+
|
|
110
|
+
## The plugin class skeleton
|
|
111
|
+
|
|
112
|
+
`lib/rigor-<id>.rb` — the require entry point:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
# frozen_string_literal: true
|
|
116
|
+
|
|
117
|
+
require_relative "rigor/plugin/<id>"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`lib/rigor/plugin/<id>.rb` — the plugin class:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
# frozen_string_literal: true
|
|
124
|
+
|
|
125
|
+
require "rigor/plugin"
|
|
126
|
+
|
|
127
|
+
module Rigor
|
|
128
|
+
module Plugin
|
|
129
|
+
class MyappDsl < Rigor::Plugin::Base
|
|
130
|
+
manifest(
|
|
131
|
+
id: "<id>",
|
|
132
|
+
version: "0.1.0",
|
|
133
|
+
description: "<one line>",
|
|
134
|
+
# Optional: declare config keys the user may set under
|
|
135
|
+
# `.rigor.yml` plugins: [{ gem:, config: { … } }].
|
|
136
|
+
config_schema: {
|
|
137
|
+
# "module_name" => :string,
|
|
138
|
+
# "rules" => :array,
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Called once at load time with the service container.
|
|
143
|
+
# Read config defaults here. `config` is the validated
|
|
144
|
+
# user config Hash.
|
|
145
|
+
def init(_services)
|
|
146
|
+
@module_name = config.fetch("module_name", "Default")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Called per analysed file. `root` is the file's Prism AST,
|
|
150
|
+
# `scope` answers inferred-type queries. Return an Array of
|
|
151
|
+
# Rigor::Analysis::Diagnostic. See 02-walker-and-types.md.
|
|
152
|
+
def diagnostics_for_file(path:, scope:, root:)
|
|
153
|
+
[]
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
Rigor::Plugin.register(MyappDsl)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`Rigor::Plugin.register` at the bottom is mandatory — the loader
|
|
163
|
+
`require`s the gem, then looks for a freshly-registered plugin
|
|
164
|
+
class. A gem that registers nothing fails to load with a clear
|
|
165
|
+
error.
|
|
166
|
+
|
|
167
|
+
## Activate the plugin in `.rigor.yml`
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
plugins:
|
|
171
|
+
- rigor-<id>
|
|
172
|
+
|
|
173
|
+
# Hash form when the plugin takes config:
|
|
174
|
+
# plugins:
|
|
175
|
+
# - gem: rigor-<id>
|
|
176
|
+
# config:
|
|
177
|
+
# module_name: MyApp
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Confirm activation with the public CLI:
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
rigor check
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
A misconfigured plugin surfaces as a `plugin-loader` diagnostic
|
|
187
|
+
rather than crashing the run — read that message if the plugin seems
|
|
188
|
+
inert.
|
|
189
|
+
|
|
190
|
+
## Output of this module
|
|
191
|
+
|
|
192
|
+
A scaffolded plugin that loads (even if `diagnostics_for_file` still
|
|
193
|
+
returns `[]`) and is activated in `.rigor.yml`. Proceed to Phase 2
|
|
194
|
+
([`02-walker-and-types.md`](02-walker-and-types.md)) to make it
|
|
195
|
+
actually analyse.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# 02 — The walker and types
|
|
2
|
+
|
|
3
|
+
Covers **Phase 2** — making `diagnostics_for_file` analyse the AST,
|
|
4
|
+
emit diagnostics, and (optionally) contribute return types.
|
|
5
|
+
|
|
6
|
+
## `diagnostics_for_file` — the core hook
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
def diagnostics_for_file(path:, scope:, root:)
|
|
10
|
+
# root — Prism::Node, the parsed file (a ProgramNode).
|
|
11
|
+
# scope — answers inferred-type queries: scope.type_of(node).
|
|
12
|
+
# path — the file path, for Diagnostic#path.
|
|
13
|
+
# → return Array<Rigor::Analysis::Diagnostic>
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Walk `root` with a Prism visitor or a recursive descent, recognise
|
|
18
|
+
the DSL's call shapes, and collect diagnostics. Keep the walk in a
|
|
19
|
+
separate `Analyzer` class once it grows past a few methods — pass it
|
|
20
|
+
`path` and let it return the diagnostic array.
|
|
21
|
+
|
|
22
|
+
### Recognising call sites
|
|
23
|
+
|
|
24
|
+
Most DSL plugins key off `Prism::CallNode`:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
def each_call(node, &block)
|
|
28
|
+
block.call(node) if node.is_a?(Prism::CallNode)
|
|
29
|
+
node&.compact_child_nodes&.each { |child| each_call(child, &block) }
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then match on `node.name` (the method name, a Symbol),
|
|
34
|
+
`node.receiver`, and `node.arguments&.arguments`.
|
|
35
|
+
|
|
36
|
+
## Building a `Diagnostic`
|
|
37
|
+
|
|
38
|
+
Every diagnostic the plugin emits is a `Rigor::Analysis::Diagnostic`.
|
|
39
|
+
A small constructor helper keeps the call sites clean:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
def diagnostic(path, node, severity:, rule:, message:)
|
|
43
|
+
loc = node.location
|
|
44
|
+
Rigor::Analysis::Diagnostic.new(
|
|
45
|
+
path: path,
|
|
46
|
+
line: loc.start_line,
|
|
47
|
+
column: loc.start_column + 1, # 1-based column
|
|
48
|
+
message: message,
|
|
49
|
+
severity: severity, # :error | :warning | :info
|
|
50
|
+
rule: rule # short kebab-case id
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`rule` is a short identifier (`dimension-mismatch`,
|
|
56
|
+
`unknown-state`). Rigor namespaces it under your plugin —
|
|
57
|
+
diagnostics surface as `plugin.<manifest.id>.<rule>`, and that
|
|
58
|
+
qualified id is what `.rigor.yml` `disable:` and the baseline key
|
|
59
|
+
on. Pick `severity`:
|
|
60
|
+
|
|
61
|
+
- `:error` — a real defect (a type mismatch, a call that will
|
|
62
|
+
raise). Fails `rigor check`.
|
|
63
|
+
- `:warning` — suspicious but not certainly wrong.
|
|
64
|
+
- `:info` — informational; surfaces inferred facts without
|
|
65
|
+
judgement.
|
|
66
|
+
|
|
67
|
+
## Asking the analyzer for types — `scope.type_of`
|
|
68
|
+
|
|
69
|
+
A plugin does not have to infer types itself. `scope.type_of(node)`
|
|
70
|
+
returns the type the core analyzer inferred for any AST node — the
|
|
71
|
+
plugin can build on it:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
receiver_type = scope.type_of(call_node.receiver)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The returned object is one of the `Rigor::Type::*` carriers. The
|
|
78
|
+
ones a plugin meets most:
|
|
79
|
+
|
|
80
|
+
- `Rigor::Type::Nominal` — a class type; `#class_name` is the
|
|
81
|
+
String.
|
|
82
|
+
- `Rigor::Type::Constant` — a literal value; `#value` is the Ruby
|
|
83
|
+
object.
|
|
84
|
+
- `Rigor::Type::IntegerRange` — a bounded integer.
|
|
85
|
+
|
|
86
|
+
Match with `case`/`when` on the carrier class. Treat any carrier you
|
|
87
|
+
do not recognise as "decline to act" — never crash on an unexpected
|
|
88
|
+
type.
|
|
89
|
+
|
|
90
|
+
## Optional — contribute a return type with `flow_contribution_for`
|
|
91
|
+
|
|
92
|
+
A plugin can do more than emit diagnostics: it can *supply* the
|
|
93
|
+
inferred return type for a call site the core analyzer would
|
|
94
|
+
otherwise type as `Dynamic`. Implement `flow_contribution_for`:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
def flow_contribution_for(call_node:, scope:)
|
|
98
|
+
return nil unless call_node.is_a?(Prism::CallNode)
|
|
99
|
+
# ... decide the call site's real return type ...
|
|
100
|
+
return nil if undecidable # nil = "I have nothing to add"
|
|
101
|
+
|
|
102
|
+
Rigor::FlowContribution.new(
|
|
103
|
+
return_type: a_rigor_type,
|
|
104
|
+
provenance: Rigor::FlowContribution::Provenance.new(
|
|
105
|
+
source_family: "plugin.#{manifest.id}",
|
|
106
|
+
plugin_id: manifest.id,
|
|
107
|
+
node: call_node,
|
|
108
|
+
descriptor: nil
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Build the `return_type` with `Rigor::Type::Combinator`:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
Rigor::Type::Combinator.nominal_of("Money") # a class type
|
|
118
|
+
Rigor::Type::Combinator.constant_of(true) # a literal
|
|
119
|
+
Rigor::Type::Combinator.union(a, b) # a union
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Returning `nil` is always safe — it means "no contribution", and the
|
|
123
|
+
core analyzer keeps its own answer. Contribute a type only when the
|
|
124
|
+
plugin is confident; a wrong contribution propagates downstream.
|
|
125
|
+
|
|
126
|
+
This hook is the most contract-sensitive surface — it is the part
|
|
127
|
+
most likely to shift before v0.2.0. Implement it only if the plugin
|
|
128
|
+
genuinely needs to sharpen call-site types; a diagnostics-only
|
|
129
|
+
plugin can skip it entirely.
|
|
130
|
+
|
|
131
|
+
## Shipping RBS for the DSL
|
|
132
|
+
|
|
133
|
+
If the DSL introduces methods or classes that Rigor cannot see (a
|
|
134
|
+
`Money` class defined by metaprogramming, methods mixed into
|
|
135
|
+
`Numeric`), give Rigor RBS for them so *core* inference — not just
|
|
136
|
+
your plugin — understands them. Ship `sig/*.rbs` with the plugin (or
|
|
137
|
+
in the consuming project) and point `.rigor.yml` at it:
|
|
138
|
+
|
|
139
|
+
```yaml
|
|
140
|
+
signature_paths:
|
|
141
|
+
- sig
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
RBS covers what the *shape* of the DSL is; the plugin walker covers
|
|
145
|
+
the *dynamic* parts RBS cannot express (a value computed from a
|
|
146
|
+
literal argument, a dimensional rule). They compose — many plugins
|
|
147
|
+
ship both.
|
|
148
|
+
|
|
149
|
+
## Output of this module
|
|
150
|
+
|
|
151
|
+
A plugin whose `diagnostics_for_file` recognises the DSL and emits
|
|
152
|
+
diagnostics with correct severities and rule ids — optionally a
|
|
153
|
+
`flow_contribution_for` and a `sig/` bundle. Verify by eye with
|
|
154
|
+
`rigor check`; lock it down with tests in Phase 3
|
|
155
|
+
([`03-test-and-ship.md`](03-test-and-ship.md)).
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# 03 — Test and ship
|
|
2
|
+
|
|
3
|
+
Covers **Phase 3** — testing the plugin from outside the rigor
|
|
4
|
+
monorepo, pinning against the pre-1.0 contract, and shipping.
|
|
5
|
+
|
|
6
|
+
## Testing — drive the public CLI
|
|
7
|
+
|
|
8
|
+
The rigor monorepo's plugin specs use an internal `plugin_helpers.rb`
|
|
9
|
+
(`run_plugin`, `plugin_diagnostics`). That helper is **not part of
|
|
10
|
+
the published `rigortype` surface** — an external plugin cannot use
|
|
11
|
+
it. Test against what you *do* have: the `rigor` CLI.
|
|
12
|
+
|
|
13
|
+
The robust pattern is a **fixture project + `rigor check --format
|
|
14
|
+
json`**. It exercises the real load path, the real walker, the real
|
|
15
|
+
diagnostic pipeline — exactly what your users get — and works
|
|
16
|
+
identically under RSpec or Minitest.
|
|
17
|
+
|
|
18
|
+
### Fixture layout
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
spec/fixtures/basic/ (or test/fixtures/basic/)
|
|
22
|
+
├── .rigor.yml # plugins: [rigor-<id>]
|
|
23
|
+
└── sample.rb # code that exercises the plugin
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The fixture's `.rigor.yml` activates the plugin and scopes `paths:`
|
|
27
|
+
to the sample file:
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
paths:
|
|
31
|
+
- sample.rb
|
|
32
|
+
plugins:
|
|
33
|
+
- rigor-<id>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### RSpec
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# spec/rigor_<id>_spec.rb
|
|
40
|
+
require "json"
|
|
41
|
+
require "open3"
|
|
42
|
+
|
|
43
|
+
RSpec.describe "rigor-<id>" do
|
|
44
|
+
def diagnostics_for(fixture)
|
|
45
|
+
dir = File.expand_path("fixtures/#{fixture}", __dir__)
|
|
46
|
+
out, _err, _status = Open3.capture3(
|
|
47
|
+
"bundle", "exec", "rigor", "check", "--format", "json", chdir: dir
|
|
48
|
+
)
|
|
49
|
+
JSON.parse(out).fetch("diagnostics")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "flags a dimensional mismatch" do
|
|
53
|
+
diags = diagnostics_for("basic")
|
|
54
|
+
rule = diags.map { |d| d["rule"] }
|
|
55
|
+
expect(rule).to include("plugin.<id>.dimension-mismatch")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Minitest
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# test/rigor_<id>_test.rb
|
|
64
|
+
require "minitest/autorun"
|
|
65
|
+
require "json"
|
|
66
|
+
require "open3"
|
|
67
|
+
|
|
68
|
+
class RigorPluginTest < Minitest::Test
|
|
69
|
+
def diagnostics_for(fixture)
|
|
70
|
+
dir = File.expand_path("fixtures/#{fixture}", __dir__)
|
|
71
|
+
out, = Open3.capture3("bundle", "exec", "rigor", "check",
|
|
72
|
+
"--format", "json", chdir: dir)
|
|
73
|
+
JSON.parse(out).fetch("diagnostics")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_flags_dimensional_mismatch
|
|
77
|
+
rules = diagnostics_for("basic").map { |d| d["rule"] }
|
|
78
|
+
assert_includes rules, "plugin.<id>.dimension-mismatch"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`rigor check --format json` emits one object per run; the
|
|
84
|
+
`"diagnostics"` array carries `path` / `line` / `column` / `message`
|
|
85
|
+
/ `severity` / `rule`. Assert on `rule` and `severity` — they are the
|
|
86
|
+
stable fields. Avoid asserting on exact `message` wording; it changes
|
|
87
|
+
between Rigor releases.
|
|
88
|
+
|
|
89
|
+
### Unit-test the pure parts directly
|
|
90
|
+
|
|
91
|
+
Dispatch tables, parsers, and dimension maths inside the plugin are
|
|
92
|
+
plain Ruby — test them as ordinary objects, no `rigor` process
|
|
93
|
+
needed:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
it "dispatches distance / time to speed" do
|
|
97
|
+
result = Rigor::Plugin::Units::MethodTable.dispatch(
|
|
98
|
+
receiver: :distance, method: :/, args: [:time]
|
|
99
|
+
)
|
|
100
|
+
expect(result.dimension).to eq(:speed)
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Split the suite: fast unit tests for the logic, a few fixture-driven
|
|
105
|
+
CLI tests for the end-to-end wiring.
|
|
106
|
+
|
|
107
|
+
## Version pinning — the pre-1.0 contract
|
|
108
|
+
|
|
109
|
+
The plugin contract is **not frozen until `rigortype` v0.2.0** (see
|
|
110
|
+
SKILL.md). Concretely:
|
|
111
|
+
|
|
112
|
+
- Gemspec / Gemfile: `rigortype` `>= 0.1.0, < 0.2.0`. Never `>= 0.1`
|
|
113
|
+
alone — that floats across the contract-changing v0.2.0 boundary.
|
|
114
|
+
- Your plugin's own version is normal semver, independent of
|
|
115
|
+
`rigortype`'s.
|
|
116
|
+
- When you bump the `rigortype` pin to a new minor, **re-run the
|
|
117
|
+
full test suite** — the walker hook signature or the `Diagnostic` /
|
|
118
|
+
type-carrier shapes may have changed. The fixture CLI tests are
|
|
119
|
+
what catch a contract drift.
|
|
120
|
+
- State the supported `rigortype` range in the README so users do
|
|
121
|
+
not pair the plugin with an incompatible Rigor.
|
|
122
|
+
|
|
123
|
+
When v0.2.0 lands, re-pin to the stable range and ordinary
|
|
124
|
+
compatibility rules apply.
|
|
125
|
+
|
|
126
|
+
## README
|
|
127
|
+
|
|
128
|
+
A plugin README should carry:
|
|
129
|
+
|
|
130
|
+
1. **What it does** — the DSL / framework it teaches Rigor, and one
|
|
131
|
+
sample diagnostic.
|
|
132
|
+
2. **Install** — `gem "rigor-<id>"` (or the path-gem snippet for a
|
|
133
|
+
project-private plugin).
|
|
134
|
+
3. **Activate** — the `.rigor.yml` `plugins:` entry, plus any
|
|
135
|
+
`config:` keys and `signature_paths:`.
|
|
136
|
+
4. **Compatibility** — the supported `rigortype` version range, and
|
|
137
|
+
the pre-1.0-contract caveat.
|
|
138
|
+
5. **License.**
|
|
139
|
+
|
|
140
|
+
## Ship
|
|
141
|
+
|
|
142
|
+
**Standalone gem** — `gem build rigor-<id>.gemspec` then
|
|
143
|
+
`gem push`. Tag the release. Announce the supported `rigortype`
|
|
144
|
+
range.
|
|
145
|
+
|
|
146
|
+
**Project-private plugin** — nothing to publish. Commit it with the
|
|
147
|
+
app (the `rigor-plugin/` path-gem directory, or the `RUBYLIB` file).
|
|
148
|
+
Make sure CI runs `rigor check` so the plugin stays
|
|
149
|
+
wired and the fixture tests run.
|
|
150
|
+
|
|
151
|
+
Either way, if the plugin uncovered a gap that *should* be core
|
|
152
|
+
Rigor behaviour — or if you hit a plugin-contract rough edge — report
|
|
153
|
+
it: <https://github.com/rigortype/rigor/issues>. External plugin
|
|
154
|
+
authors are the main source of pre-v0.2.0 contract feedback.
|
|
155
|
+
|
|
156
|
+
## Output of this module — plugin shipped
|
|
157
|
+
|
|
158
|
+
- A test suite: fast unit tests + fixture-driven `rigor check`
|
|
159
|
+
tests, in RSpec or Minitest.
|
|
160
|
+
- A `rigortype` pin tight to `< 0.2.0`.
|
|
161
|
+
- A README stating the compatibility range.
|
|
162
|
+
- The plugin published as a gem, or committed project-private with
|
|
163
|
+
CI wired.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rigor-project-init
|
|
3
|
+
description: |
|
|
4
|
+
Onboard a project to Rigor type-checking from scratch: detect the stack, choose an adoption mode (baseline vs. strict), select plugins, write `.rigor.dist.yml`, then snapshot a baseline or commit to a zero-diagnostic gate. Triggers: "set up Rigor in this project", "configure rigor for X", "add type checking", or running `rigor check` in a Gemfile directory with no `.rigor.yml`. NOT for reducing an existing baseline (use rigor-baseline-reduce) or authoring a plugin (use rigor-plugin-author).
|
|
5
|
+
license: MPL-2.0
|
|
6
|
+
metadata:
|
|
7
|
+
version: 0.1.0
|
|
8
|
+
homepage: https://github.com/rigortype/rigor
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Rigor Project Init
|
|
12
|
+
|
|
13
|
+
End-to-end onboarding for a project that has never run Rigor. The
|
|
14
|
+
output is a committed `.rigor.dist.yml`, an explicit adoption mode,
|
|
15
|
+
and — if the project chooses it — a `.rigor-baseline.yml` snapshot.
|
|
16
|
+
|
|
17
|
+
This skill is for **users adopting Rigor on their own project**. It
|
|
18
|
+
uses the published `rigor` executable, installed standalone — Rigor
|
|
19
|
+
is a tool, not a library, so it does **not** go in the project's
|
|
20
|
+
`Gemfile`. See the manual's
|
|
21
|
+
[Installing Rigor](../../docs/manual/01-installation.md)
|
|
22
|
+
chapter for the install channels (`mise` recommended). This skill
|
|
23
|
+
references only public CLI flags and config keys — the same surface
|
|
24
|
+
`rigor --help` documents.
|
|
25
|
+
|
|
26
|
+
## Phase 0 — When to use this skill
|
|
27
|
+
|
|
28
|
+
Trigger when the user says "set up Rigor here", "configure rigor for
|
|
29
|
+
this app", "add type checking", or runs `rigor check` in a project
|
|
30
|
+
that has no `.rigor.yml` / `.rigor.dist.yml`.
|
|
31
|
+
|
|
32
|
+
Do NOT trigger for:
|
|
33
|
+
|
|
34
|
+
- **An existing baseline the user wants to shrink** — that is the
|
|
35
|
+
`rigor-baseline-reduce` skill.
|
|
36
|
+
- **Writing a Rigor plugin** for the project's own DSL /
|
|
37
|
+
metaprogramming — that is the `rigor-plugin-author` skill. (This
|
|
38
|
+
skill *points at* plugin authoring as an escalation in Phase 7;
|
|
39
|
+
it does not do it.)
|
|
40
|
+
- **Tweaking an already-configured project** — ordinary edits to an
|
|
41
|
+
existing `.rigor.yml`; no onboarding pipeline needed.
|
|
42
|
+
|
|
43
|
+
## Note — plugin installation
|
|
44
|
+
|
|
45
|
+
All `rigor-*` plugins ship **bundled inside the `rigortype` gem**.
|
|
46
|
+
No separate installation is needed. Listing a plugin under
|
|
47
|
+
`plugins:` in `.rigor.dist.yml` is sufficient to activate it.
|
|
48
|
+
The project's `Gemfile` is untouched by this workflow.
|
|
49
|
+
See [`references/02-configure.md`](references/02-configure.md)
|
|
50
|
+
§ "No separate installation needed" for detail.
|
|
51
|
+
|
|
52
|
+
## The central decision — adoption mode
|
|
53
|
+
|
|
54
|
+
A mature Ruby codebase routinely reports hundreds to thousands of
|
|
55
|
+
diagnostics the first time `rigor check` runs. Most are not fresh
|
|
56
|
+
bugs: some are real-but-empirically-safe (`T | nil` that production
|
|
57
|
+
always initialises), some are project style, a minority are genuine
|
|
58
|
+
latent bugs. Forcing every site to zero before adoption blocks
|
|
59
|
+
adoption entirely.
|
|
60
|
+
|
|
61
|
+
So **before writing any config, present the user with two modes**
|
|
62
|
+
and let them choose. The mode drives the severity profile, whether a
|
|
63
|
+
baseline is generated, and how Phase 7 frames the leftover
|
|
64
|
+
diagnostics.
|
|
65
|
+
|
|
66
|
+
| | **Acknowledge mode** (baseline adoption) | **Strict mode** (no compromise) |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| Goal | Adopt Rigor now; make sure *ordinary coding does not increase* the diagnostic count. | Drive the project to zero outstanding diagnostics and keep it there. |
|
|
69
|
+
| Today's diagnostics | Snapshotted into `.rigor-baseline.yml`; suppressed as long as the count does not grow. | All surfaced; every one is fixed or consciously suppressed. |
|
|
70
|
+
| Hard-to-fix diagnostics | Left in the baseline. The project **trusts its test / spec suite** to cover runtime correctness for those sites — the static `T | nil` reading is worst-case-sound, the suite proves the worst case is not hit. | Fixed, or annotated `# rigor:disable <rule>` with an author-intent reason at the specific line. No blanket suppression. |
|
|
71
|
+
| `severity_profile` | `lenient` (or `balanced` for a small project). | `strict`. |
|
|
72
|
+
| Best for | Mature codebases; incremental adoption; teams that want the regression guard without a big upfront fix. | New / small projects; libraries; teams that want the maximum guarantee and have the budget to reach zero. |
|
|
73
|
+
| New diagnostics later | Surface immediately — anything beyond the baseline envelope is a regression. | Surface immediately — there is no envelope; every diagnostic is live. |
|
|
74
|
+
|
|
75
|
+
Both modes give the same core guarantee: **a change that introduces
|
|
76
|
+
a new diagnostic is caught.** They differ only in what happens to the
|
|
77
|
+
diagnostics that exist *today*. Acknowledge mode parenthesises them
|
|
78
|
+
behind a baseline and leans on the test suite; strict mode refuses to
|
|
79
|
+
parenthesise anything.
|
|
80
|
+
|
|
81
|
+
If the project's first `rigor check` reports more than ~100 errors,
|
|
82
|
+
recommend acknowledge mode as the default and let the user override.
|
|
83
|
+
Below that, either mode is reasonable — ask.
|
|
84
|
+
|
|
85
|
+
## Phase outline
|
|
86
|
+
|
|
87
|
+
| Phase | What | Reference |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| 1 | Detect the project shape (Gemfile / Gemfile.lock walk). | [`references/01-detect.md`](references/01-detect.md) |
|
|
90
|
+
| 2 | Present the two adoption modes; record the user's choice. | (this file — § "The central decision") |
|
|
91
|
+
| 3 | Select the plugin set matching the detected stack. | [`references/01-detect.md`](references/01-detect.md) |
|
|
92
|
+
| 4 | Write `.rigor.dist.yml` — severity profile follows the mode. Verify activation with `rigor plugins`. | [`references/02-configure.md`](references/02-configure.md) |
|
|
93
|
+
| 5 | Generate initial RBS sigs; uplift attr_reader precision with `--params=observed`. | [`references/04-sig-uplift.md`](references/04-sig-uplift.md) |
|
|
94
|
+
| 6 | Run `rigor triage --format json` to diagnose the diagnostic stream. | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) |
|
|
95
|
+
| 7 | Acknowledge mode only — generate the baseline and wire `baseline:`. | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) |
|
|
96
|
+
| 8 | Surface likely real bugs; offer the two escalation paths. | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) |
|
|
97
|
+
|
|
98
|
+
Load each reference when you reach its phase. Phases run in order;
|
|
99
|
+
the only branch is Phase 7 (acknowledge mode runs it, strict mode
|
|
100
|
+
skips it). Phase 5 is also optional if the project already has a
|
|
101
|
+
committed `sig/` directory.
|
|
102
|
+
|
|
103
|
+
## Reading order — modules
|
|
104
|
+
|
|
105
|
+
| Module | Read | Covers |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| 1 | [`references/01-detect.md`](references/01-detect.md) | **Phases 1 + 3.** Gemfile / Gemfile.lock walk → framework family. The plugin-recommendation table (Rails / dry-rb / Sinatra / RSpec / plain Ruby). RBS-collection presence check. |
|
|
108
|
+
| 2 | [`references/02-configure.md`](references/02-configure.md) | **Phase 4.** Severity-profile choice tied to the mode. The `.rigor.dist.yml` template and every key it uses. The `.rigor.dist.yml` vs `.rigor.yml` convention. |
|
|
109
|
+
| 3 | [`references/04-sig-uplift.md`](references/04-sig-uplift.md) | **Phase 5.** `rigor sig-gen --write` baseline. `rigor sig-gen --params=observed --write` attr_reader precision uplift. Handling residual `untyped` methods. Committing `sig/`. |
|
|
110
|
+
| 4 | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) | **Phases 6–8.** `rigor triage` as the diagnosis layer. `rigor baseline generate` + wiring `baseline:`. Surfacing likely real bugs. The two escalation paths — write a project plugin, or open a Rigor issue. |
|
|
111
|
+
|
|
112
|
+
## Escalation paths (Phase 7 preview)
|
|
113
|
+
|
|
114
|
+
Some diagnostic clusters are neither a quick fix nor honest baseline
|
|
115
|
+
material. Two of them have a dedicated answer this skill hands off to:
|
|
116
|
+
|
|
117
|
+
- **Application-specific metaprogramming** — a project DSL,
|
|
118
|
+
`define_method` factory, or in-house macro that Rigor cannot follow
|
|
119
|
+
produces a cluster of `call.undefined-method`. The durable fix is a
|
|
120
|
+
**project-private Rigor plugin** that teaches Rigor the DSL. Hand
|
|
121
|
+
off to the `rigor-plugin-author` skill.
|
|
122
|
+
- **An external gem Rigor does not understand** — a dependency ships
|
|
123
|
+
no RBS and Rigor has no built-in coverage for it. Try
|
|
124
|
+
`rbs collection install` first; if that gem genuinely needs Rigor
|
|
125
|
+
support, **open an issue on the Rigor project** asking for it:
|
|
126
|
+
<https://github.com/rigortype/rigor/issues>.
|
|
127
|
+
|
|
128
|
+
Neither is a Phase 7 obligation — they are options to *offer* the
|
|
129
|
+
user when the triage report points at one of these causes.
|