rigortype 0.3.9 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -4
- 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/08-understanding-errors.md +3 -2
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +454 -13
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +65 -4
- data/docs/manual/03-configuration.md +12 -0
- data/docs/manual/04-diagnostics.md +4 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- 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 +427 -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 +638 -136
- data/lib/rigor/analysis/dependency_recorder.rb +222 -17
- data/lib/rigor/analysis/fact_store.rb +9 -0
- data/lib/rigor/analysis/incremental.rb +26 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
- data/lib/rigor/analysis/incremental_session.rb +309 -53
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- 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 +3 -1
- data/lib/rigor/analysis/rule_catalog.rb +127 -0
- data/lib/rigor/analysis/run_cache_key.rb +46 -18
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
- data/lib/rigor/analysis/runner.rb +603 -54
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +93 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +409 -0
- data/lib/rigor/analysis/worker_session.rb +57 -13
- 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 +45 -1
- data/lib/rigor/cache/engine_source.rb +29 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +102 -8
- 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 +63 -4
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/check_command.rb +83 -13
- data/lib/rigor/cli/doc_links.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +4 -4
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- 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/unused_command.rb +3 -3
- data/lib/rigor/cli.rb +21 -3
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +24 -3
- data/lib/rigor/configuration.rb +71 -7
- 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 +49 -37
- 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/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/lockfile_resolver.rb +27 -1
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_loader.rb +400 -134
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +104 -96
- 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 +88 -26
- 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 +393 -15
- data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +3 -2
- 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/definer_resolution.rb +266 -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 +1665 -538
- data/lib/rigor/inference/external_ancestor_resolution.rb +264 -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 +488 -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 +179 -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 +901 -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/module_function_state.rb +213 -0
- data/lib/rigor/inference/multi_target_binder.rb +345 -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 +674 -124
- 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 +13 -11
- 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/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +5913 -701
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +3249 -497
- 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 +196 -13
- 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/io_boundary.rb +82 -4
- data/lib/rigor/plugin/isolation.rb +1 -1
- 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/trust_policy.rb +3 -3
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/discovery_seed.rb +20 -9
- data/lib/rigor/protection/kill_signature.rb +0 -1
- data/lib/rigor/protection/mutation_cache.rb +4 -5
- data/lib/rigor/rbs_extended.rb +27 -0
- data/lib/rigor/reflection/constant_ancestors.rb +146 -0
- data/lib/rigor/reflection/constant_path.rb +41 -13
- data/lib/rigor/reflection.rb +73 -107
- data/lib/rigor/scope/discovery_index.rb +351 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +884 -0
- data/lib/rigor/scope.rb +981 -198
- 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 +580 -47
- 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 +112 -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-ffi/lib/rigor/plugin/ffi.rb +10 -0
- 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 +14 -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 +2 -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 +177 -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 +46 -18
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +40 -10
- 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 +132 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# ADR-119 WD2 — the candidate-set read over {Scope::ResolutionChain}, for a reader that can decline. It is
|
|
6
|
+
# internal (not a `Scope` method, so the pinned public surface is unchanged) and answers one of three
|
|
7
|
+
# things about a method name on a class:
|
|
8
|
+
#
|
|
9
|
+
# - {Known} — every candidate gives the same answer to the question asked;
|
|
10
|
+
# - {UNKNOWN} — the chain does not stand for this name (a mark not discharged, a fork, a retro world that
|
|
11
|
+
# answers otherwise), a candidate disagrees, the walk was cut by the budget, an external ancestor ahead of
|
|
12
|
+
# the candidate may answer first (one RBS does not know, or knows and declares the name in; one it knows
|
|
13
|
+
# and whose declaration lacks the name is skipped);
|
|
14
|
+
# - {ABSENT} — no project entry, no external entry and not the implicit `Object` can answer.
|
|
15
|
+
#
|
|
16
|
+
# On the singleton side (ADR-119 WD3, errata 2026-10-08) the chain is `K`'s singleton, its extends' instance
|
|
17
|
+
# chains, then the superclass's singleton chain; an external superclass entry stands for its whole tail and is
|
|
18
|
+
# tested against RBS's singleton side, an extended external module against its instance side, the implicit tail
|
|
19
|
+
# after a project superclass against `Object`'s singleton side. A standing chain is then read past
|
|
20
|
+
# {SingletonHookDecline}' positional decline: a hook's singleton-side edge is recorded on no includer, so the read
|
|
21
|
+
# declines where one could have landed ahead of its last candidate.
|
|
22
|
+
#
|
|
23
|
+
# THE CALL SITE IS A CONTRACT. A result is consumed only by an exhaustive `case/in` that is the call's own
|
|
24
|
+
# direct predicate, with one arm per answer and no `else` or `in _`, and is never stored, returned or
|
|
25
|
+
# truth-tested: {UNKNOWN} and {ABSENT} are both truthy, so any other use folds a decline into a firing arm.
|
|
26
|
+
# `spec/rigor/inference/definer_resolution_case_in_spec.rb` fails on a call site of any other shape.
|
|
27
|
+
#
|
|
28
|
+
# It migrates one question at a time: `SourceArity` (C1b) and the relationship lints (C1c) read the instance
|
|
29
|
+
# side; no production reader reads the singleton side yet (C2-a landed it for C2's typing sites).
|
|
30
|
+
module DefinerResolution
|
|
31
|
+
Known = Data.define(:answer, :owner)
|
|
32
|
+
UnknownResult = Data.define
|
|
33
|
+
AbsentResult = Data.define
|
|
34
|
+
UNKNOWN = UnknownResult.new
|
|
35
|
+
ABSENT = AbsentResult.new
|
|
36
|
+
|
|
37
|
+
# One candidate definer as a per-chain answer function reports it: the `answer` to the question, the
|
|
38
|
+
# `owner` entry's name, the `index` of that entry on the chain and its `side` (`:instance` or `:singleton`).
|
|
39
|
+
Hit = Data.define(:answer, :owner, :index, :side)
|
|
40
|
+
|
|
41
|
+
QUESTIONS = %i[definer visibility arity override own_side].freeze
|
|
42
|
+
NONE = :rigor_definer_resolution_none
|
|
43
|
+
STRAY = :rigor_definer_resolution_stray
|
|
44
|
+
private_constant :NONE, :STRAY
|
|
45
|
+
|
|
46
|
+
module_function
|
|
47
|
+
|
|
48
|
+
# `answer_in` is the question's per-chain answer function: called with a chain and a position, it returns
|
|
49
|
+
# the {Hit} of the first project entry at or after the position that defines `method_name`, or nil. `:definer`
|
|
50
|
+
# and `:visibility` have a default; every other question supplies its own. It is called once per candidate
|
|
51
|
+
# on the chain, and again on the retro world when the chain has one fork, where `from` is mapped to the
|
|
52
|
+
# entry it followed.
|
|
53
|
+
def resolve(scope, class_name, method_name, side, question:, from: 0, &answer_in)
|
|
54
|
+
raise ArgumentError, "unknown question #{question.inspect}" unless QUESTIONS.include?(question)
|
|
55
|
+
|
|
56
|
+
answer_in = override_answer(scope, &answer_in) if question == :override
|
|
57
|
+
answer_in ||= default_answer(scope, method_name, question)
|
|
58
|
+
flavor = question == :arity ? :arity : :methods
|
|
59
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, side, flavor)
|
|
60
|
+
|
|
61
|
+
hits = candidates(scope, chain, method_name, from, answer_in)
|
|
62
|
+
return UNKNOWN if hits.nil?
|
|
63
|
+
|
|
64
|
+
verdict = chain.settle(scope, outcomes(hits), owner: hits.first&.owner, unknown_for: method_name) do |retro|
|
|
65
|
+
retro_from = retro_position(chain, retro, from)
|
|
66
|
+
outcomes(retro_from && candidates(scope, retro, method_name, retro_from, answer_in))
|
|
67
|
+
end
|
|
68
|
+
return UNKNOWN unless verdict == :chain
|
|
69
|
+
return UNKNOWN if side == :singleton && SingletonHookDecline.decline?(scope, chain, method_name, from, hits)
|
|
70
|
+
|
|
71
|
+
collapse(hits)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The `:override` question's answer function, built from the caller's block: that block is NOT a per-chain
|
|
75
|
+
# function but `|owner_name| value`, called on each project entry in order, and its first non-nil `value` is
|
|
76
|
+
# the entry's answer, as `[owner_name, value]` (the owner is part of the answer so that two worlds naming
|
|
77
|
+
# different parents disagree). It is the override lints' parent read: the nearest project ancestor after the
|
|
78
|
+
# class whose block answers.
|
|
79
|
+
def override_answer(scope)
|
|
80
|
+
lambda do |chain, from|
|
|
81
|
+
first_hit(chain, scope, from) do |entry|
|
|
82
|
+
value = yield(entry.name)
|
|
83
|
+
[entry.name, value] unless value.nil?
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# `from` is a position on `chain`; the same position on the retro world is just after the entry it followed.
|
|
89
|
+
# An entry the chain carries twice cannot be matched, which declines.
|
|
90
|
+
def retro_position(chain, retro, from)
|
|
91
|
+
return 0 if from.zero?
|
|
92
|
+
|
|
93
|
+
entry = chain.entries[from - 1]
|
|
94
|
+
return nil if entry.nil? || chain.entries.count(entry) > 1
|
|
95
|
+
|
|
96
|
+
found = retro.entries.index(entry)
|
|
97
|
+
found && (found + 1)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The first definer and, while that definer is `possible`, the next; a nil joins the set when nothing
|
|
101
|
+
# follows (the absent candidate). A cut chain, and a chain on which an external ancestor ahead of the
|
|
102
|
+
# candidate (or, with no candidate, the implicit `Object`) may define the name, is a decline, answered by
|
|
103
|
+
# nil instead of a set.
|
|
104
|
+
def candidates(scope, chain, method_name, from, answer_in)
|
|
105
|
+
hits = []
|
|
106
|
+
position = from
|
|
107
|
+
loop do
|
|
108
|
+
hit = answer_in.call(chain, position)
|
|
109
|
+
hit = past_fold_copies(scope, chain, method_name, hit, answer_in) if chain.side == :singleton
|
|
110
|
+
return nil if hit.equal?(STRAY)
|
|
111
|
+
|
|
112
|
+
raise ArgumentError, "answer function went backwards" if hit && hit.index < position
|
|
113
|
+
return nil if hit.nil? && chain.truncated?
|
|
114
|
+
|
|
115
|
+
hits << hit
|
|
116
|
+
return decided(scope, chain, method_name, from, hits) if hit.nil? || !possible?(scope, hit, method_name)
|
|
117
|
+
|
|
118
|
+
position = hit.index + 1
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# A class entry answering through a copy the extends fold made is asked past
|
|
123
|
+
# (`SingletonHookDecline.copy_kind`), so the module that wrote the `def` answers at its own position; a copy
|
|
124
|
+
# from a module the chain does not hold on that level is {STRAY}, a decline.
|
|
125
|
+
def past_fold_copies(scope, chain, method_name, hit, answer_in)
|
|
126
|
+
while hit
|
|
127
|
+
case SingletonHookDecline.copy_kind(scope, chain, method_name, hit)
|
|
128
|
+
when :copy then hit = answer_in.call(chain, hit.index + 1)
|
|
129
|
+
when :stray then return STRAY
|
|
130
|
+
else return hit
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
hit
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def decided(scope, chain, method_name, from, hits)
|
|
137
|
+
external_may_answer?(scope, chain, method_name, from, hits.last) ? nil : hits
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# An external entry ahead of the last candidate that RBS does not know, or knows and declares the name in,
|
|
141
|
+
# may be the definer Ruby calls; with no candidate, so may the implicit `Object` (`Kernel`, for the object
|
|
142
|
+
# that every class is). Both decline. Each tested external files the negative class edge on its spelling.
|
|
143
|
+
def external_may_answer?(scope, chain, method_name, from, last)
|
|
144
|
+
stop = last.nil? ? chain.entries.size : last.index
|
|
145
|
+
recording = Analysis::DependencyRecorder.active?
|
|
146
|
+
(from...stop).any? do |index|
|
|
147
|
+
entry = chain.entries[index]
|
|
148
|
+
next false unless entry.external?
|
|
149
|
+
|
|
150
|
+
Analysis::DependencyRecorder.read_missing(:class, entry.raw.to_s.split("::").last) if recording
|
|
151
|
+
!external_lacks?(scope, entry.candidates, entry.side, method_name)
|
|
152
|
+
end || (last.nil? && implicit_object_answers?(scope, chain, method_name))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The implicit tail after a chain's last project superclass: `Object` (with `Kernel` and `BasicObject`) on the
|
|
156
|
+
# instance side, `#<Class:Object>` through `Class`, `Module` and `Kernel` on the singleton side.
|
|
157
|
+
def implicit_object_answers?(scope, chain, method_name)
|
|
158
|
+
!external_lacks?(scope, ["Object"], chain.side, method_name)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# True only for an ancestor RBS knows whose declaration lacks the name, on the side the entry stands for: an
|
|
162
|
+
# external superclass on a singleton chain (`:singleton`, its whole tail) is asked for a singleton method, an
|
|
163
|
+
# extended module (`:instance`) and every instance-side entry for an instance method.
|
|
164
|
+
def external_lacks?(scope, candidates, side, method_name)
|
|
165
|
+
return Scope::ResolutionChain::Relevance.external_lacks?(scope, candidates, method_name) if side == :instance
|
|
166
|
+
|
|
167
|
+
known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
|
|
168
|
+
return false if known.nil?
|
|
169
|
+
|
|
170
|
+
Rigor::Reflection.singleton_method_definition(known, method_name, scope: scope).nil?
|
|
171
|
+
rescue StandardError
|
|
172
|
+
false
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# What a candidate set says, for {Scope::ResolutionChain#settle} to compare across worlds.
|
|
176
|
+
def outcomes(hits)
|
|
177
|
+
return NONE if hits.nil?
|
|
178
|
+
|
|
179
|
+
hits.map { |hit| hit.nil? ? NONE : hit.answer }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def collapse(hits)
|
|
183
|
+
first = hits.first
|
|
184
|
+
return ABSENT if hits.all?(&:nil?)
|
|
185
|
+
return UNKNOWN if hits.any?(&:nil?) || hits.any? { |hit| hit.answer != first.answer }
|
|
186
|
+
|
|
187
|
+
Known.new(first.answer, first.owner)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# A definer that rests on a fact the walk could not prove executes: the member's `possible` sibling, or a
|
|
191
|
+
# contested slot of one of the single-valued members.
|
|
192
|
+
def possible?(scope, hit, method_name)
|
|
193
|
+
discovery = scope.discovery
|
|
194
|
+
owner = hit.owner
|
|
195
|
+
kind = hit.side == :singleton ? :singleton : :instance
|
|
196
|
+
nodes = kind == :singleton ? :discovered_singleton_def_nodes : :discovered_def_nodes
|
|
197
|
+
discovery.possible_method?(owner, method_name, kind) ||
|
|
198
|
+
discovery.contested?(nodes, [owner, method_name.to_sym]) ||
|
|
199
|
+
discovery.contested?(:discovered_method_visibilities, [owner, method_name.to_sym]) ||
|
|
200
|
+
discovery.contested?(:discovered_parameter_envelopes, [owner, [kind, method_name.to_sym]])
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def default_answer(scope, method_name, question)
|
|
204
|
+
case question
|
|
205
|
+
when :definer then definer_answer(scope, method_name)
|
|
206
|
+
when :visibility then visibility_answer(scope, method_name)
|
|
207
|
+
else raise ArgumentError, "question #{question.inspect} needs an answer function"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# The first project entry with a `def` of the name, as `[def node, owner]`.
|
|
212
|
+
def definer_answer(scope, method_name)
|
|
213
|
+
lambda do |chain, from|
|
|
214
|
+
first_hit(chain, scope, from) do |entry|
|
|
215
|
+
node = if entry.side == :singleton
|
|
216
|
+
scope.singleton_def_for(entry.name,
|
|
217
|
+
method_name)
|
|
218
|
+
else
|
|
219
|
+
scope.user_def_for(entry.name,
|
|
220
|
+
method_name)
|
|
221
|
+
end
|
|
222
|
+
[node, entry.name] if node
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# The first project entry that records the name (a discovered method, an instance `def` the def table
|
|
228
|
+
# holds, or a visibility change such as `private :foo` recorded for it), with its visibility (`:public` where
|
|
229
|
+
# none was stated).
|
|
230
|
+
def visibility_answer(scope, method_name)
|
|
231
|
+
lambda do |chain, from|
|
|
232
|
+
first_hit(chain, scope, from) do |entry|
|
|
233
|
+
kind = entry.side == :singleton ? :singleton : :instance
|
|
234
|
+
next unless scope.discovered_method?(entry.name, method_name, kind) ||
|
|
235
|
+
(kind == :instance && (scope.user_def_for(entry.name, method_name) ||
|
|
236
|
+
scope.discovered_method_visibility(entry.name, method_name)))
|
|
237
|
+
|
|
238
|
+
scope.discovered_method_visibility(entry.name, method_name) || :public
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def first_hit(chain, scope, from)
|
|
244
|
+
index = from - 1
|
|
245
|
+
found = nil
|
|
246
|
+
chain.search(scope, from) do |entry|
|
|
247
|
+
index += 1
|
|
248
|
+
next if entry.external?
|
|
249
|
+
|
|
250
|
+
answer = yield(entry)
|
|
251
|
+
next if answer.nil?
|
|
252
|
+
|
|
253
|
+
found = Hit.new(answer, entry.name, index, entry.side)
|
|
254
|
+
end
|
|
255
|
+
found
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
private_class_method :override_answer, :retro_position, :candidates, :past_fold_copies, :decided,
|
|
259
|
+
:external_may_answer?, :implicit_object_answers?, :external_lacks?,
|
|
260
|
+
:outcomes, :collapse, :possible?,
|
|
261
|
+
:default_answer, :definer_answer, :visibility_answer, :first_hit
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
require_relative "singleton_hook_decline"
|
|
@@ -45,7 +45,7 @@ module Rigor
|
|
|
45
45
|
widened = widen_through_path(current_scope.local(read.name), steps, call_node.name, arg_types)
|
|
46
46
|
return current_scope if widened.nil?
|
|
47
47
|
|
|
48
|
-
current_scope.
|
|
48
|
+
current_scope.with_mutated_local(read.name, widened)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# True when `receiver` is an element read whose target element is a literal-shape carrier —
|
|
@@ -62,7 +62,12 @@ module Rigor
|
|
|
62
62
|
# `[<local read>, [step, …]]` for a chain of element reads rooted at a local variable, or
|
|
63
63
|
# `nil` when the expression is anything else. A step is an Integer position (negative counts
|
|
64
64
|
# from the end, as Ruby's own indexing does) or `:all` when the index is not a literal.
|
|
65
|
+
#
|
|
66
|
+
# Every mutation site's receiver is asked, and nearly every one is a plain variable read, so a
|
|
67
|
+
# receiver that is no call answers before the step list is allocated.
|
|
65
68
|
def element_read_path(node)
|
|
69
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
70
|
+
|
|
66
71
|
steps = []
|
|
67
72
|
cursor = node
|
|
68
73
|
while cursor.is_a?(Prism::CallNode)
|
|
@@ -118,22 +123,29 @@ module Rigor
|
|
|
118
123
|
end
|
|
119
124
|
|
|
120
125
|
# Rebuilds `type` with the element at `steps` widened against `method_name`, or `nil` when
|
|
121
|
-
# nothing along the path widened.
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
# nothing along the path widened. A block, when given, replaces the straight-line widening of the
|
|
127
|
+
# element itself: it receives the element's type and answers the widened one, or `nil` to decline.
|
|
128
|
+
# {UnknownStoreWidening} passes one so a site on an element takes the same unknown-store widening a
|
|
129
|
+
# site on a variable does.
|
|
130
|
+
def widen_through_path(type, steps, method_name, arg_types, &element_widening)
|
|
131
|
+
if steps.empty?
|
|
132
|
+
return element_widening.call(type) if element_widening
|
|
133
|
+
|
|
134
|
+
return MutationWidening.widen_for_mutator(type, method_name, arg_types: arg_types)
|
|
135
|
+
end
|
|
124
136
|
|
|
125
137
|
case type
|
|
126
|
-
when Type::Tuple then widen_tuple_path(type, steps, method_name, arg_types)
|
|
127
|
-
when Type::Union then widen_union_path(type, steps, method_name, arg_types)
|
|
138
|
+
when Type::Tuple then widen_tuple_path(type, steps, method_name, arg_types, &element_widening)
|
|
139
|
+
when Type::Union then widen_union_path(type, steps, method_name, arg_types, &element_widening)
|
|
128
140
|
end
|
|
129
141
|
end
|
|
130
142
|
|
|
131
|
-
def widen_tuple_path(tuple, steps, method_name, arg_types)
|
|
143
|
+
def widen_tuple_path(tuple, steps, method_name, arg_types, &)
|
|
132
144
|
elements = tuple.elements.dup
|
|
133
145
|
rest = steps.drop(1)
|
|
134
146
|
widened_any = false
|
|
135
147
|
tuple_positions(tuple, steps.first).each do |position|
|
|
136
|
-
inner = widen_through_path(elements[position], rest, method_name, arg_types)
|
|
148
|
+
inner = widen_through_path(elements[position], rest, method_name, arg_types, &)
|
|
137
149
|
next if inner.nil?
|
|
138
150
|
|
|
139
151
|
elements[position] = inner
|
|
@@ -142,10 +154,10 @@ module Rigor
|
|
|
142
154
|
widened_any ? Type::Tuple.new(elements) : nil
|
|
143
155
|
end
|
|
144
156
|
|
|
145
|
-
def widen_union_path(union, steps, method_name, arg_types)
|
|
157
|
+
def widen_union_path(union, steps, method_name, arg_types, &)
|
|
146
158
|
widened_any = false
|
|
147
159
|
members = union.members.map do |member|
|
|
148
|
-
widened = widen_through_path(member, steps, method_name, arg_types)
|
|
160
|
+
widened = widen_through_path(member, steps, method_name, arg_types, &)
|
|
149
161
|
next member if widened.nil?
|
|
150
162
|
|
|
151
163
|
widened_any = true
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../reflection"
|
|
6
|
+
require_relative "../source/node_children"
|
|
7
|
+
require_relative "../type"
|
|
8
|
+
require_relative "block_call_timing"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
module Inference
|
|
12
|
+
# Issue #1360 — `$!`, the exception being rescued, and `$@`, its backtrace. Ruby does not keep them in the method
|
|
13
|
+
# frame's special-variable slot, as it keeps `$~` and `$_`: a read of `$!` walks the running execution context's
|
|
14
|
+
# frames to the nearest rescue frame. So:
|
|
15
|
+
#
|
|
16
|
+
# - inside a `rescue` clause, and inside a rescue modifier's fallback, `$!` is the exception that clause rescued,
|
|
17
|
+
# in every block the clause runs and every method it calls;
|
|
18
|
+
# - once the `begin` or the modifier exits, however it exits, `$!` is what it was before it started: `nil` at the
|
|
19
|
+
# top of a method called from nowhere special, the outer exception after a nested `begin` in a rescue clause;
|
|
20
|
+
# - an `else` clause and the body read that earlier value too, and an `ensure` clause reads it when the body or a
|
|
21
|
+
# rescue clause finished, but the exception in flight when one raised.
|
|
22
|
+
#
|
|
23
|
+
# A method body cannot know what its caller is rescuing, so it starts unbound; a thread's or fiber's root block
|
|
24
|
+
# runs in an execution context of its own, where `$!` is `nil` ({FreshFrameBlocks.entry}); and a closure's body
|
|
25
|
+
# runs whenever it is called, usually after the clause it is written in exited ({FreshFrameBlocks.closure_entry}).
|
|
26
|
+
module ErrorInfo
|
|
27
|
+
ERROR_INFO = :$!
|
|
28
|
+
BACKTRACE = :$@
|
|
29
|
+
NAMES = [ERROR_INFO, BACKTRACE].freeze
|
|
30
|
+
# The globals a rescue modifier's fallback reads differently from the modifier's entry ({.modifier_entry}).
|
|
31
|
+
MODIFIER_NAMES = [ERROR_INFO, BACKTRACE, :$?].freeze
|
|
32
|
+
EXCEPTION_ORDERINGS = Set[:equal, :subclass].freeze
|
|
33
|
+
DEFINERS = Set[:define_method, :define_singleton_method].freeze
|
|
34
|
+
# The class guards a clause may put on `$!` ({.guarded?}).
|
|
35
|
+
GUARDS = Set[:is_a?, :kind_of?, :instance_of?, :respond_to?].freeze
|
|
36
|
+
private_constant :ERROR_INFO, :BACKTRACE, :NAMES, :MODIFIER_NAMES, :EXCEPTION_ORDERINGS, :GUARDS, :DEFINERS
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# The scope a `rescue` clause, or a rescue modifier's fallback, runs from: `$!` bound to the exception it rescued
|
|
41
|
+
# and `$@` to that exception's backtrace, with `$?` unbound. `exception_type` is what `rescue … => e` binds `e` to
|
|
42
|
+
# (`StatementEvaluator#rescue_exception_type`): the union of the named classes' instances, `StandardError` for a
|
|
43
|
+
# bare `rescue`. A member that is not a class RBS or the program places below `Exception` reads `Dynamic[top]`
|
|
44
|
+
# instead: a rescue list may name a module with its own `===` (`rescue NetworkErrors`), which matches exceptions
|
|
45
|
+
# that are not instances of it, and a class the analyzer cannot place may be such an object too. So does a class
|
|
46
|
+
# that it or a project ancestor gives its own singleton `===` (`class Matchy < StandardError; def self.===(o) =
|
|
47
|
+
# true`).
|
|
48
|
+
#
|
|
49
|
+
# `body` is the clause's statements or the fallback, and `reference` the name of the local a `rescue … => e`
|
|
50
|
+
# clause binds. When the body guards `$!`, or that local, which is the same object, by its class ({.guarded?}),
|
|
51
|
+
# the clause reads `$!` and `$@` unbound, as it did before issue #1360: `$!.key if e.is_a?(KeyError)` would report
|
|
52
|
+
# the bound `StandardError` against the guard (ADR-117, point 3), since narrowing `e` does not narrow `$!`. #1429
|
|
53
|
+
# made a class guard on `$!` itself narrow it; #1447 narrows this decline to the guards that still do not.
|
|
54
|
+
#
|
|
55
|
+
# `$@` calls the exception's `backtrace`, which is an `Array[String]` for a raised exception. It is left unbound
|
|
56
|
+
# in a program that defines a method named `backtrace` anywhere, which may return anything, and in a clause
|
|
57
|
+
# whose body calls `set_backtrace`, which may set it to nil.
|
|
58
|
+
#
|
|
59
|
+
# `$?` is unbound because the exception may have been raised while a subprocess waited: a backtick, `%x` or
|
|
60
|
+
# `system` sets `$?` to nil before it runs the child, so a `Timeout::Error`, an `Interrupt` or a `Thread#raise`
|
|
61
|
+
# there leaves it nil.
|
|
62
|
+
def rescue_entry(scope, exception_type, body = nil, reference = nil)
|
|
63
|
+
entered = scope.forget_error_info.forget_last_status
|
|
64
|
+
# Narrowed to the guards #1429 does not narrow in #1447.
|
|
65
|
+
return entered if guarded?(body, reference)
|
|
66
|
+
|
|
67
|
+
entered = entered.with_global(ERROR_INFO, rescued_type(exception_type, scope))
|
|
68
|
+
return entered if calls_set_backtrace?(body) || BlockCallTiming.project_defines_anywhere?(:backtrace, scope)
|
|
69
|
+
|
|
70
|
+
entered.with_global(BACKTRACE, backtrace_type)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The scope a rescue modifier's fallback (`expr rescue fallback`) runs from, which rescues a `StandardError`.
|
|
74
|
+
def modifier_entry(scope, fallback = nil)
|
|
75
|
+
rescue_entry(scope, Type::Combinator.nominal_of("StandardError"), fallback)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The class guards on `$!` that {.rescue_entry} declines on, anywhere in `node`: `$!.is_a?`, `kind_of?`,
|
|
79
|
+
# `instance_of?` or `respond_to?`, a `===` whose argument is `$!` (`KeyError === $!`), and `case $!`, and the
|
|
80
|
+
# same on a read of the local named `reference`.
|
|
81
|
+
def guarded?(node, reference = nil)
|
|
82
|
+
return false unless node.is_a?(Prism::Node)
|
|
83
|
+
return true if guard?(node, reference)
|
|
84
|
+
|
|
85
|
+
found = false
|
|
86
|
+
node.rigor_each_child { |child| found ||= guarded?(child, reference) }
|
|
87
|
+
found
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def guard?(node, reference)
|
|
91
|
+
case node
|
|
92
|
+
when Prism::CallNode
|
|
93
|
+
(GUARDS.include?(node.name) && error_info_read?(node.receiver, reference)) ||
|
|
94
|
+
(node.name == :=== && error_info_read?(node.arguments&.arguments&.first, reference))
|
|
95
|
+
when Prism::CaseNode, Prism::CaseMatchNode then error_info_read?(node.predicate, reference)
|
|
96
|
+
else false
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def error_info_read?(node, reference)
|
|
101
|
+
case node
|
|
102
|
+
when Prism::GlobalVariableReadNode then node.name == ERROR_INFO
|
|
103
|
+
when Prism::LocalVariableReadNode then !reference.nil? && node.name == reference
|
|
104
|
+
else false
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# True when `node` calls `set_backtrace` anywhere, on any receiver: `$!.set_backtrace(nil)` makes `$@` nil.
|
|
109
|
+
def calls_set_backtrace?(node)
|
|
110
|
+
return false unless node.is_a?(Prism::Node)
|
|
111
|
+
return true if node.is_a?(Prism::CallNode) && node.name == :set_backtrace
|
|
112
|
+
|
|
113
|
+
found = false
|
|
114
|
+
node.rigor_each_child { |child| found ||= calls_set_backtrace?(child) }
|
|
115
|
+
found
|
|
116
|
+
end
|
|
117
|
+
private_class_method :guard?, :error_info_read?, :calls_set_backtrace?
|
|
118
|
+
|
|
119
|
+
# True when `node` reads `$!`, `$@` or `$?` anywhere in it: the gate on typing a rescue modifier's fallback
|
|
120
|
+
# under {.modifier_entry}, which most fallbacks (`rescue nil`) never need.
|
|
121
|
+
def read_in?(node)
|
|
122
|
+
return false unless node.is_a?(Prism::Node)
|
|
123
|
+
return MODIFIER_NAMES.include?(node.name) if node.is_a?(Prism::GlobalVariableReadNode)
|
|
124
|
+
|
|
125
|
+
found = false
|
|
126
|
+
node.rigor_each_child { |child| found ||= read_in?(child) }
|
|
127
|
+
found
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# `scope` with `$!` and `$@` as `entry` binds them, and unbound where `entry` leaves them unbound: the scope past
|
|
131
|
+
# a `begin` or a rescue modifier that started from `entry`, whichever way it left.
|
|
132
|
+
def restore(scope, entry)
|
|
133
|
+
return scope if NAMES.all? { |name| scope.global(name).equal?(entry.global(name)) }
|
|
134
|
+
|
|
135
|
+
NAMES.reduce(scope.forget_error_info) do |acc, name|
|
|
136
|
+
bound = entry.global(name)
|
|
137
|
+
bound ? acc.with_global(name, bound) : acc
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def rescued_type(exception_type, scope)
|
|
142
|
+
members = exception_type.is_a?(Type::Union) ? exception_type.members : [exception_type]
|
|
143
|
+
return exception_type if members.all? { |member| exception_instance?(member, scope) }
|
|
144
|
+
|
|
145
|
+
Type::Combinator.union(
|
|
146
|
+
*members.map { |member| exception_instance?(member, scope) ? member : Type::Combinator.untyped }
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# True when `type` is an instance of a class below `Exception`: one RBS places there, or a project class whose
|
|
151
|
+
# superclass chain reaches one (`class MyError < StandardError`), which the RBS environment does not know.
|
|
152
|
+
def exception_instance?(type, scope)
|
|
153
|
+
return false unless type.is_a?(Type::Nominal)
|
|
154
|
+
|
|
155
|
+
class_name = type.class_name
|
|
156
|
+
return false if own_case_equality?(class_name, scope)
|
|
157
|
+
return true if exception_class?(class_name, scope)
|
|
158
|
+
return false unless scope.known_user_class?(class_name)
|
|
159
|
+
|
|
160
|
+
scope.external_ancestor_name_candidates(class_name, mixins: false).any? do |candidates|
|
|
161
|
+
known = candidates.find { |candidate| Reflection.rbs_class_known?(candidate, scope: scope) }
|
|
162
|
+
!known.nil? && exception_class?(known, scope)
|
|
163
|
+
end
|
|
164
|
+
rescue StandardError
|
|
165
|
+
false
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# True when the program gives `class_name`, or a project ancestor of it, a singleton `===`, which `rescue` calls
|
|
169
|
+
# to match and which may accept an exception that is not an instance of it: a `def self.===` or `class << self`
|
|
170
|
+
# `def ===`, or, anywhere in the file being read, a `define_method` or `define_singleton_method` naming `===`
|
|
171
|
+
# ({.defines_case_equality?}), which the discovery tables do not place on a class, so it counts for every class.
|
|
172
|
+
def own_case_equality?(class_name, scope)
|
|
173
|
+
scope.discovery.defines_case_equality ||
|
|
174
|
+
!scope.singleton_def_through_ancestors(class_name, :===).first.nil?
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# True when `node` is a `define_method` or `define_singleton_method` call whose literal name is `===`.
|
|
178
|
+
# `Inference::ScopeIndexer` records whether a file holds one ({Scope::DiscoveryIndex#defines_case_equality}).
|
|
179
|
+
def defines_case_equality?(node)
|
|
180
|
+
return false unless node.is_a?(Prism::CallNode) && DEFINERS.include?(node.name)
|
|
181
|
+
|
|
182
|
+
name = node.arguments&.arguments&.first
|
|
183
|
+
(name.is_a?(Prism::SymbolNode) || name.is_a?(Prism::StringNode)) && name.unescaped == "==="
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def exception_class?(class_name, scope)
|
|
187
|
+
EXCEPTION_ORDERINGS.include?(scope.environment.class_ordering(class_name, "Exception"))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def backtrace_type
|
|
191
|
+
Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.nominal_of("String")])
|
|
192
|
+
end
|
|
193
|
+
private_class_method :rescued_type, :exception_instance?, :own_case_equality?, :exception_class?, :backtrace_type
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|