rigortype 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/core_overlay/enumerable.rbs +51 -0
- data/data/core_overlay/enumerator.rbs +84 -0
- data/data/core_overlay/hash_rbs3.rbs +41 -0
- data/data/core_overlay/process.rbs +40 -0
- data/data/core_overlay/string_io.rbs +33 -0
- data/data/effects/core.yml +3 -3
- data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
- data/docs/handbook/03-narrowing.md +95 -10
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/07-rbs-and-extended.md +16 -11
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +454 -13
- data/docs/manual/02-cli-reference.md +63 -2
- data/docs/manual/03-configuration.md +7 -0
- data/docs/manual/04-diagnostics.md +4 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/16-rbs-extended-annotations.md +40 -7
- data/docs/manual/19-effect-labels.md +10 -0
- data/docs/manual/plugins/README.md +7 -0
- data/docs/manual/plugins/rigor-actioncable.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +7 -0
- data/docs/manual/plugins/rigor-actionpack.md +243 -0
- data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
- data/docs/manual/plugins/rigor-activejob.md +7 -0
- data/docs/manual/plugins/rigor-activerecord.md +110 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
- data/docs/manual/plugins/rigor-grape.md +106 -0
- data/docs/manual/plugins/rigor-graphql.md +23 -2
- data/docs/manual/plugins/rigor-pundit.md +8 -1
- data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
- data/docs/manual/plugins/rigor-rails-routes.md +7 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
- data/docs/manual/plugins/rigor-sidekiq.md +8 -1
- data/docs/manual/plugins/rigor-sorbet.md +21 -5
- data/exe/rigor +19 -4
- data/lib/rigor/analysis/baseline.rb +1 -1
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
- data/lib/rigor/analysis/check_rules.rb +520 -49
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/fact_store.rb +9 -0
- data/lib/rigor/analysis/incremental.rb +26 -0
- data/lib/rigor/analysis/incremental_session.rb +36 -4
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
- data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
- data/lib/rigor/analysis/rule_catalog.rb +127 -0
- data/lib/rigor/analysis/run_cache_key.rb +20 -11
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
- data/lib/rigor/analysis/runner.rb +278 -20
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +91 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +399 -0
- data/lib/rigor/analysis/worker_session.rb +53 -10
- data/lib/rigor/bleeding_edge.rb +0 -2
- data/lib/rigor/builtins/hkt_builtins.rb +1 -0
- data/lib/rigor/builtins/imported_refinements.rb +4 -0
- data/lib/rigor/builtins/regex_refinement.rb +17 -9
- data/lib/rigor/builtins/static_return_refinements.rb +2 -0
- data/lib/rigor/cache/descriptor.rb +6 -1
- data/lib/rigor/cache/engine_source.rb +29 -1
- data/lib/rigor/cache/incremental_snapshot.rb +33 -1
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_descriptor.rb +37 -7
- data/lib/rigor/cache/store.rb +0 -1
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/doc_links.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +4 -4
- data/lib/rigor/cli/plugin_command.rb +3 -3
- data/lib/rigor/cli/prism_colorizer.rb +0 -1
- data/lib/rigor/cli/sig_gen_command.rb +210 -29
- data/lib/rigor/cli/skill_command.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +0 -1
- data/lib/rigor/cli/type_of_command.rb +19 -5
- data/lib/rigor/cli/type_of_renderer.rb +20 -9
- data/lib/rigor/cli/type_of_template_probe.rb +189 -0
- data/lib/rigor/cli.rb +21 -3
- data/lib/rigor/configuration/severity_profile.rb +19 -3
- data/lib/rigor/configuration.rb +66 -3
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +11 -2
- data/lib/rigor/effects/callee_rule.rb +368 -0
- data/lib/rigor/effects/catalog.rb +7 -4
- data/lib/rigor/effects/collector.rb +11 -5
- data/lib/rigor/effects/config_envelopes.rb +9 -2
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/effect_table.rb +11 -3
- data/lib/rigor/effects/envelope_check.rb +1 -1
- data/lib/rigor/effects/envelope_index.rb +15 -0
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/identity.rb +16 -0
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +21 -0
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +43 -31
- data/lib/rigor/effects/propagator.rb +295 -16
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +121 -72
- data/lib/rigor/effects/signature_sources.rb +1 -1
- data/lib/rigor/effects/snapshot.rb +2 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +385 -30
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/lockfile_resolver.rb +17 -0
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/rbs_loader.rb +399 -133
- data/lib/rigor/environment.rb +103 -38
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +139 -9
- data/lib/rigor/inference/block_auto_splat.rb +216 -0
- data/lib/rigor/inference/block_call_timing.rb +338 -0
- data/lib/rigor/inference/block_parameter_binder.rb +73 -27
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +2 -1
- data/lib/rigor/inference/budget_trace.rb +2 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +387 -15
- data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/def_return_typer.rb +11 -7
- data/lib/rigor/inference/define_method_block_self.rb +64 -0
- data/lib/rigor/inference/element_read_widening.rb +22 -10
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +1532 -496
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
- data/lib/rigor/inference/global_write_census.rb +239 -0
- data/lib/rigor/inference/guard_rebinding.rb +447 -0
- data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
- data/lib/rigor/inference/index_write_widening.rb +16 -3
- data/lib/rigor/inference/indexed_narrowing.rb +61 -9
- data/lib/rigor/inference/jump_targets.rb +82 -0
- data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
- data/lib/rigor/inference/last_line.rb +340 -0
- data/lib/rigor/inference/last_status.rb +144 -0
- data/lib/rigor/inference/macro_block_self_type.rb +167 -17
- data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
- data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
- data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
- data/lib/rigor/inference/match_rebinding.rb +392 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
- data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +214 -13
- data/lib/rigor/inference/method_parameter_binder.rb +8 -3
- data/lib/rigor/inference/multi_target_binder.rb +340 -52
- data/lib/rigor/inference/mutation_rejoin.rb +4 -1
- data/lib/rigor/inference/mutation_widening.rb +70 -48
- data/lib/rigor/inference/narrowing.rb +540 -120
- data/lib/rigor/inference/operand_effects.rb +167 -0
- data/lib/rigor/inference/operand_walk.rb +88 -0
- data/lib/rigor/inference/optimistic_origin.rb +152 -9
- data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +7 -2
- data/lib/rigor/inference/project_patched_scanner.rb +7 -3
- data/lib/rigor/inference/receiver_alias.rb +90 -1
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +15 -11
- data/lib/rigor/inference/repeated_or_writes.rb +463 -0
- data/lib/rigor/inference/return_barrier.rb +54 -0
- data/lib/rigor/inference/rewrite_mutation.rb +120 -0
- data/lib/rigor/inference/scope_indexer.rb +4615 -551
- data/lib/rigor/inference/statement_evaluator.rb +3176 -490
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +44 -7
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
- data/lib/rigor/language_server/hover_renderer.rb +3 -3
- data/lib/rigor/language_server/project_context.rb +5 -3
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +168 -5
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +1 -1
- data/lib/rigor/plugin/effect_attribution.rb +58 -4
- data/lib/rigor/plugin/loader.rb +2 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/manifest.rb +71 -10
- data/lib/rigor/plugin/registry.rb +35 -1
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/discovery_seed.rb +3 -1
- data/lib/rigor/protection/kill_signature.rb +0 -1
- data/lib/rigor/protection/mutation_cache.rb +1 -2
- data/lib/rigor/rbs_extended.rb +27 -0
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +19 -6
- data/lib/rigor/reflection.rb +72 -105
- data/lib/rigor/scope/discovery_index.rb +135 -2
- data/lib/rigor/scope.rb +859 -49
- data/lib/rigor/sig_gen/alias_index.rb +289 -0
- data/lib/rigor/sig_gen/classification.rb +22 -5
- data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
- data/lib/rigor/sig_gen/generator.rb +558 -31
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +124 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +166 -35
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +1 -1
- data/lib/rigor/source/node_walker.rb +0 -3
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/combinator.rb +88 -12
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/hash_shape.rb +1 -1
- data/lib/rigor/type/refined.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
- data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
- data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
- data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
- data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
- data/plugins/rigor-grape/sig/grape.rbs +263 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
- data/sig/rigor/analysis/baseline.rbs +69 -7
- data/sig/rigor/analysis/fact_store.rbs +1 -1
- data/sig/rigor/analysis/project_scan.rbs +74 -0
- data/sig/rigor/effects/config_envelopes.rbs +100 -0
- data/sig/rigor/effects/effect_table.rbs +60 -0
- data/sig/rigor/effects/envelope.rbs +97 -0
- data/sig/rigor/effects/envelope_index.rbs +33 -0
- data/sig/rigor/effects/file_collection.rbs +81 -0
- data/sig/rigor/effects/label.rbs +26 -0
- data/sig/rigor/effects/label_set.rbs +46 -0
- data/sig/rigor/effects/method_key.rbs +24 -0
- data/sig/rigor/effects/origin.rbs +50 -0
- data/sig/rigor/effects/plugin_facts.rbs +141 -0
- data/sig/rigor/effects/registry.rbs +68 -0
- data/sig/rigor/effects/summary.rbs +50 -0
- data/sig/rigor/effects/taint_cause.rbs +12 -0
- data/sig/rigor/environment.rbs +12 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +6 -4
- data/sig/rigor/plugin/additional_initializer.rbs +36 -0
- data/sig/rigor/plugin/base.rbs +30 -7
- data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
- data/sig/rigor/plugin/effect_attribution.rbs +66 -0
- data/sig/rigor/plugin/effect_edge.rbs +33 -0
- data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
- data/sig/rigor/plugin/io_boundary.rbs +1 -1
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +50 -11
- data/sig/rigor/plugin/protocol_contract.rbs +68 -0
- data/sig/rigor/plugin/registry.rbs +62 -1
- data/sig/rigor/plugin.rbs +1 -1
- data/sig/rigor/rbs_extended.rbs +1 -1
- data/sig/rigor/reflection.rbs +7 -6
- data/sig/rigor/scope.rbs +135 -20
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +6 -0
- data/sig/rigor.rbs +38 -20
- data/skills/rigor-ask/SKILL.md +8 -5
- data/skills/rigor-baseline-reduce/SKILL.md +4 -6
- data/skills/rigor-ci-setup/SKILL.md +17 -21
- data/skills/rigor-doctor/SKILL.md +24 -22
- data/skills/rigor-doctor/references/01-checks.md +97 -33
- data/skills/rigor-editor-setup/SKILL.md +6 -4
- data/skills/rigor-mcp-setup/SKILL.md +5 -4
- data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
- data/skills/rigor-next-steps/SKILL.md +4 -2
- data/skills/rigor-plugin-author/SKILL.md +19 -23
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
- data/skills/rigor-plugin-review/SKILL.md +6 -4
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
- data/skills/rigor-plugin-tune/SKILL.md +4 -2
- data/skills/rigor-project-init/SKILL.md +9 -7
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +33 -8
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
- data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
- data/skills/rigor-protection-uplift/SKILL.md +4 -6
- data/skills/rigor-rbs-setup/SKILL.md +4 -2
- data/skills/rigor-type-oracle/SKILL.md +4 -6
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
- data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +108 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
|
|
5
5
|
require_relative "../version"
|
|
6
|
+
require_relative "descriptor"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Cache
|
|
@@ -63,6 +64,9 @@ module Rigor
|
|
|
63
64
|
# to whatever else happened to be there, which is exactly the weakening this module forbids.
|
|
64
65
|
REQUIRED_DIRECTORY = "lib"
|
|
65
66
|
|
|
67
|
+
# The config-row key every computed-value slot carries the identity under.
|
|
68
|
+
CONFIG_KEY = "engine-source"
|
|
69
|
+
|
|
66
70
|
module_function
|
|
67
71
|
|
|
68
72
|
# @return the gem root — the directory holding `lib/`, three levels above this file.
|
|
@@ -100,10 +104,34 @@ module Rigor
|
|
|
100
104
|
# exists at all. The same reading covers the long-running `rigor lsp` process.
|
|
101
105
|
#
|
|
102
106
|
# {Unavailable} propagates and is deliberately NOT memoised: the ivar is only assigned on success.
|
|
107
|
+
#
|
|
108
|
+
# The memo is FROZEN because a Ractor-pool worker reads it (#1055): the auto-wired ADR-93
|
|
109
|
+
# `rigor-rbs-inline` synthesizer reaches `.key_config_entries` from inside
|
|
110
|
+
# `Environment.for_project`, which is inside `WorkerSession#initialize`, which is inside the worker's
|
|
111
|
+
# own Ractor — and a non-main Ractor may read a class/module ivar only when its value is shareable.
|
|
112
|
+
# An unfrozen digest String therefore killed every worker in its constructor on any run with the
|
|
113
|
+
# synthesizer on, which is every real CLI run (the spec suite pins it off). The worker never WRITES
|
|
114
|
+
# it: `PoolCoordinator#analyze_files_in_pool` pre-warms this on the main Ractor first, which is where
|
|
115
|
+
# the argument above already says the value belongs.
|
|
103
116
|
def process_identity
|
|
104
117
|
return @process_identity if defined?(@process_identity)
|
|
105
118
|
|
|
106
|
-
@process_identity = identity
|
|
119
|
+
@process_identity = identity.freeze
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The `engine-source` key row for a cache whose value this engine computed, built from
|
|
123
|
+
# {.process_identity}: empty for a version-pinned tree, so a released gem's keys stay byte-identical.
|
|
124
|
+
#
|
|
125
|
+
# Issue #1009 — a slot keyed only on its inputs and a plugin's manifest version outlives the engine
|
|
126
|
+
# that filled it. The per-file synthesizer slot and every plugin producer were keyed that way, so after
|
|
127
|
+
# an engine or bundled-plugin edit the run-result key moved and the run re-analysed, but through the
|
|
128
|
+
# previous build's synthesized RBS and producer values. {Unavailable} propagates, so a caller that
|
|
129
|
+
# cannot identify the engine bypasses its cache instead of falling back to the weaker key.
|
|
130
|
+
def key_config_entries
|
|
131
|
+
identity = process_identity
|
|
132
|
+
return [] if identity.nil?
|
|
133
|
+
|
|
134
|
+
[Descriptor::ConfigEntry.new(key: CONFIG_KEY, value_hash: Digest::SHA256.hexdigest(identity))]
|
|
107
135
|
end
|
|
108
136
|
|
|
109
137
|
# Discards the {.process_identity} memo; production code MUST NOT call this — a run that recomputed
|
|
@@ -106,7 +106,39 @@ module Rigor
|
|
|
106
106
|
# blob has no such section and would deserialise it as nil, which reads as "nothing cached" and is
|
|
107
107
|
# therefore not misread — but a snapshot written by an engine that did not yet REPLAY them was also
|
|
108
108
|
# written by one whose narrowed runs under-reported, so the gate rejects it and the next run is cold.
|
|
109
|
-
|
|
109
|
+
# 23: issue #992 gives each seed bundle a `parameter_envelopes` table — the joined positional envelope
|
|
110
|
+
# of every method a file records, plus its module / dynamic-surface marks — which `call.wrong-arity`
|
|
111
|
+
# reads for a method no signature declares. A pre-23 bundle folds as "no envelopes", which only ever
|
|
112
|
+
# withholds a check, but a warm run that withholds where a cold run fires is still the
|
|
113
|
+
# `--verify-incremental` divergence 14 was bumped for.
|
|
114
|
+
# 24: issue #1097 gives each seed bundle a `deferred_ranges` table — the per-file def / block /
|
|
115
|
+
# lambda body ranges `Scope#*_def_shadows_call?` orders a project-defined `sig` override
|
|
116
|
+
# against. A pre-24 bundle would fold as "no ranges", which the predicate reads as "cannot
|
|
117
|
+
# order → shadow" — a warm run declining the binding where a cold run binds.
|
|
118
|
+
# 25: issue #1097 extends each `deferred_ranges` row to `[start, end, name, kind, owner]` —
|
|
119
|
+
# owner-scoped ordering. A 24 bundle folds as "owner = nil", which the predicate reads as
|
|
120
|
+
# "cannot order", under-shadowing `module_function` defs whose only table evidence is the row
|
|
121
|
+
# itself.
|
|
122
|
+
# 26: issue #1123 gives each seed bundle a `prepends` table — the instance-ancestor ORDER of a
|
|
123
|
+
# class's `prepend`ed modules, which `Scope#user_def_through_ancestors` searches ahead of the
|
|
124
|
+
# class's own `def`s. A pre-26 bundle would fold as "prepends nothing": the warm run would answer
|
|
125
|
+
# the pre-fix MRO where a cold run answers the prepended module, exactly the `--verify-incremental`
|
|
126
|
+
# divergence 14 was bumped for (and a silently WRONG dispatch answer, not a withheld check).
|
|
127
|
+
# 27: issue #1173 re-MEANs each seed bundle's `includes` VALUES without changing their shape: the
|
|
128
|
+
# lists now store instance-ancestor SEARCH order (nearest first, prepends ahead of includes)
|
|
129
|
+
# instead of call order — the same flip 17 made for def-row `nesting`. A pre-27 blob deserialises
|
|
130
|
+
# cleanly and would serve the old call order on a warm run while a cold run searches nearer-first:
|
|
131
|
+
# `include A; include B` would resolve A's methods where a cold run resolves B's — the
|
|
132
|
+
# `--verify-incremental` divergence 14 was bumped for.
|
|
133
|
+
# 28: issue #1120 gives each seed bundle a `refinements` table — the `def`s of every `refine X do … end`
|
|
134
|
+
# body, which `call.undefined-method` reads where a `using` of the refining module is in effect. A
|
|
135
|
+
# pre-28 bundle would fold as "refines nothing", and a warm run would report a refined call a cold run
|
|
136
|
+
# declines.
|
|
137
|
+
# 29: issue #1367 gives each seed bundle a `global_write_census` — the globals its `alias` statements name and
|
|
138
|
+
# the `write` / `to_str` / `to_int` / hatch definitions and top-level mixins it holds, which the `global.*`
|
|
139
|
+
# write rules decline on project-wide. A pre-29 bundle would fold as "none of those", and a warm run would
|
|
140
|
+
# report a write a cold run exempts.
|
|
141
|
+
SCHEMA = 29
|
|
110
142
|
|
|
111
143
|
# The persisted per-file state.
|
|
112
144
|
# `cache` maps an analyzed file to its diagnostics.
|
|
@@ -29,6 +29,10 @@ module Rigor
|
|
|
29
29
|
# files, same libraries), so the loader memoises one build per process instead of re-digesting every
|
|
30
30
|
# .rbs file once per producer.
|
|
31
31
|
descriptor = loader.rbs_cache_descriptor
|
|
32
|
+
# Issue #1014 — nil is an engine the descriptor could not identify ({EngineSource::Unavailable}).
|
|
33
|
+
# Compute uncached rather than keying the value without the engine that produced it.
|
|
34
|
+
return compute(loader) if descriptor.nil?
|
|
35
|
+
|
|
32
36
|
store.fetch_or_compute(producer_id: self::PRODUCER_ID, params: {}, descriptor: descriptor,
|
|
33
37
|
generation_cap: generation_cap) do
|
|
34
38
|
compute(loader)
|
|
@@ -3,20 +3,47 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
|
|
5
5
|
require_relative "descriptor"
|
|
6
|
+
require_relative "engine_source"
|
|
6
7
|
require_relative "file_digest"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Cache
|
|
10
11
|
# Shared descriptor builder for cache producers that depend on the RBS environment (constant table,
|
|
11
|
-
# known-class set, future Marshal-clean reflection artefacts). Every consumer attaches the same
|
|
12
|
-
#
|
|
13
|
-
#
|
|
12
|
+
# known-class set, future Marshal-clean reflection artefacts). Every consumer attaches the same slots —
|
|
13
|
+
# the `rbs` gem row, a digest row per signature file, and the `rbs.libraries` / `rbs.virtual_rbs` /
|
|
14
|
+
# `rbs.deferred_signature_paths` / `engine-source` config rows — so factoring the construction here keeps
|
|
15
|
+
# the producers small and ensures invalidation behaves identically across them.
|
|
14
16
|
module RbsDescriptor
|
|
17
|
+
# The key every `rbs.*` producer shares ({RbsCacheProducer#fetch}): `rbs.environment`,
|
|
18
|
+
# `rbs.constant_type_table`, `rbs.class_ancestor_table`, `rbs.known_class_names`,
|
|
19
|
+
# `rbs.class_type_param_names`.
|
|
20
|
+
#
|
|
21
|
+
# Issue #1014 — the engine-source row is here, not in {.config_entries}, because these are
|
|
22
|
+
# computed-value slots: what they hold is not the RBS the descriptor digests but what Rigor's own code
|
|
23
|
+
# made of it. `rbs.constant_type_table` stores `Inference::RbsTypeTranslator.translate` output and
|
|
24
|
+
# `rbs.class_ancestor_table` the chains `RbsClassAncestorTable.compute` walks out of
|
|
25
|
+
# `instance_definition`, so an engine edit to either moved the value while the key stood still: the
|
|
26
|
+
# run-result key moved (#285) and the run re-analysed, then read the previous build's table. Both were
|
|
27
|
+
# reproduced cross-build on a fixture — a true positive dropped, and a `flow.unreachable-clause` false
|
|
28
|
+
# positive served — before this row was added.
|
|
29
|
+
#
|
|
30
|
+
# {.config_entries} deliberately does NOT get it: that half is what the ADR-45 run cache key reads
|
|
31
|
+
# ({.build_run}), and `RunCacheKey` already contributes the identical row from
|
|
32
|
+
# `EngineSource.key_config_entries` itself. Adding it there would duplicate the row in the run key and
|
|
33
|
+
# would have to be reconstructible by the boot-slimming probe, which builds `config_entries` without a
|
|
34
|
+
# loader.
|
|
35
|
+
#
|
|
36
|
+
# @raise EngineSource::Unavailable — an engine that cannot be identified must not be keyed by its
|
|
37
|
+
# inputs alone; {RbsCacheProducer.fetch} turns this into an uncached compute.
|
|
15
38
|
def self.build(loader)
|
|
39
|
+
# FIRST, before {.file_entries} digests the whole signature tree: the {EngineSource::Unavailable}
|
|
40
|
+
# path discards this descriptor entirely, and a SHA-256 of every `.rbs` under every signature root is
|
|
41
|
+
# the expensive way to reach that conclusion.
|
|
42
|
+
engine = EngineSource.key_config_entries
|
|
16
43
|
Descriptor.new(
|
|
17
44
|
gems: [rbs_gem_entry],
|
|
18
45
|
files: file_entries(loader),
|
|
19
|
-
configs: config_entries(loader) + env_only_config_entries(loader)
|
|
46
|
+
configs: config_entries(loader) + env_only_config_entries(loader) + engine
|
|
20
47
|
)
|
|
21
48
|
end
|
|
22
49
|
|
|
@@ -25,8 +52,10 @@ module Rigor
|
|
|
25
52
|
# MISS, by the dependency descriptor ({Runner#run_dependency_descriptor}). So a warm HIT never digests
|
|
26
53
|
# the (large, vendored) RBS tree. {RunDescriptor} is NOT a {Descriptor} — it is never composed, hashed,
|
|
27
54
|
# or `==`'d, only its four readers are consulted — so deferring `files` costs no soundness, and `gems`
|
|
28
|
-
# / `configs` match {.build}'s shared slots (the key is unchanged; the
|
|
29
|
-
#
|
|
55
|
+
# / `configs` match {.build}'s shared slots (the key is unchanged; the slots the env key adds on top,
|
|
56
|
+
# {.env_only_config_entries} and the #1014 `engine-source` row, are never read here — `RunCacheKey`
|
|
57
|
+
# contributes the identical engine-source row itself, from the same
|
|
58
|
+
# `EngineSource.key_config_entries`).
|
|
30
59
|
def self.build_run(loader)
|
|
31
60
|
RunDescriptor.new(loader: loader, gems: [rbs_gem_entry], configs: config_entries(loader))
|
|
32
61
|
end
|
|
@@ -112,7 +141,8 @@ module Rigor
|
|
|
112
141
|
# overlays. Rigor's own `data/` trees ({RbsLoader.vendored_gem_sig_paths}, `.core_overlay_sig_paths`,
|
|
113
142
|
# `.capability_role_sig_paths`) are engine-owned: a file appears under one only when the engine tree is
|
|
114
143
|
# edited, which normally arrives with the `lib/` change that reads it and so re-keys every
|
|
115
|
-
# computed-value slot through {EngineSource}
|
|
144
|
+
# computed-value slot through {EngineSource} — including, since #1014, this descriptor itself, which
|
|
145
|
+
# until then carried no such row. They get file rows and no glob — a glob there would cost
|
|
116
146
|
# every project a stat walk of the vendored gem-signature tree per warm run to catch an
|
|
117
147
|
# engine-development edit.
|
|
118
148
|
def self.glob_entries(loader)
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -65,7 +65,6 @@ module Rigor
|
|
|
65
65
|
|
|
66
66
|
VALID_PRODUCER_ID = /\A[a-z][a-z0-9._-]*\z/
|
|
67
67
|
|
|
68
|
-
# @param root — cache root directory.
|
|
69
68
|
# @param read_only — when true, every disk-side side-effect is suppressed: `fetch_or_compute`
|
|
70
69
|
# still reads existing entries (hits, gated on a current `schema_version.txt` marker — see
|
|
71
70
|
# {#ensure_schema_version!}) and still runs the producer block on miss, but it does NOT write the
|
data/lib/rigor/ci_detector.rb
CHANGED
data/lib/rigor/cli/doc_links.rb
CHANGED
|
@@ -31,7 +31,7 @@ module Rigor
|
|
|
31
31
|
private_constant :MARKDOWN_LINK
|
|
32
32
|
|
|
33
33
|
# Where the gem's `docs/` tree sits, so a rendered path can be made repository-relative.
|
|
34
|
-
GEM_ROOT = File.expand_path("../../..", __dir__)
|
|
34
|
+
GEM_ROOT = File.expand_path("../../..", __dir__).freeze
|
|
35
35
|
private_constant :GEM_ROOT
|
|
36
36
|
|
|
37
37
|
module_function
|
|
@@ -47,10 +47,10 @@ module Rigor
|
|
|
47
47
|
USAGE
|
|
48
48
|
|
|
49
49
|
# The bundled docs live at `<gem_root>/docs/`. From `lib/rigor/cli/docs_command.rb` that is three directories up.
|
|
50
|
-
DOCS_ROOT = File.expand_path("../../../docs", __dir__)
|
|
51
|
-
MANUAL_ROOT = File.join(DOCS_ROOT, "manual")
|
|
52
|
-
HANDBOOK_ROOT = File.join(DOCS_ROOT, "handbook")
|
|
53
|
-
LLMS_INDEX = File.join(DOCS_ROOT, "llms.txt")
|
|
50
|
+
DOCS_ROOT = File.expand_path("../../../docs", __dir__).freeze
|
|
51
|
+
MANUAL_ROOT = File.join(DOCS_ROOT, "manual").freeze
|
|
52
|
+
HANDBOOK_ROOT = File.join(DOCS_ROOT, "handbook").freeze
|
|
53
|
+
LLMS_INDEX = File.join(DOCS_ROOT, "llms.txt").freeze
|
|
54
54
|
|
|
55
55
|
# @return CLI exit status.
|
|
56
56
|
def run
|
|
@@ -60,9 +60,9 @@ module Rigor
|
|
|
60
60
|
|
|
61
61
|
# The bundled plugins/examples/source live at `<gem_root>/...`. From `lib/rigor/cli/plugin_command.rb` the gem
|
|
62
62
|
# root is three directories up (matching SkillCommand::SKILLS_ROOT).
|
|
63
|
-
GEM_ROOT = File.expand_path("../../..", __dir__)
|
|
64
|
-
PLUGINS_ROOT = File.join(GEM_ROOT, "plugins")
|
|
65
|
-
EXAMPLES_ROOT = File.join(GEM_ROOT, "examples")
|
|
63
|
+
GEM_ROOT = File.expand_path("../../..", __dir__).freeze
|
|
64
|
+
PLUGINS_ROOT = File.join(GEM_ROOT, "plugins").freeze
|
|
65
|
+
EXAMPLES_ROOT = File.join(GEM_ROOT, "examples").freeze
|
|
66
66
|
|
|
67
67
|
# @return CLI exit status.
|
|
68
68
|
def run
|
|
@@ -35,7 +35,6 @@ module Rigor
|
|
|
35
35
|
|
|
36
36
|
VARIABLE_TOKENS = %i[INSTANCE_VARIABLE CLASS_VARIABLE GLOBAL_VARIABLE].freeze
|
|
37
37
|
|
|
38
|
-
# @param source — Ruby source.
|
|
39
38
|
# @return the source with ANSI colour escapes, or
|
|
40
39
|
# the input unchanged when lexing surfaces an error.
|
|
41
40
|
def colorize(source)
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "optionparser"
|
|
4
4
|
|
|
5
5
|
require_relative "../configuration"
|
|
6
|
+
require_relative "../effects/config_envelopes"
|
|
7
|
+
require_relative "../effects/registry"
|
|
6
8
|
require_relative "options"
|
|
7
9
|
require_relative "../sig_gen"
|
|
8
10
|
require_relative "command"
|
|
@@ -22,21 +24,27 @@ module Rigor
|
|
|
22
24
|
# AND the candidate is a `tighter-return`.
|
|
23
25
|
#
|
|
24
26
|
# Parameter policy defaults to `untyped`. `--params=observed` (slice 3) opts in to caller-side observation
|
|
25
|
-
# harvesting: the `ObservationCollector` walks `--observe=PATH...` (default
|
|
26
|
-
#
|
|
27
|
+
# harvesting: the `ObservationCollector` walks `--observe=PATH...` (default: the project's test roots,
|
|
28
|
+
# {Configuration#resolved_test_paths}), unions per-position arg types, and the generator emits the union per ADR-5
|
|
29
|
+
# clause 2.
|
|
27
30
|
# `--params=observed-strict` stays reserved-but-inert until the capability-role catalog ships (rejected with a usage
|
|
28
31
|
# error so the surface stays stable).
|
|
29
|
-
|
|
32
|
+
#
|
|
33
|
+
# `--check` (ADR-112 WD4) is the CI freshness gate: it runs the `--write` merge with every other flag as given,
|
|
34
|
+
# writes nothing, prints what would change, and exits 1 when anything would.
|
|
35
|
+
class SigGenCommand < Command # rubocop:disable Metrics/ClassLength
|
|
30
36
|
USAGE = "Usage: rigor sig-gen [options] [paths]"
|
|
31
37
|
|
|
32
|
-
VALID_MODES = %w[print diff write].freeze
|
|
38
|
+
VALID_MODES = %w[print diff write check].freeze
|
|
33
39
|
VALID_PARAM_POLICIES = %w[untyped observed observed-strict].freeze
|
|
34
40
|
VALID_FORMATS = %w[text json].freeze
|
|
35
41
|
|
|
36
|
-
# The skip reasons {#report_skipped} counts. The
|
|
37
|
-
# ({#report_unrenderable}, {#report_unresolvable_superclasses}
|
|
42
|
+
# The skip reasons {#report_skipped} counts. The four left out each have a report of their own
|
|
43
|
+
# ({#report_unrenderable}, {#report_unresolvable_superclasses}, {#report_inline_declared}, and the refusal
|
|
44
|
+
# lines `--write` / `--check` print for `inline_differs`), so a method never shows up in two tallies.
|
|
38
45
|
SUMMARISED_SKIP_REASONS = (SigGen::Classification::SKIP_DIAGNOSTIC_IDS.keys -
|
|
39
|
-
%i[unrenderable_rbs unresolvable_superclass
|
|
46
|
+
%i[unrenderable_rbs unresolvable_superclass inline_declared
|
|
47
|
+
inline_differs]).freeze
|
|
40
48
|
private_constant :SUMMARISED_SKIP_REASONS
|
|
41
49
|
|
|
42
50
|
# @return CLI exit status.
|
|
@@ -50,17 +58,22 @@ module Rigor
|
|
|
50
58
|
observations = collect_observations(configuration, options)
|
|
51
59
|
generator = SigGen::Generator.new(configuration: configuration, paths: paths,
|
|
52
60
|
observations: observations,
|
|
53
|
-
include_private: options.fetch(:include_private)
|
|
61
|
+
include_private: options.fetch(:include_private),
|
|
62
|
+
effect_annotator: effect_annotator(configuration, paths, options),
|
|
63
|
+
overwrite: options.fetch(:overwrite))
|
|
54
64
|
candidates = generator.run
|
|
55
65
|
mode = options.fetch(:mode).to_sym
|
|
56
66
|
|
|
57
|
-
status =
|
|
58
|
-
|
|
67
|
+
status = case mode
|
|
68
|
+
when :write then dispatch_write(candidates, configuration, options)
|
|
69
|
+
when :check then dispatch_check(candidates, configuration, options)
|
|
59
70
|
else
|
|
60
71
|
dispatch_print_or_diff(candidates, mode, options)
|
|
61
72
|
0
|
|
62
73
|
end
|
|
63
74
|
report_skipped(candidates, options)
|
|
75
|
+
report_inline_declared(candidates, options)
|
|
76
|
+
report_withheld_annotations(candidates, options)
|
|
64
77
|
report_unrenderable(generator.unrenderable)
|
|
65
78
|
report_unresolvable_superclasses(generator.unresolvable_superclasses)
|
|
66
79
|
status
|
|
@@ -68,6 +81,74 @@ module Rigor
|
|
|
68
81
|
|
|
69
82
|
private
|
|
70
83
|
|
|
84
|
+
# ADR-103 WD9 — the effect table sig-gen writes annotations from, or `nil`.
|
|
85
|
+
#
|
|
86
|
+
# The gate is the project's own `effects:` opt-in and nothing else. An annotation is read back as an
|
|
87
|
+
# enforced envelope, so turning emission on from a sig-gen flag alone would let one command commit a
|
|
88
|
+
# project to a contract `rigor check` was never asked to keep; and a project that has not opted in
|
|
89
|
+
# pays neither the analysis this needs nor a single changed byte of output.
|
|
90
|
+
#
|
|
91
|
+
# `--effect-envelopes` is the second, narrower switch: `%a{pure}` is the ecosystem's existing purity
|
|
92
|
+
# spelling and round-trips through Steep as well as Rigor, while `%a{rigor:v1:effect …}` is Rigor's
|
|
93
|
+
# own and belongs in a project's `sig/` only when the author asked for it by name.
|
|
94
|
+
def effect_annotator(configuration, paths, options)
|
|
95
|
+
unless configuration.effects_enabled?
|
|
96
|
+
if options.fetch(:effect_envelopes)
|
|
97
|
+
@err.puts("rigor sig-gen: --effect-envelopes needs the `effects:` opt-in in .rigor.yml; " \
|
|
98
|
+
"no effect annotation was emitted.")
|
|
99
|
+
end
|
|
100
|
+
return nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
require_relative "check_runner_factory"
|
|
104
|
+
# Through the same factory `rigor check` and `rigor doctor` use, so the LRU cap, the worker
|
|
105
|
+
# resolution and the tolerated-effects switch cannot drift from the command whose cache this run
|
|
106
|
+
# shares. `workers: 0` because a collecting run is pinned to the sequential path anyway, and
|
|
107
|
+
# `--no-cache` mirrors `rigor check`'s flag of the same name.
|
|
108
|
+
runner = CheckRunnerFactory.build(
|
|
109
|
+
configuration: configuration,
|
|
110
|
+
options: { no_cache: options.fetch(:no_cache), explain: false, stats: false, workers: 0 },
|
|
111
|
+
buffer: nil, cache_root: configuration.cache_path
|
|
112
|
+
)
|
|
113
|
+
runner.run((configuration.paths + paths).uniq)
|
|
114
|
+
SigGen::EffectAnnotation::Annotator.new(
|
|
115
|
+
table: runner.effect_table, envelopes: options.fetch(:effect_envelopes),
|
|
116
|
+
envelope_index: runner.effect_envelopes, config_envelopes: config_envelopes(configuration)
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The project's `effects.envelopes:` entries, for gate 0's `match:` half. Built with a plain
|
|
121
|
+
# registry rather than the run's: gate 0 asks only WHETHER an entry selects this class, never what
|
|
122
|
+
# it bounds, so the vocabulary a plugin would add cannot change the answer.
|
|
123
|
+
def config_envelopes(configuration)
|
|
124
|
+
Effects::ConfigEnvelopes.build(
|
|
125
|
+
entries: configuration.effects_envelopes,
|
|
126
|
+
registry: Effects::Registry.for_configuration(configuration)
|
|
127
|
+
)
|
|
128
|
+
rescue StandardError
|
|
129
|
+
[]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The withheld half of the emission, counted the way {#report_skipped} counts a skip: a method that
|
|
133
|
+
# did NOT get `%a{pure}` is the interesting case for a reader who expected one, and silence would
|
|
134
|
+
# read as "sig-gen does not do this" rather than "this method did not earn it".
|
|
135
|
+
def report_withheld_annotations(candidates, options)
|
|
136
|
+
return unless options.fetch(:format) == "text"
|
|
137
|
+
|
|
138
|
+
counts = candidates.each_with_object(Hash.new(0)) do |candidate, acc|
|
|
139
|
+
reason = candidate.effect_reason
|
|
140
|
+
acc[reason] += 1 if reason && reason != :emitted
|
|
141
|
+
end
|
|
142
|
+
return if counts.empty?
|
|
143
|
+
|
|
144
|
+
breakdown = counts.map { |reason, n| "#{SigGen::EffectAnnotation::DIAGNOSTIC_IDS.fetch(reason)}: #{n}" }
|
|
145
|
+
@err.puts(
|
|
146
|
+
"rigor sig-gen: withheld an effect annotation from #{counts.values.sum} method(s) " \
|
|
147
|
+
"(#{breakdown.join(', ')}). An annotation is enforced once written, so it is emitted only " \
|
|
148
|
+
"from an exhaustive, undischarged summary."
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
71
152
|
# Issue #778 — one stderr line per run saying how many methods the generator declined and why, so a
|
|
72
153
|
# method missing from the output is never a silent absence. Text mode only: under `--format=json` every
|
|
73
154
|
# skipped row is already in the payload with its `skip_reason`, and stderr stays clean for the consumer.
|
|
@@ -90,6 +171,18 @@ module Rigor
|
|
|
90
171
|
)
|
|
91
172
|
end
|
|
92
173
|
|
|
174
|
+
# ADR-112 WD4 — `sig_gen.inline_declared: skip` leaving methods out is the project's own choice, not a
|
|
175
|
+
# method sig-gen "could not type or would not overwrite", so it is counted on a line of its own.
|
|
176
|
+
def report_inline_declared(candidates, options)
|
|
177
|
+
return unless options.fetch(:format) == "text"
|
|
178
|
+
|
|
179
|
+
count = candidates.count { |c| c.skip_reason == :inline_declared }
|
|
180
|
+
return if count.zero?
|
|
181
|
+
|
|
182
|
+
@err.puts("rigor sig-gen: left #{count} method(s) declared inline out of sig/, as " \
|
|
183
|
+
"`sig_gen.inline_declared: skip` asks (sig.skipped.inline-declared).")
|
|
184
|
+
end
|
|
185
|
+
|
|
93
186
|
# A method whose rendered RBS does not parse is a Rigor rendering defect, not a fact about the user's
|
|
94
187
|
# code — the generator skipped it (so the rest of the signatures are still usable and still valid), but
|
|
95
188
|
# staying silent would leave the user with a quietly incomplete `sig/` and us with an unreported bug.
|
|
@@ -137,30 +230,99 @@ module Rigor
|
|
|
137
230
|
|
|
138
231
|
# @return exit status — non-zero when a file the user asked to write could not be written.
|
|
139
232
|
def dispatch_write(candidates, configuration, options)
|
|
140
|
-
|
|
141
|
-
path_mapper = SigGen::PathMapper.new(configuration: configuration, layout_index: layout_index)
|
|
142
|
-
writer = SigGen::Writer.new(path_mapper: path_mapper, overwrite: options.fetch(:overwrite))
|
|
143
|
-
|
|
144
|
-
results = writer.write_all(candidates)
|
|
233
|
+
results = build_writer(configuration, options).write_all(candidates)
|
|
145
234
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
#
|
|
235
|
+
refused = refused_candidates(candidates)
|
|
236
|
+
SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format), refused: refused)
|
|
237
|
+
SigGen::Renderer.refusal_lines(refused).each { |line| @err.puts(line) }
|
|
238
|
+
# A refused write (an assembled file that does not parse, an existing target that is not valid UTF-8,
|
|
239
|
+
# or a method whose inline and `sig/` declarations disagree without `--overwrite`) means the user asked for a
|
|
240
|
+
# write and did not get one, so the command must not report success — a green `sig-gen --write` in CI
|
|
241
|
+
# would otherwise mean nothing.
|
|
150
242
|
refusals = %i[skipped_invalid_rbs skipped_invalid_encoding]
|
|
151
|
-
results.any? { |result| refusals.include?(result.action) } ? 1 : 0
|
|
243
|
+
results.any? { |result| refusals.include?(result.action) } || !refused.empty? ? 1 : 0
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# ADR-112 WD4 — the methods whose inline and `sig/` declarations disagree, refused because the run did not
|
|
247
|
+
# pass `--overwrite`. Unlike every other skip, `sig/` contradicts the source while one stands.
|
|
248
|
+
def refused_candidates(candidates)
|
|
249
|
+
candidates.select { |candidate| candidate.skip_reason == :inline_differs }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# ADR-112 WD4 — the same writer as {#dispatch_write}, flags included, in dry-run mode. Defined by what
|
|
253
|
+
# `--write` would do rather than by whether `--diff` prints anything: a tighter return against an existing
|
|
254
|
+
# declaration is a proposal `--write` declines without `--overwrite`, and a gate that counted it could
|
|
255
|
+
# never pass on a project that reviewed it and said no. With `--overwrite` it counts, because `--write
|
|
256
|
+
# --overwrite` would apply it.
|
|
257
|
+
#
|
|
258
|
+
# @return 1 when `sig/` is out of date (or a write would be refused), 0 when it is current.
|
|
259
|
+
def dispatch_check(candidates, configuration, options)
|
|
260
|
+
results = build_writer(configuration, options, dry_run: true).write_all(candidates)
|
|
261
|
+
refused = refused_candidates(candidates)
|
|
262
|
+
SigGen::Renderer.new(out: @out).render_check(results: results, format: options.fetch(:format),
|
|
263
|
+
refused: refused)
|
|
264
|
+
stale = SigGen::Renderer.out_of_date(results)
|
|
265
|
+
return 0 if stale.empty? && refused.empty?
|
|
266
|
+
|
|
267
|
+
report_check_failure(stale, refused) if options.fetch(:format) == "text"
|
|
268
|
+
1
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def report_check_failure(stale, refused)
|
|
272
|
+
unless stale.empty?
|
|
273
|
+
@err.puts("rigor sig-gen --check: #{stale.size} signature file(s) out of date; " \
|
|
274
|
+
"run `rigor sig-gen --write` with the same options to update them.")
|
|
275
|
+
end
|
|
276
|
+
return if refused.empty?
|
|
277
|
+
|
|
278
|
+
@err.puts("rigor sig-gen --check: #{refused.size} method(s) whose inline declaration and sig/ copy " \
|
|
279
|
+
"disagree; make them agree, or pass --overwrite to replace the sig/ member.")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def build_writer(configuration, options, dry_run: false)
|
|
283
|
+
layout_index = SigGen::LayoutIndex.new(signature_paths: configuration.signature_paths)
|
|
284
|
+
path_mapper = SigGen::PathMapper.new(configuration: configuration, layout_index: layout_index)
|
|
285
|
+
SigGen::Writer.new(path_mapper: path_mapper, overwrite: options.fetch(:overwrite), dry_run: dry_run)
|
|
152
286
|
end
|
|
153
287
|
|
|
154
288
|
# Slice 3 — collect call-site argument observations when `--params=observed` is set. When `--observe=PATH` is not
|
|
155
|
-
# specified,
|
|
289
|
+
# specified, observe the project's test roots: `test_paths:`, or whichever of `spec/` and `test/` exist. With no
|
|
290
|
+
# root at all the run still succeeds, but says so — otherwise every parameter stays `untyped` with nothing
|
|
291
|
+
# naming the reason.
|
|
156
292
|
def collect_observations(configuration, options)
|
|
157
293
|
return {} if options.fetch(:params) != "observed"
|
|
158
294
|
|
|
159
295
|
observe_paths = options.fetch(:observe)
|
|
160
|
-
observe_paths =
|
|
296
|
+
observe_paths = configuration.resolved_test_paths if observe_paths.empty?
|
|
297
|
+
if observe_paths.empty?
|
|
298
|
+
warn_no_test_roots(configuration)
|
|
299
|
+
else
|
|
300
|
+
warn_missing_test_roots(observe_paths)
|
|
301
|
+
end
|
|
161
302
|
SigGen::ObservationCollector.new(configuration: configuration, paths: observe_paths).collect
|
|
162
303
|
end
|
|
163
304
|
|
|
305
|
+
# A declared root that does not exist is read as empty, so name it: `sig-gen` is the only command that reads
|
|
306
|
+
# the test roots, and without this the run looks exactly like one whose tests pass no typed arguments.
|
|
307
|
+
def warn_missing_test_roots(observe_paths)
|
|
308
|
+
missing = observe_paths.reject { |path| File.exist?(path) }
|
|
309
|
+
return if missing.empty?
|
|
310
|
+
|
|
311
|
+
consequence = missing.size == observe_paths.size ? "; every parameter stays untyped" : ""
|
|
312
|
+
@err.puts("rigor sig-gen: no call sites are observed from #{missing.map(&:inspect).join(', ')}, " \
|
|
313
|
+
"which does not exist#{consequence}. Check `test_paths:` or --observe=PATH.")
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def warn_no_test_roots(configuration)
|
|
317
|
+
reason = if configuration.test_paths.nil?
|
|
318
|
+
"no spec/ or test/ directory was found and `test_paths:` is unset"
|
|
319
|
+
else
|
|
320
|
+
"`test_paths:` is empty"
|
|
321
|
+
end
|
|
322
|
+
@err.puts("rigor sig-gen: --params=observed has no test roots to observe (#{reason}); every parameter " \
|
|
323
|
+
"stays untyped. Declare `test_paths:` in the configuration or pass --observe=PATH.")
|
|
324
|
+
end
|
|
325
|
+
|
|
164
326
|
def parse_options
|
|
165
327
|
options = {
|
|
166
328
|
mode: "print",
|
|
@@ -170,6 +332,8 @@ module Rigor
|
|
|
170
332
|
overwrite: false,
|
|
171
333
|
observe: [],
|
|
172
334
|
include_private: false,
|
|
335
|
+
effect_envelopes: false,
|
|
336
|
+
no_cache: false,
|
|
173
337
|
config: nil
|
|
174
338
|
}
|
|
175
339
|
build_option_parser(options).parse!(@argv)
|
|
@@ -181,23 +345,33 @@ module Rigor
|
|
|
181
345
|
nil
|
|
182
346
|
end
|
|
183
347
|
|
|
184
|
-
def build_option_parser(options) # rubocop:disable Metrics/AbcSize
|
|
348
|
+
def build_option_parser(options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
185
349
|
OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
|
|
186
350
|
opts.banner = USAGE
|
|
187
|
-
opts.on("--print", "Write RBS skeletons to stdout (default)") { options
|
|
188
|
-
opts.on("--diff", "Write a unified diff against existing RBS") { options
|
|
189
|
-
opts.on("--write", "Write generated RBS to sig/<path>.rbs files") { options
|
|
190
|
-
opts.on("--
|
|
351
|
+
opts.on("--print", "Write RBS skeletons to stdout (default)") { select_mode(options, "print") }
|
|
352
|
+
opts.on("--diff", "Write a unified diff against existing RBS") { select_mode(options, "diff") }
|
|
353
|
+
opts.on("--write", "Write generated RBS to sig/<path>.rbs files") { select_mode(options, "write") }
|
|
354
|
+
opts.on("--check", "Exit 1 when --write would change sig/; write nothing") { select_mode(options, "check") }
|
|
355
|
+
opts.on("--overwrite", "Allow tighter-return updates, and inline declarations that disagree with sig/, " \
|
|
356
|
+
"to replace user-authored RBS") do
|
|
191
357
|
options[:overwrite] = true
|
|
192
358
|
end
|
|
193
359
|
opts.on("--include-private", "Emit private / protected instance methods (default: public only)") do
|
|
194
360
|
options[:include_private] = true
|
|
195
361
|
end
|
|
362
|
+
opts.on("--effect-envelopes", "Also emit %a{rigor:v1:effect ...} for effectful methods " \
|
|
363
|
+
"(requires the effects: opt-in)") do
|
|
364
|
+
options[:effect_envelopes] = true
|
|
365
|
+
end
|
|
366
|
+
opts.on("--no-cache", "Do not read or write the analysis cache (effect collection only)") do
|
|
367
|
+
options[:no_cache] = true
|
|
368
|
+
end
|
|
196
369
|
opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
|
|
197
370
|
opts.on("--params=POLICY", "Parameter policy: untyped (default), observed, observed-strict") do |value|
|
|
198
371
|
options[:params] = value
|
|
199
372
|
end
|
|
200
|
-
opts.on("--observe=PATH", "Directory / file to scan for call-site observations (repeatable
|
|
373
|
+
opts.on("--observe=PATH", "Directory / file to scan for call-site observations (repeatable; " \
|
|
374
|
+
"default: the configured test_paths)") do |value|
|
|
201
375
|
options[:observe] << value
|
|
202
376
|
end
|
|
203
377
|
opts.on("--new-files", "Emit only new-file classifications") do
|
|
@@ -213,12 +387,19 @@ module Rigor
|
|
|
213
387
|
end
|
|
214
388
|
end
|
|
215
389
|
|
|
390
|
+
# A second, different mode flag is a usage error rather than last-one-wins: `--check --write` must not
|
|
391
|
+
# quietly write in a CI job that meant to gate, nor `--write --check` quietly not.
|
|
392
|
+
def select_mode(options, mode)
|
|
393
|
+
options[:mode] = options[:mode_given] && options[:mode] != mode ? "conflict" : mode
|
|
394
|
+
options[:mode_given] = true
|
|
395
|
+
end
|
|
396
|
+
|
|
216
397
|
def validation_error(options)
|
|
217
398
|
mode = options.fetch(:mode)
|
|
218
399
|
format = options.fetch(:format)
|
|
219
400
|
params = options.fetch(:params)
|
|
220
401
|
|
|
221
|
-
return "--print, --diff, and --
|
|
402
|
+
return "--print, --diff, --write, and --check are mutually exclusive flags; pick one" if mode == "conflict"
|
|
222
403
|
return "unsupported --format=#{format}" unless VALID_FORMATS.include?(format)
|
|
223
404
|
return "unsupported --params=#{params}" unless VALID_PARAM_POLICIES.include?(params)
|
|
224
405
|
if params == "observed-strict"
|
|
@@ -74,7 +74,7 @@ module Rigor
|
|
|
74
74
|
|
|
75
75
|
# The bundled skills live at `<gem_root>/skills/`. From `lib/rigor/cli/skill_command.rb` that is three directories
|
|
76
76
|
# up.
|
|
77
|
-
SKILLS_ROOT = File.expand_path("../../../skills", __dir__)
|
|
77
|
+
SKILLS_ROOT = File.expand_path("../../../skills", __dir__).freeze
|
|
78
78
|
|
|
79
79
|
# @return CLI exit status.
|
|
80
80
|
def run
|