rigortype 0.4.6 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
- 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 +5 -2
- data/docs/handbook/11-sig-gen.md +50 -3
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/manual/02-cli-reference.md +152 -26
- data/docs/manual/03-configuration.md +36 -3
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/09-editor-integration.md +13 -2
- data/docs/manual/10-mcp-server.md +38 -2
- data/docs/manual/12-caching.md +4 -3
- data/docs/manual/plugins/rigor-actionpack.md +19 -11
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
- data/lib/rigor/analysis/baseline.rb +24 -27
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -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 +3 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
- 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 +1 -1
- data/lib/rigor/analysis/check_rules.rb +124 -44
- data/lib/rigor/analysis/crash_signature.rb +2 -2
- data/lib/rigor/analysis/dependency_recorder.rb +4 -4
- 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_run_slot/write_guard.rb +2 -2
- data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
- data/lib/rigor/analysis/incremental_session.rb +27 -16
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
- data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
- 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 +77 -71
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
- 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 +24 -31
- data/lib/rigor/bleeding_edge.rb +3 -6
- 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/incremental_snapshot.rb +31 -12
- 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 +54 -3
- data/lib/rigor/cache/rbs_environment.rb +4 -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 +145 -55
- data/lib/rigor/cli/catalogue.rb +256 -0
- data/lib/rigor/cli/check_command.rb +108 -102
- 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 +18 -17
- 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 +12 -8
- 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 +84 -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 +1 -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 +209 -54
- data/lib/rigor/config_audit.rb +1 -1
- data/lib/rigor/configuration/severity_profile.rb +5 -5
- data/lib/rigor/configuration.rb +72 -30
- 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 +5 -9
- data/lib/rigor/environment/class_registry.rb +3 -3
- data/lib/rigor/environment/core_root.rb +77 -0
- data/lib/rigor/environment/default_libraries.rb +1 -1
- data/lib/rigor/environment/deferred_signature_plan.rb +283 -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/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_hierarchy.rb +6 -15
- data/lib/rigor/environment/rbs_loader.rb +480 -67
- 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 +41 -10
- data/lib/rigor/flow_contribution/merger.rb +4 -4
- data/lib/rigor/inference/acceptance.rb +93 -32
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/block_run_count.rb +178 -0
- data/lib/rigor/inference/budget_trace.rb +8 -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/construction_escape.rb +217 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
- 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 +1 -1
- data/lib/rigor/inference/def_return_typer.rb +46 -4
- data/lib/rigor/inference/definer_resolution.rb +4 -2
- data/lib/rigor/inference/definers_index.rb +106 -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/expression_typer.rb +116 -49
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- data/lib/rigor/inference/global_write_census.rb +76 -2
- 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/indexed_narrowing.rb +4 -7
- data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
- data/lib/rigor/inference/ivar_writer_census.rb +51 -0
- data/lib/rigor/inference/key_presence_guard.rb +2 -1
- data/lib/rigor/inference/loop_value_record.rb +8 -4
- data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
- data/lib/rigor/inference/match_rebinding.rb +11 -1
- data/lib/rigor/inference/memo_audit.rb +55 -0
- 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 +1 -1
- 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/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 +39 -27
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
- 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 +2 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
- 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/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 +64 -14
- data/lib/rigor/inference/module_function_state.rb +11 -1
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_rejoin.rb +240 -4
- data/lib/rigor/inference/mutation_widening.rb +157 -27
- data/lib/rigor/inference/narrowing.rb +47 -33
- data/lib/rigor/inference/optimistic_origin.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/project_method_ownership.rb +6 -3
- data/lib/rigor/inference/project_yielder.rb +11 -17
- data/lib/rigor/inference/receiver_alias.rb +20 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
- data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
- data/lib/rigor/inference/remover_holder_scan.rb +233 -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/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
- 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 +135 -0
- data/lib/rigor/inference/scope_indexer.rb +137 -46
- data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
- data/lib/rigor/inference/set_mutation.rb +189 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
- data/lib/rigor/inference/statement_evaluator.rb +2328 -138
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
- data/lib/rigor/inference/toplevel_def_census.rb +59 -0
- data/lib/rigor/inference/unknown_store_widening.rb +103 -4
- data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
- data/lib/rigor/inference/void_tail_summary.rb +2 -1
- 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/plugins.rb +1 -1
- data/lib/rigor/mcp/server.rb +73 -2
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- data/lib/rigor/plugin/bundled_catalog.rb +1 -12
- data/lib/rigor/plugin/io_boundary.rb +1 -1
- 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 +9 -13
- data/lib/rigor/plugin/registry.rb +3 -3
- 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 +2 -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/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 +5 -3
- data/lib/rigor/rooted_glob.rb +15 -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 +19 -6
- data/lib/rigor/scope/resolution_chain.rb +3 -1
- data/lib/rigor/scope.rb +365 -31
- data/lib/rigor/sig_gen/classification.rb +4 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
- data/lib/rigor/sig_gen/generator.rb +371 -58
- 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 +23 -4
- data/lib/rigor/sig_gen/observation_collector.rb +3 -5
- 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/renderer.rb +15 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -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 +217 -37
- 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 +9 -13
- data/lib/rigor/type/constant.rb +4 -5
- data/lib/rigor/type/hash_shape.rb +21 -40
- data/lib/rigor/type/union.rb +1 -1
- 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/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 +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
- 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 +2 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
- 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 +7 -12
- 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 +19 -6
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
- 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 +4 -5
- 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-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 +2 -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 +1 -3
- 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 +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- 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 +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
- 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/fact_store.rbs +1 -0
- data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
- 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/inference.rbs +4 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +1 -0
- 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/io_boundary.rbs +2 -1
- data/sig/rigor/plugin/manifest.rbs +9 -9
- data/sig/rigor/plugin/protocol_contract.rbs +3 -7
- data/sig/rigor/scope.rbs +23 -3
- 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/03-baseline-and-bugs.md +12 -1
- metadata +36 -3
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../type"
|
|
6
|
+
require_relative "block_run_count"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Inference
|
|
@@ -12,10 +13,12 @@ module Rigor
|
|
|
12
13
|
# counted as running the block once, and a `return` in the block that only a later run reaches (`each { return
|
|
13
14
|
# false if (seen += 1) > 3 }`) never joined the method's return type.
|
|
14
15
|
#
|
|
15
|
-
# Only the later-run `return` pass asks
|
|
16
|
-
# entry scopes and the ADR-56 write-back keep
|
|
17
|
-
# return type, so the rule here errs toward
|
|
18
|
-
#
|
|
16
|
+
# Only the later-run `return` pass asks {.may_run_block_repeatedly?}
|
|
17
|
+
# (`StatementEvaluator#collect_repeated_block_returns`); the block's entry scopes and the ADR-56 write-back keep
|
|
18
|
+
# {BlockRepetition}'s rule. That pass only widens the method's return type, so the rule here errs toward
|
|
19
|
+
# "repeats". The expression-position block-value pass asks {.block_value_may_repeat?} instead, whose rule
|
|
20
|
+
# ({BlockRunCount}) errs the other way. The callee repeats when its body, outside a nested `def` (a `yield` in a
|
|
21
|
+
# class body is a syntax error inside a method):
|
|
19
22
|
#
|
|
20
23
|
# - holds two or more sites that run the block (`yield`, or `call` / `yield` / `===` / `[]` on the `&blk`
|
|
21
24
|
# parameter), or one inside a loop, a block or a lambda, which may run it many times; or
|
|
@@ -41,26 +44,17 @@ module Rigor
|
|
|
41
44
|
false
|
|
42
45
|
end
|
|
43
46
|
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
# times, so the caller's block is typed as one that may repeat. Answers false where the callee does not resolve.
|
|
48
|
-
def hands_block_on?(call_node, receiver_type, scope)
|
|
47
|
+
# Issues #2301 and #2303 — whether the callee may run the block more than once on one path, for the
|
|
48
|
+
# expression-position block-value pass ({BlockRunCount}). Answers false where the callee does not resolve.
|
|
49
|
+
def block_value_may_repeat?(call_node, receiver_type, scope)
|
|
49
50
|
def_node = callee_def(call_node, receiver_type, scope)
|
|
50
51
|
return false unless def_node.is_a?(Prism::DefNode) && def_node.body
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
BlockRunCount.may_repeat?(def_node, scope)
|
|
53
54
|
rescue StandardError
|
|
54
55
|
false
|
|
55
56
|
end
|
|
56
57
|
|
|
57
|
-
def hand_off_in?(node, block_name)
|
|
58
|
-
return false if node.is_a?(Prism::DefNode)
|
|
59
|
-
return true if node.is_a?(Prism::BlockArgumentNode) && forwards_block?(node, block_name)
|
|
60
|
-
|
|
61
|
-
node.compact_child_nodes.any? { |child| hand_off_in?(child, block_name) }
|
|
62
|
-
end
|
|
63
|
-
|
|
64
58
|
# The `def` an instance call on a project class, a singleton call on one, or an implicit-self call to a
|
|
65
59
|
# top-level method resolves to.
|
|
66
60
|
def callee_def(call_node, receiver_type, scope)
|
|
@@ -85,6 +85,26 @@ module Rigor
|
|
|
85
85
|
candidates(first, depth + 1) + candidates(second, depth + 1)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# Calls whose result hands its caller the receiver's own element objects (issue #2287): `h.values` the hash's
|
|
89
|
+
# values, `a.dup` / `a.clone` a shallow copy sharing them, and the blockless enumerator spellings (`a.each`,
|
|
90
|
+
# `a.map`, `a.each_with_index`, `e.with_index`) the elements themselves.
|
|
91
|
+
ELEMENT_EXPOSING_CALLS = %i[values dup clone each map each_with_index with_index].to_set.freeze
|
|
92
|
+
private_constant :ELEMENT_EXPOSING_CALLS
|
|
93
|
+
|
|
94
|
+
# The variables whose elements a loop over `receiver` is handed: {.candidates} of `receiver` once every
|
|
95
|
+
# blockless {ELEMENT_EXPOSING_CALLS} call is peeled off, so `h.values.each { |v| v << 2 }` and
|
|
96
|
+
# `a.each_with_index.map { |x, i| x << i }` name `h` and `a`. Only a loop's element widening reads it; an
|
|
97
|
+
# in-place mutation OF such a result (`h.values << 1`, `a.dup << 1`) changes a new object, not the variable.
|
|
98
|
+
def element_sources(receiver)
|
|
99
|
+
depth = 0
|
|
100
|
+
while receiver.is_a?(Prism::CallNode) && receiver.block.nil? && ELEMENT_EXPOSING_CALLS.include?(receiver.name)
|
|
101
|
+
return [] if (depth += 1) > WALK_DEPTH_CAP
|
|
102
|
+
|
|
103
|
+
receiver = receiver.receiver
|
|
104
|
+
end
|
|
105
|
+
candidates(receiver)
|
|
106
|
+
end
|
|
107
|
+
|
|
88
108
|
# The variable reads an in-place mutation of `receiver` changes: {.candidates}' locals and instance
|
|
89
109
|
# variables, or the class variable or global the receiver reads or writes directly, parenthesised or not
|
|
90
110
|
# (`(@@c ||= []) << x` mutates `@@c`, as `(buf ||= []) << x` mutates `buf`). A class variable or global counts
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../run_scoped_slots"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Inference
|
|
5
7
|
module RefinedDispatch
|
|
@@ -16,7 +18,7 @@ module Rigor
|
|
|
16
18
|
# the name itself, and no constant write of it. Any other shape answers false, as does a class a
|
|
17
19
|
# `K = Class.new(...)` write names.
|
|
18
20
|
module ConstructedClass
|
|
19
|
-
MEMO_KEY = :__rigor_constructed_class__
|
|
21
|
+
MEMO_KEY = RunScopedSlots.register(:__rigor_constructed_class__)
|
|
20
22
|
EMPTY = [].freeze
|
|
21
23
|
private_constant :MEMO_KEY, :EMPTY
|
|
22
24
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../builtins/cruby_definers"
|
|
4
4
|
require_relative "constructed_class"
|
|
5
|
+
require_relative "../../run_scoped_slots"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module Inference
|
|
@@ -317,7 +318,7 @@ module Rigor
|
|
|
317
318
|
all.include?(name) ? all - [name] : all
|
|
318
319
|
end
|
|
319
320
|
|
|
320
|
-
SUBCLASS_MEMO_KEY = :__rigor_own_method_subclasses__
|
|
321
|
+
SUBCLASS_MEMO_KEY = RunScopedSlots.register(:__rigor_own_method_subclasses__)
|
|
321
322
|
private_constant :SUBCLASS_MEMO_KEY
|
|
322
323
|
|
|
323
324
|
# One thread-local slot keyed on the scope's discovery index (a frozen `Data`) and environment, the only
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../run_scoped_slots"
|
|
6
|
+
require_relative "closure_escape_analyzer"
|
|
7
|
+
require_relative "construction_escape"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Inference
|
|
11
|
+
# Issue #2608, review rounds 1 and 2 of #2621 — which locals of a body another holder may write, so a remover on
|
|
12
|
+
# them is not narrowed ({RemoverNarrowing}): a narrowing is a fact about the object, and a write through any other
|
|
13
|
+
# holder after the call would outlive it. One flow-insensitive scan per frame body; a local is a holder's when:
|
|
14
|
+
#
|
|
15
|
+
# - a closure that may run later references it: a lambda, or a block passed to a call that is not a catalogued
|
|
16
|
+
# iterator or `tap` / `then` / `yield_self` (`proc { l << 1 }`), created before or after the remover;
|
|
17
|
+
# - it is read anywhere inside the value of any write (local, instance, class, global or constant variable, an
|
|
18
|
+
# index or attribute write, an operator or `||=` / `&&=` write, a multiple assignment) or inside the subject of a
|
|
19
|
+
# `case`/`in`, `=>` or `in` pattern: `al = c ? l : []`, `al = l.push(1)`, `@k = l`, `case l; in al`. The local
|
|
20
|
+
# such a write or pattern binds is a holder too (both names of a copy), unless the write's value is a fresh
|
|
21
|
+
# object: an Array or Hash literal, or `new` / `[]` on a bare or top-level `Array`, `Hash` or `Set`
|
|
22
|
+
# (`a = Array.new(n, "a")`);
|
|
23
|
+
# - a method's parameter, when its body has a zero-argument `super`, which passes every parameter on;
|
|
24
|
+
# - it is read inside an Array element, a Hash key or value, a splat, or an argument of any call, `yield` or
|
|
25
|
+
# `super` (positional, keyword, splat or block-pass), unless the read is the receiver of a call that answers
|
|
26
|
+
# a value other than the receiver and keeps no reference to it ({PURE_READERS}: #2555's per-class
|
|
27
|
+
# `ConstructionEscape::READERS`, less the conversions that return self on another class): `puts(l.size)`,
|
|
28
|
+
# `dump_type(l.dup)` and `dump_type(l.first)` hold nothing, `Keep.new(l)` and `foo(l.push(1))` do;
|
|
29
|
+
# - it receives a reflective call (`l.send(...)`, `l.instance_eval { }`, `l.tap { }`);
|
|
30
|
+
# - and every local, when the body calls `binding`, `eval`, `local_variable_get` or `local_variable_set` on any
|
|
31
|
+
# receiver (`-> {}.binding`), or a receiverless `instance_variable_set`.
|
|
32
|
+
#
|
|
33
|
+
# A receiver position, a `return` and a body's last expression hand nothing on.
|
|
34
|
+
module RemoverHolderScan
|
|
35
|
+
REFLECTIVE_CALLS = %i[
|
|
36
|
+
send public_send __send__ instance_eval instance_exec tap then yield_self itself method
|
|
37
|
+
define_singleton_method
|
|
38
|
+
].to_set.freeze
|
|
39
|
+
# A conversion one class lists as a reader returns the receiver itself on another (`Array#to_a`, `Hash#to_h`,
|
|
40
|
+
# `Set#to_set`), so the union of the per-class lists leaves them out.
|
|
41
|
+
SELF_CONVERSIONS = %i[to_a to_h to_set].freeze
|
|
42
|
+
PURE_READERS = (ConstructionEscape::READERS.values.reduce(:|) - SELF_CONVERSIONS).freeze
|
|
43
|
+
IMMEDIATE_BLOCK_CALLS = %i[tap then yield_self].to_set.freeze
|
|
44
|
+
FRAME_CALLS = %i[binding eval local_variable_get local_variable_set instance_variable_set].to_set.freeze
|
|
45
|
+
# Frame calls that reach the locals whatever their receiver (`pr.binding`, `Kernel.eval`).
|
|
46
|
+
ANY_RECEIVER_FRAME_CALLS = %i[binding eval local_variable_get local_variable_set].to_set.freeze
|
|
47
|
+
NON_WRITER_OPERATORS = %i[== != <= >= ===].to_set.freeze
|
|
48
|
+
CORE_COLLECTIONS = %i[Array Hash Set].to_set.freeze
|
|
49
|
+
LOCAL_REFERENCES = [
|
|
50
|
+
Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode,
|
|
51
|
+
Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode, Prism::LocalVariableTargetNode
|
|
52
|
+
].freeze
|
|
53
|
+
LOCAL_WRITES = [
|
|
54
|
+
Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
|
|
55
|
+
Prism::LocalVariableAndWriteNode
|
|
56
|
+
].freeze
|
|
57
|
+
ALL = :all
|
|
58
|
+
# A run-scoped slot ({RunScopedSlots}): emptied when a run ends, so no verdict outlives the body it read.
|
|
59
|
+
UNSAFE_CACHE_KEY = RunScopedSlots.register(:__rigor_remover_narrowing_unsafe)
|
|
60
|
+
private_constant :REFLECTIVE_CALLS, :SELF_CONVERSIONS, :PURE_READERS, :IMMEDIATE_BLOCK_CALLS, :FRAME_CALLS,
|
|
61
|
+
:ANY_RECEIVER_FRAME_CALLS, :NON_WRITER_OPERATORS, :CORE_COLLECTIONS,
|
|
62
|
+
:LOCAL_REFERENCES, :LOCAL_WRITES, :ALL, :UNSAFE_CACHE_KEY
|
|
63
|
+
|
|
64
|
+
module_function
|
|
65
|
+
|
|
66
|
+
# Whether the local `name` may be written through another holder somewhere in the body `scope` runs. A scope
|
|
67
|
+
# with no known body answers true.
|
|
68
|
+
def unsafe_receiver?(scope, name)
|
|
69
|
+
frame = scope.match_frame
|
|
70
|
+
body = frame&.body
|
|
71
|
+
return true if body.nil?
|
|
72
|
+
|
|
73
|
+
table = (Thread.current[UNSAFE_CACHE_KEY] ||= ObjectSpace::WeakKeyMap.new)
|
|
74
|
+
names = table[body] ||= unsafe_names(body, frame.parameters)
|
|
75
|
+
names.equal?(ALL) || names.include?(name.to_sym)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def unsafe_names(body, parameters = nil)
|
|
79
|
+
names = Set.new
|
|
80
|
+
catch(ALL) do
|
|
81
|
+
collect_unsafe(body, false, names)
|
|
82
|
+
parameter_names(parameters, names) if forwards_parameters?(body)
|
|
83
|
+
return names.freeze
|
|
84
|
+
end
|
|
85
|
+
ALL
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def collect_unsafe(node, in_closure, names)
|
|
89
|
+
in_closure ||= node.is_a?(Prism::LambdaNode)
|
|
90
|
+
names << node.name if in_closure && LOCAL_REFERENCES.any? { |k| node.is_a?(k) }
|
|
91
|
+
record_node(node, names)
|
|
92
|
+
|
|
93
|
+
node.compact_child_nodes.each do |child|
|
|
94
|
+
child_in_closure = in_closure || (child.is_a?(Prism::BlockNode) && deferred_block?(node))
|
|
95
|
+
collect_unsafe(child, child_in_closure, names)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def record_node(node, names) # rubocop:disable Metrics/CyclomaticComplexity
|
|
100
|
+
case node
|
|
101
|
+
when Prism::CallNode then record_call(node, names)
|
|
102
|
+
when Prism::YieldNode, Prism::SuperNode then node.arguments&.arguments&.each { |a| record_held(a, names) }
|
|
103
|
+
when Prism::ArrayNode then node.elements.each { |element| record_held(element, names) }
|
|
104
|
+
when Prism::AssocNode then [node.key, node.value].each { |part| record_held(part, names) }
|
|
105
|
+
when Prism::SplatNode, Prism::BlockArgumentNode then record_held(node.expression, names)
|
|
106
|
+
when Prism::CaseMatchNode then record_pattern(node.predicate, node.conditions, names)
|
|
107
|
+
when Prism::MatchRequiredNode, Prism::MatchPredicateNode then record_pattern(node.value, [node.pattern], names)
|
|
108
|
+
when Prism::MultiWriteNode then record_multi_write(node, names)
|
|
109
|
+
else record_write(node, names)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def record_call(node, names)
|
|
114
|
+
throw ALL if frame_call?(node)
|
|
115
|
+
|
|
116
|
+
receiver = unparenthesised(node.receiver)
|
|
117
|
+
names << receiver.name if receiver.is_a?(Prism::LocalVariableReadNode) && REFLECTIVE_CALLS.include?(node.name)
|
|
118
|
+
arguments = node.arguments&.arguments || []
|
|
119
|
+
if writer_call?(node) && !arguments.empty?
|
|
120
|
+
*keys, value = arguments
|
|
121
|
+
keys.each { |key| record_held(key, names) }
|
|
122
|
+
every_read(value, names)
|
|
123
|
+
else
|
|
124
|
+
arguments.each { |argument| record_held(argument, names) }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# A zero-argument `super` passes every parameter of the method on, implicitly. (A `...` forward passes only the
|
|
129
|
+
# anonymous parameters, which name no local.)
|
|
130
|
+
def forwards_parameters?(node)
|
|
131
|
+
node.is_a?(Prism::ForwardingSuperNode) || node.compact_child_nodes.any? { |child| forwards_parameters?(child) }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def parameter_names(node, names)
|
|
135
|
+
return if node.nil?
|
|
136
|
+
|
|
137
|
+
names << node.name if node.class.name.end_with?("ParameterNode") && node.respond_to?(:name) && node.name
|
|
138
|
+
node.compact_child_nodes.each { |child| parameter_names(child, names) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def frame_call?(node)
|
|
142
|
+
ANY_RECEIVER_FRAME_CALLS.include?(node.name) || (node.receiver.nil? && FRAME_CALLS.include?(node.name))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def unparenthesised(node)
|
|
146
|
+
node = node.body.body.first while node.is_a?(Prism::ParenthesesNode) && node.body&.body&.size == 1
|
|
147
|
+
node
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# An index or attribute write (`h[k] = v`, `o.x = v`, `o&.x = v`): its value is a write's value.
|
|
151
|
+
def writer_call?(node)
|
|
152
|
+
name = node.name
|
|
153
|
+
name.end_with?("=") && !NON_WRITER_OPERATORS.include?(name)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# A write's value hands on every local read anywhere inside it, and the local it binds holds what it read.
|
|
157
|
+
def record_write(node, names)
|
|
158
|
+
return unless node.class.name.end_with?("WriteNode") && node.respond_to?(:value)
|
|
159
|
+
return unless every_read(node.value, names)
|
|
160
|
+
|
|
161
|
+
names << node.name if LOCAL_WRITES.any? { |k| node.is_a?(k) } && !fresh_value?(node.value)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# `Array`, `Hash` or `Set`, bare or as a top-level path (`::Set`): a project class may define its own `self.[]` or
|
|
165
|
+
# `self.new` that returns its argument.
|
|
166
|
+
def core_collection_constant?(node)
|
|
167
|
+
top_level = node.is_a?(Prism::ConstantReadNode) || (node.is_a?(Prism::ConstantPathNode) && node.parent.nil?)
|
|
168
|
+
top_level && CORE_COLLECTIONS.include?(node.name)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# A value that is a new object whatever it reads, so the local it binds aliases nothing.
|
|
172
|
+
def fresh_value?(value)
|
|
173
|
+
case value
|
|
174
|
+
when Prism::ArrayNode, Prism::HashNode then true
|
|
175
|
+
when Prism::CallNode then %i[new []].include?(value.name) && core_collection_constant?(value.receiver)
|
|
176
|
+
else false
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def record_multi_write(node, names)
|
|
181
|
+
return unless every_read(node.value, names)
|
|
182
|
+
|
|
183
|
+
local_targets([*node.lefts, node.rest, *node.rights], names)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def record_pattern(subject, patterns, names)
|
|
187
|
+
return unless every_read(subject, names)
|
|
188
|
+
|
|
189
|
+
local_targets(patterns, names)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Adds every local `node` reads, at any depth; answers whether it read any.
|
|
193
|
+
def every_read(node, names)
|
|
194
|
+
return false if node.nil?
|
|
195
|
+
|
|
196
|
+
found = node.is_a?(Prism::LocalVariableReadNode)
|
|
197
|
+
names << node.name if found
|
|
198
|
+
node.compact_child_nodes.reduce(found) { |acc, child| every_read(child, names) || acc }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def local_targets(nodes, names)
|
|
202
|
+
nodes.compact.each do |node|
|
|
203
|
+
names << node.name if node.is_a?(Prism::LocalVariableTargetNode)
|
|
204
|
+
local_targets(node.compact_child_nodes, names)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# A held position: every local read inside it but the receiver of a {PURE_READERS} call.
|
|
209
|
+
def record_held(node, names)
|
|
210
|
+
return if node.nil?
|
|
211
|
+
return if pure_read?(node)
|
|
212
|
+
|
|
213
|
+
names << node.name if node.is_a?(Prism::LocalVariableReadNode)
|
|
214
|
+
node.compact_child_nodes.each { |child| record_held(child, names) }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# A {PURE_READERS} call on a local hands the local on to nothing. Its own arguments are held positions of the
|
|
218
|
+
# call itself, which the body walk records when it reaches that call.
|
|
219
|
+
def pure_read?(node)
|
|
220
|
+
node.is_a?(Prism::CallNode) && PURE_READERS.include?(node.name) &&
|
|
221
|
+
node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# A block that may run after its call returns: any but a catalogued iterator's, or `tap` / `then` /
|
|
225
|
+
# `yield_self`'s.
|
|
226
|
+
def deferred_block?(call_node)
|
|
227
|
+
return true unless call_node.is_a?(Prism::CallNode)
|
|
228
|
+
|
|
229
|
+
!(ClosureEscapeAnalyzer.iterator_name?(call_node.name) || IMMEDIATE_BLOCK_CALLS.include?(call_node.name))
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../type"
|
|
6
|
+
require_relative "remover_holder_scan"
|
|
7
|
+
# `Narrowing` is resolved at call time: requiring it here closes a load cycle through `KeyPresenceGuard`.
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Inference
|
|
11
|
+
# Issue #2608 — a collection remover or filter narrows its receiver's element (or Hash key / value) type by what
|
|
12
|
+
# Ruby guarantees it removed, where that is provable from the call alone:
|
|
13
|
+
#
|
|
14
|
+
# - `Array#compact!` / `Hash#compact!` drop `nil` from the element / value;
|
|
15
|
+
# - `Array#delete(nil)` / `Set#delete(nil)` / `Set#delete?(nil)` drop `nil` from the element, and `Hash#delete(nil)`
|
|
16
|
+
# drops it from the key, given a literal `nil`;
|
|
17
|
+
# - `reject!` / `delete_if` keep the elements the block answers falsey for, and `select!` / `filter!` / `keep_if`
|
|
18
|
+
# the ones it answers truthy for. The block's one expression is read by the engine's own predicate narrowing
|
|
19
|
+
# ({Narrowing.predicate_scopes}) with its parameter bound to the element (the Hash `|k, v|` form binds both), and
|
|
20
|
+
# the surviving element is that parameter's binding on the matching edge. `&:nil?` reads as `{ |x| x.nil? }`;
|
|
21
|
+
# - a closed `HashShape` loses the key a literal `delete(:k)` names.
|
|
22
|
+
#
|
|
23
|
+
# Everything else (`pop`, `shift`, `slice!`, `subtract`, `delete` of a non-literal, a block whose predicate the
|
|
24
|
+
# narrowing does not read) answers `nil`: which members remain is not provable, and the caller keeps its reading.
|
|
25
|
+
#
|
|
26
|
+
# Only a receiver that is itself a local read narrows, and only when no other holder in its body may write the
|
|
27
|
+
# object ({RemoverHolderScan}): the narrowing is a fact about the object. A receiver selecting among bindings
|
|
28
|
+
# (`(f ? a : b).compact!`) mutates one of them, and narrowing every candidate would drop a member from the one the
|
|
29
|
+
# call never touched. An instance variable never narrows: any method, or any object that holds it, can write it.
|
|
30
|
+
module RemoverNarrowing
|
|
31
|
+
NIL_REJECTORS = %i[compact!].to_set.freeze
|
|
32
|
+
NIL_LITERAL_DELETERS = %i[delete delete?].to_set.freeze
|
|
33
|
+
FALSEY_KEEPERS = %i[reject! delete_if].to_set.freeze
|
|
34
|
+
TRUTHY_KEEPERS = %i[select! filter! keep_if].to_set.freeze
|
|
35
|
+
CANDIDATES = (NIL_REJECTORS | NIL_LITERAL_DELETERS | FALSEY_KEEPERS | TRUTHY_KEEPERS).freeze
|
|
36
|
+
NARROWING_RECEIVERS = [Prism::LocalVariableReadNode].freeze
|
|
37
|
+
CARRIERS = %w[Array Hash Set].freeze
|
|
38
|
+
# Nodes that make a one-expression block's value something other than its predicate, or rebind a parameter.
|
|
39
|
+
OPAQUE_BLOCK_NODES = [
|
|
40
|
+
Prism::NextNode, Prism::BreakNode, Prism::ReturnNode, Prism::RedoNode, Prism::RetryNode, Prism::YieldNode,
|
|
41
|
+
Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
|
|
42
|
+
Prism::LocalVariableAndWriteNode, Prism::LocalVariableTargetNode
|
|
43
|
+
].freeze
|
|
44
|
+
private_constant :NIL_REJECTORS, :NIL_LITERAL_DELETERS, :FALSEY_KEEPERS, :TRUTHY_KEEPERS, :NARROWING_RECEIVERS,
|
|
45
|
+
:CARRIERS, :OPAQUE_BLOCK_NODES
|
|
46
|
+
|
|
47
|
+
module_function
|
|
48
|
+
|
|
49
|
+
# Whether `call_node` may narrow its receiver at all: a cheap syntactic gate for {.narrow}.
|
|
50
|
+
def candidate?(call_node)
|
|
51
|
+
CANDIDATES.include?(call_node.name) && NARROWING_RECEIVERS.any? { |klass| call_node.receiver.is_a?(klass) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The closed `HashShape` `shape` without the key a literal `delete(:k)` names, or `nil` when the call is no such
|
|
55
|
+
# delete or the shape is open (an unseen key may be the one deleted, and the opened default reads it back).
|
|
56
|
+
def narrow_shape(shape, call_node)
|
|
57
|
+
return nil unless shape.is_a?(Type::HashShape) && shape.closed? && call_node.name == :delete
|
|
58
|
+
|
|
59
|
+
key = literal_key(single_argument(call_node))
|
|
60
|
+
return nil if key.equal?(NO_KEY)
|
|
61
|
+
return shape unless shape.pairs.key?(key)
|
|
62
|
+
return nil if shape.read_only_key?(key)
|
|
63
|
+
|
|
64
|
+
Type::Combinator.hash_shape_of(
|
|
65
|
+
shape.pairs.except(key),
|
|
66
|
+
required_keys: shape.required_keys - [key], optional_keys: shape.optional_keys - [key],
|
|
67
|
+
read_only_keys: shape.read_only_keys, extra_keys: :closed
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# `type` with each Array / Hash / Set member narrowed by what `call_node` removes, member-wise through a union.
|
|
72
|
+
# `nil` when the call narrows no member (it is no exact remover for the member's class, or its block predicate
|
|
73
|
+
# says nothing about the element). A member the call answers for without change (`compact!` on `Array[Integer]`)
|
|
74
|
+
# still counts as answered: the caller then keeps the precise reading rather than a floored one.
|
|
75
|
+
def narrow(type, call_node, scope)
|
|
76
|
+
answered = false
|
|
77
|
+
members = members_of(type).map do |member|
|
|
78
|
+
narrowed = narrow_member(member, call_node, scope)
|
|
79
|
+
next member if narrowed.nil?
|
|
80
|
+
|
|
81
|
+
answered = true
|
|
82
|
+
narrowed
|
|
83
|
+
end
|
|
84
|
+
answered ? Type::Combinator.union(*members) : nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def members_of(type)
|
|
88
|
+
type.is_a?(Type::Union) ? type.members : [type]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def narrow_member(member, call_node, scope)
|
|
92
|
+
return nil unless member.is_a?(Type::Nominal) && CARRIERS.include?(member.class_name)
|
|
93
|
+
|
|
94
|
+
args = member.type_args
|
|
95
|
+
case member.class_name
|
|
96
|
+
when "Hash" then args.size == 2 ? narrow_hash(member, call_node, scope) : nil
|
|
97
|
+
else args.size == 1 ? narrow_elements(member, call_node, scope) : nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def narrow_elements(member, call_node, scope)
|
|
102
|
+
element = member.type_args.first
|
|
103
|
+
narrowed =
|
|
104
|
+
case call_node.name
|
|
105
|
+
when :compact! then Narrowing.narrow_non_nil(element)
|
|
106
|
+
when :delete, :delete? then nil_argument?(call_node) ? Narrowing.narrow_non_nil(element) : nil
|
|
107
|
+
else block_narrowed(call_node, scope, [element])&.first
|
|
108
|
+
end
|
|
109
|
+
usable?(narrowed) ? Type::Combinator.nominal_of(member.class_name, type_args: [narrowed]) : nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def narrow_hash(member, call_node, scope)
|
|
113
|
+
key, value = member.type_args
|
|
114
|
+
narrowed =
|
|
115
|
+
case call_node.name
|
|
116
|
+
when :compact! then [key, Narrowing.narrow_non_nil(value)]
|
|
117
|
+
when :delete then nil_argument?(call_node) ? [Narrowing.narrow_non_nil(key), value] : nil
|
|
118
|
+
when :delete? then nil
|
|
119
|
+
else block_narrowed(call_node, scope, [key, value])
|
|
120
|
+
end
|
|
121
|
+
narrowed&.all? { |arg| usable?(arg) } ? Type::Combinator.nominal_of("Hash", type_args: narrowed) : nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# A parameter narrowed to `bot` (`select! { |x| x.nil? }` on `Array[Integer]`) says the carrier is empty; that
|
|
125
|
+
# is left to the caller's reading rather than spelled as `Array[bot]`.
|
|
126
|
+
def usable?(type)
|
|
127
|
+
!type.nil? && !type.is_a?(Type::Bot)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def nil_argument?(call_node)
|
|
131
|
+
single_argument(call_node).is_a?(Prism::NilNode)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def single_argument(call_node)
|
|
135
|
+
arguments = call_node.arguments&.arguments
|
|
136
|
+
arguments&.size == 1 ? arguments.first : nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
NO_KEY = Object.new.freeze
|
|
140
|
+
private_constant :NO_KEY
|
|
141
|
+
|
|
142
|
+
def literal_key(node)
|
|
143
|
+
case node
|
|
144
|
+
when Prism::SymbolNode then node.unescaped.to_sym
|
|
145
|
+
else NO_KEY
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The block parameters' types on the edge that keeps an element, or `nil` when the call has no block the
|
|
150
|
+
# narrowing reads: one expression, no jump or local write, explicit required parameters matching the carrier
|
|
151
|
+
# (`|x|` for an element, `|k, v|` for a Hash pair), and a predicate that narrows one of them on some edge.
|
|
152
|
+
def block_narrowed(call_node, scope, types)
|
|
153
|
+
keep = keep_edge(call_node.name)
|
|
154
|
+
return nil if keep.nil?
|
|
155
|
+
|
|
156
|
+
block = call_node.block
|
|
157
|
+
return symbol_nil_narrowed(block, keep, types) if block.is_a?(Prism::BlockArgumentNode)
|
|
158
|
+
|
|
159
|
+
names = block_parameter_names(block, types.size)
|
|
160
|
+
predicate = names && block_predicate(block)
|
|
161
|
+
return nil if predicate.nil?
|
|
162
|
+
|
|
163
|
+
predicate_narrowed(predicate, scope, names, types, keep)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def keep_edge(method_name)
|
|
167
|
+
if FALSEY_KEEPERS.include?(method_name) then :falsey
|
|
168
|
+
elsif TRUTHY_KEEPERS.include?(method_name) then :truthy
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def symbol_nil_narrowed(block, keep, types)
|
|
173
|
+
symbol = block.expression
|
|
174
|
+
return nil unless types.size == 1 && symbol.is_a?(Prism::SymbolNode) && symbol.unescaped == "nil?"
|
|
175
|
+
|
|
176
|
+
[keep == :falsey ? Narrowing.narrow_non_nil(types.first) : Narrowing.narrow_nil(types.first)]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def block_parameter_names(block, arity)
|
|
180
|
+
return nil unless block.is_a?(Prism::BlockNode)
|
|
181
|
+
|
|
182
|
+
parameters = block.parameters
|
|
183
|
+
return nil unless parameters.is_a?(Prism::BlockParametersNode)
|
|
184
|
+
|
|
185
|
+
list = parameters.parameters
|
|
186
|
+
return nil if list.nil? || !simple_parameters?(list) || list.requireds.size != arity
|
|
187
|
+
return nil unless list.requireds.all?(Prism::RequiredParameterNode)
|
|
188
|
+
|
|
189
|
+
list.requireds.map(&:name)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def simple_parameters?(list)
|
|
193
|
+
list.optionals.empty? && list.rest.nil? && list.posts.empty? && list.keywords.empty? &&
|
|
194
|
+
list.keyword_rest.nil? && list.block.nil?
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def block_predicate(block)
|
|
198
|
+
body = block.body
|
|
199
|
+
return nil unless body.is_a?(Prism::StatementsNode) && body.body.size == 1
|
|
200
|
+
|
|
201
|
+
statement = body.body.first
|
|
202
|
+
opaque?(statement) ? nil : statement
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def opaque?(node)
|
|
206
|
+
return true if OPAQUE_BLOCK_NODES.any? { |klass| node.is_a?(klass) }
|
|
207
|
+
|
|
208
|
+
node.compact_child_nodes.any? { |child| opaque?(child) }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def predicate_narrowed(predicate, scope, names, types, keep)
|
|
212
|
+
bound = names.zip(types).reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
213
|
+
truthy, falsey = Narrowing.predicate_scopes(predicate, bound)
|
|
214
|
+
read = ->(edge) { names.map { |name| edge.local(name) || bound.local(name) } }
|
|
215
|
+
entry_types = read.call(bound)
|
|
216
|
+
truthy_types = read.call(truthy)
|
|
217
|
+
falsey_types = read.call(falsey)
|
|
218
|
+
return nil if truthy_types == entry_types && falsey_types == entry_types
|
|
219
|
+
|
|
220
|
+
keep == :truthy ? truthy_types : falsey_types
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -24,7 +24,8 @@ module Rigor
|
|
|
24
24
|
# replacement, and the seed it would otherwise keep beside it is gone.
|
|
25
25
|
REPLACED = Ractor.make_shareable({
|
|
26
26
|
"Array" => { map!: [0], collect!: [0], flatten!: [0], replace: [0] },
|
|
27
|
-
"Hash" => { transform_keys!: [0], transform_values!: [1], replace: [0, 1] }
|
|
27
|
+
"Hash" => { transform_keys!: [0], transform_values!: [1], replace: [0, 1] },
|
|
28
|
+
"Set" => { map!: [0], collect!: [0], flatten!: [0], replace: [0] }
|
|
28
29
|
})
|
|
29
30
|
|
|
30
31
|
# Mutators that store values nothing describes BESIDE values they may keep: `fill(x, start)` leaves the slots
|
|
@@ -66,6 +67,56 @@ module Rigor
|
|
|
66
67
|
Type::Combinator.nominal_of(carrier.class_name, type_args: args)
|
|
67
68
|
end
|
|
68
69
|
|
|
70
|
+
# The element rewriters {.typed} answers for, keyed to the non-bang call whose result is the rewritten
|
|
71
|
+
# receiver: `a.map!(&:to_i)` holds what `a.map(&:to_i)` returns, and `a.flatten!` what `a.flatten` does.
|
|
72
|
+
TYPED_COUNTERPARTS = Ractor.make_shareable({ map!: :map, collect!: :collect, flatten!: :flatten })
|
|
73
|
+
|
|
74
|
+
# The carrier classes {.typed} rewrites, with the rewriters each takes. `Set#flatten!` is absent: it flattens
|
|
75
|
+
# only nested Sets, so `ARGV.map { |x| [x] }.to_set.flatten!` still holds `[String]` Arrays, while `Set#flatten`
|
|
76
|
+
# answers an element its RBS does not narrow.
|
|
77
|
+
TYPED_CARRIERS = Ractor.make_shareable({ "Array" => %i[map! collect! flatten!], "Set" => %i[map! collect!] })
|
|
78
|
+
|
|
79
|
+
# Issue #2427 — `carrier` with its element replaced by `element`, the element of what the non-bang counterpart
|
|
80
|
+
# ({TYPED_COUNTERPARTS}) answers for the same call: `a = ARGV.dup; a.map!(&:to_i)` reads `Array[Integer]` and
|
|
81
|
+
# `a.map!(&:strip)` keeps `Array[String]`. It is the typed form of {.arm}'s {REPLACED} answer, and it replaces
|
|
82
|
+
# the gradual and the precise element alike, since nothing the seed held survives the rewrite.
|
|
83
|
+
#
|
|
84
|
+
# Answers for an `Array` / `Set` nominal and an empty-witness refinement of one (keeping the witness only where
|
|
85
|
+
# {RefinementMutation.preserves_witness?} does: `map!` keeps it, `flatten!` can empty `[[]]`). `nil` for any
|
|
86
|
+
# other carrier or name, so the caller takes the gradual arm instead.
|
|
87
|
+
def typed(carrier, method_name, element)
|
|
88
|
+
return nil unless TYPED_COUNTERPARTS.key?(method_name)
|
|
89
|
+
|
|
90
|
+
case carrier
|
|
91
|
+
when Type::Nominal
|
|
92
|
+
return nil unless carrier.type_args.size == 1 && TYPED_CARRIERS[carrier.class_name]&.include?(method_name)
|
|
93
|
+
|
|
94
|
+
Type::Combinator.nominal_of(carrier.class_name, type_args: [element])
|
|
95
|
+
when Type::Difference
|
|
96
|
+
typed_refinement(carrier, method_name, element)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def typed_refinement(difference, method_name, element)
|
|
101
|
+
base = difference.base
|
|
102
|
+
return nil unless difference.removes_empty_witness? && base.is_a?(Type::Nominal)
|
|
103
|
+
|
|
104
|
+
rewritten = typed(base, method_name, element)
|
|
105
|
+
return nil if rewritten.nil?
|
|
106
|
+
|
|
107
|
+
kept = RefinementMutation.preserves_witness?(base.class_name, method_name)
|
|
108
|
+
kept ? Type::Combinator.difference(rewritten, difference.removed) : rewritten
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The element of `result`, the non-bang counterpart's answer: an `Array[R]` / `Set[R]` nominal, or an
|
|
112
|
+
# empty-witness refinement of one. `nil` when the answer has no single element to read.
|
|
113
|
+
def counterpart_element(result)
|
|
114
|
+
result = result.base if result.is_a?(Type::Difference) && result.removes_empty_witness?
|
|
115
|
+
return nil unless result.is_a?(Type::Nominal) && %w[Array Set].include?(result.class_name)
|
|
116
|
+
|
|
117
|
+
result.type_args.size == 1 ? result.type_args.first : nil
|
|
118
|
+
end
|
|
119
|
+
|
|
69
120
|
# {.arm} through a `Union` (member by member) and an empty-witness refinement (its base, keeping the witness
|
|
70
121
|
# only when the mutator cannot empty the receiver — {RefinementMutation.preserves_witness?}). For a carrier
|
|
71
122
|
# the ADR-56 slice-C join rebuilt from its seed, which knows the adders' content but not the rewrite.
|
|
@@ -114,7 +165,7 @@ module Rigor
|
|
|
114
165
|
when Type::Nominal then type.class_name
|
|
115
166
|
end
|
|
116
167
|
end
|
|
117
|
-
private_class_method :value_carrier_class, :arm_refinement
|
|
168
|
+
private_class_method :value_carrier_class, :arm_refinement, :typed_refinement
|
|
118
169
|
end
|
|
119
170
|
end
|
|
120
171
|
end
|