rigortype 0.2.9 → 0.3.1
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 +2 -2
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/string_scanner.rbs +6 -5
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +19 -10
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -246
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +81 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +59 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +7 -6
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +112 -0
- data/lib/rigor/analysis/check_rules.rb +581 -98
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +78 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +344 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +132 -27
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +223 -32
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +169 -31
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +22 -33
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +36 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +29 -28
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +17 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +46 -6
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/server.rb +19 -9
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/base.rb +38 -24
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +207 -49
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +18 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +6 -6
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +48 -26
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -78,7 +78,9 @@ catalogue of bugs:
|
|
|
78
78
|
- arithmetic that can be proved to raise (`5 / 0`);
|
|
79
79
|
- arguments whose type does not satisfy a refined parameter
|
|
80
80
|
contract;
|
|
81
|
-
- a few more, all
|
|
81
|
+
- a few more, all catalogued in
|
|
82
|
+
[manual — Diagnostics](../manual/04-diagnostics.md) and
|
|
83
|
+
explained in
|
|
82
84
|
[Chapter 8 — Understanding errors](08-understanding-errors.md).
|
|
83
85
|
|
|
84
86
|
Rigor does **not** ask you to write type annotations in
|
|
@@ -170,9 +172,11 @@ on the offending line:
|
|
|
170
172
|
The same identifier also drives the `disable:` and
|
|
171
173
|
`severity_overrides:` config keys, and family wildcards work
|
|
172
174
|
(`# rigor:disable call` suppresses every `call.*` rule on that
|
|
173
|
-
line). The full list of families and rules
|
|
174
|
-
|
|
175
|
-
[Chapter 8 — Understanding errors](08-understanding-errors.md)
|
|
175
|
+
line). The full list of families and rules is in
|
|
176
|
+
[manual — Diagnostics](../manual/04-diagnostics.md);
|
|
177
|
+
[Chapter 8 — Understanding errors](08-understanding-errors.md)
|
|
178
|
+
is where to read about choosing between the suppression
|
|
179
|
+
mechanisms.
|
|
176
180
|
|
|
177
181
|
## The "no annotations" stance
|
|
178
182
|
|
|
@@ -278,36 +282,20 @@ non-default behaviours: extra `paths`, an alternative
|
|
|
278
282
|
If you used the [AI-assisted setup](#the-fast-path-let-an-ai-agent-set-it-up),
|
|
279
283
|
the `rigor-project-init` skill already wrote one for you. To
|
|
280
284
|
write a starter by hand, `rigor init` emits `.rigor.dist.yml`
|
|
281
|
-
— the project default that gets committed
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
# disable: []
|
|
297
|
-
|
|
298
|
-
# plugins: []
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
That is all most projects need. The remaining mechanics —
|
|
302
|
-
editor autocomplete from the bundled JSON schema, the
|
|
303
|
-
`.rigor.yml` vs `.rigor.dist.yml` precedence rule, `includes:`
|
|
304
|
-
composition, and how path-bearing keys resolve relative to the
|
|
305
|
-
declaring file — are covered in
|
|
306
|
-
[Configuration](../manual/03-configuration.md). The one rule
|
|
307
|
-
worth knowing up front: when a developer keeps a local
|
|
308
|
-
`.rigor.yml`, it is the *sole* source of config for their runs
|
|
309
|
-
(the two files are never merged automatically), so to extend
|
|
310
|
-
the shared default it must list it under `includes:`.
|
|
285
|
+
— the project default that gets committed, with `target_ruby`,
|
|
286
|
+
`paths`, and a `severity_profile` filled in and the rest
|
|
287
|
+
commented out. That is all most projects need; the key
|
|
288
|
+
reference, the JSON-schema editor integration, and `includes:`
|
|
289
|
+
composition are in
|
|
290
|
+
[Configuration](../manual/03-configuration.md).
|
|
291
|
+
|
|
292
|
+
Two things are worth knowing up front, because they surprise
|
|
293
|
+
people. `target_ruby` is *your project's* Ruby, not the 4.0
|
|
294
|
+
Rigor itself runs on — those are independent on purpose. And
|
|
295
|
+
when a developer keeps a local `.rigor.yml`, it is the *sole*
|
|
296
|
+
source of config for their runs (the two files are never merged
|
|
297
|
+
automatically), so to extend the shared default it must list it
|
|
298
|
+
under `includes:`.
|
|
311
299
|
|
|
312
300
|
## What's next
|
|
313
301
|
|
|
@@ -275,7 +275,8 @@ end
|
|
|
275
275
|
(Narrowing the truthy edge further to specific refinement
|
|
276
276
|
carriers — so `\d{4}` would produce `decimal-int-string` — is a
|
|
277
277
|
demand-driven follow-up on the regex-pattern → refinement-name
|
|
278
|
-
recogniser track
|
|
278
|
+
recogniser track, tracked as
|
|
279
|
+
[#164](https://github.com/rigortype/rigor/issues/164).)
|
|
279
280
|
|
|
280
281
|
## Negation and `unless`
|
|
281
282
|
|
|
@@ -221,9 +221,13 @@ arr = [1, ARGV.first]
|
|
|
221
221
|
# Array[Constant<1> | String?]
|
|
222
222
|
```
|
|
223
223
|
|
|
224
|
-
The same goes for hashes whose keys are not provably
|
|
225
|
-
|
|
226
|
-
`
|
|
224
|
+
The same goes for hashes whose keys are not provably scalar
|
|
225
|
+
literals (symbols, plain strings, integers, floats, `true` /
|
|
226
|
+
`false` / `nil`) — Rigor produces `Hash[K, V]` rather than
|
|
227
|
+
`HashShape`. Within a shape, key identity follows `Hash#eql?`:
|
|
228
|
+
`1` and `1.0` are distinct keys, and a repeated literal key
|
|
229
|
+
keeps the last entry, exactly like the runtime. Non-symbol,
|
|
230
|
+
non-string keys display in hashrocket form (`{ 1 => 2 }`).
|
|
227
231
|
|
|
228
232
|
## Deriving new shapes — `pick_of` / `omit_of` / `partial_of` / `required_of` / `readonly_of`
|
|
229
233
|
|
data/docs/handbook/06-classes.md
CHANGED
|
@@ -97,7 +97,7 @@ both. The setter's argument type is whatever the call site
|
|
|
97
97
|
provides. The `def.ivar-write-mismatch` rule (v0.1.2) checks
|
|
98
98
|
that two writes to the same ivar in the same class body
|
|
99
99
|
agree on the concrete class — see
|
|
100
|
-
[
|
|
100
|
+
[manual — Diagnostics](../manual/04-diagnostics.md)
|
|
101
101
|
for the rule's exact contract; it lets you catch an
|
|
102
102
|
accidental rebind from `String` to `Array` in the same class
|
|
103
103
|
without authoring an explicit ivar type.
|
|
@@ -170,8 +170,8 @@ discovery composes uniformly across both shapes.
|
|
|
170
170
|
|
|
171
171
|
`Struct.new(*Symbol)` produces a positional-arg constructor
|
|
172
172
|
plus the same accessors as `Data.define`. Rigor folds struct
|
|
173
|
-
member reads too,
|
|
174
|
-
|
|
173
|
+
member reads too, and — because a `Struct` is mutable — tracks a
|
|
174
|
+
straight-line setter so the read stays precise:
|
|
175
175
|
|
|
176
176
|
```ruby
|
|
177
177
|
Coord = Struct.new(:x, :y)
|
|
@@ -181,18 +181,27 @@ c = Coord.new(10, 20)
|
|
|
181
181
|
assert_type("10", c.x)
|
|
182
182
|
assert_type("20", c.y)
|
|
183
183
|
|
|
184
|
-
# A
|
|
185
|
-
# its reads degrade to Dynamic, never a stale value.
|
|
184
|
+
# A straight-line setter re-types the member; the sibling stays precise.
|
|
186
185
|
m = Coord.new(1, 2)
|
|
187
186
|
m.x = 9
|
|
188
|
-
assert_type("
|
|
187
|
+
assert_type("9", m.x)
|
|
188
|
+
assert_type("2", m.y)
|
|
189
|
+
|
|
190
|
+
# A local that is aliased, escapes, or is mutated inside a loop or
|
|
191
|
+
# block is not fold-safe — its reads degrade to Dynamic, never a
|
|
192
|
+
# stale value, because a write Rigor cannot see could have changed it.
|
|
193
|
+
a = Coord.new(1, 2)
|
|
194
|
+
alias_ref = a
|
|
195
|
+
a.x = 9
|
|
196
|
+
assert_type("Dynamic[top]", a.x)
|
|
189
197
|
```
|
|
190
198
|
|
|
191
199
|
Because `Struct` accessors are also writers, the fold is gated:
|
|
192
|
-
a member read off a freshly constructed instance (`Coord.new(1,
|
|
193
|
-
2).x`)
|
|
194
|
-
|
|
195
|
-
|
|
200
|
+
a member read folds off a freshly constructed instance (`Coord.new(1,
|
|
201
|
+
2).x`), off a local the analysis proves is never written, aliased, or
|
|
202
|
+
passed away, or off one whose only writes are straight-line member
|
|
203
|
+
setters (their assigned type flows into the later read); anything else
|
|
204
|
+
widens to `Dynamic[top]`. `Data` is read-only, so its reads always fold.
|
|
196
205
|
|
|
197
206
|
## Inheritance and method resolution
|
|
198
207
|
|
|
@@ -100,27 +100,24 @@ Rigor sees a tightening.
|
|
|
100
100
|
|
|
101
101
|
## The directive grammar
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
There are seven per-method directives, and they divide by
|
|
104
|
+
*when* the fact they carry becomes true: `return:` and `param:`
|
|
105
|
+
retype the signature itself, `predicate-if-true` /
|
|
106
|
+
`predicate-if-false` narrow a variable across the branches of a
|
|
107
|
+
condition, and `assert` / `assert-if-true` / `assert-if-false`
|
|
108
|
+
narrow one after the call returns. Each is one
|
|
109
|
+
`%a{rigor:v1:…}` annotation above the `def` it refines; they
|
|
110
|
+
stack, and order does not matter.
|
|
111
|
+
|
|
112
|
+
The exhaustive table — every directive, its payload syntax, and
|
|
113
|
+
what the `<type>` slot accepts (RBS class names, refinement
|
|
114
|
+
payloads, the parameterised and bounded forms, and where `~T`
|
|
115
|
+
negation is and is not allowed) — is
|
|
116
|
+
[manual — RBS::Extended annotations](../manual/16-rbs-extended-annotations.md#per-method-directives);
|
|
117
|
+
the normative rules for conflicts, merging and provenance are
|
|
104
118
|
[`docs/type-specification/rbs-extended.md`](../type-specification/rbs-extended.md).
|
|
105
|
-
The
|
|
106
|
-
|
|
107
|
-
| Directive | Says |
|
|
108
|
-
| --- | --- |
|
|
109
|
-
| `%a{rigor:v1:return: <type>}` | Tighten the method's return type. |
|
|
110
|
-
| `%a{rigor:v1:param: <name> is <type>}` | Tighten a parameter's accepted type at the call site, AND narrow the local in the body. |
|
|
111
|
-
| `%a{rigor:v1:assert <name> is <type>}` | After this method returns, the named local in the caller's scope is `<type>`. |
|
|
112
|
-
| `%a{rigor:v1:predicate-if-true <name> is <type>}` | When this method returns truthy, the named local in the caller's scope is `<type>`. (Symmetric `predicate-if-false`.) |
|
|
113
|
-
| `%a{rigor:v1:assert-if-true <name> is <type>}` | When this method returns a truthy value, the named local in the caller's scope is `<type>`. (Symmetric `assert-if-false` for `false` / `nil` returns.) |
|
|
114
|
-
|
|
115
|
-
The `<type>` slot accepts:
|
|
116
|
-
|
|
117
|
-
- **RBS class names** — `String`, `Integer`, `::Foo::Bar`.
|
|
118
|
-
- **Imported refinement names** —
|
|
119
|
-
`non-empty-string`, `lowercase-string`, `numeric-string`,
|
|
120
|
-
`int<5, 10>`, `non-empty-array[Integer]`, `literal-string`,
|
|
121
|
-
…
|
|
122
|
-
- **Negation `~T`** — `~lowercase-string` means
|
|
123
|
-
"non-lowercase-string."
|
|
119
|
+
The rest of this chapter works through the directives one
|
|
120
|
+
example at a time.
|
|
124
121
|
|
|
125
122
|
## Refinement names
|
|
126
123
|
|
|
@@ -140,9 +137,8 @@ A short reference:
|
|
|
140
137
|
## Declaring conformance — `conforms-to`
|
|
141
138
|
|
|
142
139
|
The directives above attach to a `def`. One more attaches to a
|
|
143
|
-
`class` / `module` declaration and asserts the whole class
|
|
144
|
-
satisfies a named structural interface
|
|
145
|
-
assertion — whether or not any call site exercises it:
|
|
140
|
+
`class` / `module` declaration and asserts that the whole class
|
|
141
|
+
satisfies a named structural interface:
|
|
146
142
|
|
|
147
143
|
```rbs
|
|
148
144
|
%a{rigor:v1:conforms-to _RewindableStream}
|
|
@@ -153,13 +149,21 @@ end
|
|
|
153
149
|
```
|
|
154
150
|
|
|
155
151
|
If `MyBuffer` is missing a method the `_RewindableStream`
|
|
156
|
-
interface requires
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
structural compatibility
|
|
162
|
-
|
|
152
|
+
interface requires, Rigor reports
|
|
153
|
+
`rbs_extended.unsatisfied-conformance`; a class that satisfies
|
|
154
|
+
the interface is silent.
|
|
155
|
+
|
|
156
|
+
The reason to reach for this is that Rigor already checks
|
|
157
|
+
structural compatibility *implicitly*, wherever a value flows
|
|
158
|
+
into a position that needs a structural interface — so a class
|
|
159
|
+
nobody currently passes anywhere is never checked at all.
|
|
160
|
+
`conforms-to` turns the contract into a design assertion that
|
|
161
|
+
holds whether or not a call site exercises it, which is what a
|
|
162
|
+
library wants when the structural shape is the point. It is
|
|
163
|
+
purely additive: nothing that type-checked before stops doing
|
|
164
|
+
so because you added it. The stacking and diagnostic semantics
|
|
165
|
+
are in
|
|
166
|
+
[manual — `conforms-to`](../manual/16-rbs-extended-annotations.md#conforms-to--a-checked-structural-contract).
|
|
163
167
|
|
|
164
168
|
## Worked example: an assertion gate
|
|
165
169
|
|
|
@@ -181,6 +185,45 @@ end
|
|
|
181
185
|
The runtime side is whatever `assert_non_empty` does (raise
|
|
182
186
|
on empty, log, ...) — Rigor only reads the directive.
|
|
183
187
|
|
|
188
|
+
## Worked example: asserting a negative
|
|
189
|
+
|
|
190
|
+
An assertion payload can be negated with `~T`, which is how you
|
|
191
|
+
model the "this is definitely not nil any more" helper every
|
|
192
|
+
codebase grows:
|
|
193
|
+
|
|
194
|
+
```rbs
|
|
195
|
+
# sig/asserts.rbs
|
|
196
|
+
class Asserts
|
|
197
|
+
%a{rigor:v1:assert x is ~nil}
|
|
198
|
+
def self.not_nil: (untyped x) -> void
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# lib/configure.rb
|
|
204
|
+
def configure(maybe)
|
|
205
|
+
Asserts.not_nil(maybe)
|
|
206
|
+
# maybe: (~nil), so .upcase resolves on the narrowed type
|
|
207
|
+
maybe.upcase
|
|
208
|
+
end
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The target can also be the receiver itself — name it with
|
|
212
|
+
`self`, and the fact lands on the object the method was called
|
|
213
|
+
on:
|
|
214
|
+
|
|
215
|
+
```rbs
|
|
216
|
+
class Connection
|
|
217
|
+
%a{rigor:v1:assert self is Connected}
|
|
218
|
+
def assert_connected!: () -> void
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
If PHPDoc's `@phpstan-assert` family is your mental model for
|
|
223
|
+
all of this, the reading is nearly one-for-one; the mapping
|
|
224
|
+
table is in
|
|
225
|
+
[appendix: Coming from PHPStan](appendix-phpstan.md#the-phpstan-assert-family).
|
|
226
|
+
|
|
184
227
|
## Worked example: a type predicate
|
|
185
228
|
|
|
186
229
|
```rbs
|
|
@@ -343,74 +386,6 @@ dynamic boundaries (deserialisation, `eval`, plugin entry
|
|
|
343
386
|
points). The static analysis you lose is made up by the
|
|
344
387
|
honesty of admitting "this could be anything."
|
|
345
388
|
|
|
346
|
-
## Coming from PHPStan? The `@phpstan-assert` family
|
|
347
|
-
|
|
348
|
-
If you are familiar with PHPStan's PHPDoc annotations,
|
|
349
|
-
Rigor's `RBS::Extended` directives map directly onto the
|
|
350
|
-
post-return / conditional narrowing primitives PHPStan calls
|
|
351
|
-
"asserts" and "type-specifying functions." The behaviour is
|
|
352
|
-
identical:
|
|
353
|
-
|
|
354
|
-
> "After this method returns, the named argument is `T`."
|
|
355
|
-
|
|
356
|
-
That is `@phpstan-assert` in PHPStan and
|
|
357
|
-
`%a{rigor:v1:assert}` in Rigor.
|
|
358
|
-
|
|
359
|
-
| PHPStan PHPDoc | Rigor RBS::Extended | Effect |
|
|
360
|
-
| --- | --- | --- |
|
|
361
|
-
| `@phpstan-assert T $x` | `%a{rigor:v1:assert x is T}` | After this method returns normally, the caller's `x` is `T`. |
|
|
362
|
-
| `@phpstan-assert-if-true T $x` | `%a{rigor:v1:predicate-if-true x is T}` | If this method returns truthy, the caller's `x` is `T`. |
|
|
363
|
-
| `@phpstan-assert-if-false T $x` | `%a{rigor:v1:predicate-if-false x is T}` | If this method returns falsey, the caller's `x` is `T`. |
|
|
364
|
-
| `@phpstan-assert !T $x` | `%a{rigor:v1:assert x is ~T}` | After this method returns, the caller's `x` is **not** `T` (negation form). |
|
|
365
|
-
| `@phpstan-assert-if-true !T $x` | `%a{rigor:v1:predicate-if-true x is ~T}` | Conditional negation. Symmetric with `predicate-if-false`. |
|
|
366
|
-
|
|
367
|
-
Worked example — the canonical "assertNotNull" pattern from
|
|
368
|
-
PHPStan's docs:
|
|
369
|
-
|
|
370
|
-
```rbs
|
|
371
|
-
# sig/asserts.rbs
|
|
372
|
-
class Asserts
|
|
373
|
-
%a{rigor:v1:assert x is ~nil}
|
|
374
|
-
def self.not_nil: (untyped x) -> void
|
|
375
|
-
end
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
```ruby
|
|
379
|
-
# lib/configure.rb
|
|
380
|
-
def configure(maybe)
|
|
381
|
-
Asserts.not_nil(maybe)
|
|
382
|
-
# maybe: (~nil), so .upcase resolves on the narrowed type
|
|
383
|
-
maybe.upcase
|
|
384
|
-
end
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
Self-targeted forms are supported too — the PHPStan
|
|
388
|
-
analogue would be a method on `$this` that narrows
|
|
389
|
-
`$this`. Name the receiver with `self`:
|
|
390
|
-
|
|
391
|
-
```rbs
|
|
392
|
-
class Connection
|
|
393
|
-
%a{rigor:v1:assert self is Connected}
|
|
394
|
-
def assert_connected!: () -> void
|
|
395
|
-
end
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
Rigor's directive grammar covers what PHPStan ships in the
|
|
399
|
-
`@phpstan-assert*` family. The directives **only fire from
|
|
400
|
-
RBS** (per ADR-5: strict on returns, lenient on parameters);
|
|
401
|
-
in PHPStan-land you can also write `@phpstan-assert` in
|
|
402
|
-
PHPDoc directly above the function — Rigor's equivalent is
|
|
403
|
-
the same RBS file's `def` line.
|
|
404
|
-
|
|
405
|
-
If you need plugin-side equivalents (when the assertion is
|
|
406
|
-
recognised by **call shape** rather than by sig — PHPStan's
|
|
407
|
-
"Type-Specifying Extensions"), see
|
|
408
|
-
[Chapter 9](09-plugins.md). The plugin contract surfaces
|
|
409
|
-
the same `Fact(target_kind: :self)` and
|
|
410
|
-
`Fact(target_kind: :parameter)` carriers that the directives
|
|
411
|
-
use, so a plugin author writes the equivalent of a PHPStan
|
|
412
|
-
`StaticMethodTypeSpecifyingExtension` from Ruby.
|
|
413
|
-
|
|
414
389
|
## When RBS cannot help — the plugin escape hatch
|
|
415
390
|
|
|
416
391
|
When a method's behaviour depends on the **shape of its
|
|
@@ -422,6 +397,6 @@ see [Chapter 9](09-plugins.md) and the
|
|
|
422
397
|
|
|
423
398
|
## What's next
|
|
424
399
|
|
|
425
|
-
Chapter 8
|
|
426
|
-
|
|
427
|
-
|
|
400
|
+
Chapter 8 is about reading a diagnostic — what each rule
|
|
401
|
+
family claims, why one fires when you did not expect it, and
|
|
402
|
+
which layer to reach for when you want it quieter.
|