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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# 03 — Target: the project's own code
|
|
2
|
+
|
|
3
|
+
A gem's shipped `sig/`, an application's `sig/`, or rbs-inline annotations (`#:`, `# @rbs`) in either.
|
|
4
|
+
The project's owner is the authority on every contract this code makes, so this target is
|
|
5
|
+
**interactive**: Rigor and the documentation narrow the questions, and the owner answers the ones they
|
|
6
|
+
leave open.
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
The project's Rigor configuration already analyses its source with its `sig/` on the signature path, so
|
|
11
|
+
the commands in [`01-finding-candidates.md`](01-finding-candidates.md) run as they are:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
rigor check --no-baseline --format=json lib # Signal 1
|
|
15
|
+
rigor sig-gen --diff --format=json lib # Signal 2
|
|
16
|
+
rigor check --no-baseline --format=json spec # Signal 3, or test/, or the application's callers
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If `rigor` is not configured yet, run `rigor-next-steps` first. Record the current `rigor check` result
|
|
20
|
+
(with the baseline, as the project normally runs it) before changing anything; the gate below compares
|
|
21
|
+
against it.
|
|
22
|
+
|
|
23
|
+
## Ask, in one batch
|
|
24
|
+
|
|
25
|
+
After triage, sort what is left into three groups:
|
|
26
|
+
|
|
27
|
+
1. **Settled by evidence** — a `def.return-type-mismatch`, a `sig-gen` proposal over `untyped`, a
|
|
28
|
+
documented value. Apply these, with their citations.
|
|
29
|
+
2. **Settled as "leave it"** — a `void` over a value nobody documented, a wide nominal, a proposal that
|
|
30
|
+
is an inference gap. Do not ask about these one by one; list them.
|
|
31
|
+
3. **Open contract questions** — everything that needs the owner. Ask them together, each with the
|
|
32
|
+
evidence and the concrete options:
|
|
33
|
+
|
|
34
|
+
> `Client#push` is declared `-> String`. Its body returns `nil` when a middleware halts the job
|
|
35
|
+
> (`lib/client.rb:88`, `return nil unless item`), and `rigor check` cannot see that early return.
|
|
36
|
+
> Should callers be told to expect `nil`?
|
|
37
|
+
> (a) `-> String?` — callers must handle a halted job; (b) keep `-> String` and raise instead of
|
|
38
|
+
> returning `nil`; (c) `-> void` — callers should not use the value.
|
|
39
|
+
|
|
40
|
+
Never ask an open question as "what type is this?". The owner should choose a contract, not reverse-
|
|
41
|
+
engineer one.
|
|
42
|
+
|
|
43
|
+
## Write the answer where it can be cited
|
|
44
|
+
|
|
45
|
+
An answer that lives only in the conversation is lost to the next contributor and to anyone writing RBS
|
|
46
|
+
for this library elsewhere. Offer to record each answered contract as a doc comment in prose — "Returns
|
|
47
|
+
the job id, or nil when a middleware stops the job." — next to the method. The declaration then has a
|
|
48
|
+
documented source, which is what makes it checkable later.
|
|
49
|
+
|
|
50
|
+
If the project keeps types out of doc comments, prose describing behaviour is still fine; a bracketed
|
|
51
|
+
YARD type is not, and `rigor-type-oracle` covers why.
|
|
52
|
+
|
|
53
|
+
## The gate
|
|
54
|
+
|
|
55
|
+
1. `rigor check` on the project, run as the project normally runs it, gains no new diagnostic. A new one
|
|
56
|
+
means a caller relied on the old contract: show it to the owner. It is either a bug the new contract
|
|
57
|
+
exposed, or a sign the contract change is wrong.
|
|
58
|
+
2. If the project ships its RBS (a gem's `sig/`), it still loads: `rbs -I sig validate`, or the
|
|
59
|
+
project's own RBS task.
|
|
60
|
+
3. If the project also runs Steep or another RBS consumer, run it. Rigor is not the only reader of these
|
|
61
|
+
files.
|
|
62
|
+
4. Where you widened a return, a witness shows the case is real (see `01-finding-candidates.md`
|
|
63
|
+
§ "Signal 6") and a test or a use site now handles it; where you widened a parameter, a test now
|
|
64
|
+
passes the newly accepted argument. The owner may accept a change without a witness — say so in the
|
|
65
|
+
write-up — but a Rigor finding alone is never presented as proof.
|
|
66
|
+
5. If the project has a test suite, use it as the command for the declaration-mutation pass
|
|
67
|
+
([`05-declaration-mutants.md`](05-declaration-mutants.md)): it runs the suite under `rbs/test/setup`
|
|
68
|
+
once per broken declaration. A killed mutant is a declaration the suite checks against real values,
|
|
69
|
+
which no static signal does; a survivor is one it does not check, however green the suite is. Report
|
|
70
|
+
the survivors to the owner.
|
|
71
|
+
6. For a gem with no RBS yet, the skeleton and new-method rules in `04-target-gem-rbs-collection.md`
|
|
72
|
+
§ "New declaration set" apply to its `sig/` as well; ship it with the gem rather than through the
|
|
73
|
+
collection.
|
|
74
|
+
|
|
75
|
+
## When the wrong signature belongs to a dependency
|
|
76
|
+
|
|
77
|
+
If a diagnostic in the project traces to a dependency's community RBS, the fix belongs upstream:
|
|
78
|
+
follow [`04-target-gem-rbs-collection.md`](04-target-gem-rbs-collection.md) for that gem. Until the fix
|
|
79
|
+
is released, a local corrected copy can stand in:
|
|
80
|
+
|
|
81
|
+
- in `rbs_collection.yaml`, set that gem to `ignore: true`, then run `rbs collection install` again so
|
|
82
|
+
`rbs_collection.lock.yaml` drops it (the old directory under `.gem_rbs_collection/` stays, but only
|
|
83
|
+
gems listed in the lockfile load) — otherwise the collection copy still loads next to yours and the duplicate declarations fail to build, which reads as a quieter run, not a
|
|
84
|
+
fixed one;
|
|
85
|
+
- copy its RBS into a project directory on `signature_paths`, apply the fix, and note the upstream pull
|
|
86
|
+
request next to it;
|
|
87
|
+
- remove the copy and the `ignore:` once the fix is in a collection release you have installed.
|
|
88
|
+
|
|
89
|
+
Do not patch the installed collection under `.gem_rbs_collection/`; the next `rbs collection install`
|
|
90
|
+
overwrites it.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# 04 — Target: a gem_rbs_collection entry
|
|
2
|
+
|
|
3
|
+
RBS for a gem you do not own, in a checkout of
|
|
4
|
+
[`ruby/gem_rbs_collection`](https://github.com/ruby/gem_rbs_collection). The gem's documentation is the
|
|
5
|
+
authority, so this target can run without questions up to a finished branch and pull request
|
|
6
|
+
description. **Pushing the branch and opening the pull request are the user's call**: both are
|
|
7
|
+
outward-facing, even to a fork. Show them the local branch and the description, and push or open only
|
|
8
|
+
on their go-ahead, unless they already gave it for this change.
|
|
9
|
+
|
|
10
|
+
The collection's own guide is `docs/CONTRIBUTING.md` in the checkout; read it once. Two of its points
|
|
11
|
+
shape this workflow: focus on the API the README and documentation present, and write `_test/` code that
|
|
12
|
+
checks RBS against *application* code, not against the gem's implementation.
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
Paths below use `gems/NAME/X.Y` for the entry, `$COLLECTION` for the checkout, `$GEM_DIR` for the gem's
|
|
17
|
+
source and `$SCRATCH` for a scratch directory. **Run every block from here to Signal 3 in one shell
|
|
18
|
+
session**, or as one script: an agent's shell tool often keeps no variables between calls, and an empty
|
|
19
|
+
`$GEM_DIR` or `$SCRATCH` turns `cp -R "$GEM_DIR/lib" "$SCRATCH/lib"` into a copy of `/lib`. If the
|
|
20
|
+
variables cannot persist, set them again at the top of each block and guard them, for example
|
|
21
|
+
`: "${GEM_DIR:?}" "${SCRATCH:?}"`.
|
|
22
|
+
|
|
23
|
+
1. In the checkout: `bin/setup`.
|
|
24
|
+
2. Fetch the source of the version the entry describes. Unpacking reads it without building native
|
|
25
|
+
extensions or running anything:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
COLLECTION=$PWD
|
|
29
|
+
SRC=$(mktemp -d)
|
|
30
|
+
(cd "$SRC" && gem fetch NAME -v '~> X.Y.0' && gem unpack NAME-*.gem)
|
|
31
|
+
GEM_DIR=$(ls -d "$SRC"/NAME-*/ | head -n 1)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If the gem is already installed, `ruby -e 'puts Gem::Specification.find_by_name("NAME", "~> X.Y.0").gem_dir'`
|
|
35
|
+
gives the same directory.
|
|
36
|
+
|
|
37
|
+
3. Run the entry's tests once before changing anything, so you know they were green:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
bin/test gems/NAME/X.Y
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This also leaves an `rbs_collection.lock.yaml` in `_test/`, which puts the entry's dependency RBS in
|
|
44
|
+
reach for the use-site check below.
|
|
45
|
+
|
|
46
|
+
## Signals 1 and 2 — the declarations against the gem's source
|
|
47
|
+
|
|
48
|
+
Work in a scratch directory outside the checkout, so nothing Rigor writes ends up in the diff, and copy
|
|
49
|
+
in only what is under review:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
SCRATCH=$(mktemp -d)
|
|
53
|
+
mkdir -p "$SCRATCH/sig"
|
|
54
|
+
cp -R "$GEM_DIR/lib" "$SCRATCH/lib"
|
|
55
|
+
rsync -a --exclude _test --exclude .gem_rbs_collection "$COLLECTION/gems/NAME/X.Y/" "$SCRATCH/sig/"
|
|
56
|
+
printf 'paths: ["lib"]\nsignature_paths: ["sig"]\n' > "$SCRATCH/.rigor.yml"
|
|
57
|
+
cd "$SCRATCH"
|
|
58
|
+
rigor check --format=json lib
|
|
59
|
+
rigor sig-gen --diff --format=json lib
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Excluding `_test/` matters: its `.rbs` files and any `.gem_rbs_collection/` left by an earlier
|
|
63
|
+
`bin/test` would load as duplicate declarations. A gem whose RBS depends on another gem's RBS will show
|
|
64
|
+
`rbs.coverage.*` warnings and undefined-method noise here; copy that dependency's entry into `sig/` too,
|
|
65
|
+
or read Signal 1 for the methods whose types do not touch the dependency.
|
|
66
|
+
|
|
67
|
+
## Signal 3 — the entry's `_test/` code
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
cd "$COLLECTION/gems/NAME/X.Y/_test"
|
|
71
|
+
printf 'paths: ["."]\nsignature_paths: ["."]\n' > .rigor.yml # with rbs_collection.lock.yaml present
|
|
72
|
+
rigor check --no-cache --format=json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Without an `rbs_collection.lock.yaml`, use `signature_paths: [".."]` instead, and expect missing
|
|
76
|
+
dependency RBS. `--no-cache` keeps a `.rigor/` cache directory out of the entry. Keep `.rigor.yml` out of
|
|
77
|
+
the commit by listing it in `.git/info/exclude`, and check `git status` before committing: nothing under
|
|
78
|
+
`_test/` but your test files belongs in the diff.
|
|
79
|
+
|
|
80
|
+
## Signal 5 — the gem's documentation
|
|
81
|
+
|
|
82
|
+
Read the doc comments in `$GEM_DIR/lib` and the README in `$GEM_DIR` — the source of the version under
|
|
83
|
+
review. Quote what you rely on in the write-up with its path and line in that source. A website or the
|
|
84
|
+
default branch's README may describe another release: use it to ask, not to decide.
|
|
85
|
+
|
|
86
|
+
## Signal 6 — the runtime witness
|
|
87
|
+
|
|
88
|
+
Every change that makes a declaration say less needs a witness or that version's documentation here; a
|
|
89
|
+
Rigor finding alone does not go upstream. Give both when you can. Run the witness from Signal 6 of
|
|
90
|
+
`01-finding-candidates.md` in `$SCRATCH`, against the unpacked source and the copied declarations:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
: "${SCRATCH:?}"
|
|
94
|
+
cd "$SCRATCH"
|
|
95
|
+
RBS_TEST_TARGET='Namespace::*' RBS_TEST_OPT='-I sig' ruby -I lib -rrbs/test/setup witness.rb
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**This executes the gem's code with the user's privileges, and installing what it needs at runtime
|
|
99
|
+
installs more.** A scratch directory is not isolation. Run it in a container or VM, or ask the user
|
|
100
|
+
before executing a gem that is not already in their bundle. Record the call and the error. After editing the declaration,
|
|
101
|
+
copy it into `$SCRATCH/sig` again and confirm the same run passes. `witness.rb` is evidence for the pull
|
|
102
|
+
request description, not a committed test: the collection's `_test/` code is checked statically.
|
|
103
|
+
|
|
104
|
+
## Writing the change
|
|
105
|
+
|
|
106
|
+
- Edit `gems/NAME/X.Y/*.rbs`. Keep types inside the gem's namespace; no new top-level aliases or
|
|
107
|
+
interfaces.
|
|
108
|
+
- A new standard-library or gem dependency in the RBS, or in the test, goes into `manifest.yaml`
|
|
109
|
+
(marcel's test needed `tempfile`).
|
|
110
|
+
- Apply the same contract to every version directory whose code and documentation agree, and only to
|
|
111
|
+
those.
|
|
112
|
+
|
|
113
|
+
## New declaration set (a new entry)
|
|
114
|
+
|
|
115
|
+
When the gem has no entry yet, there is nothing for Signals 1 and 2 to compare against, and three jobs
|
|
116
|
+
take the place of the audit:
|
|
117
|
+
|
|
118
|
+
1. **The skeleton.** `"$COLLECTION"/bin/init_new_gem NAME` creates the directory and `_test/`. It
|
|
119
|
+
prompts on stdin for the version and then a GitHub account for `_reviewers.yaml`, so answer both
|
|
120
|
+
explicitly — `printf '0.6\n\n' | "$COLLECTION"/bin/init_new_gem NAME` — and leave the account empty
|
|
121
|
+
unless the user gives you one; never put their name in it on your own. Take the
|
|
122
|
+
module structure — classes, modules, `include` / `extend`, superclasses, `attr_*`, aliases,
|
|
123
|
+
constants, and methods that exist only through metaprogramming — from the loaded gem, because
|
|
124
|
+
`sig-gen` writes none of these:
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
RUBYLIB="$GEM_DIR/lib" rbs prototype runtime -r FEATURE Namespace 'Namespace::*' > "$SCRATCH/skeleton.rbs"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`-r` takes the gem's feature name (`rgl/adjacency`); `-R` means `require_relative`, and there is no
|
|
131
|
+
`-I`. Name the top-level module on its own as well as `Namespace::*`: the pattern alone excludes it. rbs 4.2 marks the command deprecated (ruby/rbs#2841) but it still works. It loads the gem, so
|
|
132
|
+
the witness safety rule applies.
|
|
133
|
+
2. **The types.** Run `rigor sig-gen --print` in `$SCRATCH` with an empty `sig/`, and take each
|
|
134
|
+
method's types from it under `02-contract-rules.md`: a literal, empty-collection, `nil` or
|
|
135
|
+
procedure-tail proposal becomes `void` or `bool` unless documented; blocks follow § "Blocks".
|
|
136
|
+
3. **The contracts.** Fill what `sig-gen` skipped from the documentation, witness every `nil`, and leave
|
|
137
|
+
the rest `untyped`. A method that appears in the skeleton but in neither `sig-gen` list was silently
|
|
138
|
+
dropped (#3068); give it a disposition too.
|
|
139
|
+
|
|
140
|
+
**A dependency without an entry.** When the new entry must reference a type from another gem — a mixin
|
|
141
|
+
(`include Stream`), a superclass, a parameter type — and that gem has no entry either, the other entry
|
|
142
|
+
comes first, in its own pull request, or the reference is dropped (with the methods it would have
|
|
143
|
+
supplied declared directly). Say which in the description. Never ship a stub of another gem inside this
|
|
144
|
+
entry.
|
|
145
|
+
|
|
146
|
+
## The gate
|
|
147
|
+
|
|
148
|
+
1. Add or extend a test under `_test/` that exercises the contract you changed, as application code
|
|
149
|
+
would: `&.` or an `if` on a newly nilable return, calls to newly declared methods, the newly accepted
|
|
150
|
+
argument shapes. A new file pair `test_N.rb` / `test_N.rbs` is fine when the existing test does not
|
|
151
|
+
fit.
|
|
152
|
+
2. `"$COLLECTION"/bin/test gems/NAME/X.Y`, run from `$COLLECTION`, passes for every version you
|
|
153
|
+
touched. It runs `rbs validate`, `steep check` and the untyped-call check.
|
|
154
|
+
3. `rigor check` on `_test/` reports nothing new compared with before the change.
|
|
155
|
+
4. Every change that says less has a witness that fails under the old declaration and passes under the
|
|
156
|
+
new one, or a quote from that version's documentation, for each version you changed.
|
|
157
|
+
5. The declaration-mutation pass ([`05-declaration-mutants.md`](05-declaration-mutants.md)) has run over
|
|
158
|
+
the entry with `_test/` and your witnesses as the command, and its survivors are listed in the
|
|
159
|
+
description.
|
|
160
|
+
|
|
161
|
+
When `bin/test` cannot run — it needs the network to lock and install the collection — run the same
|
|
162
|
+
three checks offline and say in the description that `bin/test` was not run. Offline there is no
|
|
163
|
+
installed collection, so the Steepfile names the entry (`signature ".."`) and each dependency entry
|
|
164
|
+
itself:
|
|
165
|
+
|
|
166
|
+
Add `-r <library>` to `rbs` and a `library "<library>"` line to the Steepfile for each standard library
|
|
167
|
+
`manifest.yaml` lists, which `bin/test` would otherwise install. A library that is part of core in the
|
|
168
|
+
`rbs` you run (`set` under rbs 4.2) has no separate signatures, and `-r set` fails with "Cannot find
|
|
169
|
+
type definitions"; leave it out there:
|
|
170
|
+
|
|
171
|
+
```sh
|
|
172
|
+
rbs -r <each manifest library> -I "$COLLECTION/gems/NAME/X.Y" -I <each dependency entry> validate
|
|
173
|
+
cd "$COLLECTION/gems/NAME/X.Y/_test" # Steepfile: bin/test's generate_steepfile, plus signature ".." and each dependency entry
|
|
174
|
+
steep check
|
|
175
|
+
steep stats --format=csv # every row's "Untyped calls" must be 0, as bin/check-untyped-call.rb checks
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## The pull request
|
|
179
|
+
|
|
180
|
+
Branch from the collection's default branch and keep one gem, one concern per pull request. Title it
|
|
181
|
+
`NAME: <what the contract now says>` — for example `sidekiq: Client#push and client_push return
|
|
182
|
+
String?`. The body follows the shape reviewers have accepted:
|
|
183
|
+
|
|
184
|
+
```markdown
|
|
185
|
+
## Summary
|
|
186
|
+
|
|
187
|
+
What was wrong with the contract and why, with the quote that settles it.
|
|
188
|
+
|
|
189
|
+
Changed in `gems/NAME/X.Y`:
|
|
190
|
+
|
|
191
|
+
- `Klass#meth` now returns `String?`. <the evidence, one line>
|
|
192
|
+
|
|
193
|
+
Deliberately left unchanged:
|
|
194
|
+
|
|
195
|
+
- `Klass#other` stays `void`. Its implementation returns the same value, but NAME does not document a return value for it.
|
|
196
|
+
- `Klass#memo` stays `Chain`. Rigor reports `Chain?`, but no input returns `nil` (the value is memoised).
|
|
197
|
+
|
|
198
|
+
## How this was found
|
|
199
|
+
|
|
200
|
+
The signal that surfaced it — the Rigor command and the finding, or the documentation sweep.
|
|
201
|
+
|
|
202
|
+
## Verification
|
|
203
|
+
|
|
204
|
+
The witness for each change that says less: the call, and the runtime type checker's error under the old declaration (it passes under the new one). The declaration-mutation result (killed / survived) and every survivor, named as unchecked at runtime. The test added, and the `bin/test gems/NAME/X.Y` result for each version.
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Write each paragraph and list item on one line; GitHub renders a newline inside a paragraph as a line
|
|
208
|
+
break. "Deliberately left unchanged" lists every Rigor finding you did not act on and every `void`,
|
|
209
|
+
`boolish` or wide declaration kept over a body that returns more: it is the evidence that the restraint
|
|
210
|
+
was a decision.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# 05 — Declaration mutants: which declarations does any run actually check?
|
|
2
|
+
|
|
3
|
+
A runtime witness proves something only if the checker looked. RBS's runtime checker silently skips
|
|
4
|
+
some declarations: it checks a method only against a declaration on the same class or module that
|
|
5
|
+
defines the method in Ruby, so a declaration inherited from a module or superclass by a class that
|
|
6
|
+
defines or overrides the method is skipped (unless it carries `%a{rbs:test:target}`). In a field trial
|
|
7
|
+
on rgl 0.6.7, rgl's own test suite ran under `rbs/test/setup` with one expected failure while several
|
|
8
|
+
declarations of that kind were never checked. A single witness has the same problem: a clean exit means
|
|
9
|
+
nothing until a deliberately broken declaration makes the same run fail.
|
|
10
|
+
|
|
11
|
+
This is mutation testing aimed at the declarations instead of the code. Break one method return at a
|
|
12
|
+
time, re-run the witnesses, and see whether the run notices:
|
|
13
|
+
|
|
14
|
+
- **killed** — the run reports a type error, naming the mutated method or one of its RBS aliases, that
|
|
15
|
+
the clean run did not. The return is checked.
|
|
16
|
+
- **SURVIVED** — no such error. Nothing you ran checks this return: no call reaches it, or the checker
|
|
17
|
+
skips it. Treat it as unverified at runtime, whatever the suite reported.
|
|
18
|
+
- **ERROR** — the mutant does not load. Nothing was learned; fix the script or the declarations.
|
|
19
|
+
- **UNKNOWN** — the clean run already reports a return error on this method. Fix that first.
|
|
20
|
+
|
|
21
|
+
| Operator | Mutation | A survivor means |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `bot` | the return type becomes `bot`, which no value inhabits | no run checks this return at all |
|
|
24
|
+
| `drop-nil` | a `T?` return becomes `(T)` | no run reaches the `nil` arm, so the `?` has no witness |
|
|
25
|
+
|
|
26
|
+
`void` returns are skipped (the checker does not compare them, by design), and so is `drop-nil` on
|
|
27
|
+
`untyped?` (`untyped` already admits `nil`). Parameters, blocks and `attr_*` declarations are not mutated:
|
|
28
|
+
the pass says nothing about them.
|
|
29
|
+
|
|
30
|
+
A built-in command for this is proposed in #3072; until it exists, the script below is the procedure.
|
|
31
|
+
|
|
32
|
+
## Running it
|
|
33
|
+
|
|
34
|
+
The script needs only the `rbs` gem. Give it the directory of declarations under review and the
|
|
35
|
+
command that runs your witnesses — a witness script, the project's test suite, or a collection entry's
|
|
36
|
+
`_test/` — under `rbs/test/setup` with `RBS_TEST_TARGET` set. The script sets `RBS_TEST_OPT` to the
|
|
37
|
+
mutant directory itself; pass any further `-I` / `-r` options in `RBS_DECL_MUTANTS_EXTRA_OPT`. Save it as
|
|
38
|
+
`rbs_decl_mutants.rb` outside the project.
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
RBS_DECL_MUTANTS_EXTRA_OPT='-I ../deps-sig' ruby rbs_decl_mutants.rb sig -- ./run-witnesses.sh
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`run-witnesses.sh` is whatever runs your witnesses, for example:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
#!/bin/sh
|
|
48
|
+
RBS_TEST_TARGET='MyGem::*' ruby -I lib -I test -rrbs/test/setup -e 'Dir["test/*_test.rb"].each { |f| require_relative File.expand_path(f) }'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Each mutant is one full run, so the pass costs the command's runtime times the number of non-`void`
|
|
52
|
+
returns (rgl: 61 mutants, about 70 seconds). It runs the library's code, so the safety rules for a
|
|
53
|
+
witness apply: for a gem not already in the user's bundle, use a container or VM or ask first.
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
# frozen_string_literal: true
|
|
57
|
+
|
|
58
|
+
# Break each RBS method return in turn and check whether a witness command notices.
|
|
59
|
+
#
|
|
60
|
+
# ruby rbs_decl_mutants.rb SIG_DIR -- COMMAND...
|
|
61
|
+
#
|
|
62
|
+
# COMMAND runs the witnesses under rbs/test/setup (RBS_TEST_TARGET set) and must read declarations from
|
|
63
|
+
# $RBS_TEST_OPT, which this script sets to `-I <mutant dir>` plus RBS_DECL_MUTANTS_EXTRA_OPT.
|
|
64
|
+
# Operators: `bot` (nothing inhabits it: is this declaration checked at all?) and `drop-nil` (`T?` -> `T`:
|
|
65
|
+
# does any witness reach the nil arm?). A mutant is killed only by a type error that names the mutated
|
|
66
|
+
# method and that the clean run did not report; a mutant that does not load is an ERROR, never a survivor;
|
|
67
|
+
# a method whose return the clean run already fails on is UNKNOWN.
|
|
68
|
+
require "rbs"
|
|
69
|
+
require "rbs/cli"
|
|
70
|
+
require "optparse"
|
|
71
|
+
require "fileutils"
|
|
72
|
+
require "open3"
|
|
73
|
+
require "set"
|
|
74
|
+
require "shellwords"
|
|
75
|
+
require "tmpdir"
|
|
76
|
+
|
|
77
|
+
Encoding.default_external = Encoding::UTF_8
|
|
78
|
+
sig_dir = File.expand_path(ARGV.shift.to_s)
|
|
79
|
+
ARGV.shift if ARGV.first == "--"
|
|
80
|
+
command = ARGV
|
|
81
|
+
abort "usage: ruby rbs_decl_mutants.rb SIG_DIR -- COMMAND..." if !File.directory?(sig_dir) || command.empty?
|
|
82
|
+
extra = ENV.fetch("RBS_DECL_MUTANTS_EXTRA_OPT", "")
|
|
83
|
+
|
|
84
|
+
# `[Owner#name] KindError: ...` -> ["Owner#name", "KindError"]; the value after the colon is dropped
|
|
85
|
+
# because it carries object addresses and sampled elements that differ from run to run.
|
|
86
|
+
ERROR_TAG = /^.*?\[(.+?)\] (\w+Error):/
|
|
87
|
+
RETURN_KINDS = %w[ReturnTypeError UnresolvedOverloadingError].freeze
|
|
88
|
+
def type_errors(out) = out.each_line.filter_map { |l| (m = l.match(ERROR_TAG)) && [m[1], m[2]] }.to_set
|
|
89
|
+
|
|
90
|
+
def run(command, dir, extra)
|
|
91
|
+
out, = Open3.capture2e({ "RBS_TEST_OPT" => "-I #{dir} #{extra}".strip }, *command, stdin_data: "")
|
|
92
|
+
out
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Load the mutant exactly as rbs/test/setup parses RBS_TEST_OPT, so one that does not load is reported.
|
|
96
|
+
def loads?(dir, extra)
|
|
97
|
+
options = RBS::CLI::LibraryOptions.new
|
|
98
|
+
OptionParser.new { |o| options.setup_library_options(o) }.parse!(Shellwords.split("-I #{dir} #{extra}"))
|
|
99
|
+
env = RBS::Environment.from_loader(options.loader).resolve_type_names
|
|
100
|
+
RBS::DefinitionBuilder.new(env: env)
|
|
101
|
+
true
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
warn " load error: #{e.class}: #{e.message.lines.first}"
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
files = Dir[File.join(sig_dir, "**", "*.rbs")].reject { |f| f.split(File::SEPARATOR).include?("_test") }
|
|
108
|
+
sites = []
|
|
109
|
+
collect = lambda do |decl, ns, file, aliases = Hash.new { [] }|
|
|
110
|
+
case decl
|
|
111
|
+
when RBS::AST::Declarations::Class, RBS::AST::Declarations::Module
|
|
112
|
+
# The checker reports a call made through an alias under the alias's name, so a kill may carry it.
|
|
113
|
+
aliases = Hash.new { |h, k| h[k] = [] }
|
|
114
|
+
decl.members.grep(RBS::AST::Members::Alias).each do |a|
|
|
115
|
+
aliases[[a.kind, a.old_name]] << a.new_name
|
|
116
|
+
aliases[[a.kind, a.new_name]] << a.old_name
|
|
117
|
+
end
|
|
118
|
+
decl.members.each { |m| collect.call(m, ns + [decl.name.to_s.delete_prefix("::")], file, aliases) }
|
|
119
|
+
when RBS::AST::Members::MethodDefinition
|
|
120
|
+
sep = decl.kind == :singleton ? "." : "#"
|
|
121
|
+
names = [decl.name, *aliases[[decl.kind == :singleton ? :singleton : :instance, decl.name]]]
|
|
122
|
+
decl.overloads.each_with_index do |overload, index|
|
|
123
|
+
ret = overload.method_type.type.return_type
|
|
124
|
+
next if ret.location.nil? || ret.is_a?(RBS::Types::Bases::Void)
|
|
125
|
+
label = "#{ns.join('::')}#{sep}#{decl.name} (overload #{index}, #{ret})"
|
|
126
|
+
tag_suffix = names.map { |n| "#{sep}#{n}" }
|
|
127
|
+
sites << [file, ret.location, "bot", label, tag_suffix]
|
|
128
|
+
next unless ret.is_a?(RBS::Types::Optional) && !ret.type.is_a?(RBS::Types::Bases::Any)
|
|
129
|
+
sites << [file, ret.location, "(#{ret.type})", "#{label} drop-nil", tag_suffix]
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
files.each { |f| RBS::Parser.parse_signature(File.read(f, encoding: "UTF-8"))[2].each { |d| collect.call(d, [], f) } }
|
|
134
|
+
|
|
135
|
+
Dir.mktmpdir("rbs-decl-mutants") do |tmp|
|
|
136
|
+
clean = File.join(tmp, "clean")
|
|
137
|
+
FileUtils.cp_r(sig_dir, clean)
|
|
138
|
+
FileUtils.rm_rf(Dir[File.join(clean, "**", "_test")])
|
|
139
|
+
abort "the clean declarations do not load" unless loads?(clean, extra)
|
|
140
|
+
baseline = type_errors(run(command, clean, extra))
|
|
141
|
+
warn "clean run reports #{baseline.size} type error(s); those tags are ignored" unless baseline.empty?
|
|
142
|
+
|
|
143
|
+
tally = Hash.new(0)
|
|
144
|
+
sites.each do |file, loc, replacement, label, tag_suffix|
|
|
145
|
+
dir = File.join(tmp, "mutant")
|
|
146
|
+
FileUtils.rm_rf(dir)
|
|
147
|
+
FileUtils.cp_r(clean, dir)
|
|
148
|
+
target = File.join(dir, file.delete_prefix(sig_dir).delete_prefix(File::SEPARATOR))
|
|
149
|
+
src = File.read(file, encoding: "UTF-8") # RBS locations are character offsets
|
|
150
|
+
File.write(target, src[0...loc.start_pos] + replacement + src[loc.end_pos..])
|
|
151
|
+
verdict =
|
|
152
|
+
if baseline.any? { |tag, kind| RETURN_KINDS.include?(kind) && tag.end_with?(*tag_suffix) }
|
|
153
|
+
"UNKNOWN" # the clean run already fails on this method; fix that first
|
|
154
|
+
elsif !loads?(dir, extra)
|
|
155
|
+
"ERROR"
|
|
156
|
+
else
|
|
157
|
+
fresh = type_errors(run(command, dir, extra)) - baseline
|
|
158
|
+
fresh.any? { |tag, _kind| tag.end_with?(*tag_suffix) } ? "killed" : "SURVIVED"
|
|
159
|
+
end
|
|
160
|
+
tally[verdict] += 1
|
|
161
|
+
puts "#{verdict.ljust(8)} #{label}"
|
|
162
|
+
end
|
|
163
|
+
puts "#{sites.size} mutants: #{tally['killed']} killed, #{tally['SURVIVED']} survived, " \
|
|
164
|
+
"#{tally['ERROR']} did not load, #{tally['UNKNOWN']} already failing in the clean run"
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Reading the result
|
|
169
|
+
|
|
170
|
+
- Report every survivor in the write-up as "unchecked at runtime", with the operator. It is not a
|
|
171
|
+
defect in the declaration; it is the limit of what your evidence covers.
|
|
172
|
+
- A `bot` survivor on a method your witnesses call is the checker's skip rule at work. Redeclare the
|
|
173
|
+
member on the class that defines the method (where that is also a correct declaration), add
|
|
174
|
+
`%a{rbs:test:target}` to the inherited declaration, or rely on documentation for it and say so.
|
|
175
|
+
- A `bot` survivor on a method nothing calls is a missing witness: add a call, or say so.
|
|
176
|
+
- A `drop-nil` survivor means the `?` rests on documentation alone. If the documentation does not
|
|
177
|
+
mention `nil` either, the `?` has no evidence: re-examine it.
|
|
178
|
+
- A killed mutant on a "say less" change is the negative control that change needs.
|
|
179
|
+
|
|
180
|
+
On the rgl draft the pass reported 61 mutants: 56 killed, 5 survived. Three survivors were the skip
|
|
181
|
+
rule (`Graph#directed?`, `#has_vertex?`, `#has_edge?`, declared on the module `Graph` and overridden in
|
|
182
|
+
classes); `Graph#to_s` was never called; and the `?` on `Edge::DirectedEdge#<=>` had no witness.
|
|
@@ -82,7 +82,9 @@ rbs --version # or: bundle exec rbs --version
|
|
|
82
82
|
If it is missing, add it to the project's development group
|
|
83
83
|
(`bundle add rbs --group development`) or install it standalone
|
|
84
84
|
(`gem install rbs`). Prefer `bundle exec rbs …` when `rbs` is in the
|
|
85
|
-
Gemfile so it matches the project's resolved versions.
|
|
85
|
+
Gemfile so it matches the project's resolved versions. If `bundle exec rbs`
|
|
86
|
+
runs an `rbs` that a mise-installed `gem:rigortype` put on `PATH`, see the
|
|
87
|
+
[install guide's note on `rbs` shadowing](https://github.com/rigortype/rigor/blob/master/docs/install.md#case-a--mise-is-available-recommended).
|
|
86
88
|
|
|
87
89
|
### Phase 3 — initialise the collection config (first time only)
|
|
88
90
|
|
|
@@ -69,6 +69,9 @@ scope the moment you are about to write or assert one:
|
|
|
69
69
|
- **Working an existing `.rigor-baseline.yml` down** →
|
|
70
70
|
`rigor-baseline-reduce`. That is fixing diagnostics, not sourcing a
|
|
71
71
|
type.
|
|
72
|
+
- **Auditing, repairing, or contributing RBS declarations** — a gem's
|
|
73
|
+
`sig/`, an application's `sig/`, a gem_rbs_collection entry →
|
|
74
|
+
`rigor-rbs-contract`. This skill still governs every type it writes.
|
|
72
75
|
- **Raising type protection from a `coverage --protection` list** →
|
|
73
76
|
`rigor-protection-uplift`. It owns the "where should a type go, and how
|
|
74
77
|
do I verify it" loop; this skill owns "where does the type itself come
|
|
@@ -76,7 +79,9 @@ scope the moment you are about to write or assert one:
|
|
|
76
79
|
|
|
77
80
|
## The three allowed sources
|
|
78
81
|
|
|
79
|
-
Every type you write comes from exactly one of these. There is no fourth
|
|
82
|
+
Every type you write comes from exactly one of these. There is no fourth
|
|
83
|
+
source for a type itself; what a *declaration* promises is the separate
|
|
84
|
+
contract question below.
|
|
80
85
|
|
|
81
86
|
| What you need | Ask Rigor with | What you write |
|
|
82
87
|
| --- | --- | --- |
|
|
@@ -96,6 +101,26 @@ adopting, under the gate that `rigor check` gains no new diagnostic.
|
|
|
96
101
|
Exact command forms, flags, position syntax, JSON shapes, and how to read
|
|
97
102
|
each output: [`references/01-oracle-commands.md`](references/01-oracle-commands.md).
|
|
98
103
|
|
|
104
|
+
## A declaration is a contract, not a missing type
|
|
105
|
+
|
|
106
|
+
None of the three sources says what a declaration *promises* to callers.
|
|
107
|
+
Ruby returns every method's last expression, so a body almost always has
|
|
108
|
+
a value; a declared `void` says callers must not depend on it, and
|
|
109
|
+
`boolish` says only its truthiness counts. **Never tighten `void`,
|
|
110
|
+
`boolish`, or a declared type deliberately wider than the body (anything
|
|
111
|
+
but `untyped`) to what the body happens to return** — in `sig/`, in an
|
|
112
|
+
inline annotation, or in RBS sent upstream — unless the library documents
|
|
113
|
+
that value or, for the project's own code, the owner decides. A
|
|
114
|
+
`sig-gen` proposal over `untyped` is not this case; it is the second
|
|
115
|
+
source above.
|
|
116
|
+
|
|
117
|
+
The reverse also holds. A contract the body does not prove — `String?`
|
|
118
|
+
because the method's documentation says it returns `nil` when a job is
|
|
119
|
+
halted — is not a guess when it is cited: the library's documentation,
|
|
120
|
+
quoted, for code you do not own; the owner's explicit answer for code
|
|
121
|
+
they do. Auditing, repairing, or contributing a set of RBS declarations
|
|
122
|
+
on that basis is **`rigor-rbs-contract`**.
|
|
123
|
+
|
|
99
124
|
## The gap protocol — a gap is a finding, not a blank to fill
|
|
100
125
|
|
|
101
126
|
`Dynamic[top]`, `untyped`, and a `sig.skipped.*` classification are
|
|
@@ -17,7 +17,7 @@ the project is already onboarded, add it now.
|
|
|
17
17
|
only if it is not already pulled in.
|
|
18
18
|
- **Neither exists** → create `AGENTS.md` with this as its first section.
|
|
19
19
|
- **A "Types" / "Type checking" section already exists** → merge into it
|
|
20
|
-
rather than adding a second one, and keep the
|
|
20
|
+
rather than adding a second one, and keep the six bullets intact.
|
|
21
21
|
|
|
22
22
|
Never overwrite unrelated content, and never rewrite a rule the project
|
|
23
23
|
already wrote for itself — show the user the conflict instead.
|
|
@@ -27,13 +27,14 @@ already wrote for itself — show the user the conflict instead.
|
|
|
27
27
|
```markdown
|
|
28
28
|
## Types come from Rigor, not from reading code
|
|
29
29
|
|
|
30
|
-
This project is type-checked by [Rigor](https://github.com/rigortype/rigor). A type you did not obtain from Rigor is a guess, and a guessed type is never written anywhere: not in `sig/`, not in an inline annotation (`#:`, `# @rbs`), not in a doc comment, not in a review comment, and not as the reason for a nil check or an `is_a?` guard.
|
|
30
|
+
This project is type-checked by [Rigor](https://github.com/rigortype/rigor). A type you did not obtain from Rigor is a guess, and a guessed type is never written anywhere: not in `sig/`, not in an inline annotation (`#:`, `# @rbs`), not in a doc comment, not in a review comment, and not as the reason for a nil check or an `is_a?` guard. The one other source is a contract: what a declaration promises callers may rest on the library's documentation, quoted, or for this project's own code on the owner's explicit decision.
|
|
31
31
|
|
|
32
32
|
- The type of an expression: `rigor type-of FILE:LINE:COL`, or `rigor annotate FILE` for a whole file.
|
|
33
33
|
- The signature of a method: `rigor sig-gen --print FILE`; paste what it prints, never what you expect.
|
|
34
34
|
- A parameter type is the one thing inference does not give you: derive it from the call sites with `rigor sig-gen --observe PATH` and keep it only while `rigor check` stays green.
|
|
35
|
-
- When Rigor answers `Dynamic[top]` or `untyped`, or `sig-gen` skips the method, do not fill the gap. Report the exact command and its output; the gap is the finding.
|
|
35
|
+
- When Rigor answers `Dynamic[top]` or `untyped`, or `sig-gen` skips the method, do not fill the gap from your reading. Report the exact command and its output; the gap is the finding. Only a contract source, cited as above, fills it.
|
|
36
36
|
- Every type you state to a human carries the command that produced it, so it can be re-run.
|
|
37
|
+
- A declared `void` or `boolish` return, or a declared type deliberately wider than the body (anything but `untyped`), is a contract, not a missing type: never tighten it to what the body happens to return unless the library documents that value or, for this project's own code, the owner decides. Auditing or repairing RBS declarations is the `rigor-rbs-contract` skill.
|
|
37
38
|
|
|
38
39
|
The `rigor-type-oracle` skill (`rigor skill --full rigor-type-oracle`) has the full procedure. With the Rigor MCP server connected, `rigor_type_of`, `rigor_annotate`, and `rigor_sig_gen` are the same oracle as tool calls.
|
|
39
40
|
```
|