rigortype 0.4.7 → 0.4.8
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 +3 -1
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/docs/handbook/01-getting-started.md +1 -1
- data/docs/handbook/11-sig-gen.md +35 -0
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +307 -91
- data/docs/manual/03-configuration.md +41 -3
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +8 -0
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +15 -6
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +598 -99
- data/lib/rigor/analysis/crash_signature.rb +43 -1
- data/lib/rigor/analysis/dependency_recorder.rb +233 -76
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +279 -37
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +37 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner.rb +222 -39
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
- data/lib/rigor/analysis/worker_session.rb +42 -41
- data/lib/rigor/bleeding_edge.rb +19 -4
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +66 -7
- data/lib/rigor/cache/rbs_descriptor.rb +37 -2
- data/lib/rigor/cache/rbs_environment.rb +5 -1
- data/lib/rigor/cli/baseline_command.rb +15 -2
- data/lib/rigor/cli/check_command.rb +4 -2
- data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
- data/lib/rigor/cli/lens_command.rb +17 -2
- data/lib/rigor/cli/sig_gen_command.rb +8 -0
- data/lib/rigor/cli/skill_describe.rb +1 -0
- data/lib/rigor/cli.rb +5 -2
- data/lib/rigor/config_audit.rb +32 -18
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/configuration.rb +32 -5
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/default_libraries.rb +17 -0
- data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
- data/lib/rigor/environment/rbs_loader.rb +312 -63
- data/lib/rigor/environment.rb +131 -35
- data/lib/rigor/inference/acceptance.rb +50 -15
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_run_count.rb +9 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +56 -15
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/def_node_resolver.rb +32 -14
- data/lib/rigor/inference/def_return_typer.rb +1 -1
- data/lib/rigor/inference/definer_resolution.rb +15 -7
- data/lib/rigor/inference/definers_index.rb +72 -16
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1459 -287
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +8 -6
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +57 -8
- data/lib/rigor/inference/ivar_writer_census.rb +106 -11
- data/lib/rigor/inference/key_presence_guard.rb +51 -25
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +63 -15
- data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
- data/lib/rigor/inference/match_rebinding.rb +21 -8
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher.rb +66 -18
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +13 -2
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_widening.rb +44 -4
- data/lib/rigor/inference/narrowing.rb +201 -27
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +133 -8
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +28 -10
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +76 -22
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2079 -183
- data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +970 -250
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
- data/lib/rigor/inference/toplevel_def_census.rb +14 -6
- data/lib/rigor/inference/unknown_store_widening.rb +1 -1
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -10
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/io_boundary.rb +25 -3
- data/lib/rigor/plugin/manifest.rb +7 -3
- data/lib/rigor/plugin/registry.rb +13 -2
- data/lib/rigor/plugin/trust_policy.rb +46 -0
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/reflection/constant_ancestors.rb +79 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +18 -1
- data/lib/rigor/run_scoped_slots.rb +20 -20
- data/lib/rigor/scope/discovery_index.rb +149 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +900 -55
- data/lib/rigor/scope.rb +307 -34
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +14 -1
- data/lib/rigor/sig_gen/generator.rb +222 -13
- data/lib/rigor/sig_gen/method_candidate.rb +14 -0
- data/lib/rigor/sig_gen/observation_collector.rb +11 -2
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
- data/lib/rigor/sig_gen/writer.rb +21 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/combinator.rb +164 -8
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -3
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/plugin/base.rbs +53 -19
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +2 -3
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +22 -3
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +48 -2
- data/lib/rigor/inference/memo_audit.rb +0 -55
data/skills/rigor-ask/SKILL.md
CHANGED
|
@@ -130,6 +130,7 @@ A good hand-off is two or three sentences of orientation plus the pointer:
|
|
|
130
130
|
- baseline reduction → **`rigor-baseline-reduce`** · coverage holes → **`rigor-protection-uplift`**
|
|
131
131
|
- a missing gem/DSL → **`rigor-plugin-author`** · monkey-patch clusters → **`rigor-monkeypatch-resolve`**
|
|
132
132
|
- about to *write* a type (RBS, `#:`, a Sorbet `sig`, a YARD tag, a doc sentence) → **`rigor-type-oracle`**
|
|
133
|
+
- auditing, fixing, or contributing RBS declarations (a gem's `sig/`, a gem_rbs_collection entry) → **`rigor-rbs-contract`**
|
|
133
134
|
|
|
134
135
|
When in doubt, give less and point — it respects the user's "three skills
|
|
135
136
|
to remember" promise and keeps each answer to the part only `rigor-ask`
|
|
@@ -56,6 +56,7 @@ bundle exec rbs collection install
|
|
|
56
56
|
Use `bundle exec` when `rbs` appears in `Gemfile` or `Gemfile.lock`
|
|
57
57
|
(the common case — Steep / rbs_rails workflows put it there). If `rbs`
|
|
58
58
|
is not in the Gemfile, use the bare `rbs collection install` instead.
|
|
59
|
+
If `bundle exec rbs` runs an `rbs` that `gem:rigortype` put on `PATH` instead of the project's, see the [install guide's note on `rbs` shadowing](https://github.com/rigortype/rigor/blob/master/docs/install.md#case-a--mise-is-available-recommended).
|
|
59
60
|
|
|
60
61
|
Ask the user for permission before running, since this installs files
|
|
61
62
|
into `.gem_rbs_collection/` (a generated directory that belongs in
|
|
@@ -170,6 +170,8 @@ bundle exec rbs collection install # if rbs is in Gemfile
|
|
|
170
170
|
# or: rbs collection install
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
If `bundle exec rbs` runs an `rbs` that `gem:rigortype` put on `PATH` instead of the project's, see the [install guide's note on `rbs` shadowing](https://github.com/rigortype/rigor/blob/master/docs/install.md#case-a--mise-is-available-recommended).
|
|
174
|
+
|
|
173
175
|
Re-run `rigor triage`. If the `gem-without-rbs` count drops, carry
|
|
174
176
|
the new, smaller count into Phase 7.
|
|
175
177
|
|
|
@@ -397,7 +399,9 @@ information and Rigor has no built-in coverage. In order:
|
|
|
397
399
|
|
|
398
400
|
1. `bundle exec rbs collection install` (bare `rbs collection install`
|
|
399
401
|
if `rbs` is not in the project's Gemfile) — pulls community RBS for
|
|
400
|
-
the gem if it exists. Re-run triage afterwards.
|
|
402
|
+
the gem if it exists. Re-run triage afterwards. If `bundle exec rbs`
|
|
403
|
+
runs a `gem:rigortype` `rbs` instead of the project's, see the
|
|
404
|
+
[install guide's note on `rbs` shadowing](https://github.com/rigortype/rigor/blob/master/docs/install.md#case-a--mise-is-available-recommended).
|
|
401
405
|
2. `dependencies.source_inference:` in `.rigor.dist.yml` — opt the
|
|
402
406
|
gem into Rigor inferring `Dynamic`-typed returns from its source.
|
|
403
407
|
3. If the gem is widely used and genuinely warrants first-class
|
|
@@ -28,13 +28,13 @@ rigor skill --full rigor-type-oracle
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Its `references/02-agents-md-fragment.md` section carries the paragraph
|
|
31
|
-
and the
|
|
31
|
+
and the six bullets. Copy that block **verbatim** — including its
|
|
32
32
|
single-long-line shape, which is deliberate (some renderers turn a
|
|
33
33
|
newline inside a paragraph into a line break).
|
|
34
34
|
|
|
35
|
-
Do not paraphrase it from memory, and do not shorten it: the
|
|
35
|
+
Do not paraphrase it from memory, and do not shorten it: the six bullets
|
|
36
36
|
are the load-bearing part — expression, method signature, parameter,
|
|
37
|
-
the gap rule, and
|
|
37
|
+
the gap rule, provenance, and the declared-`void` contract.
|
|
38
38
|
|
|
39
39
|
## Where it goes
|
|
40
40
|
|
|
@@ -44,7 +44,7 @@ the gap rule, and provenance.
|
|
|
44
44
|
| Only `CLAUDE.md` exists | Append it there. |
|
|
45
45
|
| Both exist | Put it in `AGENTS.md`. Add `@AGENTS.md` to `CLAUDE.md` only if it is not already pulled in. |
|
|
46
46
|
| Neither exists | **Create `AGENTS.md`** with this as its first section. |
|
|
47
|
-
| A types / type-checking section already exists | Merge into it; keep the
|
|
47
|
+
| A types / type-checking section already exists | Merge into it; keep the six bullets intact. Never write a second, competing rule. |
|
|
48
48
|
|
|
49
49
|
Never overwrite unrelated content. If the project already states a rule
|
|
50
50
|
about type authorship that contradicts this one, stop and show the user
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rigor-rbs-contract
|
|
3
|
+
description: >-
|
|
4
|
+
Audit, repair, and extend RBS declarations as contracts — a gem's own `sig/`, an application's `sig/`,
|
|
5
|
+
or a gem_rbs_collection entry — by checking each one against Rigor's inference and the library's
|
|
6
|
+
documented behaviour, then proving and explaining the change. Use when correcting or contributing RBS;
|
|
7
|
+
not for sourcing one type mid-task (`rigor-type-oracle`) or raising `coverage --protection` numbers.
|
|
8
|
+
license: MPL-2.0
|
|
9
|
+
metadata:
|
|
10
|
+
version: 0.1.0
|
|
11
|
+
homepage: https://github.com/rigortype/rigor
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Rigor RBS Contract
|
|
15
|
+
|
|
16
|
+
An RBS declaration is not a record of what a method happens to return. It is a promise about what
|
|
17
|
+
callers may rely on. Ruby returns every method's last expression, so inference always finds *some*
|
|
18
|
+
type; whether callers may depend on it is a choice only a declaration makes. This skill turns that into
|
|
19
|
+
a repeatable loop: find where declarations and code disagree, decide which side is wrong on cited
|
|
20
|
+
evidence, change the declaration, prove the change, and explain it so a reviewer can re-check it.
|
|
21
|
+
|
|
22
|
+
## First: load the version-current copy
|
|
23
|
+
|
|
24
|
+
Commands, flags, rule ids and skip-reason ids drift between Rigor releases, so follow the copy that
|
|
25
|
+
ships with the **installed** Rigor rather than a vendored or frozen copy of this file:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
rigor skill --full rigor-rbs-contract # this body + all its references/, inline
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If you already loaded it via `rigor skill`, proceed. If `rigor` is not on `PATH`, run
|
|
32
|
+
**`rigor-next-steps`** first.
|
|
33
|
+
|
|
34
|
+
## When to use
|
|
35
|
+
|
|
36
|
+
- A gem author wants their shipped `sig/` checked, corrected or extended.
|
|
37
|
+
- An application's quality owner wants the RBS their `rigor check` relies on — the project's `sig/`, or
|
|
38
|
+
a dependency's community RBS — audited, or a wrong dependency signature fixed.
|
|
39
|
+
- Someone is contributing to `ruby/gem_rbs_collection`: adding methods, fixing a declaration, or acting
|
|
40
|
+
on a discrepancy Rigor surfaced.
|
|
41
|
+
- A diagnostic or a review comment says "the RBS says X but the code does Y", and the task is to settle
|
|
42
|
+
which one is right.
|
|
43
|
+
|
|
44
|
+
## When NOT to use
|
|
45
|
+
|
|
46
|
+
- **Writing one type in the middle of another task** → `rigor-type-oracle`. It owns where a single type
|
|
47
|
+
comes from; this skill owns what a set of declarations should promise.
|
|
48
|
+
- **Raising `rigor coverage --protection` numbers** on the project's own code → `rigor-protection-uplift`.
|
|
49
|
+
- **Installing community RBS** that exists but is not set up → `rigor-rbs-setup`.
|
|
50
|
+
- **A Rigor diagnostic you believe is wrong about correct code** → `rigor-ask`; that is an engine
|
|
51
|
+
question, not a contract one.
|
|
52
|
+
|
|
53
|
+
## The two rules
|
|
54
|
+
|
|
55
|
+
**Contract rule.** Inference proves what a body does. It may *refute* a declaration, or fill a slot
|
|
56
|
+
nobody declared, but it never chooses the abstraction a declaration makes. A contract the body does not
|
|
57
|
+
prove is written only on the word of the party entitled to make it, and cited: the library's own
|
|
58
|
+
documentation for code you do not own, the owner's explicit answer for code they do.
|
|
59
|
+
|
|
60
|
+
**Direction rule.** Making a declaration *say less* — a return that admits `nil`, a parameter that
|
|
61
|
+
accepts more — needs a **witness** that the case is real: a call shown, by RBS's own runtime type checker,
|
|
62
|
+
to return or to need what the old declaration excludes; or the library's documentation. Rigor's findings
|
|
63
|
+
locate these cases and name the exit to aim at, but they have false positives, so on their own they are
|
|
64
|
+
a lead, not a witness. **A witness counts only once its negative control has failed**: break the
|
|
65
|
+
declaration on purpose and confirm the same run reports a type error. RBS's checker silently skips
|
|
66
|
+
some declarations (one inherited from a module or superclass by a class that defines the method itself
|
|
67
|
+
is never checked), so a clean run means nothing until the broken one fails. Making it *say more* — a narrower return, a narrower parameter, `void` replaced by
|
|
68
|
+
a value — needs a contract source. A `sig-gen` proposal counts only where the slot was undeclared or
|
|
69
|
+
`untyped`.
|
|
70
|
+
|
|
71
|
+
Five consequences come up on almost every run:
|
|
72
|
+
|
|
73
|
+
- **`void`, `boolish` and a deliberately wide nominal stand** unless the library documents a value.
|
|
74
|
+
The body returning something is not documentation.
|
|
75
|
+
- **A new declaration is where that choice is made.** A `sig-gen` proposal for an undeclared method
|
|
76
|
+
that is a literal (`-> 0`, `-> false`), an empty collection (`-> {}`), `nil`, or the incidental value of
|
|
77
|
+
a procedure's last statement becomes `void` (`bool` for a predicate) unless the documentation names
|
|
78
|
+
the value. Adopting it would create the contract the `void` rule protects.
|
|
79
|
+
- **Reading the implementation finds candidates; it does not decide them.** The one exception is an
|
|
80
|
+
undeclared or `untyped` parameter's structural need — an argument used only through `to_s` accepts
|
|
81
|
+
`_ToS`. A *declared* parameter is a promise too; today's body is no reason to widen it.
|
|
82
|
+
- **A wholly wrong declaration is treated as missing.** When the declared type and the body share
|
|
83
|
+
nothing (declared a connection pool, returns an `Integer`), it is neither saying less nor more: type
|
|
84
|
+
the slot as if it were undeclared, and witness that the declared type never comes back.
|
|
85
|
+
- **One alias that is both input and output carries two contracts.** When they differ, split it rather
|
|
86
|
+
than weaken it until both fit.
|
|
87
|
+
|
|
88
|
+
The full decision table, with the evidence each change needs and the shapes that look like findings but
|
|
89
|
+
are not: [`references/02-contract-rules.md`](references/02-contract-rules.md).
|
|
90
|
+
|
|
91
|
+
## Step 0 — Pick the target; it decides who has authority
|
|
92
|
+
|
|
93
|
+
| Target | Who decides a contract | Mode | Reference |
|
|
94
|
+
| --- | --- | --- | --- |
|
|
95
|
+
| The project's own code — a gem's `sig/`, an application's `sig/` | the owner | **interactive**: batch the open contract questions and ask | [`03-target-own-code.md`](references/03-target-own-code.md) |
|
|
96
|
+
| A gem you do not own — a gem_rbs_collection entry, or a local override of one | the gem's documentation | **autonomous** up to a finished branch and description; undocumented contracts are left alone and listed; pushing and opening the pull request wait for the user's go-ahead | [`04-target-gem-rbs-collection.md`](references/04-target-gem-rbs-collection.md) |
|
|
97
|
+
|
|
98
|
+
An application whose wrong signature comes from a dependency's community RBS is the second row: fix it
|
|
99
|
+
upstream, and keep a local override only until the fix is released.
|
|
100
|
+
|
|
101
|
+
Either target may be a **new declaration set** rather than an audit: a gem with no RBS anywhere, or a
|
|
102
|
+
new collection entry. Then there is nothing to compare against, the skeleton (mixins, attributes,
|
|
103
|
+
aliases, metaprogrammed methods) does not come from `sig-gen`, and most decisions are about
|
|
104
|
+
undocumented proven slots and blocks. The reference for the target has a "New declaration set" section.
|
|
105
|
+
|
|
106
|
+
## The loop
|
|
107
|
+
|
|
108
|
+
1. **Find.** Collect candidates from Rigor and from the documentation, not from a read-through. How,
|
|
109
|
+
per signal, and what each one can and cannot see:
|
|
110
|
+
[`references/01-finding-candidates.md`](references/01-finding-candidates.md).
|
|
111
|
+
2. **Triage.** Drop the shapes that are not contract findings — abstract methods, mutable registries
|
|
112
|
+
proposed as `[]`, platform-guarded files, a `Dynamic` inference, a memoised value Rigor reads as
|
|
113
|
+
nilable — before spending evidence on them.
|
|
114
|
+
3. **Decide.** For each remaining candidate, gather the evidence the direction rule asks for and cite
|
|
115
|
+
it: the witness call and the error it raised, the command and its output, the quote with its
|
|
116
|
+
`file:line` in the version under review, or the owner's answer. A candidate without the evidence it
|
|
117
|
+
needs is not changed; it is reported.
|
|
118
|
+
4. **Write.** Change the declaration. Prefer `rigor sig-gen` output verbatim where it applies; write by
|
|
119
|
+
hand only what the evidence supports, and nothing beyond it.
|
|
120
|
+
5. **Prove.** Run the target's gate. For every change that says less, keep the runtime witness: the
|
|
121
|
+
call fails RBS's type checker under the old declaration and passes under the new one. Before trusting
|
|
122
|
+
any clean run — one witness or a whole test suite — run the declaration-mutation pass: break each
|
|
123
|
+
method's return in turn and list the ones no run notices. Those returns are unchecked, whatever the
|
|
124
|
+
suite says (parameters and attributes are not covered by the pass). How: [`references/05-declaration-mutants.md`](references/05-declaration-mutants.md). Add
|
|
125
|
+
a use-site test that exercises the new contract — a `nil` check on a newly optional return, a call
|
|
126
|
+
with the newly accepted argument.
|
|
127
|
+
6. **Explain.** Every change carries its evidence. Every Rigor finding and every candidate you
|
|
128
|
+
deliberately left alone — above all a `void` whose body returns a value, and a Rigor finding no
|
|
129
|
+
witness confirmed — carries its reason, so a reviewer can tell restraint from oversight.
|
|
130
|
+
|
|
131
|
+
## Done when
|
|
132
|
+
|
|
133
|
+
Every candidate from step 1, including every row Rigor reported, is changed with its cited evidence,
|
|
134
|
+
left alone with its reason, or reported as an open question; the target's gate is green; every
|
|
135
|
+
declaration the mutation pass left surviving is named in the write-up as unchecked at runtime; and the
|
|
136
|
+
write-up lists all three groups.
|
|
137
|
+
|
|
138
|
+
When the owner does not answer an open question, or the documentation cannot be found, stop there: leave
|
|
139
|
+
those declarations as they are, finish and prove the changes the evidence did settle, and hand back the
|
|
140
|
+
open list. Never resolve an open question by picking the option the body happens to match.
|
|
141
|
+
|
|
142
|
+
## Gaps are findings here too
|
|
143
|
+
|
|
144
|
+
When Rigor answers `Dynamic[top]`, `untyped`, or skips a method, that is not permission to fill the
|
|
145
|
+
slot from your reading — `rigor-type-oracle`'s gap protocol applies unchanged. What this skill adds is
|
|
146
|
+
the other source: where the library documents the contract, the documentation fills the slot, with the
|
|
147
|
+
quote. Where neither speaks, the slot stays as declared.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# 01 — Finding candidates
|
|
2
|
+
|
|
3
|
+
A candidate is a declaration that may promise the wrong thing, or a public method that promises nothing
|
|
4
|
+
because it is not declared. Collect them from the signals below, record the command that produced each,
|
|
5
|
+
and only then decide anything. Every signal sees part of the picture; the table at the end says which
|
|
6
|
+
part.
|
|
7
|
+
|
|
8
|
+
All commands run with the RBS under review on Rigor's signature path and the library's source as the
|
|
9
|
+
analysed paths. How to arrange that for each target: [`03-target-own-code.md`](03-target-own-code.md),
|
|
10
|
+
[`04-target-gem-rbs-collection.md`](04-target-gem-rbs-collection.md).
|
|
11
|
+
|
|
12
|
+
## Signal 1 — `rigor check` over the library's own source
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
rigor check --no-baseline --format=json lib
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The rule to look for is **`def.return-type-mismatch`**: the body's last expression has a type the
|
|
19
|
+
declared return does not accept. Its message names both sides — `declared String, inferred String?`, or
|
|
20
|
+
a literal such as `inferred "a"?`. It compares a union member by member, so a typed member beside
|
|
21
|
+
`Dynamic` still counts: sidekiq 7.0's `Client#push` against its old `-> String` reports
|
|
22
|
+
`declared String, inferred Dynamic[top]?`. `--no-baseline` matters in an application: a baseline may
|
|
23
|
+
already be hiding exactly these.
|
|
24
|
+
|
|
25
|
+
A mismatch is a **lead**, not a witness. The rule has false positives: a memoised value re-read after a
|
|
26
|
+
`yield` (`@c ||= Chain.new(self); yield @c if block_given?; @c`) reads as `Chain?` although it can
|
|
27
|
+
never be `nil` — sidekiq's `Config#client_middleware` is that shape. A mismatch becomes a change only
|
|
28
|
+
with a runtime witness (Signal 6) or documentation.
|
|
29
|
+
|
|
30
|
+
Also worth reading here: `def.override-return-widened` (a subclass's declaration widens what its parent
|
|
31
|
+
promised). `static.value-use.void` (code uses a value a declaration says is `void`) is off in every
|
|
32
|
+
profile and fires only under the `use-of-void-value` bleeding-edge feature, so its silence proves
|
|
33
|
+
nothing.
|
|
34
|
+
|
|
35
|
+
What it cannot see: only the **last expression** is compared. An explicit `return nil` earlier in the
|
|
36
|
+
body, or a falsy arm behind an untyped value (`mime && new(mime[0])` where `mime` is untyped reads as
|
|
37
|
+
`Dynamic[top] | Marcel::Magic`), produces no diagnostic. The rule is also silent when the declared return
|
|
38
|
+
is `untyped` or `void`, and when the whole inferred type is `Dynamic`.
|
|
39
|
+
|
|
40
|
+
## Signal 2 — `rigor sig-gen --diff` against the declarations
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
rigor sig-gen --diff --format=json lib
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Each entry in `candidates` has a `classification`:
|
|
47
|
+
|
|
48
|
+
| `classification` | Meaning | What it is evidence for |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `new_method` | a public method with no declaration; `rbs` is the proposal | filling an undeclared slot — adopt the proposal verbatim, or leave the method out |
|
|
51
|
+
| `tighter_return` | Rigor proved a return narrower than `declared_return_rbs` | over `untyped`: an adoptable proposal. Over anything else: a "say more" change, which needs a contract source — the proposal alone is not enough |
|
|
52
|
+
| `skipped` | no proposal; `skip_reason` says why | nothing — see triage below |
|
|
53
|
+
|
|
54
|
+
`sig-gen` never proposes over a declared `void` and never tightens a declared nominal to a literal. It
|
|
55
|
+
also compares a declaration that is *narrower* than the body over every exit — an early `return nil`
|
|
56
|
+
included — but classifies the result `equivalent`, which `--diff` does not print (#3062). So an empty
|
|
57
|
+
`--diff` does not mean the declarations are sound; Signal 1, the leads below and Signal 6 cover that
|
|
58
|
+
half.
|
|
59
|
+
|
|
60
|
+
Private and protected methods are skipped unless you pass `--include-private`; a collection entry rarely
|
|
61
|
+
wants them.
|
|
62
|
+
|
|
63
|
+
`sig-gen` writes method signatures and nothing else: no `include` / `extend`, no `attr_*`, no `alias`, no
|
|
64
|
+
constants, and no method that only exists through metaprogramming (`define_method`, `class_eval` strings).
|
|
65
|
+
Readers come back as `untyped-return` skips rather than `attr_reader` lines. Some methods are also
|
|
66
|
+
absent without a skip row (#3068). So for a new declaration set its output is the types, not the
|
|
67
|
+
skeleton: see the target reference's "New declaration set" section.
|
|
68
|
+
|
|
69
|
+
## Signal 3 — `rigor check` over code that uses the library
|
|
70
|
+
|
|
71
|
+
Use-site code is where an input contract shows up: the project's own callers, the gem's `spec/` or
|
|
72
|
+
`test/`, or a collection entry's `_test/`.
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
rigor check --no-baseline --format=json <use-site paths>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Read each finding against the question "is this call correct Ruby that the library accepts?":
|
|
79
|
+
|
|
80
|
+
- **`call.argument-type-mismatch` on a call the library accepts** — the declared parameter is too
|
|
81
|
+
narrow. retryable's `{ sleep: 3.2 }` failed because one record type required all ten keys.
|
|
82
|
+
- **`call.undefined-method` on a method the library defines** — a missing declaration.
|
|
83
|
+
- **`call.possible-nil-receiver`** — usually the declaration is right and the caller is wrong; check
|
|
84
|
+
before "fixing" the RBS to silence it.
|
|
85
|
+
|
|
86
|
+
A use-site diagnostic can also be a Rigor false positive. If the call is correct and the declaration is
|
|
87
|
+
correct too, reproduce it minimally and route it through `rigor-ask`; do not bend the RBS around it.
|
|
88
|
+
|
|
89
|
+
## Signal 4 — `rigor type-of` on a returned value
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
rigor type-of lib/foo.rb:LINE:COL
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Point it at the returned *value*, not the `return` keyword: in `return nil unless ok`, the column of
|
|
96
|
+
`nil` answers `nil`, while the column of `return` answers `bot` (the statement itself never produces a
|
|
97
|
+
value). Use it to confirm or rule out a lead. `String?` is proof. `Dynamic[top] | Foo` means one arm is unknown —
|
|
98
|
+
the declaration may or may not be right, and the decision needs documentation or a run.
|
|
99
|
+
|
|
100
|
+
## Leads from reading — locate only
|
|
101
|
+
|
|
102
|
+
These are worth searching for because no command reports them, but each is a lead, not evidence. A
|
|
103
|
+
lead becomes a change only with documentation or a witness (Signal 6).
|
|
104
|
+
|
|
105
|
+
- an explicit `return` or `return nil` before the last expression;
|
|
106
|
+
- `x && y`, `x&.y`, or `find`/`detect`/`Hash#[]` whose `nil` result is returned as is;
|
|
107
|
+
- `rescue … nil` or a `rescue` clause whose value is returned;
|
|
108
|
+
- a method whose declaration is `void` but whose doc comment describes a return value (the reverse
|
|
109
|
+
case: the declaration may be too wide, and the documentation is the evidence).
|
|
110
|
+
|
|
111
|
+
## Signal 5 — the documentation
|
|
112
|
+
|
|
113
|
+
Read the documentation that ships with the version under review — doc comments and the README in that
|
|
114
|
+
version's source — as its own sweep, not only to confirm a lead, and note each quote's `file:line`. A
|
|
115
|
+
website or wiki describes some other release; see `02-contract-rules.md` for what it may support.
|
|
116
|
+
|
|
117
|
+
- doc comments containing "Returns", "nil", "or", "raises", "yields";
|
|
118
|
+
- the README's examples, which show which calls the authors consider the public API;
|
|
119
|
+
- the API reference, for public methods the RBS does not declare.
|
|
120
|
+
|
|
121
|
+
A documented return that the declaration contradicts is a candidate even when every Rigor signal is
|
|
122
|
+
silent. For sidekiq's `Client#push`, Signal 1 found the candidate and the doc comment — "If middleware
|
|
123
|
+
stops the job, nil will be returned instead" — settled it.
|
|
124
|
+
|
|
125
|
+
## Signal 6 — a runtime witness
|
|
126
|
+
|
|
127
|
+
RBS ships a runtime type checker. It wraps the methods of the classes you name, runs your Ruby, and
|
|
128
|
+
reports every argument or return value that does not match the loaded declarations. A call it rejects
|
|
129
|
+
is a **witness**: the evidence a "say less" change needs, and the way to tell a real Rigor finding from
|
|
130
|
+
a false positive. This **executes the library's code with the user's privileges**. For the project's
|
|
131
|
+
own code or a gem already in the user's bundle that is the same trust as running its tests. For any
|
|
132
|
+
other gem, run it in a container or VM, or ask the user before executing it.
|
|
133
|
+
|
|
134
|
+
Write a short script that calls the method with an input aimed at the exit Rigor or your reading
|
|
135
|
+
pointed to — for `return nil unless carrier_accepts?(parcel)`, a parcel the carrier rejects:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
# witness.rb
|
|
139
|
+
require "courier"
|
|
140
|
+
Courier::Client.new.deliver(Courier::Parcel.new(2, 50))
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
RBS_TEST_TARGET='Courier::*' RBS_TEST_OPT='-I sig' ruby -I lib -rrbs/test/setup witness.rb
|
|
145
|
+
# [Courier::Client#deliver] ReturnTypeError: expected `::String` but returns `nil`
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Under Bundler, run the same line as `bundle exec ruby -I lib -rrbs/test/setup witness.rb`. Do not add
|
|
149
|
+
`-rbundler/setup` instead: when Bundler switches versions it re-executes Ruby without `-I` and `-r`, the
|
|
150
|
+
script fails to load, and the non-zero exit looks like a witness. When the target's bundle does not
|
|
151
|
+
contain `rbs` (a gem's own `Gemfile` usually does not), run plain `ruby` with `GEM_PATH` naming both the
|
|
152
|
+
directory where the target's gems are installed and one where `rbs` is installed. Add `-r <library>` to
|
|
153
|
+
`RBS_TEST_OPT` for each standard library the declarations use.
|
|
154
|
+
|
|
155
|
+
A witness is the `ReturnTypeError` (or `ArgumentTypeError`) naming the method, not a non-zero exit: read
|
|
156
|
+
the error. Run it again after the change: it must exit 0. The same check
|
|
157
|
+
fits a test suite as `RBS::UnitTest`'s `assert_send_type "(untyped) -> String?", receiver, :deliver,
|
|
158
|
+
arg`, which also checks the call against the declaration loaded for the class, so it fails before the
|
|
159
|
+
fix and passes after it.
|
|
160
|
+
|
|
161
|
+
**Run the negative control first.** Run every witness once under a declaration you have broken on
|
|
162
|
+
purpose and confirm it reports the type error; only then does a clean run under the real declaration
|
|
163
|
+
mean anything. The checker silently skips some declarations. It checks a method only against a
|
|
164
|
+
declaration written on the same class or module that defines the method in Ruby. A declaration
|
|
165
|
+
inherited from a module or superclass, applied to a method an including class or subclass defines or
|
|
166
|
+
overrides, is skipped unless it carries `%a{rbs:test:target}`. So are methods of classes created
|
|
167
|
+
without `class … end` (`Struct.new`, `Class.new`, `Data.define`) and of classes loaded before
|
|
168
|
+
`rbs/test/setup`. A witness that calls a skipped method exits 0 whatever the declaration says. On rgl,
|
|
169
|
+
`remove_edge` declared `-> bool` on the module `MutableGraph` (which defines an abstract version) and
|
|
170
|
+
overridden in the class `DirectedAdjacencyGraph` returned a `Set` and the run passed; redeclared on the
|
|
171
|
+
class, the same run raised `expected bool but returns Set[5]`.
|
|
172
|
+
|
|
173
|
+
A run that raises nothing is not a witness the other way: it shows only that the inputs you tried
|
|
174
|
+
behave. When no input you can justify produces the excluded value, the finding is "left unchanged — no
|
|
175
|
+
witness", not a change. A whole test suite run under `rbs/test/setup` checks only the declarations its
|
|
176
|
+
calls reach and the checker does not skip; the declaration-mutation pass in
|
|
177
|
+
[`05-declaration-mutants.md`](05-declaration-mutants.md) lists the ones it does not, which is the
|
|
178
|
+
negative control for every declaration at once.
|
|
179
|
+
|
|
180
|
+
`rbs prototype runtime` is a different tool: it prints the methods, arities, mixins and constants of the
|
|
181
|
+
loaded classes, with every type `untyped`. It is useful for the skeleton of a new declaration set and
|
|
182
|
+
for finding undeclared methods — including ones `define_method` creates — and parameter shapes, never
|
|
183
|
+
for a return type. It loads the library, so the witness safety rule applies to it too.
|
|
184
|
+
|
|
185
|
+
## Triage before deciding
|
|
186
|
+
|
|
187
|
+
Drop these before spending evidence on them:
|
|
188
|
+
|
|
189
|
+
| Shape | Why it is not a contract finding |
|
|
190
|
+
| --- | --- |
|
|
191
|
+
| `skip_reason` `sig.skipped.abstract-method`, `template-method`, `overridden-bot`, `overridden-by-unsigned-subclass` | abstract or template methods; the declaration states what overriders return, which the base body cannot show |
|
|
192
|
+
| `sig.skipped.platform-guarded` | the definition loads only under a `RUBY_ENGINE` / `RUBY_PLATFORM` guard; the declaration describes the other platform's implementation |
|
|
193
|
+
| `sig.skipped.untyped-return` | Rigor has no answer; the gap protocol in `rigor-type-oracle` applies |
|
|
194
|
+
| a proposal of `[]` or `{}` | usually a collection the method fills later, or a registry callers append to; the element type is the contract, not "empty" |
|
|
195
|
+
| a proposal of `bot` or `top` | `bot` is almost always an abstract or raising method; `top` says nothing a declaration should adopt |
|
|
196
|
+
| a `tighter_return` that drops an arm the code clearly has (an `else`, a `rescue`) | an inference gap; report it, do not adopt it |
|
|
197
|
+
| a `def.return-type-mismatch` adding `nil` to a memoised value (`@x ||= …`, possibly re-read after a `yield`) | a known false-positive shape; it needs a witness, and usually none exists |
|
|
198
|
+
| a mismatch whose `nil` comes only from a lookup (`Hash#[]`, `find`, `detect`) on a key or element the library always supplies | worst-case reading; it needs a witness with an input the library allows |
|
|
199
|
+
|
|
200
|
+
`rigor explain <skip-reason>` documents each skip reason in full.
|
|
201
|
+
|
|
202
|
+
## What each signal sees
|
|
203
|
+
|
|
204
|
+
| Question | Signal |
|
|
205
|
+
| --- | --- |
|
|
206
|
+
| Is a declared return narrower than the body's last expression? | 1, confirmed by 6 |
|
|
207
|
+
| Is a declared return narrower than an early `return` or an untyped falsy arm? | leads, confirmed by 6 or 5 |
|
|
208
|
+
| Is a Rigor finding real or a false positive? | 6 |
|
|
209
|
+
| Is a public method undeclared? | 2 (`new_method`), 5, `rbs prototype runtime` |
|
|
210
|
+
| Could an `untyped` return be precise? | 2 (`tighter_return` over `untyped`) |
|
|
211
|
+
| Is a parameter too narrow? | 3 |
|
|
212
|
+
| Is a `void` hiding a documented value? | 5 |
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# 02 — Deciding a candidate
|
|
2
|
+
|
|
3
|
+
Every change to a declaration either makes it say less, makes it say more, or replaces a declaration
|
|
4
|
+
that is wholly wrong. The kind decides the evidence it needs. Cite that evidence with the change;
|
|
5
|
+
without it, the candidate is reported, not changed.
|
|
6
|
+
|
|
7
|
+
## What counts as evidence
|
|
8
|
+
|
|
9
|
+
| Evidence | How to cite it | Good for |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| The library's documentation **for the version under review**: a doc comment on the method, the README shipped in that gem | quote it with its `file:line` in that version's source | any contract on code you do not own |
|
|
12
|
+
| The owner's explicit answer, for the project's own code | the question and the answer; better, a doc comment written from it | any contract on that code |
|
|
13
|
+
| A runtime witness: a call that RBS's runtime type checker (`rbs/test/setup` or `RBS::UnitTest`) rejects under the old declaration and accepts under the new one | the call, and the error it raised under the old declaration | saying less; showing a wholly wrong declaration is never returned |
|
|
14
|
+
| Rigor's findings: `def.return-type-mismatch`, a `sig-gen` proposal, a `type-of` answer | the command and its output | locating a candidate and the exit to aim a witness at; filling an undeclared or `untyped` slot |
|
|
15
|
+
| A correct call the old declaration rejects, found by `rigor check` on use-site code | the call and the diagnostic | widening a parameter or an input record |
|
|
16
|
+
| An undeclared or `untyped` parameter's structural need, read from the body | the line that uses it | typing that parameter as an interface (`_ToS`, `_Each[T]`) |
|
|
17
|
+
| Reading the implementation for anything else | — | nothing. It finds candidates; it does not decide them |
|
|
18
|
+
|
|
19
|
+
How to run a witness: [`01-finding-candidates.md`](01-finding-candidates.md) § "Signal 6".
|
|
20
|
+
|
|
21
|
+
**Documentation is pinned to the version.** Quote the doc comment or README from the unpacked source of
|
|
22
|
+
the version whose declaration you are changing. A website, wiki or default-branch README describes some
|
|
23
|
+
other release, so it may support a question in the write-up but is never the sole support for a change —
|
|
24
|
+
least of all for `void` → a value. Grade what you quote: a doc comment that states the return ("Returns
|
|
25
|
+
a unique Job ID. If middleware stops the job, nil will be returned instead") is strong; a README example
|
|
26
|
+
that happens to use the value is weak. Say which kind you are citing.
|
|
27
|
+
|
|
28
|
+
**The gem's own type annotations are documentation, and can be wrong.** A YARD `@return [Foo]` or
|
|
29
|
+
`@param` in the gem's source is the author's statement, so you may read and quote it; `rigor-type-oracle`'s
|
|
30
|
+
rule against bracketed YARD types governs what *you* write, not what you read. Grade a tag like any other
|
|
31
|
+
documentation, and expect it to drift: on rgl 0.6.7, `Graph#reverse` is tagged
|
|
32
|
+
`@return [DirectedAdjacencyGraph]` while its own prose says an undirected graph returns `self`, and the
|
|
33
|
+
README's `remove_edge … #=> true` is stale (the call returns a `Set`). **When two pieces of the same
|
|
34
|
+
version's documentation disagree, or a tag disagrees with Rigor's proof, a witness decides.** Quote
|
|
35
|
+
both and cite the witness.
|
|
36
|
+
|
|
37
|
+
## Saying less
|
|
38
|
+
|
|
39
|
+
| Change | Needs | Example |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| A return admits a value it excluded (`String` → `String?`) | a runtime witness or that version's documentation. On code you do not own, a Rigor finding alone is never enough | sidekiq `Client#push` (documented, and a halting middleware returns `nil`); marcel `Magic.by_magic` (`instance` → `instance?`, witnessed by a lookup on input that matches nothing) |
|
|
42
|
+
| A parameter accepts more (`String` → `String \| Pathname`) | documentation, or a correct call the old declaration rejects | — |
|
|
43
|
+
| An undeclared or `untyped` parameter is typed as an interface | the body's structural need | marcel `by_extension` takes `_ToS`, because the gem only calls `to_s` on it |
|
|
44
|
+
| A record key becomes optional on input | a correct call that omits it | retryable `{ sleep: 3.2 }` |
|
|
45
|
+
| A declaration is added for a public method, as `untyped` slots | nothing beyond the method existing | — |
|
|
46
|
+
|
|
47
|
+
**Rigor's findings have false positives, so they are leads.** sidekiq 7.0's `Config#client_middleware`
|
|
48
|
+
is `@client_chain ||= Chain.new(self); yield @client_chain if block_given?; @client_chain`, and
|
|
49
|
+
`rigor check` reports `declared Chain, inferred Chain?` although it never returns `nil`. An agent that
|
|
50
|
+
widened on that finding would push a `nil` check onto every caller of the gem. A witness is what tells
|
|
51
|
+
the two apart: no input makes `client_middleware` return `nil`, so none can be produced.
|
|
52
|
+
|
|
53
|
+
**A missing witness is not proof the other way.** A run that raises nothing shows only that the inputs
|
|
54
|
+
you tried behave; it never justifies a narrower declaration. When you cannot produce a witness and the
|
|
55
|
+
documentation is silent, leave the declaration and record the finding.
|
|
56
|
+
|
|
57
|
+
**A declared parameter is a promise both ways.** Widening it promises callers that the library accepts
|
|
58
|
+
more, and the next release may not keep that promise (a body that only calls `to_s` today may call
|
|
59
|
+
`downcase` tomorrow). So a declared parameter widens only on documentation or a correct call it rejects,
|
|
60
|
+
never on the body's current structural need alone.
|
|
61
|
+
|
|
62
|
+
Before widening a return, look at what callers lose. Every caller now has to handle the new case, so the
|
|
63
|
+
change is right only if the case is real. "It could be nil if someone passes nonsense" is not a witness;
|
|
64
|
+
an input the documentation allows, or a use the library's own callers make, is.
|
|
65
|
+
|
|
66
|
+
## Saying more
|
|
67
|
+
|
|
68
|
+
| Change | Needs |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| A narrower return over a declared non-`untyped` type | a contract source. A `tighter_return` proposal alone is not enough |
|
|
71
|
+
| A narrower return over `untyped`, or a new method's return | Rigor's proof (`sig-gen`'s proposal, verbatim) — or a contract source for anything it did not propose |
|
|
72
|
+
| A narrower parameter | a contract source. Observed call sites (`sig-gen --params=observed`) show what callers pass today, not what the method accepts; widen literals before adopting them |
|
|
73
|
+
| `void` or `boolish` → a value type | that version's documentation of the value. The body returning something is not documentation |
|
|
74
|
+
| A deliberately wide nominal (`Numeric`, `Object`) → what the body returns | that version's documentation of the narrower value |
|
|
75
|
+
| A new method whose `sig-gen` proposal is a literal (`-> 0`, `-> false`), an empty collection (`-> {}`, `-> []`), `nil`, or the incidental value of a procedure's last statement | nothing adopts it as is: write `void` (`bool` for a predicate) unless the documentation names the value. A literal default that subclasses override (`directed? -> false`, overridden with `true`) is a predicate: `bool` |
|
|
76
|
+
| `instance` or `self` instead of the class `sig-gen` printed (a `new` in a singleton method, `self.class.new`, a method returning `self`) | the body's construction (`new` on the receiver's own class) plus documentation that subclasses get their own type. A witness cannot show it, because the checker accepts a subclass instance either way (#3069) |
|
|
77
|
+
|
|
78
|
+
## Replacing a wholly wrong declaration
|
|
79
|
+
|
|
80
|
+
Sometimes the declared type and the body have nothing in common: sidekiq 7.0's `Config#total_concurrency`
|
|
81
|
+
is declared `ConnectionPool[RedisClientAdapter]`, and its body is `capsules.each_value.sum(&:concurrency)`
|
|
82
|
+
(`rigor check` reports `inferred Integer`). That is neither saying less nor saying more. Treat the slot
|
|
83
|
+
as undeclared:
|
|
84
|
+
|
|
85
|
+
1. Take the type from that version's documentation, or from `rigor sig-gen --print` run on a copy of the
|
|
86
|
+
declarations with this member removed — the same source an undeclared method would get.
|
|
87
|
+
2. Witness that the old declaration is wrong: one call whose result RBS's type checker rejects against
|
|
88
|
+
the old type.
|
|
89
|
+
3. Say in the write-up that the declaration was replaced, not widened or narrowed.
|
|
90
|
+
|
|
91
|
+
## Blocks
|
|
92
|
+
|
|
93
|
+
Most of a new declaration's work is in its parameters and blocks, and `sig-gen` does not always type
|
|
94
|
+
blocks (#3067). Decide them in this order:
|
|
95
|
+
|
|
96
|
+
- **Required or optional.** Optional (`?{ … }`) when every `yield` or `block.call` is guarded by
|
|
97
|
+
`block_given?` (or the block is passed on to a method whose declaration makes it optional); required
|
|
98
|
+
when one is unconditional. If the documentation says optional and the body uses the block
|
|
99
|
+
unconditionally, see "When the documented contract crashes" below.
|
|
100
|
+
- **Two overloads, not an optional block,** when the method returns something else without a block
|
|
101
|
+
(`return enum_for(__method__) unless block_given?`): `() { (T) -> void } -> self` and
|
|
102
|
+
`() -> Enumerator[T, self]`, rather than one optional block with a union return.
|
|
103
|
+
- **Block parameters** come from what is yielded, and from `:yields:` / `@yield` / `@yieldparam` in the
|
|
104
|
+
documentation.
|
|
105
|
+
- **Block return** is `void` unless the method uses the value; `boolish` when it is used only for
|
|
106
|
+
truthiness (a filter or predicate block); a precise type only when the method's documented result
|
|
107
|
+
depends on it.
|
|
108
|
+
|
|
109
|
+
## When the documented contract crashes
|
|
110
|
+
|
|
111
|
+
Sometimes a witness shows the documentation promising more than the code delivers: rgl documents
|
|
112
|
+
`depth_first_search`'s block as optional, and calling it without one raises `NoMethodError`. Keep the
|
|
113
|
+
documented declaration — it is the contract — note the defect as an upstream bug for the user to
|
|
114
|
+
report (filing an issue on someone else's repository is outward-facing; do not do it unasked), and put
|
|
115
|
+
it under open questions in the write-up. Narrowing the declaration to what the code survives would be saying more
|
|
116
|
+
without a contract source.
|
|
117
|
+
|
|
118
|
+
## Rules that come up on almost every run
|
|
119
|
+
|
|
120
|
+
**`void` and `boolish` stand.** (For the project's own code, the owner may decide otherwise; that is a
|
|
121
|
+
contract decision, so ask rather than infer.) Every Ruby method returns its last expression, so a body
|
|
122
|
+
nearly always has a value. `-> void` says that value is not part of the contract; `boolish` says only
|
|
123
|
+
its truthiness is. `static.value-use.void` reports a use of the first where a project enables it. Do not
|
|
124
|
+
tighten either to the body's value unless the library documents that value. In sidekiq 7.0,
|
|
125
|
+
`perform_async` and `Client.enqueue*` return `push`'s result from their bodies, but nothing documents
|
|
126
|
+
it, so their `void` stayed while `push` itself became `String?`.
|
|
127
|
+
|
|
128
|
+
**One alias, two contracts.** When a type alias, record or interface is both what callers pass in and
|
|
129
|
+
what the library hands back, check whether the two agree. Inputs usually accept partial or loose values;
|
|
130
|
+
outputs usually guarantee more. If they differ, split the alias — for example `option_overrides` (keys
|
|
131
|
+
optional) for input and `options` (keys required) for output — rather than making it loose enough for
|
|
132
|
+
both and weakening every return that uses it. Inputs stay lenient; outputs stay as precise as their
|
|
133
|
+
documented guarantee.
|
|
134
|
+
|
|
135
|
+
**A nilable wrapper on a value is not the same as an optional key.** `{ ?sleep: Numeric }` says the key may
|
|
136
|
+
be absent; `{ sleep: Numeric? }` says it may be present and `nil`. Use the second only where a present
|
|
137
|
+
`nil` is accepted and means something.
|
|
138
|
+
|
|
139
|
+
**Interfaces describe what the method uses.** When an undeclared or `untyped` parameter is only used
|
|
140
|
+
through one or two methods, an interface (`_ToS`, `_ToPath`, `_Each[T]`, or one you declare inside the
|
|
141
|
+
library's namespace) is the honest contract. Do not add top-level aliases or interfaces to a shared RBS
|
|
142
|
+
tree.
|
|
143
|
+
|
|
144
|
+
**Consistency across versions.** A collection entry often has several version directories. Apply the same
|
|
145
|
+
contract to each version whose code and documentation say the same thing — witnessed or quoted in that
|
|
146
|
+
version — and say in the write-up which versions you changed and which you did not and why.
|
|
147
|
+
|
|
148
|
+
## Every finding gets a disposition
|
|
149
|
+
|
|
150
|
+
Each row Rigor reported (Signal 1 and Signal 2) and each lead you followed ends in exactly one list:
|
|
151
|
+
|
|
152
|
+
- **Changed** — with its evidence.
|
|
153
|
+
- **Deliberately left unchanged** — you looked, and the declaration is the right contract: a `void` over
|
|
154
|
+
a value; a wide nominal; a proposal that is an inference gap; a Rigor finding no witness confirmed
|
|
155
|
+
(say so — "`rigor check` reports `Chain?`; no input returns `nil`").
|
|
156
|
+
- **Open questions** — the evidence does not settle it. For your own code, ask the owner. Upstream, a
|
|
157
|
+
question in the pull request description is better than a guess in the diff.
|