rigortype 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -2
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
- data/docs/handbook/01-getting-started.md +6 -3
- data/docs/handbook/11-sig-gen.md +85 -3
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +455 -113
- data/docs/manual/03-configuration.md +76 -5
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +21 -2
- data/docs/manual/10-mcp-server.md +38 -2
- data/docs/manual/12-caching.md +4 -3
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +32 -15
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/baseline.rb +24 -27
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
- data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
- data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules.rb +720 -141
- data/lib/rigor/analysis/crash_signature.rb +45 -3
- data/lib/rigor/analysis/dependency_recorder.rb +237 -80
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/edit_replay.rb +93 -6
- data/lib/rigor/analysis/fact_store.rb +3 -3
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
- data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
- data/lib/rigor/analysis/incremental_session.rb +306 -53
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +39 -3
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
- data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
- data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
- data/lib/rigor/analysis/runner.rb +298 -109
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
- data/lib/rigor/analysis/template_unit_positions.rb +2 -3
- data/lib/rigor/analysis/template_units.rb +9 -1
- data/lib/rigor/analysis/worker_session.rb +65 -71
- data/lib/rigor/bleeding_edge.rb +22 -10
- data/lib/rigor/builtins/imported_refinements.rb +6 -8
- data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
- data/lib/rigor/builtins/regex_refinement.rb +4 -6
- data/lib/rigor/cache/descriptor.rb +6 -5
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +95 -17
- data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
- data/lib/rigor/cache/rbs_constant_table.rb +1 -1
- data/lib/rigor/cache/rbs_descriptor.rb +90 -4
- data/lib/rigor/cache/rbs_environment.rb +8 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
- data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
- data/lib/rigor/cache/store.rb +11 -10
- data/lib/rigor/ci_detector.rb +1 -1
- data/lib/rigor/cli/annotate_command.rb +24 -19
- data/lib/rigor/cli/baseline_command.rb +160 -57
- data/lib/rigor/cli/catalogue.rb +256 -0
- data/lib/rigor/cli/check_command.rb +112 -104
- data/lib/rigor/cli/check_runner_factory.rb +2 -2
- data/lib/rigor/cli/command.rb +15 -1
- data/lib/rigor/cli/coverage_command.rb +21 -8
- data/lib/rigor/cli/coverage_mutation.rb +5 -7
- data/lib/rigor/cli/coverage_renderer.rb +2 -1
- data/lib/rigor/cli/coverage_scan.rb +3 -5
- data/lib/rigor/cli/diff_command.rb +25 -20
- data/lib/rigor/cli/docs_command.rb +1 -2
- data/lib/rigor/cli/doctor_command.rb +12 -12
- data/lib/rigor/cli/effects_command.rb +50 -40
- data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
- data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
- data/lib/rigor/cli/effects_renderer.rb +2 -1
- data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
- data/lib/rigor/cli/explain_command.rb +18 -8
- data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
- data/lib/rigor/cli/json_document.rb +41 -0
- data/lib/rigor/cli/lens_command.rb +29 -10
- data/lib/rigor/cli/lsp_command.rb +250 -23
- data/lib/rigor/cli/mcp_command.rb +12 -7
- data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
- data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
- data/lib/rigor/cli/options.rb +40 -1
- data/lib/rigor/cli/plugin_command.rb +93 -46
- data/lib/rigor/cli/plugins_command.rb +17 -18
- data/lib/rigor/cli/plugins_renderer.rb +10 -6
- data/lib/rigor/cli/protection_fork_scan.rb +2 -2
- data/lib/rigor/cli/protection_renderer.rb +2 -1
- data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
- data/lib/rigor/cli/sig_gen_command.rb +92 -27
- data/lib/rigor/cli/skill_command.rb +32 -3
- data/lib/rigor/cli/skill_deep_probe.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +14 -9
- data/lib/rigor/cli/triage_command.rb +10 -11
- data/lib/rigor/cli/triage_renderer.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +16 -11
- data/lib/rigor/cli/type_of_project_scope.rb +1 -4
- data/lib/rigor/cli/type_of_renderer.rb +4 -1
- data/lib/rigor/cli/type_scan_command.rb +14 -9
- data/lib/rigor/cli/type_scan_renderer.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +27 -17
- data/lib/rigor/cli/upgrade_command.rb +5 -0
- data/lib/rigor/cli.rb +214 -56
- data/lib/rigor/config_audit.rb +33 -19
- data/lib/rigor/configuration/severity_profile.rb +14 -5
- data/lib/rigor/configuration.rb +104 -35
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/effects/definition_context.rb +0 -2
- data/lib/rigor/effects/envelope.rb +1 -1
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/mutation_classifier.rb +4 -7
- data/lib/rigor/effects/propagator.rb +1 -1
- data/lib/rigor/effects/registry.rb +2 -2
- data/lib/rigor/effects/snapshot.rb +1 -1
- data/lib/rigor/effects/taint_cause.rb +0 -2
- data/lib/rigor/effects/unit_scan.rb +9 -13
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
- data/lib/rigor/environment/class_registry.rb +3 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/core_root.rb +77 -0
- data/lib/rigor/environment/default_libraries.rb +18 -1
- data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
- data/lib/rigor/environment/gated_signature_guard.rb +1 -1
- data/lib/rigor/environment/gated_signature_plan.rb +5 -2
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/lockfile_resolver.rb +28 -12
- data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
- data/lib/rigor/environment/member_consistency.rb +56 -7
- data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
- data/lib/rigor/environment/project_path_gems.rb +124 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
- data/lib/rigor/environment/rbs_loader.rb +772 -110
- data/lib/rigor/environment/reflection.rb +7 -7
- data/lib/rigor/environment/required_features.rb +0 -3
- data/lib/rigor/environment/ruby_library_roots.rb +1 -1
- data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
- data/lib/rigor/environment.rb +169 -42
- data/lib/rigor/flow_contribution/merger.rb +4 -4
- data/lib/rigor/inference/acceptance.rb +136 -40
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/block_run_count.rb +185 -0
- data/lib/rigor/inference/budget_trace.rb +8 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
- data/lib/rigor/inference/callee_write_census.rb +815 -0
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +258 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/deep_erasure.rb +39 -7
- data/lib/rigor/inference/def_handle.rb +1 -1
- data/lib/rigor/inference/def_node_resolver.rb +33 -15
- data/lib/rigor/inference/def_return_typer.rb +47 -5
- data/lib/rigor/inference/definer_resolution.rb +19 -9
- data/lib/rigor/inference/definers_index.rb +162 -0
- data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
- data/lib/rigor/inference/element_read_widening.rb +49 -3
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1559 -320
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +81 -5
- data/lib/rigor/inference/global_writer_reach.rb +260 -0
- data/lib/rigor/inference/hkt_body.rb +1 -2
- data/lib/rigor/inference/hkt_body_parser.rb +3 -5
- data/lib/rigor/inference/hkt_reducer.rb +2 -2
- data/lib/rigor/inference/hkt_registry.rb +3 -5
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +61 -15
- data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
- data/lib/rigor/inference/ivar_writer_census.rb +154 -8
- data/lib/rigor/inference/key_presence_guard.rb +51 -24
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +66 -14
- data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
- data/lib/rigor/inference/match_rebinding.rb +28 -5
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +123 -25
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +24 -3
- data/lib/rigor/inference/multi_target_binder.rb +2 -2
- data/lib/rigor/inference/mutation_rejoin.rb +240 -4
- data/lib/rigor/inference/mutation_widening.rb +199 -29
- data/lib/rigor/inference/narrowing.rb +238 -50
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +134 -9
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
- data/lib/rigor/inference/pre_eval_constants.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +30 -9
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/project_yielder.rb +11 -17
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_alias.rb +20 -0
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +287 -0
- data/lib/rigor/inference/remover_narrowing.rb +224 -0
- data/lib/rigor/inference/rewrite_mutation.rb +53 -2
- data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
- data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
- data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2213 -226
- data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/set_mutation.rb +189 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +3268 -358
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
- data/lib/rigor/inference/toplevel_def_census.rb +67 -0
- data/lib/rigor/inference/unknown_store_widening.rb +103 -4
- data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -9
- data/lib/rigor/language_server/buffer_table.rb +3 -3
- data/lib/rigor/language_server/completion_provider.rb +26 -20
- data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -2
- data/lib/rigor/language_server/hover_provider.rb +1 -7
- data/lib/rigor/language_server/hover_renderer.rb +6 -8
- data/lib/rigor/language_server/project_context.rb +39 -1
- data/lib/rigor/language_server/publish_batcher.rb +3 -4
- data/lib/rigor/language_server/selection_range_provider.rb +2 -4
- data/lib/rigor/language_server/server.rb +7 -3
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/plugins.rb +1 -1
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/mcp/server.rb +73 -2
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/bundled_catalog.rb +1 -12
- data/lib/rigor/plugin/io_boundary.rb +26 -4
- data/lib/rigor/plugin/loader.rb +25 -8
- data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
- data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
- data/lib/rigor/plugin/manifest.rb +16 -16
- data/lib/rigor/plugin/registry.rb +16 -5
- data/lib/rigor/plugin/services.rb +2 -3
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
- data/lib/rigor/plugin/trust_policy.rb +48 -2
- data/lib/rigor/plugin/type_node_resolver.rb +2 -4
- data/lib/rigor/plugin.rb +6 -8
- data/lib/rigor/plugin_gap_advisory.rb +5 -1
- data/lib/rigor/project_environment.rb +16 -2
- data/lib/rigor/protection/analysis_guard.rb +1 -1
- data/lib/rigor/protection/discovery_seed.rb +44 -5
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
- data/lib/rigor/rbs_extended/reporter.rb +27 -3
- data/lib/rigor/rbs_extended.rb +2 -3
- data/lib/rigor/reflection/constant_ancestors.rb +81 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +32 -1
- data/lib/rigor/run_scoped_slots.rb +38 -0
- data/lib/rigor/runtime/jit.rb +1 -1
- data/lib/rigor/scope/discovery_index.rb +168 -8
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +901 -54
- data/lib/rigor/scope.rb +644 -37
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +18 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
- data/lib/rigor/sig_gen/generator.rb +588 -66
- data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
- data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
- data/lib/rigor/sig_gen/layout_index.rb +3 -3
- data/lib/rigor/sig_gen/method_candidate.rb +37 -4
- data/lib/rigor/sig_gen/observation_collector.rb +14 -7
- data/lib/rigor/sig_gen/observed_call.rb +1 -1
- data/lib/rigor/sig_gen/path_mapper.rb +6 -0
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/renderer.rb +15 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
- data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
- data/lib/rigor/sig_gen/write_reporter.rb +63 -14
- data/lib/rigor/sig_gen/write_result.rb +23 -12
- data/lib/rigor/sig_gen/writer.rb +238 -37
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/acceptance_router.rb +2 -2
- data/lib/rigor/type/accepts_result.rb +1 -1
- data/lib/rigor/type/app.rb +4 -6
- data/lib/rigor/type/combinator.rb +173 -21
- data/lib/rigor/type/constant.rb +4 -5
- data/lib/rigor/type/hash_shape.rb +21 -40
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type/union.rb +1 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/identifier.rb +1 -2
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node/string_literal.rb +1 -2
- data/lib/rigor/type_node/symbol_literal.rb +1 -2
- data/lib/rigor/type_node/union.rb +3 -6
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/value_semantics.rb +1 -10
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
- data/sig/rigor/analysis/baseline.rbs +0 -3
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -2
- data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/cli/diff_command.rbs +1 -1
- data/sig/rigor/cli/explain_command.rbs +1 -1
- data/sig/rigor/effects/registry.rbs +2 -2
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +53 -18
- data/sig/rigor/plugin/effect_attribution.rbs +3 -3
- data/sig/rigor/plugin/effect_edge.rbs +1 -1
- data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +9 -9
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/plugin/protocol_contract.rbs +3 -7
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +43 -4
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-plugin-author/SKILL.md +4 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +82 -3
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../element_read_widening"
|
|
6
|
+
require_relative "../mutation_widening"
|
|
7
|
+
require_relative "../receiver_alias"
|
|
8
|
+
require_relative "../remover_holder_scan"
|
|
9
|
+
require_relative "../set_mutation"
|
|
10
|
+
require_relative "foreign_locals"
|
|
11
|
+
|
|
12
|
+
module Rigor
|
|
13
|
+
module Inference
|
|
14
|
+
module AliasMutationFloor
|
|
15
|
+
ALWAYS = -1
|
|
16
|
+
LOOPS = [Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::BlockNode, Prism::LambdaNode].freeze
|
|
17
|
+
KEEPING_CALLS = (MutationWidening::SHAPE_MUTATORS | SetMutation::MUTATORS |
|
|
18
|
+
%i[new add add? send public_send __send__ instance_variable_set instance_exec instance_eval
|
|
19
|
+
class_exec module_exec define_method define_singleton_method]).freeze
|
|
20
|
+
private_constant :ALWAYS, :LOOPS, :KEEPING_CALLS
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
IMMUTABLE_CLASSES = %w[Integer Float Rational Complex Symbol NilClass TrueClass FalseClass].to_set.freeze
|
|
25
|
+
private_constant :IMMUTABLE_CLASSES
|
|
26
|
+
|
|
27
|
+
# Whether `type` is a container that may hold a mutable element, which a write through an element read (`x =
|
|
28
|
+
# a[0]; x << 1`) can change: `[[1]]` and `Array[String]` may, `[1, :a]` may not.
|
|
29
|
+
def mutable_parts?(type)
|
|
30
|
+
case type
|
|
31
|
+
when Type::Tuple then type.elements.any? { |element| mutable_value?(element) }
|
|
32
|
+
when Type::HashShape then type.pairs.values.any? { |value| mutable_value?(value) }
|
|
33
|
+
when Type::Nominal then type.type_args.any? { |arg| mutable_value?(arg) }
|
|
34
|
+
when Type::Difference then mutable_parts?(type.base)
|
|
35
|
+
when Type::Union then type.members.any? { |member| mutable_parts?(member) }
|
|
36
|
+
else true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def mutable_value?(type)
|
|
41
|
+
case type
|
|
42
|
+
when Type::Constant then type.value.is_a?(String) || type.value.is_a?(::Set)
|
|
43
|
+
when Type::Nominal then !IMMUTABLE_CLASSES.include?(type.class_name)
|
|
44
|
+
when Type::Union then type.members.any? { |member| mutable_value?(member) }
|
|
45
|
+
else true
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Each local or ivar read `node` may evaluate to: through the branches of an `if` / `unless` / ternary, `case`,
|
|
50
|
+
# `case`/`in`, `begin` (its body, rescue and else clauses), `rescue` modifier and `||` / `&&`, the last statement
|
|
51
|
+
# of a body, and a call that answers its receiver (`a.tap { }`, `a.itself`, `a.freeze`, `a.to_a`); anything else
|
|
52
|
+
# as {ReceiverAlias.candidates} reads it. A nest deeper than {SELECTION_DEPTH} reads as no local.
|
|
53
|
+
def each_may_be(node, depth = 0, &)
|
|
54
|
+
node = RemoverHolderScan.unparenthesised(node)
|
|
55
|
+
return if node.nil? || depth > SELECTION_DEPTH
|
|
56
|
+
return each_may_be(node.receiver, depth + 1, &) if self_returning_call?(node)
|
|
57
|
+
|
|
58
|
+
branches = selection_branches(node)
|
|
59
|
+
return ReceiverAlias.candidates(node).each(&) if branches.nil?
|
|
60
|
+
|
|
61
|
+
branches.each { |branch| each_may_be(branch, depth + 1, &) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
SELECTION_DEPTH = 6
|
|
65
|
+
|
|
66
|
+
def self_returning_call?(node) = node.is_a?(Prism::CallNode) && SELF_RETURNERS.include?(node.name)
|
|
67
|
+
|
|
68
|
+
# The sub-expressions a selection's value is one of, or nil for a node that selects nothing.
|
|
69
|
+
def selection_branches(node)
|
|
70
|
+
case node
|
|
71
|
+
when Prism::StatementsNode then [node.body.last]
|
|
72
|
+
when Prism::ParenthesesNode, Prism::ElseNode then [node.respond_to?(:statements) ? node.statements : node.body]
|
|
73
|
+
when Prism::IfNode then [node.statements, node.subsequent]
|
|
74
|
+
when Prism::UnlessNode then [node.statements, node.else_clause]
|
|
75
|
+
when Prism::OrNode, Prism::AndNode then [node.left, node.right]
|
|
76
|
+
when Prism::CaseNode, Prism::CaseMatchNode
|
|
77
|
+
node.conditions.map(&:statements) << node.else_clause
|
|
78
|
+
when Prism::BeginNode then begin_branches(node)
|
|
79
|
+
when Prism::RescueModifierNode then [node.expression, node.rescue_expression]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def begin_branches(node)
|
|
84
|
+
branches = [node.statements, node.else_clause]
|
|
85
|
+
clause = node.rescue_clause
|
|
86
|
+
while clause
|
|
87
|
+
branches << clause.statements
|
|
88
|
+
clause = clause.subsequent
|
|
89
|
+
end
|
|
90
|
+
branches
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def local_name(read) = ReceiverAlias.read_name(read)
|
|
94
|
+
|
|
95
|
+
# `[base, depth]` for an element read (`a[i]`, `a.first`, `h.fetch(:k)`, `a.dig(:k, 0)`, a chain of them): what
|
|
96
|
+
# it reads from and how many levels inside it, `[node, 0]` for anything else.
|
|
97
|
+
def element_path(node)
|
|
98
|
+
node = RemoverHolderScan.unparenthesised(node)
|
|
99
|
+
depth = 0
|
|
100
|
+
while node.is_a?(Prism::CallNode) && node.receiver && node.block.nil? && ELEMENT_READS.include?(node.name)
|
|
101
|
+
depth += node.name == :dig ? [node.arguments&.arguments&.size.to_i, 1].max : 1
|
|
102
|
+
node = RemoverHolderScan.unparenthesised(node.receiver)
|
|
103
|
+
end
|
|
104
|
+
[node, depth]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# `[root, depth]` for an element read rooted at a local, or nil.
|
|
108
|
+
def element_root(node)
|
|
109
|
+
root, depth = element_path(node)
|
|
110
|
+
[root, depth] if depth.positive? && root.is_a?(Prism::LocalVariableReadNode)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The {RemoverHolderScan} collector, which reads every fact {.scan} needs in that one walk: for each local the
|
|
114
|
+
# least key of the sites that hand it on ({.handed_by}), the ranges of the closures that may run later, and the
|
|
115
|
+
# body's foreign and written locals ({ForeignLocals.visit}).
|
|
116
|
+
class SiteCollector
|
|
117
|
+
# Non-local reads: a local bound to one, or to an element read rooted at one (`a = @rows[0]`), is held by that
|
|
118
|
+
# variable. ({ReceiverAlias.candidates} answers a bare ivar read itself.)
|
|
119
|
+
NON_LOCAL_READS = [Prism::InstanceVariableReadNode, Prism::GlobalVariableReadNode,
|
|
120
|
+
Prism::ClassVariableReadNode].to_set.freeze
|
|
121
|
+
LOCAL_BINDS = [Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode,
|
|
122
|
+
Prism::LocalVariableAndWriteNode].to_set.freeze
|
|
123
|
+
|
|
124
|
+
attr_writer :site
|
|
125
|
+
attr_reader :held, :held_part, :links, :deferred, :foreign, :written
|
|
126
|
+
|
|
127
|
+
def initialize
|
|
128
|
+
@held = {}
|
|
129
|
+
@held_part = {}
|
|
130
|
+
@links = []
|
|
131
|
+
@site = nil
|
|
132
|
+
@outer = nil
|
|
133
|
+
@outer_owner = nil
|
|
134
|
+
@deferred = []
|
|
135
|
+
@foreign = Set.new
|
|
136
|
+
@written = Set.new
|
|
137
|
+
@literal_blocks = Set.new.compare_by_identity
|
|
138
|
+
@def_depth = 0
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# A local handed on whole: a capture by a closure that may run later, a reflective receiver, a zero-argument
|
|
142
|
+
# `super`'s parameter.
|
|
143
|
+
def <<(name)
|
|
144
|
+
note(@held, name)
|
|
145
|
+
self
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A value position another holder keeps (an element, an argument of a keeping call, an index or attribute
|
|
149
|
+
# write's value): each local the value may be is handed on, and the root of an element read hands on a part.
|
|
150
|
+
def value_held(node)
|
|
151
|
+
AliasMutationFloor.each_may_be(node) do |read|
|
|
152
|
+
note(@held, AliasMutationFloor.local_name(read)) if local?(read)
|
|
153
|
+
end
|
|
154
|
+
root, = AliasMutationFloor.element_root(node)
|
|
155
|
+
note(@held_part, root.name) if root
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# A write: a local bound to a value that may be another local's object links the two (`b = a`), an element read
|
|
159
|
+
# links them as a part (`x = a[i]`), and a local bound to a non-local holder's object is held by that holder
|
|
160
|
+
# (`a = @q`). A non-local write hands the value on as {#value_held} does.
|
|
161
|
+
def value_written(node)
|
|
162
|
+
return if node.is_a?(Prism::LocalVariableOperatorWriteNode)
|
|
163
|
+
return value_held(node.value) unless LOCAL_BINDS.include?(node.class)
|
|
164
|
+
|
|
165
|
+
bind(node.name, node.value)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# A multiple assignment or a pattern: each local target is bound as {#value_written} binds one, to each element
|
|
169
|
+
# of an Array literal value or to the value whole.
|
|
170
|
+
def value_bound(value, target_nodes)
|
|
171
|
+
names = []
|
|
172
|
+
RemoverHolderScan.local_targets(target_nodes, names)
|
|
173
|
+
return if names.empty?
|
|
174
|
+
|
|
175
|
+
values = value.is_a?(Prism::ArrayNode) ? value.elements : [value]
|
|
176
|
+
names.each { |name| values.each { |part| bind(name, part) } }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def enter(node, parent)
|
|
180
|
+
if @outer.nil? && LOOPS.any? { |klass| node.is_a?(klass) }
|
|
181
|
+
@outer = node.location.start_offset
|
|
182
|
+
@outer_owner = node
|
|
183
|
+
end
|
|
184
|
+
note_deferred(node, parent)
|
|
185
|
+
if node.is_a?(Prism::DefNode)
|
|
186
|
+
@def_depth += 1
|
|
187
|
+
elsif @def_depth.zero?
|
|
188
|
+
ForeignLocals.visit(node, @foreign, @written, @literal_blocks)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def leave(node)
|
|
193
|
+
if node.equal?(@outer_owner)
|
|
194
|
+
@outer = nil
|
|
195
|
+
@outer_owner = nil
|
|
196
|
+
end
|
|
197
|
+
@def_depth -= 1 if node.is_a?(Prism::DefNode)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Only a call that may keep its argument hands it to another holder: a constructor, an in-place mutator (a
|
|
201
|
+
# container adder), and a reflective or `define_method` call. Any other call's arguments are passed, not kept.
|
|
202
|
+
def keeps_arguments?(call_node) = KEEPING_CALLS.include?(call_node.name)
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
def local?(read) = read.is_a?(Prism::LocalVariableReadNode) || read.is_a?(Prism::ItLocalVariableReadNode)
|
|
207
|
+
|
|
208
|
+
def key = @site ? @outer || AliasMutationFloor.handed_by(@site) : ALWAYS
|
|
209
|
+
|
|
210
|
+
def note(table, name)
|
|
211
|
+
site_key = key
|
|
212
|
+
current = table[name]
|
|
213
|
+
table[name] = site_key if current.nil? || site_key < current
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def bind(name, value)
|
|
217
|
+
AliasMutationFloor.each_may_be(value) do |read|
|
|
218
|
+
next unless local?(read)
|
|
219
|
+
|
|
220
|
+
other = AliasMutationFloor.local_name(read)
|
|
221
|
+
@links << [name, other, key, 0] unless other == name
|
|
222
|
+
end
|
|
223
|
+
note(@held, name) if NON_LOCAL_READS.include?(AliasMutationFloor.element_path(value).first.class)
|
|
224
|
+
root, depth = AliasMutationFloor.element_root(value)
|
|
225
|
+
@links << [name, root.name, key, depth] if root && root.name != name
|
|
226
|
+
link_elements(name, value)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# A container literal's elements are the local's elements (`c = [inner]`): each local an element may be is
|
|
230
|
+
# linked to it as a part.
|
|
231
|
+
def link_elements(name, value)
|
|
232
|
+
value = RemoverHolderScan.unparenthesised(value)
|
|
233
|
+
elements = value.elements if value.is_a?(Prism::ArrayNode)
|
|
234
|
+
elements = value.elements.grep(Prism::AssocNode).map(&:value) if value.is_a?(Prism::HashNode)
|
|
235
|
+
elements&.each do |element|
|
|
236
|
+
AliasMutationFloor.each_may_be(element) do |read|
|
|
237
|
+
@links << [AliasMutationFloor.local_name(read), name, key, 1] if local?(read)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def note_deferred(node, parent)
|
|
243
|
+
return unless node.is_a?(Prism::LambdaNode) ||
|
|
244
|
+
(node.is_a?(Prism::BlockNode) && RemoverHolderScan.deferred_block?(parent))
|
|
245
|
+
|
|
246
|
+
start = node.location.start_offset
|
|
247
|
+
@deferred << [start, start + node.location.length]
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../typing_state"
|
|
6
|
+
require_relative "../source/node_children"
|
|
7
|
+
require_relative "closure_escape_analyzer"
|
|
8
|
+
require_relative "element_read_widening"
|
|
9
|
+
require_relative "mutation_widening"
|
|
10
|
+
require_relative "receiver_alias"
|
|
11
|
+
require_relative "remover_holder_scan"
|
|
12
|
+
require_relative "set_mutation"
|
|
13
|
+
require_relative "string_mutation"
|
|
14
|
+
require_relative "unknown_store_widening"
|
|
15
|
+
require_relative "alias_mutation_floor/foreign_locals"
|
|
16
|
+
require_relative "alias_mutation_floor/memos"
|
|
17
|
+
require_relative "alias_mutation_floor/site_collector"
|
|
18
|
+
require_relative "alias_mutation_floor/reach"
|
|
19
|
+
|
|
20
|
+
module Rigor
|
|
21
|
+
module Inference
|
|
22
|
+
# Issue #2335 — a local's literal shape describes its object, and a mutation through another path to that object
|
|
23
|
+
# makes it stale: `y = x; y << "s"`, `a.each { |x| x << "s" }` on an Array holding `inner`, `@a = a; @a << "s"`,
|
|
24
|
+
# `h[:k] = a; h[:k] << "s"`, `Hash.new(a)[:k] << "s"`, `[acc].each { |a| poke(a) }`. The named-binding widening
|
|
25
|
+
# ({MutationWidening.widen_after_call}) and the callee floor only rebind the binding a receiver or argument names,
|
|
26
|
+
# so every other local holding the object kept its literal and a later read reported on correct code.
|
|
27
|
+
#
|
|
28
|
+
# One scan per frame body, in {RemoverHolderScan}'s walk ({SiteCollector}), answers:
|
|
29
|
+
#
|
|
30
|
+
# - the locals handed whole to another holder: an element of a container literal, an argument of a call that may
|
|
31
|
+
# keep it, an ivar, global, index or attribute write's value, a capture by a closure that may run later, a
|
|
32
|
+
# reflective receiver, a local bound to an ivar, global or class variable (`a = @q`); every local under
|
|
33
|
+
# `binding` / `eval`. A value counts only where it may evaluate to the local itself ({.each_may_be}: the local,
|
|
34
|
+
# a selection of it, a call that answers its receiver), so `n = a.size`, `b = a.dup` and `[a.first]` hand
|
|
35
|
+
# nothing on whole;
|
|
36
|
+
# - the locals one of whose elements is handed on (`[a.first]`), and the links between locals: a copy (`b = a`)
|
|
37
|
+
# and an element read (`x = a[i]`);
|
|
38
|
+
# - the *foreign* locals, whose object may have come from elsewhere ({ForeignLocals}).
|
|
39
|
+
#
|
|
40
|
+
# A local is *aliased* at a call once a site that hands it on may have run before the call's message (a site
|
|
41
|
+
# earlier in the body, or one inside a loop or block that also holds the call; a capture by a closure that may run
|
|
42
|
+
# later counts everywhere, and a call inside such a closure sees every site), or it is linked to such a local. A
|
|
43
|
+
# local reached only through an element link is a *part*, floored only when its binding can hold a mutable
|
|
44
|
+
# element ({.mutable_parts?}).
|
|
45
|
+
#
|
|
46
|
+
# A mutating call whose receiver may be another local's object ({.targets}: the locals linked to the receiver, and
|
|
47
|
+
# the aliased ones when the receiver may hold any object) gives each local it may reach the
|
|
48
|
+
# unknown store {UnknownStoreWidening.widen_store} gives a site whose stored values nothing typed: the same
|
|
49
|
+
# widening the named path applies, fed `Dynamic[top]` arguments, with its gradual arm. A callee that writes such
|
|
50
|
+
# an argument, and a call on one to a method that mutates its instance variables, floor them the same way. The
|
|
51
|
+
# bindings the receiver itself names are left to the named path, so its precise answer stands.
|
|
52
|
+
#
|
|
53
|
+
# A body where no local is handed on, and a receiver only ever written a new object that is not aliased, change
|
|
54
|
+
# nothing.
|
|
55
|
+
module AliasMutationFloor
|
|
56
|
+
# `held` maps each local handed to another holder to the least key of those sites ({.handed_by}), or is {ALL};
|
|
57
|
+
# `held_part` does the same for a local an element of which was handed on (`[a.first]`); `links` lists
|
|
58
|
+
# `[child, parent, key, levels]` for each write that binds a local to another local's object (`b = a`, 0 levels)
|
|
59
|
+
# or to an element of it (`x = a[i]`, 1; `x = a[i][j]`, 2; `c = [x]` links `x` as `c`'s element). `deferred`
|
|
60
|
+
# lists the `[start, end)` ranges of the closures that may run later. `memo` and `named_memo` keep each call's and
|
|
61
|
+
# each read's targets for the run: a loop or block body is evaluated more than once.
|
|
62
|
+
Facts = Data.define(:held, :held_part, :links, :foreign, :entry, :deferred, :memo, :named_memo)
|
|
63
|
+
# Depths ({.reach}): the mutated object is the local's object itself, or one of its elements.
|
|
64
|
+
FULL = 0
|
|
65
|
+
PART = 1
|
|
66
|
+
NO_KEYS = {}.freeze
|
|
67
|
+
NO_LINKS = [].freeze
|
|
68
|
+
# Calls whose result is their receiver (or, for `to_a` / `to_h` / `to_s`, may be on one core class), as
|
|
69
|
+
# {CalleeWriteCensus} reads them.
|
|
70
|
+
SELF_RETURNERS = %i[tap itself freeze to_a to_h to_s].to_set.freeze
|
|
71
|
+
|
|
72
|
+
ALL = :all
|
|
73
|
+
NUMBERED_PARAMETER = /\A_[1-9]\z/
|
|
74
|
+
SEND_NAMES = %i[send public_send __send__].to_set.freeze
|
|
75
|
+
ELEMENT_READS = %i[[] first last fetch dig].to_set.freeze
|
|
76
|
+
private_constant :FULL, :PART, :NO_KEYS, :NO_LINKS, :SELF_RETURNERS, :ALL, :NUMBERED_PARAMETER,
|
|
77
|
+
:SEND_NAMES, :ELEMENT_READS
|
|
78
|
+
private_constant :Memos
|
|
79
|
+
|
|
80
|
+
module_function
|
|
81
|
+
|
|
82
|
+
# `scope` after `call_node`, a shape mutator ({.candidate?}), ran on a receiver that may be another local's
|
|
83
|
+
# object. The block answers the locals the receiver itself names, whose own widening (the named path's)
|
|
84
|
+
# stands; it runs only once the call has targets.
|
|
85
|
+
def floor_after_call(call_node, scope)
|
|
86
|
+
return scope unless local_besides?(scope, nil) { |type| UnknownStoreWidening.callee_floorable?(type) }
|
|
87
|
+
|
|
88
|
+
facts = facts_for(scope)
|
|
89
|
+
return scope if facts.nil?
|
|
90
|
+
|
|
91
|
+
targets = call_targets(facts, call_node) { receiver_targets(call_node.receiver, facts, call_node) }
|
|
92
|
+
return scope if targets.empty?
|
|
93
|
+
|
|
94
|
+
whole = !targets.equal?(ALL)
|
|
95
|
+
floor_targets(scope, targets, yield) do |type, kind|
|
|
96
|
+
unknown_store(call_node, type, removed_keys: whole && kind == FULL)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Whether `call_node` is a shape mutator on an explicit receiver, the only call {.floor_after_call} floors after.
|
|
101
|
+
def candidate?(call_node)
|
|
102
|
+
!call_node.receiver.nil? && MutationWidening::SHAPE_MUTATORS.include?(call_node.name)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# `scope` after `call_node` wrote the object the local `read` reads (a callee that writes its argument or mutates
|
|
106
|
+
# the ivars of an object holding it): every local that may be that object and holds a literal shape
|
|
107
|
+
# ({.literal_shape?}) takes one unknown store ({UnknownStoreWidening.callee_block_floor}). A method parameter the
|
|
108
|
+
# body never rebinds floors nothing here: the callee census reads every opaque hand-off as a write, and flooring
|
|
109
|
+
# each local a parameter was copied into at every such call cost precision across the corpus for no finding (a
|
|
110
|
+
# known limit of #2335).
|
|
111
|
+
def floor_after_written_local(scope, read, call_node)
|
|
112
|
+
name = read.name
|
|
113
|
+
return scope unless local_besides?(scope, name) { |type| literal_shape?(type) }
|
|
114
|
+
|
|
115
|
+
facts = facts_for(scope)
|
|
116
|
+
return scope if facts.nil?
|
|
117
|
+
return scope if facts.entry.include?(name)
|
|
118
|
+
|
|
119
|
+
targets = read_targets(facts, read) { targets(name, facts, aliased_at(facts, call_node), call_node) }
|
|
120
|
+
return scope if targets.empty?
|
|
121
|
+
|
|
122
|
+
floor_targets(scope, targets, name) { |type| callee_floor(type) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# `type` after `call_node`'s unknown store ({UnknownStoreWidening.widen_store}), memoised per call site for the
|
|
126
|
+
# run as {.callee_floor} is.
|
|
127
|
+
def unknown_store(call_node, type, removed_keys:)
|
|
128
|
+
Memos.unknown_store(call_node, removed_keys, type) do
|
|
129
|
+
method_name, arg_types = UnknownStoreWidening.unknown_store(call_node)
|
|
130
|
+
(removed_keys && removed_keys_shape(type, method_name)) ||
|
|
131
|
+
UnknownStoreWidening.widen_store(type, method_name, arg_types)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# A `HashShape` under a remover (`delete`, `clear`, `reject!`, ...) through another holder: the same shape with
|
|
136
|
+
# every key optional. Nothing is stored, so every value stands, and a read of a key the remover may have taken
|
|
137
|
+
# still answers its `nil` (`g = h; g.delete(k); h[k].upcase` raises in Ruby), which the nominal `Hash` the unknown
|
|
138
|
+
# store widens to would not. Only for a target that may be the mutated object itself, and not under `binding` /
|
|
139
|
+
# `eval`: a remover on an element (`x = h[:k]; x.delete(:a)`) must not mark `h`'s own keys optional and leave the
|
|
140
|
+
# element's removed key required. Nil for anything else.
|
|
141
|
+
def removed_keys_shape(type, method_name)
|
|
142
|
+
return nil unless type.is_a?(Type::HashShape) && UnknownStoreWidening::VALUE_PRESERVING.include?(method_name)
|
|
143
|
+
|
|
144
|
+
Type::HashShape.new(type.pairs, required_keys: [], optional_keys: type.pairs.keys,
|
|
145
|
+
read_only_keys: type.read_only_keys, extra_keys: type.extra_keys)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# {UnknownStoreWidening.callee_block_floor} of a literal shape, nil for anything else, memoised for the run: the
|
|
149
|
+
# same shape is floored again on each pass over a loop or block body and by each callee channel.
|
|
150
|
+
def callee_floor(type)
|
|
151
|
+
return nil unless literal_shape?(type)
|
|
152
|
+
|
|
153
|
+
Memos.callee_floor(type) { UnknownStoreWidening.callee_block_floor(type) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Whether {.floor_after_written_local} may floor anything for the local `read` reads at `call_node`: a target
|
|
157
|
+
# bound to a literal shape.
|
|
158
|
+
def reaches_other_locals?(scope, read, call_node)
|
|
159
|
+
name = read.name
|
|
160
|
+
return false unless local_besides?(scope, name) { |type| literal_shape?(type) }
|
|
161
|
+
|
|
162
|
+
facts = facts_for(scope)
|
|
163
|
+
return false if facts.nil? || facts.entry.include?(name)
|
|
164
|
+
|
|
165
|
+
targets = read_targets(facts, read) { targets(name, facts, aliased_at(facts, call_node), call_node) }
|
|
166
|
+
return local_besides?(scope, name) { |type| literal_shape?(type) } if targets.equal?(ALL)
|
|
167
|
+
|
|
168
|
+
targets.any? do |target, kind|
|
|
169
|
+
type = scope.local(target)
|
|
170
|
+
target != name && literal_shape?(type) && (kind == FULL || mutable_parts?(type))
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Whether a call that may mutate a holder's object through `self` ({.floor_after_holder_call}) reaches an aliased
|
|
175
|
+
# local holding a literal shape.
|
|
176
|
+
def holder_call_reaches?(scope, call_node)
|
|
177
|
+
return false unless local_besides?(scope, nil) { |type| literal_shape?(type) }
|
|
178
|
+
|
|
179
|
+
facts = facts_for(scope)
|
|
180
|
+
return false if facts.nil?
|
|
181
|
+
|
|
182
|
+
targets = holder_targets(facts, call_node)
|
|
183
|
+
return true if targets.equal?(ALL)
|
|
184
|
+
|
|
185
|
+
targets.any? do |target, kind|
|
|
186
|
+
type = scope.local(target)
|
|
187
|
+
literal_shape?(type) && (kind == FULL || mutable_parts?(type))
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# `scope` after a call on `self` whose method mutates one of its ivars: every aliased local holding a literal
|
|
192
|
+
# shape takes one unknown store, as {.floor_after_written_local} gives it.
|
|
193
|
+
def floor_after_holder_call(scope, call_node)
|
|
194
|
+
facts = facts_for(scope)
|
|
195
|
+
return scope if facts.nil?
|
|
196
|
+
|
|
197
|
+
floor_targets(scope, holder_targets(facts, call_node), nil) { |type| callee_floor(type) }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def holder_targets(facts, call_node)
|
|
201
|
+
call_targets(facts, call_node) { targets(nil, facts, aliased_at(facts, call_node), call_node) }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# A literal shape a store can make stale: a `Tuple`, a `HashShape`, a literal String or Set, or an empty-witness
|
|
205
|
+
# refinement, alone or in a union. The callee paths floor only these: a nominal `Array[String]` is a claim the
|
|
206
|
+
# named path's unknown store leaves as it is too.
|
|
207
|
+
def literal_shape?(type)
|
|
208
|
+
case type
|
|
209
|
+
when Type::Tuple, Type::HashShape then true
|
|
210
|
+
when Type::Constant then StringMutation.constant?(type) || SetMutation.constant?(type)
|
|
211
|
+
when Type::Difference then type.removes_empty_witness?
|
|
212
|
+
when Type::Union then type.members.any? { |member| literal_shape?(member) }
|
|
213
|
+
else false
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Whether the body of `def_node` runs a shape mutator on one of its own instance variables or an element of one
|
|
218
|
+
# (`@items << v`, `@h[k] = v`, `@rows[0] << v`): a call to it on an object that holds another local's object
|
|
219
|
+
# (`box = Box.new(a)`, whose `initialize` keeps `@items = a`) may mutate that object. A nested def is not entered.
|
|
220
|
+
def mutates_ivar?(def_node)
|
|
221
|
+
return false if def_node.nil?
|
|
222
|
+
|
|
223
|
+
Memos.ivar_mutator(def_node) { ivar_mutation?(def_node.body) }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def ivar_mutation?(node)
|
|
227
|
+
return false if node.nil? || node.is_a?(Prism::DefNode)
|
|
228
|
+
return true if node.is_a?(Prism::CallNode) && MutationWidening::SHAPE_MUTATORS.include?(node.name) &&
|
|
229
|
+
ivar_rooted?(node.receiver)
|
|
230
|
+
|
|
231
|
+
node.rigor_each_child { |child| return true if ivar_mutation?(child) }
|
|
232
|
+
false
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def ivar_rooted?(receiver)
|
|
236
|
+
receiver = RemoverHolderScan.unparenthesised(receiver)
|
|
237
|
+
receiver = receiver.receiver while receiver.is_a?(Prism::CallNode) && ELEMENT_READS.include?(receiver.name)
|
|
238
|
+
receiver.is_a?(Prism::InstanceVariableReadNode)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# A `send` / `public_send` / `__send__` with a literal Symbol or String name as the call it makes (`a.send(:push,
|
|
242
|
+
# x)` and `a.send("push", x)` are `a.push(x)`), or nil. A name that is no valid string names no method.
|
|
243
|
+
def literal_send_target(call_node)
|
|
244
|
+
return nil unless SEND_NAMES.include?(call_node.name) && call_node.block.nil?
|
|
245
|
+
|
|
246
|
+
arguments = call_node.arguments&.arguments
|
|
247
|
+
target = arguments&.first
|
|
248
|
+
return nil unless target.is_a?(Prism::SymbolNode) || target.is_a?(Prism::StringNode)
|
|
249
|
+
return nil unless target.unescaped.valid_encoding?
|
|
250
|
+
|
|
251
|
+
rest = arguments.drop(1)
|
|
252
|
+
call_node.copy(name: target.unescaped.to_sym,
|
|
253
|
+
arguments: rest.empty? ? nil : call_node.arguments.copy(arguments: rest))
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def facts_for(scope)
|
|
257
|
+
frame = scope.match_frame
|
|
258
|
+
body = frame&.body
|
|
259
|
+
return nil if body.nil?
|
|
260
|
+
|
|
261
|
+
facts = Memos.facts(body) { scan(body, frame.parameters) }
|
|
262
|
+
facts.held.empty? && facts.held_part.empty? && facts.links.empty? ? nil : facts
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# One walk ({SiteCollector}) answers the facts. A body that calls `binding` or `eval` stops it early: every local
|
|
266
|
+
# is aliased, and the foreign locals are read in a walk of their own.
|
|
267
|
+
def scan(body, parameters)
|
|
268
|
+
collector = SiteCollector.new
|
|
269
|
+
result = RemoverHolderScan.unsafe_names(body, parameters, collector)
|
|
270
|
+
if result.equal?(ALL)
|
|
271
|
+
foreign, entry = ForeignLocals.collect(body, parameters)
|
|
272
|
+
return Facts.new(held: ALL, held_part: NO_KEYS, links: NO_LINKS, foreign:, entry:, deferred: NO_LINKS,
|
|
273
|
+
memo: {}.compare_by_identity, named_memo: {}.compare_by_identity)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
foreign, entry = ForeignLocals.finish(parameters, collector.foreign, collector.written)
|
|
277
|
+
Facts.new(held: collector.held.freeze, held_part: collector.held_part.freeze, links: collector.links.freeze,
|
|
278
|
+
foreign:, entry:, deferred: collector.deferred.freeze,
|
|
279
|
+
memo: {}.compare_by_identity, named_memo: {}.compare_by_identity)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# A site's key ({SiteCollector}) is the start of the outermost loop or block that holds it (it may run again after
|
|
283
|
+
# any later point inside that loop), else where it has handed its locals on: its end (the receiver of a call has
|
|
284
|
+
# handed its locals on before the call's message runs, while the call's own arguments are handed on by the call
|
|
285
|
+
# itself), or its block's start. A local's key is its least site's; a capture by a closure that may run later is
|
|
286
|
+
# {ALWAYS}.
|
|
287
|
+
# Where a site has handed its locals on: a call's block may receive them as soon as it starts.
|
|
288
|
+
def handed_by(node)
|
|
289
|
+
block = node.block if node.is_a?(Prism::CallNode)
|
|
290
|
+
block.is_a?(Prism::BlockNode) ? block.location.start_offset : node.location.end_offset
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# The targets of `call_node` through its receiver, computed once per call site.
|
|
294
|
+
def call_targets(facts, call_node)
|
|
295
|
+
facts.memo.fetch(call_node) { facts.memo[call_node] = yield }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# The targets of a write through the local `read` reads (a written argument, a receiver), computed once per read.
|
|
299
|
+
# The read is the call's own operand, so its call's offset is fixed with it.
|
|
300
|
+
def read_targets(facts, read)
|
|
301
|
+
facts.named_memo.fetch(read) { facts.named_memo[read] = yield }
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Whether `scope` binds a local other than `except` (a name, a list of names, or nil) whose type the block
|
|
305
|
+
# accepts: the cheap gate every trigger asks before the body scan, since a floor changes only such a binding.
|
|
306
|
+
def local_besides?(scope, except)
|
|
307
|
+
scope.locals.each { |name, type| return true if !excepted?(except, name) && yield(type) }
|
|
308
|
+
false
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def excepted?(except, name)
|
|
312
|
+
except.is_a?(Symbol) ? except == name : except&.include?(name)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def each_target(scope, targets, &)
|
|
316
|
+
return scope.locals.each_key { |name| yield name, FULL } if targets.equal?(ALL)
|
|
317
|
+
|
|
318
|
+
targets.each(&)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Each target but `except` takes what the block answers for its binding and the depth it was reached at. A target
|
|
322
|
+
# reached only below its own object (a depth past {FULL}) is floored only when its binding can hold a mutable
|
|
323
|
+
# element.
|
|
324
|
+
def floor_targets(scope, targets, except)
|
|
325
|
+
acc = scope
|
|
326
|
+
each_target(scope, targets) do |name, kind|
|
|
327
|
+
next if excepted?(except, name)
|
|
328
|
+
|
|
329
|
+
current = acc.local(name)
|
|
330
|
+
next if current.nil? || (kind > FULL && !mutable_parts?(current))
|
|
331
|
+
|
|
332
|
+
floored = yield(current, kind)
|
|
333
|
+
acc = acc.with_mutated_local(name, floored) unless floored.nil? || floored == current
|
|
334
|
+
end
|
|
335
|
+
acc
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
@@ -62,11 +62,29 @@ module Rigor
|
|
|
62
62
|
receiver_name = block_form_receiver(node)
|
|
63
63
|
return nil if receiver_name.nil?
|
|
64
64
|
|
|
65
|
+
named = struct_class_name(node) if receiver_name == :Struct
|
|
66
|
+
return named if named
|
|
67
|
+
|
|
65
68
|
location = node.location
|
|
66
69
|
key = [source_path, location.start_line, location.start_column].compact.join(":")
|
|
67
70
|
Type::AnonymousClassName.build(receiver_name, key)
|
|
68
71
|
end
|
|
69
72
|
|
|
73
|
+
# Issue #2968 — `Struct.new("Named", :a)` names its class `Struct::Named`: a leading String argument that spells a
|
|
74
|
+
# constant (Ruby raises `NameError` for any other) is the class's name under `Struct`, so a block body is that
|
|
75
|
+
# class's and a reopening `class Struct::Named` in another file meets the same table entry. Nil for any other
|
|
76
|
+
# first argument. `node` is a `Struct.new` call.
|
|
77
|
+
def struct_class_name(node)
|
|
78
|
+
first = node.arguments&.arguments&.first
|
|
79
|
+
return nil unless first.is_a?(Prism::StringNode)
|
|
80
|
+
|
|
81
|
+
name = first.unescaped
|
|
82
|
+
"Struct::#{name}" if name.match?(STRUCT_CLASS_NAME)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
STRUCT_CLASS_NAME = /\A[A-Z][A-Za-z0-9_]*\z/
|
|
86
|
+
private_constant :STRUCT_CLASS_NAME
|
|
87
|
+
|
|
70
88
|
# The bare-constant receiver's name (`Class`, `::Class`), or nil for every other receiver shape.
|
|
71
89
|
def literal_constant_name(node)
|
|
72
90
|
case node
|
|
@@ -194,7 +194,7 @@ module Rigor
|
|
|
194
194
|
softened = []
|
|
195
195
|
joined = Array.new(tables.map(&:size).max) do |i|
|
|
196
196
|
column = tables.map { |table| table[i] }
|
|
197
|
-
next Type::Combinator.untyped if column.any? { |t| t.nil? ||
|
|
197
|
+
next Type::Combinator.untyped if column.any? { |t| t.nil? || Type::Combinator.untyped?(t) }
|
|
198
198
|
|
|
199
199
|
firm = column.reject { |t| nil_literal?(t) }
|
|
200
200
|
next Type::Combinator.union(*column) if firm.empty? || firm.size == column.size
|
|
@@ -211,9 +211,8 @@ module Rigor
|
|
|
211
211
|
cursor
|
|
212
212
|
end
|
|
213
213
|
|
|
214
|
-
# `|*rest|` binds an Array of the leftover positional arguments
|
|
215
|
-
#
|
|
216
|
-
# `Array[Dynamic[Top]]`. Element-type precision for rest parameters is deferred (demand-gated).
|
|
214
|
+
# `|*rest|` binds an Array of the leftover positional arguments: `Array[Dynamic[Top]]`, unless an
|
|
215
|
+
# auto-splat answered a rest type ({BlockAutoSplat}). The expected types are per-position, not per-rest.
|
|
217
216
|
def bind_rest(params_node, bindings)
|
|
218
217
|
rest = params_node.rest
|
|
219
218
|
return unless rest.respond_to?(:name) && rest&.name
|