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,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
require_relative "multi_target_binder"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
# The auto-splat half of {BlockParameterBinder}: given a block's positional parameter shape and the one
|
|
9
|
+
# value the receiver yields, it answers the positional table the block's names are bound from.
|
|
10
|
+
#
|
|
11
|
+
# Ruby blocks (NOT lambdas) spread a single yielded array-ish value across their positionals, and CRuby
|
|
12
|
+
# decides that on the PARAMETER LIST alone ({ParameterShape#splats?}) — never on the value. So the
|
|
13
|
+
# carrier only decides how precisely each position can be filled, and a carrier this module cannot
|
|
14
|
+
# decompose lowers the positions to `Dynamic[Top]` rather than cancelling the spread (issue #1116).
|
|
15
|
+
#
|
|
16
|
+
# Each member of the carrier (a union distributes; anything else is its own single member) contributes
|
|
17
|
+
# one {Table}, and {join} folds them into the answer:
|
|
18
|
+
#
|
|
19
|
+
# - a `Type::Tuple` spreads element-wise, leading positionals (required, then optional) from the head
|
|
20
|
+
# and, with a rest present, trailing positionals from the tail with the rest absorbing the middle —
|
|
21
|
+
# the split `MultiTargetBinder` applies to `a, *r, b = tuple`, so `|*r, v|` over `[K, V]` binds `v` to
|
|
22
|
+
# `V`. A slot past the tuple is left unfilled and lands on `Dynamic[Top]`;
|
|
23
|
+
# - an `Array[T]` ({MultiTargetBinder.array_element_type}) binds `T` to every required and trailing
|
|
24
|
+
# positional, `Dynamic[Top]` to every optional one (a short array hands it its default, not `nil`),
|
|
25
|
+
# and `Array[T]` to a named rest. A short array pads the fixed positions with `nil` at runtime, so
|
|
26
|
+
# those positions are reported optimistic (issue #1093);
|
|
27
|
+
# - an opaque array carrier — a raw `Array`, an `Array[untyped]` / `Array[top]`, a `Refined` /
|
|
28
|
+
# `Difference` over either, or a `Dynamic` over any `Array` however precise — binds `Dynamic[Top]` to
|
|
29
|
+
# every position: the floor `MultiTargetBinder` binds for the same undecomposable right-hand side, and
|
|
30
|
+
# the reason `[1, 2].tap { |a, b| a.succ }` no longer reads `a` as the whole `Array` (issue #1116);
|
|
31
|
+
# - a bare `nil` binds `nil` to every position. `nil` has no `to_ary`, so CRuby passes it as the one
|
|
32
|
+
# argument and `|a, b|` sees `nil, nil` — which is also what the wrap `[nil]` pads to, so the two
|
|
33
|
+
# readings agree here, and {join} takes that `nil` back out of any position another member fills;
|
|
34
|
+
# - every other member takes the same `Dynamic[Top]` floor as the opaque one, and does NOT license the
|
|
35
|
+
# spread on its own. It is the member the union knows least about, and joining it as a value would put
|
|
36
|
+
# a nil-bearing element beside a nil-free one: `String#scan`'s `String | Array[String?]` yield joins a
|
|
37
|
+
# capture's `String?` against the whole match's `String` and fires `call.possible-nil-receiver` on the
|
|
38
|
+
# correlated-invariant `refinements[name.to_sym]`, which ADR-5 ranks above the precision.
|
|
39
|
+
#
|
|
40
|
+
# The spread needs at least one Tuple, `Array[T]` or opaque array member; a carrier with none of them
|
|
41
|
+
# ({#for} answers nil) leaves the binder's declared types alone, which binds the value to the first
|
|
42
|
+
# parameter and `Dynamic[Top]` after. So a lone value with no `to_ary` is NOT wrapped as `[value]` the
|
|
43
|
+
# way `a, b = value` wraps it: the one-value premise of auto-splat is the RBS block signature's arity,
|
|
44
|
+
# which nothing here can check against the actual yield, and a wrong wrap binds `nil` to a parameter the
|
|
45
|
+
# block receives a value in.
|
|
46
|
+
#
|
|
47
|
+
# See docs/internal-spec/inference-engine.md for the binding contract.
|
|
48
|
+
module BlockAutoSplat
|
|
49
|
+
# The positional part of a parameter list, as counts: all the auto-splat rule reads. An explicit
|
|
50
|
+
# `BlockParametersNode` list and a numbered-parameter block (`maximum` required positionals) both
|
|
51
|
+
# reduce to it, so the two spellings cannot drift.
|
|
52
|
+
ParameterShape = Data.define(:required, :optional, :post, :rest) do
|
|
53
|
+
def self.of(params_node)
|
|
54
|
+
new(required: params_node.requireds.size, optional: params_node.optionals.size,
|
|
55
|
+
post: params_node.posts.size, rest: !params_node.rest.nil?)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The `_1` / `_2` form, read as the explicit list of `arity` required positionals.
|
|
59
|
+
def self.of_arity(arity)
|
|
60
|
+
new(required: arity, optional: 0, post: 0, rest: false)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def positional_count = required + optional + post
|
|
64
|
+
|
|
65
|
+
# CRuby's own condition (`vm_callee_setup_block_arg` / `setup_parameters_complex`): a block splats a
|
|
66
|
+
# lone array argument when it has a mandatory positional (`lead + post > 0`) or more than one
|
|
67
|
+
# optional, except a bare `|a|` (the iseq's `ambiguous_param0`). So `|k, *r|`, `|*r, v|`,
|
|
68
|
+
# `|a = 1, b = 2|` and the trailing-comma `|k,|` (Prism's `ImplicitRestNode`) splat, while `|*r|`,
|
|
69
|
+
# `|a = 1, *r|` and `|a, &b|` / `|a, k: 1|` do not.
|
|
70
|
+
def splats?
|
|
71
|
+
mandatory = required + post
|
|
72
|
+
return false unless mandatory.positive? || optional > 1
|
|
73
|
+
|
|
74
|
+
!(mandatory == 1 && optional.zero? && !rest)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A positional table: the type per position, the positions bound optimistically, and the named rest's
|
|
79
|
+
# type (nil for the `Array[Dynamic[Top]]` default). One arm produces one; {join} folds them into one.
|
|
80
|
+
# Private as a name — {#for} hands the folded value out, and nothing outside constructs one.
|
|
81
|
+
Table = Data.define(:types, :optimistic_positions, :rest_type)
|
|
82
|
+
private_constant :Table
|
|
83
|
+
|
|
84
|
+
# The arm kinds that make a member an array as far as the spread is concerned, so that a union
|
|
85
|
+
# carrying one of them spreads even where its other members do not decompose.
|
|
86
|
+
ARRAY_CARRIER_ARMS = %i[tuple array opaque].freeze
|
|
87
|
+
private_constant :ARRAY_CARRIER_ARMS
|
|
88
|
+
|
|
89
|
+
module_function
|
|
90
|
+
|
|
91
|
+
# @param shape — the block's {ParameterShape}.
|
|
92
|
+
# @param carrier — the one value the receiver yields.
|
|
93
|
+
# @return the folded {Table}, or nil when no member is an array carrier and the
|
|
94
|
+
# spread must leave the declared types alone.
|
|
95
|
+
def for(shape, carrier)
|
|
96
|
+
arms = arms_of(carrier)
|
|
97
|
+
return nil if arms.nil?
|
|
98
|
+
|
|
99
|
+
join(arms.map { |member, kind| table_for(shape, member, kind) })
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def arms_of(carrier)
|
|
103
|
+
members = carrier.is_a?(Type::Union) ? carrier.members : [carrier]
|
|
104
|
+
arms = members.map { |member| [member, arm_of(member)] }
|
|
105
|
+
arms.any? { |_, kind| ARRAY_CARRIER_ARMS.include?(kind) } ? arms : nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def arm_of(member)
|
|
109
|
+
return :tuple if member.is_a?(Type::Tuple)
|
|
110
|
+
return :array if MultiTargetBinder.array_element_type(member)
|
|
111
|
+
return :opaque if opaque_array_carrier?(member)
|
|
112
|
+
return :nil_value if nil_literal?(member)
|
|
113
|
+
|
|
114
|
+
:unknown
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# A carrier that is an array the module may not decompose into per-position types: the raw `Array`, an
|
|
118
|
+
# `Array[untyped]` / `Array[top]`, a `Refined` / `Difference` over either, and a `Dynamic` over ANY
|
|
119
|
+
# carrier that would license the spread — `Dynamic[Array[Integer]]`, `Dynamic[[Integer, String]]`,
|
|
120
|
+
# `Dynamic[Array[Integer] | nil]` alike. {MultiTargetBinder.array_element_type} declines every
|
|
121
|
+
# `Dynamic` wrapper on purpose: the value is gradual, and projecting its static facet would hand each
|
|
122
|
+
# position a bare `T` that licenses the negative rules (issue #1093, ADR-5). That is a reason to floor
|
|
123
|
+
# the positions, not a reason to leave the whole value on the first parameter, so the facet decides
|
|
124
|
+
# only whether CRuby would splat.
|
|
125
|
+
def opaque_array_carrier?(type)
|
|
126
|
+
case type
|
|
127
|
+
when Type::Nominal then type.class_name == "Array"
|
|
128
|
+
when Type::Refined, Type::Difference then opaque_array_carrier?(type.base)
|
|
129
|
+
when Type::Dynamic then !arms_of(type.static_facet).nil?
|
|
130
|
+
else false
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def table_for(shape, member, kind)
|
|
135
|
+
case kind
|
|
136
|
+
when :tuple then tuple_table(shape, member.elements)
|
|
137
|
+
when :array then array_table(shape, MultiTargetBinder.array_element_type(member))
|
|
138
|
+
when :nil_value then uniform_table(shape, member)
|
|
139
|
+
else uniform_table(shape, Type::Combinator.untyped)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The split is mirrored rather than delegated to `MultiTargetBinder.decompose_tuple` because that one
|
|
144
|
+
# pads a missing slot with `Constant[nil]` and softens `X | nil`; a block slot past the tuple has
|
|
145
|
+
# always bound `Dynamic[Top]`. Without a rest the trailing positionals continue from the head (Ruby
|
|
146
|
+
# fills `|a, b = 1, c|` from the head when the tuple is long enough).
|
|
147
|
+
def tuple_table(shape, elements)
|
|
148
|
+
head = shape.required + shape.optional
|
|
149
|
+
posts = shape.post
|
|
150
|
+
tail_start = shape.rest ? [elements.size - posts, head].max : head
|
|
151
|
+
types = Array.new(head) { |i| elements[i] } + Array.new(posts) { |j| elements[tail_start + j] }
|
|
152
|
+
Table.new(types: types, optimistic_positions: [], rest_type: nil)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def array_table(shape, element)
|
|
156
|
+
required = shape.required
|
|
157
|
+
optional = shape.optional
|
|
158
|
+
posts = shape.post
|
|
159
|
+
Table.new(
|
|
160
|
+
types: Array.new(required) { element } + Array.new(optional) { Type::Combinator.untyped } +
|
|
161
|
+
Array.new(posts) { element },
|
|
162
|
+
optimistic_positions: (0...required).to_a + ((required + optional)...(required + optional + posts)).to_a,
|
|
163
|
+
rest_type: Type::Combinator.nominal_of("Array", type_args: [element])
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# One type for every position: the `Dynamic[Top]` floor of a member the module cannot decompose, or
|
|
168
|
+
# the `nil` a `nil` member hands each of them. The `nil` rest_type leaves a named rest on the binder's
|
|
169
|
+
# `Array[Dynamic[Top]]` default, which is right for both — CRuby gives `nil.tap { |a, *r| }` an empty
|
|
170
|
+
# `r`, and an opaque carrier says nothing about the leftovers.
|
|
171
|
+
def uniform_table(shape, type)
|
|
172
|
+
Table.new(types: Array.new(shape.positional_count) { type }, optimistic_positions: [], rest_type: nil)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# A single member's table is used as is. Across members, a position any member leaves unfilled (a slot
|
|
176
|
+
# past a short Tuple) or fills with `Dynamic[Top]` stays `Dynamic[Top]`, and a member's bare `nil`
|
|
177
|
+
# drops out of a position another member fills with a value, that position then being optimistic —
|
|
178
|
+
# `MultiTargetBinder`'s cross-member softening. A named rest joins only when every member supplies
|
|
179
|
+
# one; otherwise the binder's `Array[Dynamic[Top]]` default stands.
|
|
180
|
+
def join(tables)
|
|
181
|
+
types, softened = join_types(tables.map(&:types))
|
|
182
|
+
rests = tables.map(&:rest_type)
|
|
183
|
+
Table.new(
|
|
184
|
+
types: types,
|
|
185
|
+
optimistic_positions: (tables.flat_map(&:optimistic_positions) + softened).uniq,
|
|
186
|
+
rest_type: rests.include?(nil) ? nil : Type::Combinator.union(*rests)
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# @return the joined table and the positions it softened.
|
|
191
|
+
def join_types(tables)
|
|
192
|
+
return [tables.first, []] if tables.size == 1
|
|
193
|
+
|
|
194
|
+
softened = []
|
|
195
|
+
joined = Array.new(tables.map(&:size).max) do |i|
|
|
196
|
+
column = tables.map { |table| table[i] }
|
|
197
|
+
next Type::Combinator.untyped if column.any? { |t| t.nil? || t == Type::Combinator.untyped }
|
|
198
|
+
|
|
199
|
+
firm = column.reject { |t| nil_literal?(t) }
|
|
200
|
+
next Type::Combinator.union(*column) if firm.empty? || firm.size == column.size
|
|
201
|
+
|
|
202
|
+
softened << i
|
|
203
|
+
Type::Combinator.union(*firm)
|
|
204
|
+
end
|
|
205
|
+
[joined, softened]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def nil_literal?(type) = type.is_a?(Type::Constant) && type.value.nil?
|
|
209
|
+
|
|
210
|
+
# {#for} is the whole surface; the arms and the fold are this module's own business, and keeping them
|
|
211
|
+
# off the singleton stops a caller reaching past the fold for one member's table.
|
|
212
|
+
private_class_method :arms_of, :arm_of, :opaque_array_carrier?, :table_for, :tuple_table, :array_table,
|
|
213
|
+
:uniform_table, :join, :join_types, :nil_literal?
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
require_relative "../reflection"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
# Issue #1095 — the first slice of the block call-timing summary that
|
|
9
|
+
# `docs/type-specification/control-flow-analysis.md` § "Block call timing" asks for: which callees invoke
|
|
10
|
+
# their block **immediately, exactly once, and before they return**.
|
|
11
|
+
#
|
|
12
|
+
# The fact matters for the value of the call. Since #853 a block-level `break` joins the yielding call's
|
|
13
|
+
# type, unioned with what the callee returns when the block completes normally. For an arbitrary callee
|
|
14
|
+
# that normal return must stay — `each` may never yield on an empty receiver. For a callee that always
|
|
15
|
+
# yields exactly once before returning, a block whose normal completion is unreachable means the normal
|
|
16
|
+
# return is unreachable too, so the call's value is its `break` arms alone (`bot` when there are none):
|
|
17
|
+
# `[1, 2].tap { break "s" }` is `"s"`, `[1, 2].tap { raise "x" }` is `bot`.
|
|
18
|
+
#
|
|
19
|
+
# ## Catalogue
|
|
20
|
+
#
|
|
21
|
+
# Like {ClosureEscapeAnalyzer}'s `OBJECT_NON_ESCAPING` — a weaker "block is not retained" fact that `each`
|
|
22
|
+
# and `map` also satisfy — this ships as a hardcoded table keyed by `(owner, method)`, where the owner is
|
|
23
|
+
# the module whose declaration Ruby dispatches to. The sub-phase that replaces the escape catalogue with an
|
|
24
|
+
# `RBS::Extended` call-timing effect (`rbs-extended.md` reserves the bundle slot) is expected to absorb
|
|
25
|
+
# this table in the same move, which is why it is keyed by the declaring owner, the way a signature-borne
|
|
26
|
+
# effect would be, rather than by the receiver's class.
|
|
27
|
+
#
|
|
28
|
+
# The owner is `Kernel` for all three, in CRuby (`Object.instance_method(:tap).owner`) and in the bundled
|
|
29
|
+
# core RBS alike. `Object` is deliberately NOT accepted: every class inherits these methods through
|
|
30
|
+
# `Object`, so a declaration that resolves to `Object` itself is a project or gem override written on top
|
|
31
|
+
# of Kernel's, and that override is not known to yield exactly once.
|
|
32
|
+
#
|
|
33
|
+
# The summary is a pure query. It never raises on unrecognised input and answers `false` — keep the
|
|
34
|
+
# `break | normal-return` union — whenever it cannot prove the call reaches the catalogued declaration.
|
|
35
|
+
module BlockCallTiming
|
|
36
|
+
EXACTLY_ONCE_IMMEDIATE = {
|
|
37
|
+
"Kernel" => %i[tap then yield_self].freeze
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
CANDIDATE_NAMES = EXACTLY_ONCE_IMMEDIATE.values.flatten.uniq.freeze
|
|
41
|
+
private_constant :CANDIDATE_NAMES
|
|
42
|
+
|
|
43
|
+
# The roots a project monkey-patch would redefine the method on for every receiver at once.
|
|
44
|
+
PATCHABLE_ROOTS = %w[Object Kernel BasicObject].freeze
|
|
45
|
+
private_constant :PATCHABLE_ROOTS
|
|
46
|
+
|
|
47
|
+
# A class object's own metaclass ancestry adds these ahead of `Object`, so a patch on either one
|
|
48
|
+
# redefines the method for every class-object receiver.
|
|
49
|
+
SINGLETON_ROOTS = %w[Class Module].freeze
|
|
50
|
+
private_constant :SINGLETON_ROOTS
|
|
51
|
+
|
|
52
|
+
# Receiver-less (or `self.` / `Kernel.`) calls that never return normally: they raise, throw, or end
|
|
53
|
+
# the process. Anything else — including `loop`, whose RBS return is `bot` although a `StopIteration`
|
|
54
|
+
# ends it normally ({.loop_may_complete?}) — proves nothing.
|
|
55
|
+
NON_RETURNING_CALLS = %i[raise fail throw exit exit! abort].freeze
|
|
56
|
+
private_constant :NON_RETURNING_CALLS
|
|
57
|
+
|
|
58
|
+
# Issue #1107 — the nodes a `loop` body may consist of while still provably unable to raise
|
|
59
|
+
# `StopIteration`: none of them dispatches a method, so nothing in the body can run code that raises.
|
|
60
|
+
# Everything else — every call (operators and `[]` included), `yield`, `super`, interpolation, a splat,
|
|
61
|
+
# a constant read (`const_missing`), a `rescue` — may, and declines.
|
|
62
|
+
STOP_ITERATION_FREE_NODES = [
|
|
63
|
+
Prism::StatementsNode, Prism::ParenthesesNode, Prism::ArgumentsNode,
|
|
64
|
+
Prism::BreakNode, Prism::NextNode, Prism::RedoNode, Prism::ReturnNode,
|
|
65
|
+
Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode,
|
|
66
|
+
Prism::StringNode, Prism::SymbolNode, Prism::NilNode, Prism::TrueNode, Prism::FalseNode, Prism::SelfNode,
|
|
67
|
+
Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode,
|
|
68
|
+
Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode,
|
|
69
|
+
Prism::IfNode, Prism::UnlessNode, Prism::ElseNode, Prism::AndNode, Prism::OrNode, Prism::ArrayNode
|
|
70
|
+
].freeze
|
|
71
|
+
private_constant :STOP_ITERATION_FREE_NODES
|
|
72
|
+
|
|
73
|
+
module_function
|
|
74
|
+
|
|
75
|
+
# Issue #1107 — whether a `loop` call can complete normally although its declared return is `bot`.
|
|
76
|
+
#
|
|
77
|
+
# `Kernel#loop` is `() { () -> void } -> bot` in core RBS, yet it rescues a `StopIteration` its block
|
|
78
|
+
# raises and returns that exception's `result` — the enumerator-draining idiom `loop { out << e.next }`
|
|
79
|
+
# returns `e`'s own `each` value. So the declared `bot` holds only for a body that provably cannot raise
|
|
80
|
+
# `StopIteration`: one built from {STOP_ITERATION_FREE_NODES} alone (`loop {}`, `loop { break 1 }`), in a
|
|
81
|
+
# block that declares no parameters. A `&blk` block-pass has no body to prove anything about and completes
|
|
82
|
+
# too.
|
|
83
|
+
#
|
|
84
|
+
# Gated on the spellings that reach the private `Kernel#loop` — receiver-less, `self.`, `Kernel.`,
|
|
85
|
+
# `::Kernel.` — so `obj.loop { ... }` is some other method, whose declared `bot` is that author's
|
|
86
|
+
# promise. A project redefinition of `loop` itself is not excluded: widening its `bot` costs precision,
|
|
87
|
+
# never a false positive.
|
|
88
|
+
def loop_may_complete?(call_node)
|
|
89
|
+
return false unless call_node.name == :loop && kernel_spelled_receiver?(call_node.receiver)
|
|
90
|
+
|
|
91
|
+
block = call_node.block
|
|
92
|
+
return false if block.nil?
|
|
93
|
+
return true unless block.is_a?(Prism::BlockNode)
|
|
94
|
+
# A parameter default (`|v = e.next|`) is evaluated on every iteration, since `loop` yields no
|
|
95
|
+
# arguments. Rather than walk the parameter list, any declared parameter widens.
|
|
96
|
+
return true if block.parameters
|
|
97
|
+
return false if block.body.nil?
|
|
98
|
+
|
|
99
|
+
may_raise_stop_iteration?(block.body)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def may_raise_stop_iteration?(node)
|
|
103
|
+
return true unless STOP_ITERATION_FREE_NODES.any? { |klass| node.is_a?(klass) }
|
|
104
|
+
|
|
105
|
+
node.compact_child_nodes.any? { |child| may_raise_stop_iteration?(child) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Cheap name-only pre-gate, so a call that cannot be catalogued pays nothing further.
|
|
109
|
+
def candidate_name?(method_name)
|
|
110
|
+
CANDIDATE_NAMES.include?(method_name)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def exactly_once_owner?(owner_name, method_name)
|
|
114
|
+
methods = EXACTLY_ONCE_IMMEDIATE[owner_name.to_s.delete_prefix("::")]
|
|
115
|
+
methods ? methods.include?(method_name) : false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Whether a call of `method_name` on `receiver_type` reaches a catalogued exactly-once declaration.
|
|
119
|
+
#
|
|
120
|
+
# Gated on the RESOLVED owner, not the name: a class that defines its own `tap` (in source, in a
|
|
121
|
+
# project `sig/`, or through a project ancestor), a top-level `def tap` (a private `Object` method,
|
|
122
|
+
# ahead of `Kernel` in every MRO) and an `Object` / `Kernel` monkey-patch all decline. A union receiver
|
|
123
|
+
# qualifies only when every member does; `Dynamic` and the other carriers that name no class decline.
|
|
124
|
+
def exactly_once_call?(receiver_type:, method_name:, scope:)
|
|
125
|
+
return false unless candidate_name?(method_name)
|
|
126
|
+
return false if scope.nil? || project_redefines_root?(method_name, scope)
|
|
127
|
+
|
|
128
|
+
targets = receiver_targets(receiver_type)
|
|
129
|
+
return false if targets.nil? || targets.empty?
|
|
130
|
+
|
|
131
|
+
targets.all? { |class_name, kind| resolves_to_catalogue?(class_name, kind, method_name, scope) }
|
|
132
|
+
rescue StandardError
|
|
133
|
+
false
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Syntactic proof that a block body cannot complete normally: every path through it ends in a
|
|
137
|
+
# block-level `break` (which targets the yielding call), `return`, `redo`, or a non-returning Kernel call
|
|
138
|
+
# ({NON_RETURNING_CALLS}) — or in an expression that must evaluate one of those first. This is ANDed
|
|
139
|
+
# with the block-return pass's `bot`, never used alone, because a `bot` can also flow out of a callee's
|
|
140
|
+
# declared return that is not a promise the call never completes (`Kernel#loop` is `-> bot`, yet a
|
|
141
|
+
# `StopIteration` from `e.next` ends it normally). A shape the walk does not recognise declines.
|
|
142
|
+
#
|
|
143
|
+
# Only unconditionally evaluated children are descended: a nested block, lambda, `def` or loop is never
|
|
144
|
+
# entered (its body may not run, and it retargets `break`), a call's block is not a child the call
|
|
145
|
+
# promises to run, and `&&` / `||` count only their left operand. A `begin` with `rescue` qualifies only
|
|
146
|
+
# when its body and every rescue clause must exit; an `ensure` that must exit qualifies on its own.
|
|
147
|
+
def never_completes_normally?(node, scope)
|
|
148
|
+
case node
|
|
149
|
+
when Prism::StatementsNode then node.body.any? { |statement| never_completes_normally?(statement, scope) }
|
|
150
|
+
when Prism::BreakNode, Prism::ReturnNode, Prism::RedoNode, Prism::RetryNode then true
|
|
151
|
+
when Prism::ParenthesesNode then never_completes_normally?(node.body, scope)
|
|
152
|
+
when Prism::CallNode then call_never_returns?(node, scope)
|
|
153
|
+
when Prism::IfNode, Prism::UnlessNode then conditional_never_completes?(node, scope)
|
|
154
|
+
when Prism::AndNode, Prism::OrNode then never_completes_normally?(node.left, scope)
|
|
155
|
+
when Prism::BeginNode then begin_never_completes?(node, scope)
|
|
156
|
+
when Prism::ArrayNode then node.elements.any? { |element| never_completes_normally?(element, scope) }
|
|
157
|
+
when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode
|
|
158
|
+
never_completes_normally?(node.value, scope)
|
|
159
|
+
else false
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Deliberately coarse: ANY project definition of the name — top-level, on any class or module, either
|
|
164
|
+
# side, or a `pre_eval:` patch — declines. Resolving the call-site `self`'s ancestry precisely buys
|
|
165
|
+
# nothing for names this rare, and the block-return pass is no independent check here: it types a
|
|
166
|
+
# self-call to an overridden `raise` as Kernel's `bot` too, so a missed override is a wrong `bot`.
|
|
167
|
+
# {LastLine.reads_line?} declines on it the same way for `gets` and `readline`.
|
|
168
|
+
def project_defines_anywhere?(method_name, scope)
|
|
169
|
+
return true if scope.top_level_def_for(method_name)
|
|
170
|
+
# `discovered_methods` withholds a plain cross-file `def` (the ADR-17 monkey-patch contract); the
|
|
171
|
+
# def-node tables still carry it, on both sides.
|
|
172
|
+
return true if [scope.discovered_methods, scope.discovered_def_nodes, scope.discovered_singleton_def_nodes]
|
|
173
|
+
.any? { |tables| tables.any? { |_class_name, table| table.key?(method_name) } }
|
|
174
|
+
|
|
175
|
+
patched = scope.environment&.project_patched_methods
|
|
176
|
+
!patched.nil? && patched.by_key.any? { |(_class_name, name, _kind), _entry| name == method_name }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
class << self
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def call_never_returns?(node, scope)
|
|
183
|
+
return true if never_completes_normally?(node.receiver, scope)
|
|
184
|
+
return true if node.arguments&.arguments&.any? { |argument| never_completes_normally?(argument, scope) }
|
|
185
|
+
return false unless NON_RETURNING_CALLS.include?(node.name)
|
|
186
|
+
|
|
187
|
+
kernel_spelled_receiver?(node.receiver) && !project_defines_anywhere?(node.name, scope)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Implicit self, `self.`, or the `Kernel` module itself (`Kernel.` or the root-anchored `::Kernel.`) —
|
|
191
|
+
# the spellings that reach Kernel's function.
|
|
192
|
+
def kernel_spelled_receiver?(receiver)
|
|
193
|
+
case receiver
|
|
194
|
+
when nil, Prism::SelfNode then true
|
|
195
|
+
when Prism::ConstantReadNode then receiver.name == :Kernel
|
|
196
|
+
when Prism::ConstantPathNode then receiver.parent.nil? && receiver.name == :Kernel
|
|
197
|
+
else false
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def conditional_never_completes?(node, scope)
|
|
202
|
+
return true if never_completes_normally?(node.predicate, scope)
|
|
203
|
+
|
|
204
|
+
alternative = node.is_a?(Prism::IfNode) ? node.subsequent : node.else_clause
|
|
205
|
+
return false if node.statements.nil? || alternative.nil?
|
|
206
|
+
|
|
207
|
+
never_completes_normally?(node.statements, scope) && branch_never_completes?(alternative, scope)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def branch_never_completes?(branch, scope)
|
|
211
|
+
case branch
|
|
212
|
+
when Prism::ElseNode then never_completes_normally?(branch.statements, scope)
|
|
213
|
+
else never_completes_normally?(branch, scope)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def begin_never_completes?(node, scope)
|
|
218
|
+
return true if node.ensure_clause && never_completes_normally?(node.ensure_clause.statements, scope)
|
|
219
|
+
return false unless never_completes_normally?(node.statements, scope)
|
|
220
|
+
|
|
221
|
+
rescue_clause = node.rescue_clause
|
|
222
|
+
while rescue_clause
|
|
223
|
+
return false unless never_completes_normally?(rescue_clause.statements, scope)
|
|
224
|
+
|
|
225
|
+
rescue_clause = rescue_clause.subsequent
|
|
226
|
+
end
|
|
227
|
+
true
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def project_redefines_root?(method_name, scope)
|
|
231
|
+
return true if scope.top_level_def_for(method_name)
|
|
232
|
+
|
|
233
|
+
patched = scope.environment&.project_patched_methods
|
|
234
|
+
PATCHABLE_ROOTS.any? do |root|
|
|
235
|
+
scope.discovered_method?(root, method_name, :instance) ||
|
|
236
|
+
patched&.lookup(class_name: root, method_name: method_name, kind: :instance)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# `[class_name, :instance | :singleton]` pairs the receiver can dispatch through, or nil when some
|
|
241
|
+
# part of it names no class. `Tuple` / `HashShape` project to `Array` / `Hash`, `Constant[v]` through
|
|
242
|
+
# the value's class, the same projection {ClosureEscapeAnalyzer} uses.
|
|
243
|
+
def receiver_targets(receiver_type)
|
|
244
|
+
case receiver_type
|
|
245
|
+
when Type::Union
|
|
246
|
+
members = receiver_type.members.map { |member| receiver_targets(member) }
|
|
247
|
+
members.any?(&:nil?) ? nil : members.flatten(1)
|
|
248
|
+
when Type::Nominal then [[receiver_type.class_name, :instance]]
|
|
249
|
+
when Type::Singleton then [[receiver_type.class_name, :singleton]]
|
|
250
|
+
when Type::Tuple then [["Array", :instance]]
|
|
251
|
+
when Type::HashShape then [["Hash", :instance]]
|
|
252
|
+
when Type::Constant then [[receiver_type.value.class.name, :instance]]
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def resolves_to_catalogue?(class_name, kind, method_name, scope)
|
|
257
|
+
return false if class_name.nil?
|
|
258
|
+
return false if scope.discovered_method_through_ancestors?(class_name, method_name, kind)
|
|
259
|
+
|
|
260
|
+
if kind == :singleton
|
|
261
|
+
definition = Rigor::Reflection.singleton_method_definition(class_name, method_name, scope: scope)
|
|
262
|
+
return false if singleton_ancestor_patched?(class_name, method_name, scope)
|
|
263
|
+
|
|
264
|
+
return declared_on_catalogue?(definition, method_name)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
definition = Rigor::Reflection.instance_method_definition(class_name, method_name, scope: scope)
|
|
268
|
+
if definition
|
|
269
|
+
return false if rbs_ancestor_patched?(class_name, method_name, scope)
|
|
270
|
+
|
|
271
|
+
return declared_on_catalogue?(definition, method_name)
|
|
272
|
+
end
|
|
273
|
+
return false if Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
274
|
+
|
|
275
|
+
project_class_resolves_to_catalogue?(class_name, method_name, scope)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def declared_on_catalogue?(definition, method_name)
|
|
279
|
+
return false if definition.nil? || !definition.respond_to?(:defined_in)
|
|
280
|
+
|
|
281
|
+
exactly_once_owner?(definition.defined_in, method_name)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# A project class the RBS environment does not know: its project ancestry was already cleared by the
|
|
285
|
+
# discovery walk, so the answer rests on the ancestors it reaches OUTSIDE the project. Each is asked
|
|
286
|
+
# on its own terms; an ancestor nobody can resolve is uncertainty and declines. No external ancestor
|
|
287
|
+
# at all is the implicit `< Object`, which reaches `Kernel`.
|
|
288
|
+
def project_class_resolves_to_catalogue?(class_name, method_name, scope)
|
|
289
|
+
return false unless scope.known_user_class?(class_name)
|
|
290
|
+
|
|
291
|
+
scope.external_ancestor_name_candidates(class_name).all? do |candidates|
|
|
292
|
+
external_ancestor_resolves_to_catalogue?(candidates, method_name, scope)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def external_ancestor_resolves_to_catalogue?(candidates, method_name, scope)
|
|
297
|
+
known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
|
|
298
|
+
return false if known.nil?
|
|
299
|
+
|
|
300
|
+
definition = Rigor::Reflection.instance_method_definition(known, method_name, scope: scope)
|
|
301
|
+
# A mixin whose RBS does not mention the method contributes nothing; a CLASS that lacks it is a
|
|
302
|
+
# `BasicObject` lineage, where the call does not reach `Kernel` at all.
|
|
303
|
+
return false if rbs_ancestor_patched?(known, method_name, scope)
|
|
304
|
+
return scope.environment.rbs_module?(known) if definition.nil?
|
|
305
|
+
|
|
306
|
+
declared_on_catalogue?(definition, method_name)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# The RBS declaration says where the method was DECLARED; a project reopening of a core ancestor
|
|
310
|
+
# (`module Enumerable; def tap = :x; end`) redefines it without touching RBS, and discovery records
|
|
311
|
+
# it under the ancestor's own name. So every RBS ancestor — mixins included — is asked.
|
|
312
|
+
def rbs_ancestor_patched?(class_name, method_name, scope)
|
|
313
|
+
names_patched?(rbs_ancestor_names(class_name, scope), method_name, :instance, scope)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# A class object dispatches through its own and its superclasses' singleton methods, then through
|
|
317
|
+
# `Class`, `Module`, `Object`, `Kernel` as instance methods.
|
|
318
|
+
def singleton_ancestor_patched?(class_name, method_name, scope)
|
|
319
|
+
names_patched?(rbs_ancestor_names(class_name, scope), method_name, :singleton, scope) ||
|
|
320
|
+
names_patched?(SINGLETON_ROOTS, method_name, :instance, scope)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def names_patched?(names, method_name, kind, scope)
|
|
324
|
+
patched = scope.environment&.project_patched_methods
|
|
325
|
+
names.any? do |name|
|
|
326
|
+
scope.discovered_method?(name, method_name, kind) ||
|
|
327
|
+
patched&.lookup(class_name: name, method_name: method_name, kind: kind)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def rbs_ancestor_names(class_name, scope)
|
|
332
|
+
loader = scope.environment&.rbs_loader
|
|
333
|
+
loader ? loader.ancestor_names_for(class_name.to_s) : []
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|