rigortype 0.4.5 → 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 +61 -5
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/manual/02-cli-reference.md +192 -27
- data/docs/manual/03-configuration.md +38 -4
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/09-editor-integration.md +13 -2
- data/docs/manual/10-mcp-server.md +47 -3
- data/docs/manual/12-caching.md +9 -1
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +49 -11
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +2 -1
- data/lib/rigor/analysis/baseline.rb +24 -27
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- 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 +105 -12
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -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/rbs_receiver_surface.rb +66 -3
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- 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 +53 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +217 -52
- data/lib/rigor/analysis/crash_signature.rb +2 -2
- data/lib/rigor/analysis/dependency_recorder.rb +15 -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 +154 -0
- data/lib/rigor/analysis/fact_store.rb +3 -3
- data/lib/rigor/analysis/incremental.rb +37 -0
- 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 +202 -43
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +7 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
- data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
- 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/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
- data/lib/rigor/analysis/runner.rb +233 -120
- 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 +38 -33
- 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 +60 -18
- 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 +188 -93
- 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 +27 -21
- 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 +17 -13
- data/lib/rigor/cli/lsp_command.rb +250 -23
- data/lib/rigor/cli/mcp_command.rb +14 -8
- 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 +60 -4
- data/lib/rigor/cli/plugin_command.rb +93 -46
- data/lib/rigor/cli/plugins_command.rb +22 -18
- data/lib/rigor/cli/plugins_renderer.rb +34 -8
- 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 +23 -16
- data/lib/rigor/cli/sig_gen_command.rb +130 -34
- 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 +18 -12
- data/lib/rigor/cli/type_of_project_scope.rb +24 -0
- 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 +17 -6
- data/lib/rigor/configuration/severity_profile.rb +5 -5
- data/lib/rigor/configuration.rb +86 -34
- 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 +592 -70
- data/lib/rigor/environment/reflection.rb +7 -7
- data/lib/rigor/environment/required_features.rb +136 -32
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
- data/lib/rigor/environment.rb +44 -11
- 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 +126 -0
- 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 +170 -0
- 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 +204 -12
- data/lib/rigor/inference/definer_resolution.rb +21 -5
- 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/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +173 -57
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- data/lib/rigor/inference/global_write_census.rb +110 -1
- 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 +246 -0
- data/lib/rigor/inference/key_presence_guard.rb +2 -1
- data/lib/rigor/inference/loop_value_record.rb +57 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +65 -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 +113 -12
- 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 +7 -3
- 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 +59 -24
- 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 +109 -29
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +23 -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 +162 -29
- data/lib/rigor/inference/narrowing.rb +113 -38
- data/lib/rigor/inference/object_mixins.rb +13 -1
- 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_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +122 -0
- 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 +9 -1
- data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
- 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/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +614 -146
- data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
- 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 +3036 -278
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
- 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 +9 -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 +28 -9
- 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 +98 -4
- 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 +12 -6
- 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 +82 -3
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- data/lib/rigor/plugin/base.rb +26 -11
- 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 +38 -16
- data/lib/rigor/plugin/registry.rb +41 -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 +96 -2
- 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 +25 -2
- 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 +379 -36
- 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 +668 -84
- 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 +7 -6
- 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/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +21 -12
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- 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 +13 -16
- data/lib/rigor/type/constant.rb +48 -7
- data/lib/rigor/type/hash_shape.rb +26 -40
- data/lib/rigor/type/struct_instance.rb +3 -2
- 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 +89 -70
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -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 +79 -19
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -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-devise/lib/rigor/plugin/devise.rb +5 -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 +7 -6
- 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 +102 -11
- 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 +5 -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/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +0 -2
- 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/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +10 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +1 -0
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +6 -5
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +6 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/scope.rbs +51 -50
- data/sig/rigor/type.rbs +0 -6
- 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 +55 -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
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../type"
|
|
5
|
+
require_relative "../source/node_walker"
|
|
6
|
+
require_relative "mutation_rejoin"
|
|
7
|
+
require_relative "receiver_alias"
|
|
8
|
+
require_relative "set_mutation"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
module Inference
|
|
12
|
+
# A loop hands its collection's own elements to its body (issue #2262), so a body that mutates one in place
|
|
13
|
+
# changes a shape that the collection's binding still carries one level down. This module is the two halves of
|
|
14
|
+
# dropping that shape: whether the body mutates an element, and the type with every shape dropped at every depth.
|
|
15
|
+
module DeepErasure
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# {MutationWidening.widen_tuple} / {MutationWidening.widen_hash_shape} applied at every depth: each `Tuple` /
|
|
19
|
+
# `HashShape` the type holds, as a union member or a type argument, drops to its nominal, so `[[1]]` reads
|
|
20
|
+
# `Array[Array[Integer]]`. A loop's element is the collection's own object, so a body that mutates it in place
|
|
21
|
+
# (`x << 2`) changes a shape one level down that the collection's binding still carries.
|
|
22
|
+
def widen(type)
|
|
23
|
+
case type
|
|
24
|
+
when Type::Tuple then widen_args(MutationWidening.widen_tuple(type))
|
|
25
|
+
when Type::HashShape then widen_args(MutationWidening.widen_hash_shape(type))
|
|
26
|
+
when Type::Nominal then widen_args(type)
|
|
27
|
+
when Type::Difference then widen(type.base)
|
|
28
|
+
when Type::Union then Type::Combinator.union(*type.members.map { |member| widen(member) })
|
|
29
|
+
else Type::Combinator.widen_value_pinned(type)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def widen_args(nominal)
|
|
34
|
+
return nominal unless nominal.is_a?(Type::Nominal) && !nominal.type_args.empty?
|
|
35
|
+
|
|
36
|
+
Type::Combinator.nominal_of(nominal.class_name, type_args: nominal.type_args.map { |arg| widen(arg) })
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The parameter nodes whose names a block (or a `for` index) binds to the collection's elements.
|
|
40
|
+
ELEMENT_PARAMETER_NODES = [
|
|
41
|
+
Prism::RequiredParameterNode, Prism::OptionalParameterNode, Prism::RestParameterNode,
|
|
42
|
+
Prism::KeywordRestParameterNode, Prism::LocalVariableTargetNode
|
|
43
|
+
].freeze
|
|
44
|
+
ELEMENT_WRITE_NODES = [
|
|
45
|
+
Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode,
|
|
46
|
+
Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode,
|
|
47
|
+
Prism::IndexTargetNode, Prism::CallTargetNode
|
|
48
|
+
].freeze
|
|
49
|
+
private_constant :ELEMENT_PARAMETER_NODES, :ELEMENT_WRITE_NODES
|
|
50
|
+
|
|
51
|
+
# True when `body` mutates in place a value that a name in `names` (a loop's element) holds: a mutator
|
|
52
|
+
# (`MutationWidening::SHAPE_MUTATORS`) called on a read or an index chain rooted at it (`x << 1`,
|
|
53
|
+
# `x[0].push(1)`, `x[0] = 1`), or an operator write on one (`x[0] += 1`). Every name a block or a
|
|
54
|
+
# `for` inside the body binds counts as an element too (`x.each { |y| y << 1 }`, `for c in x; c << 1; end`),
|
|
55
|
+
# and so does `it` / `_1` wherever it is read, and an accumulator parameter (`inject([]) { |acc, e| acc << e }`):
|
|
56
|
+
# the rule is wider than "an element", which only loses precision. It does not follow an alias (`y = x`) or
|
|
57
|
+
# an element passed to a call (`add(x)`), which the straight-line widening does not either.
|
|
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)
|
|
64
|
+
return false if body.nil?
|
|
65
|
+
|
|
66
|
+
names |= nested_parameter_names(body)
|
|
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
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The names `parameters` (a block's parameter list, or a `for` index) binds.
|
|
86
|
+
def parameter_names(parameters)
|
|
87
|
+
return [] if parameters.nil?
|
|
88
|
+
|
|
89
|
+
Source::NodeWalker.each(parameters).filter_map do |node|
|
|
90
|
+
node.name if ELEMENT_PARAMETER_NODES.include?(node.class)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def nested_parameter_names(body)
|
|
95
|
+
Source::NodeWalker.each(body).flat_map do |node|
|
|
96
|
+
case node
|
|
97
|
+
when Prism::BlockNode then parameter_names(node.parameters)
|
|
98
|
+
when Prism::ForNode then parameter_names(node.index)
|
|
99
|
+
else []
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def element_mutation_site?(node, names, set_held)
|
|
105
|
+
if node.is_a?(Prism::CallNode)
|
|
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)
|
|
110
|
+
else
|
|
111
|
+
ELEMENT_WRITE_NODES.include?(node.class) && element_rooted?(node.receiver, names)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def element_rooted?(node, names)
|
|
116
|
+
node = node.receiver while node.is_a?(Prism::CallNode) && node.receiver
|
|
117
|
+
node = node.body.body.last while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode)
|
|
118
|
+
case node
|
|
119
|
+
when Prism::LocalVariableReadNode then names.include?(node.name) || /\A_[1-9]\z/.match?(node.name)
|
|
120
|
+
when Prism::ItLocalVariableReadNode then true
|
|
121
|
+
else false
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# A block call hands each of its receiver's elements to the block's parameters, and a body that mutates one in
|
|
126
|
+
# place (`a.each { |x| x << 2 }`) changes the shape the receiver's binding still carries one level down
|
|
127
|
+
# (`[[1]]`). The receiver's variables drop their shapes at every depth ({widen}), as a `for` loop's
|
|
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}).
|
|
130
|
+
def widen_element_aliases(receiver, body, names, scope)
|
|
131
|
+
return scope unless element_mutation?(body, names) { receiver_holds_set?(receiver, scope) }
|
|
132
|
+
|
|
133
|
+
widen_receiver(receiver, scope)
|
|
134
|
+
end
|
|
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
|
+
|
|
143
|
+
def widen_receiver(receiver, scope)
|
|
144
|
+
ReceiverAlias.element_sources(receiver).reduce(scope) { |acc, read| widen_read(read, acc) }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# `a.each(&:strip!)`: a block argument naming a mutator mutates every element it is handed.
|
|
148
|
+
def symbol_proc_mutation?(block)
|
|
149
|
+
block.is_a?(Prism::BlockArgumentNode) && block.expression.is_a?(Prism::SymbolNode) &&
|
|
150
|
+
MutationWidening::SHAPE_MUTATORS.include?(block.expression.unescaped.to_sym)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def widen_symbol_proc(call_node, scope) = widen_receiver(call_node.receiver, scope)
|
|
154
|
+
|
|
155
|
+
def widen_read(read, scope)
|
|
156
|
+
getter, builder = MutationWidening::ALIAS_ACCESSORS[read.class]
|
|
157
|
+
return scope if getter.nil?
|
|
158
|
+
|
|
159
|
+
name = ReceiverAlias.read_name(read)
|
|
160
|
+
current = scope.public_send(getter, name)
|
|
161
|
+
return scope if current.nil?
|
|
162
|
+
|
|
163
|
+
widened = widen(current)
|
|
164
|
+
return scope if widened == current
|
|
165
|
+
|
|
166
|
+
MutationRejoin.rebind(scope, builder, name, widened, pre_state: current, kind: getter)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -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?
|