rigortype 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -2
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
- data/docs/handbook/01-getting-started.md +6 -3
- data/docs/handbook/11-sig-gen.md +85 -3
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +455 -113
- data/docs/manual/03-configuration.md +76 -5
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +21 -2
- data/docs/manual/10-mcp-server.md +38 -2
- data/docs/manual/12-caching.md +4 -3
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +32 -15
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/baseline.rb +24 -27
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
- data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
- data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules.rb +720 -141
- data/lib/rigor/analysis/crash_signature.rb +45 -3
- data/lib/rigor/analysis/dependency_recorder.rb +237 -80
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/edit_replay.rb +93 -6
- data/lib/rigor/analysis/fact_store.rb +3 -3
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
- data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
- data/lib/rigor/analysis/incremental_session.rb +306 -53
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +39 -3
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
- data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
- data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
- data/lib/rigor/analysis/runner.rb +298 -109
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
- data/lib/rigor/analysis/template_unit_positions.rb +2 -3
- data/lib/rigor/analysis/template_units.rb +9 -1
- data/lib/rigor/analysis/worker_session.rb +65 -71
- data/lib/rigor/bleeding_edge.rb +22 -10
- data/lib/rigor/builtins/imported_refinements.rb +6 -8
- data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
- data/lib/rigor/builtins/regex_refinement.rb +4 -6
- data/lib/rigor/cache/descriptor.rb +6 -5
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +95 -17
- data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
- data/lib/rigor/cache/rbs_constant_table.rb +1 -1
- data/lib/rigor/cache/rbs_descriptor.rb +90 -4
- data/lib/rigor/cache/rbs_environment.rb +8 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
- data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
- data/lib/rigor/cache/store.rb +11 -10
- data/lib/rigor/ci_detector.rb +1 -1
- data/lib/rigor/cli/annotate_command.rb +24 -19
- data/lib/rigor/cli/baseline_command.rb +160 -57
- data/lib/rigor/cli/catalogue.rb +256 -0
- data/lib/rigor/cli/check_command.rb +112 -104
- data/lib/rigor/cli/check_runner_factory.rb +2 -2
- data/lib/rigor/cli/command.rb +15 -1
- data/lib/rigor/cli/coverage_command.rb +21 -8
- data/lib/rigor/cli/coverage_mutation.rb +5 -7
- data/lib/rigor/cli/coverage_renderer.rb +2 -1
- data/lib/rigor/cli/coverage_scan.rb +3 -5
- data/lib/rigor/cli/diff_command.rb +25 -20
- data/lib/rigor/cli/docs_command.rb +1 -2
- data/lib/rigor/cli/doctor_command.rb +12 -12
- data/lib/rigor/cli/effects_command.rb +50 -40
- data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
- data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
- data/lib/rigor/cli/effects_renderer.rb +2 -1
- data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
- data/lib/rigor/cli/explain_command.rb +18 -8
- data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
- data/lib/rigor/cli/json_document.rb +41 -0
- data/lib/rigor/cli/lens_command.rb +29 -10
- data/lib/rigor/cli/lsp_command.rb +250 -23
- data/lib/rigor/cli/mcp_command.rb +12 -7
- data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
- data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
- data/lib/rigor/cli/options.rb +40 -1
- data/lib/rigor/cli/plugin_command.rb +93 -46
- data/lib/rigor/cli/plugins_command.rb +17 -18
- data/lib/rigor/cli/plugins_renderer.rb +10 -6
- data/lib/rigor/cli/protection_fork_scan.rb +2 -2
- data/lib/rigor/cli/protection_renderer.rb +2 -1
- data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
- data/lib/rigor/cli/sig_gen_command.rb +92 -27
- data/lib/rigor/cli/skill_command.rb +32 -3
- data/lib/rigor/cli/skill_deep_probe.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +14 -9
- data/lib/rigor/cli/triage_command.rb +10 -11
- data/lib/rigor/cli/triage_renderer.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +16 -11
- data/lib/rigor/cli/type_of_project_scope.rb +1 -4
- data/lib/rigor/cli/type_of_renderer.rb +4 -1
- data/lib/rigor/cli/type_scan_command.rb +14 -9
- data/lib/rigor/cli/type_scan_renderer.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +27 -17
- data/lib/rigor/cli/upgrade_command.rb +5 -0
- data/lib/rigor/cli.rb +214 -56
- data/lib/rigor/config_audit.rb +33 -19
- data/lib/rigor/configuration/severity_profile.rb +14 -5
- data/lib/rigor/configuration.rb +104 -35
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/effects/definition_context.rb +0 -2
- data/lib/rigor/effects/envelope.rb +1 -1
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/mutation_classifier.rb +4 -7
- data/lib/rigor/effects/propagator.rb +1 -1
- data/lib/rigor/effects/registry.rb +2 -2
- data/lib/rigor/effects/snapshot.rb +1 -1
- data/lib/rigor/effects/taint_cause.rb +0 -2
- data/lib/rigor/effects/unit_scan.rb +9 -13
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
- data/lib/rigor/environment/class_registry.rb +3 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/core_root.rb +77 -0
- data/lib/rigor/environment/default_libraries.rb +18 -1
- data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
- data/lib/rigor/environment/gated_signature_guard.rb +1 -1
- data/lib/rigor/environment/gated_signature_plan.rb +5 -2
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/lockfile_resolver.rb +28 -12
- data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
- data/lib/rigor/environment/member_consistency.rb +56 -7
- data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
- data/lib/rigor/environment/project_path_gems.rb +124 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
- data/lib/rigor/environment/rbs_loader.rb +772 -110
- data/lib/rigor/environment/reflection.rb +7 -7
- data/lib/rigor/environment/required_features.rb +0 -3
- data/lib/rigor/environment/ruby_library_roots.rb +1 -1
- data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
- data/lib/rigor/environment.rb +169 -42
- data/lib/rigor/flow_contribution/merger.rb +4 -4
- data/lib/rigor/inference/acceptance.rb +136 -40
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/block_run_count.rb +185 -0
- data/lib/rigor/inference/budget_trace.rb +8 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
- data/lib/rigor/inference/callee_write_census.rb +815 -0
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +258 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/deep_erasure.rb +39 -7
- data/lib/rigor/inference/def_handle.rb +1 -1
- data/lib/rigor/inference/def_node_resolver.rb +33 -15
- data/lib/rigor/inference/def_return_typer.rb +47 -5
- data/lib/rigor/inference/definer_resolution.rb +19 -9
- data/lib/rigor/inference/definers_index.rb +162 -0
- data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
- data/lib/rigor/inference/element_read_widening.rb +49 -3
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1559 -320
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +81 -5
- data/lib/rigor/inference/global_writer_reach.rb +260 -0
- data/lib/rigor/inference/hkt_body.rb +1 -2
- data/lib/rigor/inference/hkt_body_parser.rb +3 -5
- data/lib/rigor/inference/hkt_reducer.rb +2 -2
- data/lib/rigor/inference/hkt_registry.rb +3 -5
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +61 -15
- data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
- data/lib/rigor/inference/ivar_writer_census.rb +154 -8
- data/lib/rigor/inference/key_presence_guard.rb +51 -24
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +66 -14
- data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
- data/lib/rigor/inference/match_rebinding.rb +28 -5
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +123 -25
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +24 -3
- data/lib/rigor/inference/multi_target_binder.rb +2 -2
- data/lib/rigor/inference/mutation_rejoin.rb +240 -4
- data/lib/rigor/inference/mutation_widening.rb +199 -29
- data/lib/rigor/inference/narrowing.rb +238 -50
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +134 -9
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
- data/lib/rigor/inference/pre_eval_constants.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +30 -9
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/project_yielder.rb +11 -17
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_alias.rb +20 -0
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +287 -0
- data/lib/rigor/inference/remover_narrowing.rb +224 -0
- data/lib/rigor/inference/rewrite_mutation.rb +53 -2
- data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
- data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
- data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2213 -226
- data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/set_mutation.rb +189 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +3268 -358
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
- data/lib/rigor/inference/toplevel_def_census.rb +67 -0
- data/lib/rigor/inference/unknown_store_widening.rb +103 -4
- data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -9
- data/lib/rigor/language_server/buffer_table.rb +3 -3
- data/lib/rigor/language_server/completion_provider.rb +26 -20
- data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -2
- data/lib/rigor/language_server/hover_provider.rb +1 -7
- data/lib/rigor/language_server/hover_renderer.rb +6 -8
- data/lib/rigor/language_server/project_context.rb +39 -1
- data/lib/rigor/language_server/publish_batcher.rb +3 -4
- data/lib/rigor/language_server/selection_range_provider.rb +2 -4
- data/lib/rigor/language_server/server.rb +7 -3
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/plugins.rb +1 -1
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/mcp/server.rb +73 -2
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/bundled_catalog.rb +1 -12
- data/lib/rigor/plugin/io_boundary.rb +26 -4
- data/lib/rigor/plugin/loader.rb +25 -8
- data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
- data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
- data/lib/rigor/plugin/manifest.rb +16 -16
- data/lib/rigor/plugin/registry.rb +16 -5
- data/lib/rigor/plugin/services.rb +2 -3
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
- data/lib/rigor/plugin/trust_policy.rb +48 -2
- data/lib/rigor/plugin/type_node_resolver.rb +2 -4
- data/lib/rigor/plugin.rb +6 -8
- data/lib/rigor/plugin_gap_advisory.rb +5 -1
- data/lib/rigor/project_environment.rb +16 -2
- data/lib/rigor/protection/analysis_guard.rb +1 -1
- data/lib/rigor/protection/discovery_seed.rb +44 -5
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
- data/lib/rigor/rbs_extended/reporter.rb +27 -3
- data/lib/rigor/rbs_extended.rb +2 -3
- data/lib/rigor/reflection/constant_ancestors.rb +81 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +32 -1
- data/lib/rigor/run_scoped_slots.rb +38 -0
- data/lib/rigor/runtime/jit.rb +1 -1
- data/lib/rigor/scope/discovery_index.rb +168 -8
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +901 -54
- data/lib/rigor/scope.rb +644 -37
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +18 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
- data/lib/rigor/sig_gen/generator.rb +588 -66
- data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
- data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
- data/lib/rigor/sig_gen/layout_index.rb +3 -3
- data/lib/rigor/sig_gen/method_candidate.rb +37 -4
- data/lib/rigor/sig_gen/observation_collector.rb +14 -7
- data/lib/rigor/sig_gen/observed_call.rb +1 -1
- data/lib/rigor/sig_gen/path_mapper.rb +6 -0
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/renderer.rb +15 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
- data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
- data/lib/rigor/sig_gen/write_reporter.rb +63 -14
- data/lib/rigor/sig_gen/write_result.rb +23 -12
- data/lib/rigor/sig_gen/writer.rb +238 -37
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/acceptance_router.rb +2 -2
- data/lib/rigor/type/accepts_result.rb +1 -1
- data/lib/rigor/type/app.rb +4 -6
- data/lib/rigor/type/combinator.rb +173 -21
- data/lib/rigor/type/constant.rb +4 -5
- data/lib/rigor/type/hash_shape.rb +21 -40
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type/union.rb +1 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/identifier.rb +1 -2
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node/string_literal.rb +1 -2
- data/lib/rigor/type_node/symbol_literal.rb +1 -2
- data/lib/rigor/type_node/union.rb +3 -6
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/value_semantics.rb +1 -10
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
- data/sig/rigor/analysis/baseline.rbs +0 -3
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -2
- data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/cli/diff_command.rbs +1 -1
- data/sig/rigor/cli/explain_command.rbs +1 -1
- data/sig/rigor/effects/registry.rbs +2 -2
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +53 -18
- data/sig/rigor/plugin/effect_attribution.rbs +3 -3
- data/sig/rigor/plugin/effect_edge.rbs +1 -1
- data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +9 -9
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/plugin/protocol_contract.rbs +3 -7
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +43 -4
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-plugin-author/SKILL.md +4 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +82 -3
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../type"
|
|
4
4
|
require_relative "range_constant"
|
|
5
5
|
require_relative "content_join"
|
|
6
|
+
require_relative "../typing_state"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Inference
|
|
@@ -75,41 +76,70 @@ module Rigor
|
|
|
75
76
|
}.freeze
|
|
76
77
|
private_constant :TYPE_HANDLERS
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
# ADR-129 WD1 — a Frame: {#refusing_unread} installs the kind of unread shape that answers `no` for one block
|
|
80
|
+
# and restores the previous one on the way out. It changes answers (`unread_shape_maybe` reads it), and the
|
|
81
|
+
# installed value is a Symbol (`:all`, `:array`) or nil.
|
|
82
|
+
UNREAD_SHAPE_REFUSED = TypingState.frame(
|
|
83
|
+
:acceptance_unread_shape_refused,
|
|
84
|
+
reason: "installed around a block by refusing_unread; read by unread_shape_maybe, which answers no for " \
|
|
85
|
+
"the refused kind of unread shape instead of maybe",
|
|
86
|
+
witness: "spec/fixtures/typing_state_witnesses/acceptance_unread_shape_refused.rb"
|
|
87
|
+
)
|
|
88
|
+
private_constant :UNREAD_SHAPE_REFUSED
|
|
89
|
+
|
|
90
|
+
# Whether a `maybe` for `param` against `arg` rests on a shape that could not read its argument: a record
|
|
91
|
+
# against a `Hash` with a gradual arm ({#gradual_hash?}) or an open `HashShape`, or a tuple against an
|
|
92
|
+
# `Array` of unknown arity ({#array_carrier?}). The shape cannot prove the key set or the length, and its
|
|
93
|
+
# `maybe` climbs through whatever holds it. `OverloadSelector`'s strict pass reads that `maybe` as no
|
|
94
|
+
# evidence for the overload, so this re-asks with each such shape answering `no` and reports whether the
|
|
95
|
+
# pair then fails. A `maybe` from anywhere else (a subclass only the Ruby source declares, a splat under a
|
|
96
|
+
# parameter that is no shape) survives the re-ask and still counts.
|
|
97
|
+
def maybe_rests_on_unread_shape?(param, arg)
|
|
98
|
+
return false unless rests_on_unread_hash?(param, arg) || rests_on_unread_array?(param, arg)
|
|
99
|
+
|
|
100
|
+
refusing_unread(:all) { accepts(param, arg, mode: :gradual).no? }
|
|
101
|
+
end
|
|
90
102
|
|
|
91
|
-
|
|
103
|
+
# Whether any of `types` holds an `Array` a tuple cannot read ({#array_carrier?}), so a match may rest on
|
|
104
|
+
# the tuple's `maybe`. `OverloadSelector`'s gradual pass asks before re-matching under
|
|
105
|
+
# {#refusing_unread_arrays}.
|
|
106
|
+
def unread_array_in?(types)
|
|
107
|
+
types.any? { |type| contains_type?(type) { |inner| array_carrier?(inner) } }
|
|
92
108
|
end
|
|
93
109
|
|
|
110
|
+
# Runs the block with each tuple answering `no`, not `maybe`, for an `Array` of unknown arity. A record's
|
|
111
|
+
# `maybe` for an unread hash is untouched.
|
|
112
|
+
def refusing_unread_arrays(&) = refusing_unread(:array, &)
|
|
113
|
+
|
|
94
114
|
# rubocop:disable-next Metrics/ClassLength
|
|
95
115
|
class << self
|
|
96
116
|
private
|
|
97
117
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def refusing_unread_hashes
|
|
102
|
-
previous = Thread.current[UNREAD_HASH_REFUSED_KEY]
|
|
103
|
-
Thread.current[UNREAD_HASH_REFUSED_KEY] = true
|
|
104
|
-
yield
|
|
105
|
-
ensure
|
|
106
|
-
Thread.current[UNREAD_HASH_REFUSED_KEY] = previous
|
|
118
|
+
def rests_on_unread_hash?(param, arg)
|
|
119
|
+
contains_type?(param) { |type| type.is_a?(Type::HashShape) } &&
|
|
120
|
+
contains_type?(arg) { |type| unread_hash?(type) }
|
|
107
121
|
end
|
|
108
122
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
def rests_on_unread_array?(param, arg)
|
|
124
|
+
contains_type?(param) { |type| type.is_a?(Type::Tuple) } &&
|
|
125
|
+
contains_type?(arg) { |type| array_carrier?(type) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The flag lives on the thread, not on a parameter, because the shape may sit any number of `accepts`
|
|
129
|
+
# calls below the one asked (a tuple element, a `Hash` value, a union member), and those calls go
|
|
130
|
+
# through each carrier's `Type#accepts`. It is set only for the duration of the block. `kind` is `:all`
|
|
131
|
+
# (records and tuples) or `:array` (tuples only).
|
|
132
|
+
def refusing_unread(kind, &)
|
|
133
|
+
TypingState.current.with(UNREAD_SHAPE_REFUSED, kind, &)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The `maybe` a shape of `kind` (`:hash` or `:array`) answers for an argument it cannot read, or `no`
|
|
137
|
+
# inside {#refusing_unread} for that kind.
|
|
138
|
+
def unread_shape_maybe(mode, reason, kind)
|
|
139
|
+
refused = TypingState.current[UNREAD_SHAPE_REFUSED]
|
|
140
|
+
if refused == :all || refused == kind
|
|
141
|
+
return Type::AcceptsResult.no(mode: mode, reasons: "#{reason} (refused as evidence)")
|
|
142
|
+
end
|
|
113
143
|
|
|
114
144
|
Type::AcceptsResult.maybe(mode: mode, reasons: reason)
|
|
115
145
|
end
|
|
@@ -402,6 +432,19 @@ module Rigor
|
|
|
402
432
|
CLASS_OBJECT_NOMINALS = %w[Module Class Object Kernel BasicObject].freeze
|
|
403
433
|
private_constant :CLASS_OBJECT_NOMINALS
|
|
404
434
|
|
|
435
|
+
# Issue #2855 — whether `type` is an unparameterised `Class` or `Module` nominal: a class object whose
|
|
436
|
+
# identity inference lost (`const_get` kept by `is_a?(Class)`, an `is_a?(Module)` guard). `Acceptance` still
|
|
437
|
+
# answers `no` for it against a `Singleton` (it is a supertype, not a subtype); `call.argument-type-mismatch`
|
|
438
|
+
# reads it to withhold that verdict. `Object`, `BasicObject` and `Kernel` are left out: they mean "any value",
|
|
439
|
+
# and `Object.new` against `singleton(X)` is a real mismatch.
|
|
440
|
+
CLASS_OR_MODULE_NOMINALS = %w[Class Module].freeze
|
|
441
|
+
private_constant :CLASS_OR_MODULE_NOMINALS
|
|
442
|
+
|
|
443
|
+
def class_or_module_nominal?(type)
|
|
444
|
+
type.is_a?(Type::Nominal) && type.type_args.empty? && CLASS_OR_MODULE_NOMINALS.include?(type.class_name)
|
|
445
|
+
end
|
|
446
|
+
public :class_or_module_nominal?
|
|
447
|
+
|
|
405
448
|
def accepts_nominal_from_singleton(self_type, other_type, mode)
|
|
406
449
|
if CLASS_OBJECT_NOMINALS.include?(self_type.class_name)
|
|
407
450
|
return Type::AcceptsResult.yes(
|
|
@@ -469,9 +512,10 @@ module Rigor
|
|
|
469
512
|
end
|
|
470
513
|
|
|
471
514
|
# Returns `other_type` rewritten so its type_args have the same arity as `self_type.type_args`, or
|
|
472
|
-
# `nil` if no projection is known. Today
|
|
473
|
-
# general RBS-driven implementation that consults
|
|
474
|
-
# subclass / module-include relations is the principled
|
|
515
|
+
# `nil` if no projection is known. Today the Hash → Enumerable and ArithmeticSequence → Enumerator
|
|
516
|
+
# projections are hand-rolled; a general RBS-driven implementation that consults
|
|
517
|
+
# `definition.ancestors[i].args` for arbitrary subclass / module-include relations is the principled
|
|
518
|
+
# follow-up.
|
|
475
519
|
def project_to_target_arity(self_type, other_type)
|
|
476
520
|
return nil if self_type.type_args.size == other_type.type_args.size
|
|
477
521
|
return nil if self_type.type_args.empty? || other_type.type_args.empty?
|
|
@@ -824,11 +868,14 @@ module Rigor
|
|
|
824
868
|
# carrier whose value set is visibly apart from `removed` — a `Constant` (compare values directly),
|
|
825
869
|
# another `Difference` with the same removed value (already exhibits the disjointness), an
|
|
826
870
|
# `IntegerRange` that skips an Integer removed value, a `Tuple` whose fixed arity differs from a
|
|
827
|
-
# removed `Tuple`,
|
|
828
|
-
#
|
|
829
|
-
# `non-empty-
|
|
830
|
-
#
|
|
831
|
-
#
|
|
871
|
+
# removed `Tuple`, a `HashShape` with a required key a closed removed `HashShape` cannot hold, or a
|
|
872
|
+
# `Refined` whose predicate recogniser rejects the removed `Constant`. The range, tuple and shape
|
|
873
|
+
# arms cover the empty witnesses of `non-zero-int`, `non-empty-array[T]` and
|
|
874
|
+
# `non-empty-hash[K, V]`; the `Refined` arm is what lets `non-empty-string` accept
|
|
875
|
+
# `decimal-int-string`, which can never hold `""`. Any other carrier — a Nominal above all — is no
|
|
876
|
+
# proof and the difference rejects it under gradual mode, even where its values happen to exclude
|
|
877
|
+
# `removed`. A Union or Intersection argument never reaches this rule whole: `accepts` splits it
|
|
878
|
+
# into members first.
|
|
832
879
|
def accepts_difference(self_type, other_type, mode)
|
|
833
880
|
base_result = accepts(self_type.base, other_type, mode: mode)
|
|
834
881
|
return base_result if base_result.no?
|
|
@@ -858,16 +905,30 @@ module Rigor
|
|
|
858
905
|
# `removed`, the meet is too.
|
|
859
906
|
other_type.members.any? { |m| provably_disjoint_from_removed?(m, removed) }
|
|
860
907
|
when Type::IntegerRange
|
|
861
|
-
|
|
908
|
+
integer_range_excludes_removed?(other_type, removed)
|
|
862
909
|
when Type::Tuple
|
|
863
910
|
# A Tuple's arity is fixed, so it shares no inhabitant with a Tuple of another arity — the
|
|
864
911
|
# zero-arity witness `non-empty-array[T]` removes included.
|
|
865
912
|
removed.is_a?(Type::Tuple) && removed.elements.size != other_type.elements.size
|
|
866
913
|
when Type::HashShape
|
|
867
914
|
hash_shape_excludes_removed?(other_type, removed)
|
|
915
|
+
when Type::Refined
|
|
916
|
+
refined_excludes_removed?(other_type, removed)
|
|
868
917
|
end
|
|
869
918
|
end
|
|
870
919
|
|
|
920
|
+
def integer_range_excludes_removed?(range, removed)
|
|
921
|
+
removed.is_a?(Type::Constant) && removed.value.is_a?(Integer) && !range.covers?(removed.value)
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
# A predicate refinement excludes the removed value when its recogniser rejects the removed
|
|
925
|
+
# constant outright — `decimal-int-string` can never hold `""`. `matches?` answers nil for a
|
|
926
|
+
# predicate the registry does not know, and nil is no proof, so the comparison is `== false`
|
|
927
|
+
# rather than `!`.
|
|
928
|
+
def refined_excludes_removed?(refined, removed)
|
|
929
|
+
removed.is_a?(Type::Constant) && refined.matches?(removed.value) == false
|
|
930
|
+
end
|
|
931
|
+
|
|
871
932
|
# Every inhabitant of `shape` holds each of its required keys, and a closed `removed` shape holds no
|
|
872
933
|
# key outside its pairs, so one required key `removed` does not list makes the two disjoint. Against
|
|
873
934
|
# the closed `{}` witness `non-empty-hash[K, V]` removes, that is any required key; an optional key
|
|
@@ -881,8 +942,8 @@ module Rigor
|
|
|
881
942
|
# `Refined[base, predicate]` accepts another type X when the base accepts the *base* of X *and* X is
|
|
882
943
|
# provably contained in the predicate's value set. The base check is delegated to
|
|
883
944
|
# `accepts(self.base, X.base)` so handlers like `accepts_nominal` see Nominal-vs-Nominal and return
|
|
884
|
-
# their normal answer (
|
|
885
|
-
#
|
|
945
|
+
# their normal answer (`accepts_nominal` projects a `Refined` / `Difference` other onto its base,
|
|
946
|
+
# which is what makes the comparison meaningful).
|
|
886
947
|
#
|
|
887
948
|
# Provability rules in gradual mode (the conservative analogue of `accepts_difference`):
|
|
888
949
|
#
|
|
@@ -1003,9 +1064,12 @@ module Rigor
|
|
|
1003
1064
|
|
|
1004
1065
|
# Tuple[A1..An] accepts:
|
|
1005
1066
|
# - Tuple[B1..Bn] when arities match and each Ai accepts Bi (covariant per-position).
|
|
1006
|
-
# -
|
|
1067
|
+
# - An `Array[E]` (see {#array_element_verdict}): maybe, or no on an element contradiction.
|
|
1068
|
+
# - Anything else: no.
|
|
1007
1069
|
def accepts_tuple(self_type, other_type, mode)
|
|
1008
1070
|
unless other_type.is_a?(Type::Tuple)
|
|
1071
|
+
return array_element_verdict(self_type, other_type, mode) if array_carrier?(other_type)
|
|
1072
|
+
|
|
1009
1073
|
return Type::AcceptsResult.no(
|
|
1010
1074
|
mode: mode,
|
|
1011
1075
|
reasons: "Tuple does not accept #{other_type.class}"
|
|
@@ -1025,6 +1089,38 @@ module Rigor
|
|
|
1025
1089
|
combine_arg_results(per_element, mode)
|
|
1026
1090
|
end
|
|
1027
1091
|
|
|
1092
|
+
# An `Array` nominal, directly or as the base of a `Difference` (`non-empty-array[E]`). Its arity is
|
|
1093
|
+
# unknown, so a tuple cannot reject it on length: `URI.encode_www_form` takes
|
|
1094
|
+
# `Enumerable[[_ToS, _ToS]]` and is handed the `Array[Array[String]]` that `URI.decode_www_form`
|
|
1095
|
+
# answers. An `Array` subclass is not read, as {#gradual_hash?} does not read a `Hash` subclass.
|
|
1096
|
+
def array_carrier?(type)
|
|
1097
|
+
nominal = type.is_a?(Type::Difference) ? type.base : type
|
|
1098
|
+
nominal.is_a?(Type::Nominal) && nominal.class_name == "Array"
|
|
1099
|
+
end
|
|
1100
|
+
|
|
1101
|
+
# `maybe`, unless the element type rules the tuple out at any length: some position accepts no member
|
|
1102
|
+
# of `E` (`[Integer, Integer]` never takes an `Array[String]`). A raw `Array` stays `maybe`, and so does
|
|
1103
|
+
# an element with a gradual arm, since no position rejects a `Dynamic` member. The accepted cost (ADR-5)
|
|
1104
|
+
# is that a wrong-length `Array[E]` passed or returned where a tuple is declared goes unreported.
|
|
1105
|
+
def array_element_verdict(self_type, other_type, mode)
|
|
1106
|
+
nominal = other_type.is_a?(Type::Difference) ? other_type.base : other_type
|
|
1107
|
+
element = nominal.type_args.first
|
|
1108
|
+
unless element.nil?
|
|
1109
|
+
members = ContentJoin.union_members(element)
|
|
1110
|
+
refused = self_type.elements.find do |formal|
|
|
1111
|
+
members.all? { |member| accepts(formal, member, mode: mode).no? }
|
|
1112
|
+
end
|
|
1113
|
+
if refused
|
|
1114
|
+
return Type::AcceptsResult.no(
|
|
1115
|
+
mode: mode,
|
|
1116
|
+
reasons: "tuple member #{refused.describe(:short)} rejects #{element.describe(:short)}"
|
|
1117
|
+
)
|
|
1118
|
+
end
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
unread_shape_maybe(mode, "tuple arity unknown for #{other_type.describe(:short)}", :array)
|
|
1122
|
+
end
|
|
1123
|
+
|
|
1028
1124
|
# HashShape{k1: T1, ...} accepts another HashShape when every required key of self is required on
|
|
1029
1125
|
# the other side and Ti accepts Ui (depth covariant). Optional keys may be absent on the other side;
|
|
1030
1126
|
# when present, their values are checked. A closed self rejects known extra keys, and answers at most
|
|
@@ -1056,7 +1152,7 @@ module Rigor
|
|
|
1056
1152
|
result = combine_arg_results(per_entry, mode)
|
|
1057
1153
|
return result unless result.yes? && self_type.closed? && other_type.open?
|
|
1058
1154
|
|
|
1059
|
-
|
|
1155
|
+
unread_shape_maybe(mode, "HashShape closed target cannot check open source", :hash)
|
|
1060
1156
|
end
|
|
1061
1157
|
|
|
1062
1158
|
# A `Hash` nominal — directly or as the base of a `Difference` (`non-empty-hash[K, V]`) — that is raw or
|
|
@@ -1085,7 +1181,7 @@ module Rigor
|
|
|
1085
1181
|
refused = refused_value_keys(self_type, value_type, mode)
|
|
1086
1182
|
return hash_shape_no(mode, "Hash value type refused for required keys: #{refused.inspect}") if refused.any?
|
|
1087
1183
|
|
|
1088
|
-
|
|
1184
|
+
unread_shape_maybe(mode, "HashShape cannot check #{other_type.describe(:short)}", :hash)
|
|
1089
1185
|
end
|
|
1090
1186
|
|
|
1091
1187
|
def unholdable_keys(self_type, key_type, mode)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../../source/node_children"
|
|
6
|
+
require_relative "../closure_escape_analyzer"
|
|
7
|
+
require_relative "../remover_holder_scan"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Inference
|
|
11
|
+
module AliasMutationFloor
|
|
12
|
+
# Issue #2335 — the *foreign* locals of a frame body ({AliasMutationFloor}): those whose object may have come
|
|
13
|
+
# from elsewhere, so they may be another local's object. A method parameter, a block or lambda parameter (but one
|
|
14
|
+
# of a catalogued iterator over literals that read nothing, whose parameters are those literals' own objects), a
|
|
15
|
+
# local any target node binds, and a local written anything but a new object.
|
|
16
|
+
module ForeignLocals
|
|
17
|
+
FRESH_VALUES = [
|
|
18
|
+
Prism::ArrayNode, Prism::HashNode, Prism::StringNode, Prism::InterpolatedStringNode, Prism::XStringNode,
|
|
19
|
+
Prism::InterpolatedXStringNode, Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode,
|
|
20
|
+
Prism::SymbolNode, Prism::InterpolatedSymbolNode, Prism::NilNode, Prism::TrueNode, Prism::FalseNode,
|
|
21
|
+
Prism::RangeNode, Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode
|
|
22
|
+
].freeze
|
|
23
|
+
# `+"x"`, `"x".dup`, `String.new`: a new String.
|
|
24
|
+
FRESH_STRING_CALLS = %i[+@ dup clone].to_set.freeze
|
|
25
|
+
LITERAL_PARTS = [Prism::ArrayNode, Prism::HashNode, Prism::KeywordHashNode, Prism::AssocNode,
|
|
26
|
+
*FRESH_VALUES].freeze
|
|
27
|
+
private_constant :FRESH_VALUES, :FRESH_STRING_CALLS, :LITERAL_PARTS
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# `[foreign, entry]`: the foreign locals, and the method parameters the body never rebinds, each frozen.
|
|
32
|
+
def collect(body, parameters)
|
|
33
|
+
foreign = Set.new
|
|
34
|
+
written = Set.new
|
|
35
|
+
collect_foreign(body, foreign, written)
|
|
36
|
+
finish(parameters, foreign, written)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# `[foreign, entry]` from the body's own `foreign` and `written` sets and the method's `parameters`.
|
|
40
|
+
def finish(parameters, foreign, written)
|
|
41
|
+
entry = Set.new
|
|
42
|
+
collect_parameters(parameters, entry)
|
|
43
|
+
[foreign.merge(entry).freeze, (entry - written).freeze]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def collect_parameters(node, names)
|
|
47
|
+
return if node.nil?
|
|
48
|
+
|
|
49
|
+
names << node.name if node.class.name.end_with?("ParameterNode") && node.respond_to?(:name) && node.name
|
|
50
|
+
node.rigor_each_child { |child| collect_parameters(child, names) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# `foreign` gains every block or lambda parameter and every local a write binds to an object from elsewhere (an
|
|
54
|
+
# operator write's value is a call result); `written` every local any write or target binds. A nested def is
|
|
55
|
+
# not entered.
|
|
56
|
+
def collect_foreign(node, foreign, written, literal_blocks = Set.new.compare_by_identity)
|
|
57
|
+
return if node.is_a?(Prism::DefNode)
|
|
58
|
+
|
|
59
|
+
visit(node, foreign, written, literal_blocks)
|
|
60
|
+
node.rigor_each_child { |child| collect_foreign(child, foreign, written, literal_blocks) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# {.collect_foreign}'s step for one node, which the site collector's walk also takes
|
|
64
|
+
# ({AliasMutationFloor::SiteCollector}).
|
|
65
|
+
def visit(node, foreign, written, literal_blocks)
|
|
66
|
+
case node
|
|
67
|
+
when Prism::CallNode then literal_blocks << node.block if literal_iterator_block?(node)
|
|
68
|
+
when Prism::BlockNode, Prism::LambdaNode
|
|
69
|
+
collect_parameters(node.parameters, foreign) unless literal_blocks.include?(node)
|
|
70
|
+
when Prism::LocalVariableTargetNode, Prism::LocalVariableOperatorWriteNode
|
|
71
|
+
foreign << written_name(node, written)
|
|
72
|
+
when Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode
|
|
73
|
+
name = written_name(node, written)
|
|
74
|
+
foreign << name unless fresh_value?(node.value)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A catalogued iterator over literals that read nothing (`[[1], [2]].each { |x| }`,
|
|
79
|
+
# `[1].each_with_object({}) { |x, h| }`): its block's parameters are those literals' own new objects. Any other
|
|
80
|
+
# block's parameters may be any object.
|
|
81
|
+
def literal_iterator_block?(call_node)
|
|
82
|
+
return false unless call_node.block.is_a?(Prism::BlockNode)
|
|
83
|
+
return false unless ClosureEscapeAnalyzer.iterator_name?(call_node.name)
|
|
84
|
+
|
|
85
|
+
[call_node.receiver, *call_node.arguments&.arguments].all? do |operand|
|
|
86
|
+
!operand.nil? && literal_only?(operand)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def literal_only?(node)
|
|
91
|
+
return false unless LITERAL_PARTS.any? { |klass| node.is_a?(klass) }
|
|
92
|
+
|
|
93
|
+
node.rigor_each_child { |child| return false unless literal_only?(child) }
|
|
94
|
+
true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def written_name(node, written)
|
|
98
|
+
written << node.name
|
|
99
|
+
node.name
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# A value that is a new object, so the local it binds holds no other holder's object.
|
|
103
|
+
def fresh_value?(value)
|
|
104
|
+
value = RemoverHolderScan.unparenthesised(value)
|
|
105
|
+
return true if FRESH_VALUES.any? { |klass| value.is_a?(klass) }
|
|
106
|
+
return false unless value.is_a?(Prism::CallNode)
|
|
107
|
+
return true if RemoverHolderScan.fresh_value?(value)
|
|
108
|
+
|
|
109
|
+
receiver = value.receiver
|
|
110
|
+
(FRESH_STRING_CALLS.include?(value.name) &&
|
|
111
|
+
(receiver.is_a?(Prism::StringNode) || receiver.is_a?(Prism::InterpolatedStringNode))) ||
|
|
112
|
+
(value.name == :new && receiver.is_a?(Prism::ConstantReadNode) && receiver.name == :String)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../typing_state"
|
|
4
|
+
require_relative "../../memo_audit"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module AliasMutationFloor
|
|
9
|
+
# ADR-129 WD1 — the floor's four run-scoped Memos, each a pure function of the key it holds, and their accessors.
|
|
10
|
+
# Each accessor's block computes the value: on a miss it is stored, on a hit {MemoAudit.check} compares it with
|
|
11
|
+
# the stored one under `RIGOR_MEMO_AUDIT=1`, and while an audit recomputation runs ({MemoAudit.suspended?}) it
|
|
12
|
+
# answers without reading or writing the memo.
|
|
13
|
+
module Memos
|
|
14
|
+
FACTS = TypingState.memo(
|
|
15
|
+
:alias_mutation_floor_facts,
|
|
16
|
+
run_scoped: true, audit: "alias floor facts",
|
|
17
|
+
reason: "keyed by the body node; the facts are a pure function of the body and its parameters"
|
|
18
|
+
)
|
|
19
|
+
IVAR = TypingState.memo(
|
|
20
|
+
:alias_mutation_floor_ivar_mutators,
|
|
21
|
+
run_scoped: true, audit: "alias floor ivar mutator",
|
|
22
|
+
reason: "keyed by def node; whether its body mutates an ivar is a pure function of the body"
|
|
23
|
+
)
|
|
24
|
+
FLOOR = TypingState.memo(
|
|
25
|
+
:alias_mutation_floor_callee_floors,
|
|
26
|
+
run_scoped: true, audit: "alias floor callee floor",
|
|
27
|
+
reason: "keyed by the literal shape; the callee block floor is a pure function of it"
|
|
28
|
+
)
|
|
29
|
+
STORE = TypingState.memo(
|
|
30
|
+
:alias_mutation_floor_unknown_stores,
|
|
31
|
+
run_scoped: true, audit: "alias floor unknown store",
|
|
32
|
+
reason: "keyed by call node, removed-keys mode and type, every input of the widened store"
|
|
33
|
+
)
|
|
34
|
+
private_constant :FACTS, :IVAR, :FLOOR, :STORE
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# The facts of `body`. A hit is audited without the facts' per-read memos, which fill as reads are answered.
|
|
39
|
+
def facts(body, &)
|
|
40
|
+
return yield if MemoAudit.enabled? && MemoAudit.suspended?
|
|
41
|
+
|
|
42
|
+
table = TypingState.current.weak_key_map(FACTS)
|
|
43
|
+
facts = table[body]
|
|
44
|
+
return table[body] = MemoAudit.key_audit("alias floor facts", &) if facts.nil?
|
|
45
|
+
|
|
46
|
+
MemoAudit.check("alias floor facts", audited(facts), body) { audited(yield) } if MemoAudit.enabled?
|
|
47
|
+
facts
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def ivar_mutator(def_node, &)
|
|
51
|
+
return yield if MemoAudit.enabled? && MemoAudit.suspended?
|
|
52
|
+
|
|
53
|
+
table = TypingState.current.weak_key_map(IVAR)
|
|
54
|
+
cached = table[def_node]
|
|
55
|
+
return table[def_node] = MemoAudit.key_audit("alias floor ivar mutator", &) if cached.nil?
|
|
56
|
+
|
|
57
|
+
MemoAudit.check("alias floor ivar mutator", cached, def_node, &)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def callee_floor(type, &)
|
|
61
|
+
return yield if MemoAudit.enabled? && MemoAudit.suspended?
|
|
62
|
+
|
|
63
|
+
state = TypingState.current
|
|
64
|
+
table = (state[FLOOR] ||= {})
|
|
65
|
+
return table[type] = MemoAudit.key_audit("alias floor callee floor", &) unless table.key?(type)
|
|
66
|
+
|
|
67
|
+
MemoAudit.check("alias floor callee floor", table[type], type, &)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def unknown_store(call_node, removed_keys, type, &)
|
|
71
|
+
return yield if MemoAudit.enabled? && MemoAudit.suspended?
|
|
72
|
+
|
|
73
|
+
state = TypingState.current
|
|
74
|
+
table = (state[STORE] ||= {}.compare_by_identity)
|
|
75
|
+
per_mode = ((table[call_node] ||= {})[removed_keys] ||= {})
|
|
76
|
+
return per_mode[type] = MemoAudit.key_audit("alias floor unknown store", &) unless per_mode.key?(type)
|
|
77
|
+
|
|
78
|
+
MemoAudit.check("alias floor unknown store", per_mode[type], call_node, type, &)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def audited(facts) = facts.with(memo: nil, named_memo: nil)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../receiver_alias"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# Issue #2335 — which locals a write through a receiver may reach ({AliasMutationFloor}), read off a frame body's
|
|
10
|
+
# {AliasMutationFloor::Facts}.
|
|
11
|
+
module AliasMutationFloor
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# The locals aliased by the time `call_node`'s message runs, as `{ name => depth }`: those a site has handed on,
|
|
15
|
+
# and those linked to one ({.reach}). A call inside a closure that may run later sees every site.
|
|
16
|
+
def aliased_at(facts, call_node)
|
|
17
|
+
return ALL if facts.held.equal?(ALL)
|
|
18
|
+
|
|
19
|
+
offset = offset_of(facts, call_node)
|
|
20
|
+
seeds = {}
|
|
21
|
+
facts.held.each { |name, key| seeds[name] = FULL if key <= offset }
|
|
22
|
+
facts.held_part.each { |name, key| seeds[name] ||= PART if key <= offset }
|
|
23
|
+
reach(seeds, facts.links, offset)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def offset_of(facts, call_node)
|
|
27
|
+
offset = (call_node.message_loc || call_node.location).start_offset
|
|
28
|
+
facts.deferred.any? { |from, to| offset >= from && offset < to } ? Float::INFINITY : offset
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# `seeds` closed over every link whose site may have run by `offset`. A depth says how far inside a local's object
|
|
32
|
+
# the mutated object may sit: 0 ({FULL}) is the object itself, 1 ({PART}) one of its elements, and so on. A copy
|
|
33
|
+
# keeps the depth; across an element link (`x = a[i]`, `x = a[i][j]`) the parent sits as many levels further out
|
|
34
|
+
# than the child as the read goes, and the child as many further in, so a mutation of the parent object itself
|
|
35
|
+
# (depth 0) does not reach the child.
|
|
36
|
+
# The least depth wins.
|
|
37
|
+
def reach(seeds, links, offset)
|
|
38
|
+
changed = !seeds.empty?
|
|
39
|
+
while changed
|
|
40
|
+
changed = false
|
|
41
|
+
links.each do |child, parent, key, levels|
|
|
42
|
+
next if key > offset
|
|
43
|
+
|
|
44
|
+
changed = true if spread(seeds, child, parent, levels) | spread(seeds, parent, child, -levels)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
seeds
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
DEPTH_CAP = 8
|
|
51
|
+
private_constant :DEPTH_CAP
|
|
52
|
+
|
|
53
|
+
# Spreads the depth `from` reached across one link to `to`, `step` levels further out; answers whether `to`
|
|
54
|
+
# changed.
|
|
55
|
+
def spread(seeds, from, to, step) # rubocop:disable Naming/PredicateMethod
|
|
56
|
+
depth = seeds[from]
|
|
57
|
+
return false if depth.nil?
|
|
58
|
+
|
|
59
|
+
depth += step
|
|
60
|
+
return false if depth.negative? || depth > DEPTH_CAP
|
|
61
|
+
return false if seeds[to] && seeds[to] <= depth
|
|
62
|
+
|
|
63
|
+
seeds[to] = depth
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The locals a write through `receiver` may reach, as {.aliased_at} answers them: through each local it may
|
|
68
|
+
# evaluate to ({ReceiverAlias.candidates}: `(c ? a : b)`), through an element read's root ({.element_targets}), or
|
|
69
|
+
# as through any other receiver.
|
|
70
|
+
def receiver_targets(receiver, facts, call_node)
|
|
71
|
+
reads = ReceiverAlias.candidates(receiver)
|
|
72
|
+
aliased = aliased_at(facts, call_node)
|
|
73
|
+
return element_targets(receiver, facts, aliased, call_node) if reads.empty?
|
|
74
|
+
return targets(nil, facts, aliased, call_node) unless reads.all?(Prism::LocalVariableReadNode)
|
|
75
|
+
|
|
76
|
+
reads.map { |read| targets(read.name, facts, aliased, call_node) }.reduce { |acc, set| union(acc, set) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Through an element read rooted at a local (`c[0] << x`, `h[:k].fetch(0) << x`), the root's own links as far
|
|
80
|
+
# inside it as the read goes (`a = c[0]` is the mutated object itself), beside what any other receiver reaches.
|
|
81
|
+
def element_targets(receiver, facts, aliased, call_node)
|
|
82
|
+
others = targets(nil, facts, aliased, call_node)
|
|
83
|
+
root, depth = element_root(receiver)
|
|
84
|
+
return others if root.nil? || others.equal?(ALL)
|
|
85
|
+
|
|
86
|
+
union(others, reach({ root.name => depth }, facts.links, offset_of(facts, call_node)))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def union(left, right)
|
|
90
|
+
return ALL if left.equal?(ALL) || right.equal?(ALL)
|
|
91
|
+
|
|
92
|
+
merged = left.dup
|
|
93
|
+
right.each { |name, depth| merged[name] = [merged[name], depth].compact.min }
|
|
94
|
+
merged
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The locals a write through the local `name` (nil for any other receiver) may reach, as `{ name => depth }`:
|
|
98
|
+
#
|
|
99
|
+
# - the locals linked to it ({.reach}): a copy of it, or what it copied (`b = a; b << x`), or the local it is an
|
|
100
|
+
# element of (`x = a[i]`);
|
|
101
|
+
# - and every aliased local, through a receiver that may hold any object: an ivar, an index read, a call result,
|
|
102
|
+
# a block parameter, or a local some write binds to an object from elsewhere;
|
|
103
|
+
# - or only the aliased locals that are foreign, through a local only ever written a new object or a method
|
|
104
|
+
# parameter the body never rebinds, once the body has handed it on: either is an object no new-object local
|
|
105
|
+
# can be, and only a holder it reached can hand it to another local.
|
|
106
|
+
def targets(name, facts, aliased, call_node)
|
|
107
|
+
return ALL if aliased.equal?(ALL)
|
|
108
|
+
return aliased if name.nil? || NUMBERED_PARAMETER.match?(name)
|
|
109
|
+
|
|
110
|
+
group = reach({ name => FULL }, facts.links, offset_of(facts, call_node))
|
|
111
|
+
group.delete(name)
|
|
112
|
+
return union(aliased, group) if facts.foreign.include?(name) && !facts.entry.include?(name)
|
|
113
|
+
return group unless aliased.key?(name)
|
|
114
|
+
|
|
115
|
+
union(group, aliased.slice(*facts.foreign))
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|