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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../source/node_walker"
|
|
5
|
+
require_relative "../source/node_children"
|
|
6
|
+
require_relative "../run_scoped_slots"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
# Issue #2555 L3 — which locals of a body may have ESCAPED, so a write to them never carries the `:synthesised`
|
|
11
|
+
# construction fact ({Scope#construction_fact?}).
|
|
12
|
+
#
|
|
13
|
+
# The fact lets the mutation seams re-grow a carrier without the `Dynamic[top]` floor, and the floor existed
|
|
14
|
+
# because a store the seam does not see leaves the carrier stale. A store reaches a local's object unseen only
|
|
15
|
+
# through another holder of it, and almost any position can make one: an alias, a container, an argument (a
|
|
16
|
+
# constructor or an attribute writer keeps it, `Array(a)` and `ident(a)` return it), a call that returns the
|
|
17
|
+
# receiver for some class (`Set#merge`, `a.each`), or a method the project defines. So every reference escapes
|
|
18
|
+
# unless it is in one of these positions:
|
|
19
|
+
#
|
|
20
|
+
# - the receiver of a call on the per-class {READERS} list, which neither returns, keeps nor mutates its receiver;
|
|
21
|
+
# - the receiver of an {ITERATORS} call or of a mutator the seams model ({.mutators_for}), with the call's value
|
|
22
|
+
# discarded;
|
|
23
|
+
# - the body's value position (its last statement, or `return a`), after which nothing in the body runs;
|
|
24
|
+
# - a bare read whose value is discarded.
|
|
25
|
+
#
|
|
26
|
+
# A listed name the project defines anywhere (a reopen, a prepended or included module, a refinement, any
|
|
27
|
+
# class at all) escapes all the same ({.project_defines?}). Every argument position escapes, and so does
|
|
28
|
+
# any reference inside a block or lambda, a write whose value is used (`a = b = …`), and every local of a
|
|
29
|
+
# body that calls `binding`, `eval`, `local_variable_set`, `instance_variable_set` or a string-form
|
|
30
|
+
# `instance_eval` / `class_eval` / `module_eval`.
|
|
31
|
+
# The scan is flow insensitive and runs once per body, so an escape anywhere drops the fact for the whole body:
|
|
32
|
+
# the conservative direction, since without the fact the seams answer as before. A nested `def` or class body is
|
|
33
|
+
# scanned too, though its locals are its own; a same-named local there can only over-report an escape.
|
|
34
|
+
module ConstructionEscape
|
|
35
|
+
CLASSES = %w[Array Hash Set].freeze
|
|
36
|
+
|
|
37
|
+
# The calls that answer a value other than the receiver, keep no reference to it, and change nothing in it.
|
|
38
|
+
READERS = Ractor.make_shareable(
|
|
39
|
+
{
|
|
40
|
+
"Array" => %i[
|
|
41
|
+
size length count empty? any? none? all? include? first last [] dig fetch index find_index sum min max
|
|
42
|
+
join dup map select filter reject sort sort_by uniq compact flatten reverse take drop to_h to_set ==
|
|
43
|
+
].to_set,
|
|
44
|
+
"Hash" => %i[
|
|
45
|
+
size length count empty? any? include? key? has_key? member? value? has_value? [] dig fetch keys values
|
|
46
|
+
dup map select filter reject to_a ==
|
|
47
|
+
].to_set,
|
|
48
|
+
"Set" => %i[size length count empty? any? include? member? first dup map select filter reject to_a ==].to_set
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# The calls that answer the receiver itself and yield only its members: safe with their value discarded.
|
|
53
|
+
ITERATORS = Ractor.make_shareable(
|
|
54
|
+
{
|
|
55
|
+
"Array" => %i[each each_with_index each_index reverse_each].to_set,
|
|
56
|
+
"Hash" => %i[each each_pair each_key each_value each_with_index].to_set,
|
|
57
|
+
"Set" => %i[each].to_set
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Calls that read or rebind the frame's locals by name: one anywhere in a body escapes every local in it.
|
|
62
|
+
FRAME_CALLS = %i[binding eval local_variable_set instance_variable_set].to_set.freeze
|
|
63
|
+
STRING_EVAL_CALLS = %i[instance_eval class_eval module_eval].to_set.freeze
|
|
64
|
+
|
|
65
|
+
CLOSURES = [Prism::BlockNode, Prism::LambdaNode].freeze
|
|
66
|
+
LOCAL_REFERENCES = [
|
|
67
|
+
Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode,
|
|
68
|
+
Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode, Prism::LocalVariableTargetNode
|
|
69
|
+
].freeze
|
|
70
|
+
LOCAL_WRITES = [
|
|
71
|
+
Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
|
|
72
|
+
Prism::LocalVariableAndWriteNode
|
|
73
|
+
].freeze
|
|
74
|
+
ALL = :all
|
|
75
|
+
EMPTY_CALLS = Set.new.freeze
|
|
76
|
+
# A run-scoped slot ({RunScopedSlots}): emptied when a run ends, so a long-lived process never serves a verdict
|
|
77
|
+
# from a body an earlier run parsed.
|
|
78
|
+
CACHE_KEY = RunScopedSlots.register(:__rigor_construction_escape)
|
|
79
|
+
private_constant :CLOSURES, :LOCAL_REFERENCES, :LOCAL_WRITES, :ALL, :EMPTY_CALLS, :CACHE_KEY,
|
|
80
|
+
:STRING_EVAL_CALLS
|
|
81
|
+
|
|
82
|
+
# One body's answer: per name, ALL or the classes it escapes as, and the calls the scan admitted on it.
|
|
83
|
+
Result = Data.define(:escapes, :calls)
|
|
84
|
+
private_constant :Result
|
|
85
|
+
|
|
86
|
+
module_function
|
|
87
|
+
|
|
88
|
+
# True when the local `name`, bound to a carrier of `class_name`, may escape somewhere in `body`, or when
|
|
89
|
+
# `body` is unknown. `scope` answers whether the project defines one of the admitted calls on the class
|
|
90
|
+
# ({.project_defines?}).
|
|
91
|
+
def escaped?(body, name, class_name, scope)
|
|
92
|
+
return true if body.nil? || class_name.nil?
|
|
93
|
+
|
|
94
|
+
table = (Thread.current[CACHE_KEY] ||= ObjectSpace::WeakKeyMap.new)
|
|
95
|
+
result = table[body] ||= scan(body)
|
|
96
|
+
return true if result.equal?(ALL)
|
|
97
|
+
|
|
98
|
+
reason = result.escapes[name]
|
|
99
|
+
return true if !reason.nil? && (reason.equal?(ALL) || reason.include?(class_name))
|
|
100
|
+
|
|
101
|
+
result.calls.fetch(name, EMPTY_CALLS).any? { |call| project_defines?(scope, class_name, call) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Whether the project defines a method named `call` anywhere ({Scope#project_method_name?}): on any class or
|
|
105
|
+
# module, instance or singleton side, in a `refine` block, in any file. An admitted call of such a name escapes
|
|
106
|
+
# the local whatever the receiver's class, because a prepended or included module, a refinement or a reopen can
|
|
107
|
+
# put that name on Array, Hash or Set, and telling which owner reaches the receiver is not worth the precision
|
|
108
|
+
# it buys: without the fact the seams answer as before. When no def is found the def-node lookup on the class
|
|
109
|
+
# itself still runs, so an incremental run records the missing `Array#size` and a later reopen re-analyses
|
|
110
|
+
# this file.
|
|
111
|
+
def project_defines?(scope, class_name, call)
|
|
112
|
+
scope.project_method_name?(call) || !scope.user_def_for(class_name, call).nil?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The receiver mutators the seams model for `class_name` ({MutationWidening}, {HashLookupMutation},
|
|
116
|
+
# {SetMutation}).
|
|
117
|
+
def mutators_for(class_name)
|
|
118
|
+
case class_name
|
|
119
|
+
when "Array" then MutationWidening::ARRAY_MUTATORS
|
|
120
|
+
when "Hash" then MutationWidening::HASH_MUTATORS | HashLookupMutation::MUTATORS
|
|
121
|
+
else SetMutation::MUTATORS
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def scan(body)
|
|
126
|
+
escapes = {}
|
|
127
|
+
calls = {}
|
|
128
|
+
Source::NodeWalker.each_with_ancestors(body) do |node, ancestors|
|
|
129
|
+
return ALL if frame_call?(node)
|
|
130
|
+
next unless LOCAL_REFERENCES.any? { |k| node.is_a?(k) }
|
|
131
|
+
|
|
132
|
+
record(escapes, calls, node, ancestors)
|
|
133
|
+
end
|
|
134
|
+
Result.new(escapes: escapes.freeze, calls: calls.freeze)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def record(escapes, calls, node, ancestors)
|
|
138
|
+
name = node.name
|
|
139
|
+
call = receiver_call(node, ancestors)
|
|
140
|
+
unsafe =
|
|
141
|
+
if call
|
|
142
|
+
unsafe_classes(call, ancestors).tap { |u| (calls[name] ||= Set.new) << call.name if u.size < CLASSES.size }
|
|
143
|
+
else
|
|
144
|
+
escape_reference?(node, ancestors) ? CLASSES : []
|
|
145
|
+
end
|
|
146
|
+
return if unsafe.empty? || escapes[name].equal?(ALL)
|
|
147
|
+
|
|
148
|
+
escapes[name] = unsafe.size == CLASSES.size ? ALL : (escapes[name] || Set.new).merge(unsafe)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def frame_call?(node)
|
|
152
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
153
|
+
|
|
154
|
+
FRAME_CALLS.include?(node.name) || (STRING_EVAL_CALLS.include?(node.name) && !node.arguments.nil?)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# The call a read outside any closure is the receiver of, or nil.
|
|
158
|
+
def receiver_call(node, ancestors)
|
|
159
|
+
return nil unless node.is_a?(Prism::LocalVariableReadNode) && !in_closure?(ancestors)
|
|
160
|
+
|
|
161
|
+
parent = ancestors.last
|
|
162
|
+
parent if parent.is_a?(Prism::CallNode) && parent.receiver.equal?(node)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The classes for which `call` on the local may hand it on.
|
|
166
|
+
def unsafe_classes(call, ancestors)
|
|
167
|
+
discarded = !value_used?(ancestors, ancestors.size - 2, call)
|
|
168
|
+
CLASSES.reject do |class_name|
|
|
169
|
+
READERS.fetch(class_name).include?(call.name) ||
|
|
170
|
+
(discarded && (ITERATORS.fetch(class_name).include?(call.name) ||
|
|
171
|
+
mutators_for(class_name).include?(call.name)))
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def in_closure?(ancestors)
|
|
176
|
+
ancestors.any? { |a| CLOSURES.any? { |k| a.is_a?(k) } }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Whether a reference that is no call's receiver may hand the object on.
|
|
180
|
+
def escape_reference?(node, ancestors)
|
|
181
|
+
return true if in_closure?(ancestors)
|
|
182
|
+
return value_used?(ancestors, ancestors.size - 1, node) if LOCAL_WRITES.any? { |k| node.is_a?(k) }
|
|
183
|
+
return false unless node.is_a?(Prism::LocalVariableReadNode)
|
|
184
|
+
|
|
185
|
+
parent = ancestors.last
|
|
186
|
+
return false if parent.is_a?(Prism::ArgumentsNode) && ancestors[-2].is_a?(Prism::ReturnNode)
|
|
187
|
+
|
|
188
|
+
!(parent.is_a?(Prism::StatementsNode) && !value_used?(ancestors, ancestors.size - 1, node))
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Whether the value of `node`, whose parent is `ancestors[index]`, reaches anything. The body's own value (its
|
|
192
|
+
# last statement) is the return position, after which nothing in the body runs, so it reads as unused.
|
|
193
|
+
def value_used?(ancestors, index, node)
|
|
194
|
+
while index >= 0
|
|
195
|
+
parent = ancestors[index]
|
|
196
|
+
case parent
|
|
197
|
+
when Prism::StatementsNode
|
|
198
|
+
return false unless parent.body.last.equal?(node)
|
|
199
|
+
when Prism::IfNode, Prism::UnlessNode, Prism::CaseNode, Prism::CaseMatchNode
|
|
200
|
+
return true if parent.predicate.equal?(node)
|
|
201
|
+
when Prism::WhenNode, Prism::InNode
|
|
202
|
+
return true unless parent.statements.equal?(node)
|
|
203
|
+
when Prism::WhileNode, Prism::UntilNode, Prism::EnsureNode
|
|
204
|
+
return !parent.statements.equal?(node)
|
|
205
|
+
when Prism::ElseNode, Prism::BeginNode, Prism::ParenthesesNode, Prism::ProgramNode, Prism::RescueNode
|
|
206
|
+
nil
|
|
207
|
+
else
|
|
208
|
+
return true
|
|
209
|
+
end
|
|
210
|
+
node = parent
|
|
211
|
+
index -= 1
|
|
212
|
+
end
|
|
213
|
+
false
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -235,7 +235,7 @@ module Rigor
|
|
|
235
235
|
# A subset of a multi-collector run that its nodes keep asking for, built the first time and reused:
|
|
236
236
|
# each collector alone (a pair in which one declines), the run without each collector (a larger run in
|
|
237
237
|
# which one declines), and the run split by `factory_block` and `lexical_prefix` variant where it mixes
|
|
238
|
-
# them. `
|
|
238
|
+
# them. `kind` names the table; `key` is the collector, or the rule's Symbol for a split.
|
|
239
239
|
def subset(kind, collector = nil)
|
|
240
240
|
table = (@subsets[kind] ||= {}.compare_by_identity)
|
|
241
241
|
key = collector || kind
|
|
@@ -5,6 +5,7 @@ require_relative "../type"
|
|
|
5
5
|
require_relative "../source/node_walker"
|
|
6
6
|
require_relative "mutation_rejoin"
|
|
7
7
|
require_relative "receiver_alias"
|
|
8
|
+
require_relative "set_mutation"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Inference
|
|
@@ -54,11 +55,31 @@ module Rigor
|
|
|
54
55
|
# and so does `it` / `_1` wherever it is read, and an accumulator parameter (`inject([]) { |acc, e| acc << e }`):
|
|
55
56
|
# the rule is wider than "an element", which only loses precision. It does not follow an alias (`y = x`) or
|
|
56
57
|
# an element passed to a call (`add(x)`), which the straight-line widening does not either.
|
|
57
|
-
|
|
58
|
+
#
|
|
59
|
+
# A Set-only mutator (`add`, `merge`, `subtract`, … — {SetMutation::MUTATORS} less `SHAPE_MUTATORS`) counts
|
|
60
|
+
# only when `set_held` is given and answers true (issue #2363). The names are read without their receiver,
|
|
61
|
+
# and `merge` on a Hash element returns a new Hash, so the caller answers from the collection's type whether
|
|
62
|
+
# an element could be a Set at all ({holds_set?}). The block runs at the first such site, at most once.
|
|
63
|
+
def element_mutation?(body, names, &set_held)
|
|
58
64
|
return false if body.nil?
|
|
59
65
|
|
|
60
66
|
names |= nested_parameter_names(body)
|
|
61
|
-
Source::NodeWalker.each(body).any? { |node| element_mutation_site?(node, names) }
|
|
67
|
+
Source::NodeWalker.each(body).any? { |node| element_mutation_site?(node, names, set_held) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# True when `type` holds a `Set` at some depth: as itself, a union member, a `Tuple` element, a `HashShape`
|
|
71
|
+
# value or a type argument. A collection whose type holds none has no element a Set-only mutator can be
|
|
72
|
+
# mutating, so a `merge` on one of its elements is `Hash#merge`, which returns a new Hash.
|
|
73
|
+
def holds_set?(type)
|
|
74
|
+
case type
|
|
75
|
+
when Type::Constant then SetMutation.constant?(type)
|
|
76
|
+
when Type::Nominal then type.class_name == "Set" || type.type_args.any? { |arg| holds_set?(arg) }
|
|
77
|
+
when Type::Tuple then type.elements.any? { |element| holds_set?(element) }
|
|
78
|
+
when Type::HashShape then type.pairs.each_value.any? { |value| holds_set?(value) }
|
|
79
|
+
when Type::Union then type.members.any? { |member| holds_set?(member) }
|
|
80
|
+
when Type::Difference then holds_set?(type.base)
|
|
81
|
+
else false
|
|
82
|
+
end
|
|
62
83
|
end
|
|
63
84
|
|
|
64
85
|
# The names `parameters` (a block's parameter list, or a `for` index) binds.
|
|
@@ -80,9 +101,12 @@ module Rigor
|
|
|
80
101
|
end
|
|
81
102
|
end
|
|
82
103
|
|
|
83
|
-
def element_mutation_site?(node, names)
|
|
104
|
+
def element_mutation_site?(node, names, set_held)
|
|
84
105
|
if node.is_a?(Prism::CallNode)
|
|
85
|
-
|
|
106
|
+
return false unless element_rooted?(node.receiver, names)
|
|
107
|
+
|
|
108
|
+
MutationWidening::SHAPE_MUTATORS.include?(node.name) ||
|
|
109
|
+
(!set_held.nil? && SetMutation::MUTATORS.include?(node.name) && set_held.call)
|
|
86
110
|
else
|
|
87
111
|
ELEMENT_WRITE_NODES.include?(node.class) && element_rooted?(node.receiver, names)
|
|
88
112
|
end
|
|
@@ -101,15 +125,23 @@ module Rigor
|
|
|
101
125
|
# A block call hands each of its receiver's elements to the block's parameters, and a body that mutates one in
|
|
102
126
|
# place (`a.each { |x| x << 2 }`) changes the shape the receiver's binding still carries one level down
|
|
103
127
|
# (`[[1]]`). The receiver's variables drop their shapes at every depth ({widen}), as a `for` loop's
|
|
104
|
-
# value does ({StatementEvaluator#for_collection_after}).
|
|
128
|
+
# value does ({StatementEvaluator#for_collection_after}). A receiver that is a call exposing a variable's
|
|
129
|
+
# elements (`h.values`, `a.dup`, `a.each_with_index`) names that variable ({ReceiverAlias.element_sources}).
|
|
105
130
|
def widen_element_aliases(receiver, body, names, scope)
|
|
106
|
-
return scope unless element_mutation?(body, names)
|
|
131
|
+
return scope unless element_mutation?(body, names) { receiver_holds_set?(receiver, scope) }
|
|
107
132
|
|
|
108
133
|
widen_receiver(receiver, scope)
|
|
109
134
|
end
|
|
110
135
|
|
|
136
|
+
def receiver_holds_set?(receiver, scope)
|
|
137
|
+
ReceiverAlias.element_sources(receiver).any? do |read|
|
|
138
|
+
getter, = MutationWidening::ALIAS_ACCESSORS[read.class]
|
|
139
|
+
getter && holds_set?(scope.public_send(getter, ReceiverAlias.read_name(read)))
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
111
143
|
def widen_receiver(receiver, scope)
|
|
112
|
-
ReceiverAlias.
|
|
144
|
+
ReceiverAlias.element_sources(receiver).reduce(scope) { |acc, read| widen_read(read, acc) }
|
|
113
145
|
end
|
|
114
146
|
|
|
115
147
|
# `a.each(&:strip!)`: a block argument naming a mutator mutates every element it is handed.
|
|
@@ -8,7 +8,7 @@ module Rigor
|
|
|
8
8
|
# file's methods: the file was never parsed this run, so there is no live node to store. A handle records
|
|
9
9
|
# the four things needed to resolve, fingerprint, or scope it without the node:
|
|
10
10
|
#
|
|
11
|
-
# - `path` + `node_id` — the resolution key. The per-run parse memo ({
|
|
11
|
+
# - `path` + `node_id` — the resolution key. The per-run parse memo ({DefNodeResolver.resolve}) parses the
|
|
12
12
|
# file once and returns the `Prism::DefNode` whose `node_id` matches (Prism `node_id` is stable across
|
|
13
13
|
# repeated parses of identical bytes). `name` is the cross-check: a resolved node whose `name` differs
|
|
14
14
|
# from the handle's forces a fresh walk (bundle/source skew that the digest gate should make impossible —
|
|
@@ -104,7 +104,7 @@ module Rigor
|
|
|
104
104
|
# is recorded like any other: it is the bundle saying the def is written at the top level, and dropping
|
|
105
105
|
# it would leave the warm path peeling the caller's namespace where a cold run resolves at the top
|
|
106
106
|
# level. Only `nil` — a bundle that recorded nothing for the def — is skipped, and
|
|
107
|
-
# `
|
|
107
|
+
# the `SCHEMA` bump to 17 (`Cache::IncrementalSnapshot`) keeps a pre-#716 blob, where `nil` MEANT top level,
|
|
108
108
|
# from being read as one.
|
|
109
109
|
def self.record_nesting(memo, node, nesting)
|
|
110
110
|
return if node.nil? || nesting.nil?
|
|
@@ -35,7 +35,7 @@ module Rigor
|
|
|
35
35
|
body = def_node.body
|
|
36
36
|
return nil if body.nil?
|
|
37
37
|
|
|
38
|
-
last =
|
|
38
|
+
last = primary_value_expression(body)
|
|
39
39
|
return nil if last.nil?
|
|
40
40
|
|
|
41
41
|
inner_scope = scope_index[last] || scope_index[body] || scope_index[def_node]
|
|
@@ -46,11 +46,54 @@ module Rigor
|
|
|
46
46
|
|
|
47
47
|
lookup_context = keep_lookup_nil ? LookupContext.new(scope_index, truthy_guards(body)) : nil
|
|
48
48
|
last_type = with_lookup_nil(last_type, last, inner_scope, lookup_context) if lookup_context
|
|
49
|
-
|
|
49
|
+
arm_types = rescue_arm_types(body, inner_scope, scope_index, lookup_context)
|
|
50
|
+
return nil if arm_types.nil?
|
|
51
|
+
|
|
52
|
+
union_with_explicit_returns(body, Type::Combinator.union(last_type, *arm_types), scope_index,
|
|
53
|
+
lookup_context: lookup_context)
|
|
50
54
|
rescue StandardError
|
|
51
55
|
nil
|
|
52
56
|
end
|
|
53
57
|
|
|
58
|
+
# Issue #2410 — the value a def body ends on when no exception escapes its primary path. A def-level
|
|
59
|
+
# `rescue … else … end` body is a `BeginNode` whose else-clause, when present, replaces the body's value (the
|
|
60
|
+
# ensure-clause's value is discarded, so it never counts).
|
|
61
|
+
def primary_value_expression(body)
|
|
62
|
+
return body_last_expression(body) unless body.is_a?(Prism::BeginNode) && body.else_clause
|
|
63
|
+
|
|
64
|
+
body_last_expression(body.else_clause.statements)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Issue #2410 — the value of each arm of a def-level rescue chain, which the method returns when that arm
|
|
68
|
+
# completes. An empty arm returns `nil`; an arm ending in `raise`, `retry` or `return` types as bottom and so
|
|
69
|
+
# adds nothing here (a `return`'s value is collected with the other explicit returns). Nil when an arm cannot
|
|
70
|
+
# be typed: the body's value alone would be a narrower, wrong contract.
|
|
71
|
+
def rescue_arm_types(body, inner_scope, scope_index, lookup_context)
|
|
72
|
+
return [] unless body.is_a?(Prism::BeginNode)
|
|
73
|
+
|
|
74
|
+
types = []
|
|
75
|
+
arm = body.rescue_clause
|
|
76
|
+
while arm
|
|
77
|
+
type = rescue_arm_type(arm, inner_scope, scope_index, lookup_context)
|
|
78
|
+
return nil if type.nil?
|
|
79
|
+
|
|
80
|
+
types << type
|
|
81
|
+
arm = arm.subsequent
|
|
82
|
+
end
|
|
83
|
+
types
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def rescue_arm_type(arm, inner_scope, scope_index, lookup_context)
|
|
87
|
+
value = body_last_expression(arm.statements)
|
|
88
|
+
return Type::Combinator.constant_of(nil) if value.nil?
|
|
89
|
+
|
|
90
|
+
scope = scope_index[value] || scope_index[arm] || inner_scope
|
|
91
|
+
type = safe_type_of(scope, value)
|
|
92
|
+
return nil if type.nil?
|
|
93
|
+
|
|
94
|
+
lookup_context ? with_lookup_nil(type, value, scope, lookup_context) : type
|
|
95
|
+
end
|
|
96
|
+
|
|
54
97
|
# What {#optimistic_value?} reads beside a node's scope: the engine's per-node scopes, and for each lookup
|
|
55
98
|
# under a guard the source of the guarding predicates ({#truthy_guards}).
|
|
56
99
|
LookupContext = Data.define(:scope_index, :guards)
|
|
@@ -227,8 +270,7 @@ module Rigor
|
|
|
227
270
|
|
|
228
271
|
scope = scope_index[return_node] || scope_index[args.first]
|
|
229
272
|
return if scope.nil?
|
|
230
|
-
# `return a, b` packs into a Tuple at runtime;
|
|
231
|
-
# returns contribute no type to keep the implementation focused.
|
|
273
|
+
# `return a, b` packs into a Tuple at runtime; only the single-value form is typed.
|
|
232
274
|
return unless args.size == 1
|
|
233
275
|
|
|
234
276
|
type = safe_type_of(scope, args.first)
|
|
@@ -25,8 +25,10 @@ module Rigor
|
|
|
25
25
|
# truth-tested: {UNKNOWN} and {ABSENT} are both truthy, so any other use folds a decline into a firing arm.
|
|
26
26
|
# `spec/rigor/inference/definer_resolution_case_in_spec.rb` fails on a call site of any other shape.
|
|
27
27
|
#
|
|
28
|
-
# It migrates one question at a time: `SourceArity` (C1b)
|
|
29
|
-
#
|
|
28
|
+
# It migrates one question at a time: `SourceArity` (C1b, C2-c) asks for the receiver's own side, instance or
|
|
29
|
+
# singleton; the override lints (C1c) and the C2 typing sites (`ExpressionTyper#resolve_typing_def_with_owner`,
|
|
30
|
+
# `StatementEvaluator#own_definer_node`) read the instance side; `RefinedDispatch::OwnMethod.certain_owner?` and
|
|
31
|
+
# `CheckRules#reached_definer_visibility` pass the side they hold.
|
|
30
32
|
module DefinerResolution
|
|
31
33
|
Known = Data.define(:answer, :owner)
|
|
32
34
|
UnknownResult = Data.define
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../run_scoped_slots"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# The `method_name (Symbol) → [owner names]` inverted index the override gate reads
|
|
8
|
+
# (`ExpressionTyper#method_definers_index`), over one instance or singleton def table.
|
|
9
|
+
#
|
|
10
|
+
# Issue #2300 — a file's def table is its own `def`s merged over the cross-file seed
|
|
11
|
+
# (`ScopeIndexer#merge_def_node_tables`, `#merge_singleton_def_nodes`, then the #526 extend fold), a new Hash per
|
|
12
|
+
# file. Inverting it whole scanned every project `def` once per analysed file. {.record_layers} remembers the
|
|
13
|
+
# seed a final merged table was built over, and {.for} then inverts the seed ONCE per seed table and, per file,
|
|
14
|
+
# only the owners whose inner table is not the seed's own object ({Layered}).
|
|
15
|
+
#
|
|
16
|
+
# The two answers hold the same owners: an owner the merge left alone shares the seed's inner table, so its
|
|
17
|
+
# names are the seed's, and every other owner is read from the merged table itself. That holds only while the
|
|
18
|
+
# seed cannot change under the cached index, so a seed whose tables are not all frozen, or a merged table
|
|
19
|
+
# missing one of the seed's owners, is inverted whole, as is a table {.record_layers} was not told about. Only
|
|
20
|
+
# the order of a name's owners can differ, and the gate asks `any?` of the list.
|
|
21
|
+
module DefinersIndex
|
|
22
|
+
# These thread slots are bounded, like `ExpressionTyper#method_definers_index`'s two ways. Each is a fixed
|
|
23
|
+
# number of entries, replaced rather than accumulated: two `[merged, seed]` pairs (the last file's two def
|
|
24
|
+
# tables) and two seed indexes. The run that filled them releases them when it ends ({RunScopedSlots}, #2399),
|
|
25
|
+
# so a finished run keeps none of its seed tables or their indexes alive.
|
|
26
|
+
LAYERS_KEY = RunScopedSlots.register(:__rigor_definers_index_layers__)
|
|
27
|
+
SEED_INDEX_KEY = RunScopedSlots.register(:__rigor_definers_index_seed__)
|
|
28
|
+
SEED_INDEX_ALT_KEY = RunScopedSlots.register(:__rigor_definers_index_seed_alt__)
|
|
29
|
+
private_constant :LAYERS_KEY, :SEED_INDEX_KEY, :SEED_INDEX_ALT_KEY
|
|
30
|
+
|
|
31
|
+
# A name's owners: the seed's, less the owners the file replaced, plus the replaced owners' own.
|
|
32
|
+
class Layered
|
|
33
|
+
def initialize(seed_index, replaced, replaced_index)
|
|
34
|
+
@seed_index = seed_index
|
|
35
|
+
@replaced = replaced
|
|
36
|
+
@replaced_index = replaced_index
|
|
37
|
+
@answers = {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def [](method_name)
|
|
41
|
+
return @answers[method_name] if @answers.key?(method_name)
|
|
42
|
+
|
|
43
|
+
from_seed = @seed_index[method_name]
|
|
44
|
+
from_seed = from_seed.reject { |owner| @replaced.key?(owner) } if from_seed && !@replaced.empty?
|
|
45
|
+
from_file = @replaced_index[method_name]
|
|
46
|
+
owners = from_file.nil? ? from_seed : (from_seed || []) + from_file
|
|
47
|
+
@answers[method_name] = owners.nil? || owners.empty? ? nil : owners
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
module_function
|
|
52
|
+
|
|
53
|
+
# Records that the final `merged` table was built over `seed`. The last two are kept (a file merges an
|
|
54
|
+
# instance and a singleton table), replaced rather than accumulated.
|
|
55
|
+
def record_layers(merged, seed)
|
|
56
|
+
previous = Thread.current[LAYERS_KEY]
|
|
57
|
+
Thread.current[LAYERS_KEY] = [[merged, seed], previous&.first].compact
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The definers index over `table`.
|
|
61
|
+
def for(table)
|
|
62
|
+
seed = Thread.current[LAYERS_KEY]&.find { |entry| entry[0].equal?(table) }&.last
|
|
63
|
+
return build(table) if seed.nil? || !deeply_frozen?(seed)
|
|
64
|
+
|
|
65
|
+
replaced = {}
|
|
66
|
+
kept = 0
|
|
67
|
+
table.each do |owner, methods|
|
|
68
|
+
if methods.equal?(seed[owner])
|
|
69
|
+
kept += 1
|
|
70
|
+
else
|
|
71
|
+
replaced[owner] = methods
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
return build(table) unless kept + replaced.count { |owner, _| seed.key?(owner) } == seed.size
|
|
75
|
+
|
|
76
|
+
Layered.new(seed_index(seed), replaced, build(replaced))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def deeply_frozen?(seed)
|
|
80
|
+
seed.frozen? && seed.each_value.all?(&:frozen?)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The seed's index, built once per seed table. Two ways, an instance and a singleton seed table.
|
|
84
|
+
def seed_index(seed)
|
|
85
|
+
[SEED_INDEX_KEY, SEED_INDEX_ALT_KEY].each do |key|
|
|
86
|
+
slot = Thread.current[key]
|
|
87
|
+
return slot[1] if slot && slot[0].equal?(seed)
|
|
88
|
+
end
|
|
89
|
+
index = build(seed)
|
|
90
|
+
Thread.current[SEED_INDEX_ALT_KEY] = Thread.current[SEED_INDEX_KEY]
|
|
91
|
+
Thread.current[SEED_INDEX_KEY] = [seed, index]
|
|
92
|
+
index
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def build(table)
|
|
96
|
+
index = {}
|
|
97
|
+
table.each do |class_name, methods|
|
|
98
|
+
next if class_name == ScopeIndexer::TOP_LEVEL_DEF_KEY
|
|
99
|
+
|
|
100
|
+
methods.each_key { |method_name| (index[method_name] ||= []) << class_name }
|
|
101
|
+
end
|
|
102
|
+
index
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|