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
|
@@ -524,7 +524,7 @@ module Rigor
|
|
|
524
524
|
|
|
525
525
|
# Issue #1429 — a global or constant subject (`case $stdout when StringIO`) narrows as a local does, and so
|
|
526
526
|
# does an instance variable (#1446).
|
|
527
|
-
slot =
|
|
527
|
+
slot = case_subject_slot(subject, scope)
|
|
528
528
|
return [body_scope, scope] if slot.nil?
|
|
529
529
|
|
|
530
530
|
truthy_type, falsey_type = case_when_types(scope, slot.current, conditions)
|
|
@@ -538,6 +538,32 @@ module Rigor
|
|
|
538
538
|
CASE_SUBJECT_KINDS = %i[local ivar global constant].freeze
|
|
539
539
|
private_constant :CASE_SUBJECT_KINDS
|
|
540
540
|
|
|
541
|
+
# The slot a `case` subject narrows. Issue #2211 — a subject that writes a local or instance variable
|
|
542
|
+
# (`case (x = v)`, `case x = v`) tests the value it stores, which the variable holds in every clause, so the
|
|
543
|
+
# clauses narrow the variable as they narrow a bare read of it: `case (x = s ? String(s) : nil) when String then
|
|
544
|
+
# x.upcase end` reads `x` as `String`. A single-statement `(…)` is unwrapped.
|
|
545
|
+
def case_subject_slot(subject, scope)
|
|
546
|
+
subject = unwrap_case_subject(subject)
|
|
547
|
+
case subject
|
|
548
|
+
when Prism::LocalVariableWriteNode
|
|
549
|
+
variable_slot(:local, subject.name, scope.local(subject.name), CASE_SUBJECT_KINDS)
|
|
550
|
+
when Prism::InstanceVariableWriteNode
|
|
551
|
+
variable_slot(:ivar, subject.name, scope.ivar(subject.name), CASE_SUBJECT_KINDS)
|
|
552
|
+
else receiver_slot(subject, scope, kinds: CASE_SUBJECT_KINDS)
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
private_class_method :case_subject_slot
|
|
556
|
+
|
|
557
|
+
# `subject` with each single-statement `(…)` around it removed: the node {.case_subject_slot} narrows through,
|
|
558
|
+
# which `StatementEvaluator#when_test_rebinds_subject?` reads the same way.
|
|
559
|
+
def unwrap_case_subject(subject)
|
|
560
|
+
while subject.is_a?(Prism::ParenthesesNode) && subject.body.is_a?(Prism::StatementsNode) &&
|
|
561
|
+
subject.body.body.size == 1
|
|
562
|
+
subject = subject.body.body.first
|
|
563
|
+
end
|
|
564
|
+
subject
|
|
565
|
+
end
|
|
566
|
+
|
|
541
567
|
# Returns `[body_scope, falsey_scope]` for one clause of a `case` with a subject: a `when` clause narrows
|
|
542
568
|
# through {.case_when_scopes}, and an `in` clause only for a bare class pattern ({.bare_class_pattern_node}),
|
|
543
569
|
# which matches exactly when `subject.is_a?(C)`. Every other pattern can fail to match where a class test
|
|
@@ -582,6 +608,26 @@ module Rigor
|
|
|
582
608
|
truthy
|
|
583
609
|
end
|
|
584
610
|
|
|
611
|
+
# Issue #2443 — `scope` with what every match binds, laid over the bindings it holds, for the block of a call
|
|
612
|
+
# that yields only after it has matched in the caller's frame ({MatchRebinding::YieldingMatch}): `$~` a
|
|
613
|
+
# `MatchData`, and `$&` and the pre- and post-match a `String`. The numbered groups and `$+` keep the bindings
|
|
614
|
+
# `scope` gives them. Typing them from the call's own pattern needs the group walk
|
|
615
|
+
# ({#unconditional_capture_groups}) to read every source right, which it does not yet (#2472), and forgetting
|
|
616
|
+
# them would report a group an earlier match in the method narrowed.
|
|
617
|
+
def yielded_match_scope(scope)
|
|
618
|
+
string_t = Type::Combinator.nominal_of("String")
|
|
619
|
+
scope.with_global(:$~, Type::Combinator.nominal_of("MatchData"))
|
|
620
|
+
.with_global(:$&, string_t).with_global(:$`, string_t).with_global(:$', string_t)
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
# Issue #2443 — true when every member of `type` is a String (a project subclass included) or a `Dynamic` that
|
|
624
|
+
# is untyped or may hold only Strings, Symbols and nil ({#match_member_kind}). A `nil` member never runs the
|
|
625
|
+
# block, so it counts neither way.
|
|
626
|
+
def string_match_receiver?(type, scope)
|
|
627
|
+
kinds = match_member_kinds(type, scope) - [:nil]
|
|
628
|
+
!kinds.empty? && kinds.all? { |kind| %i[string string_subclass dynamic].include?(kind) }
|
|
629
|
+
end
|
|
630
|
+
|
|
585
631
|
# Internal analyser. Returns `[truthy_scope, falsey_scope]` when the predicate shape is
|
|
586
632
|
# recognised, or `nil` to signal "no narrowing" so the public surface can fall back to the
|
|
587
633
|
# entry scope. Issue #1359 — a condition whose value is a `gets`-family reader's, the bare call
|
|
@@ -942,12 +988,22 @@ module Rigor
|
|
|
942
988
|
end
|
|
943
989
|
|
|
944
990
|
def analyse_local_read(node, scope)
|
|
945
|
-
|
|
991
|
+
local_truthiness_edges(node.name, scope)
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
# The falsey edge is where an optimistic lookup's miss lands (ADR-101, issue #2306): `Scope#with_local`
|
|
995
|
+
# drops the issue #286 mark as a write does, so the edge re-takes the one `scope` holds. Without it the
|
|
996
|
+
# join after `if v; …; end` with no `else` lost the mark on both arms and read `v` as nil-free, although
|
|
997
|
+
# a miss reaches that join through the implicit else. The truthy edge proves the value non-nil, so it
|
|
998
|
+
# keeps dropping the mark.
|
|
999
|
+
def local_truthiness_edges(name, scope)
|
|
1000
|
+
current = scope.local(name)
|
|
946
1001
|
return nil if current.nil?
|
|
947
1002
|
|
|
1003
|
+
falsey = scope.with_local(name, narrow_falsey(current))
|
|
948
1004
|
[
|
|
949
|
-
scope.with_local(
|
|
950
|
-
|
|
1005
|
+
scope.with_local(name, narrow_truthy(current)),
|
|
1006
|
+
OptimisticOrigin.carry_local_mark(scope, falsey, name, nil)
|
|
951
1007
|
]
|
|
952
1008
|
end
|
|
953
1009
|
|
|
@@ -961,13 +1017,7 @@ module Rigor
|
|
|
961
1017
|
# holds the just-bound value; the narrowing contract on the surrounding `if` is the
|
|
962
1018
|
# same.
|
|
963
1019
|
def analyse_local_write(node, scope)
|
|
964
|
-
|
|
965
|
-
return nil if current.nil?
|
|
966
|
-
|
|
967
|
-
[
|
|
968
|
-
scope.with_local(node.name, narrow_truthy(current)),
|
|
969
|
-
scope.with_local(node.name, narrow_falsey(current))
|
|
970
|
-
]
|
|
1020
|
+
local_truthiness_edges(node.name, scope)
|
|
971
1021
|
end
|
|
972
1022
|
|
|
973
1023
|
# Truthy guard on an instance variable. Covers both the bare read — `if @ivar`, the
|
|
@@ -981,9 +1031,11 @@ module Rigor
|
|
|
981
1031
|
current = scope.ivar(node.name)
|
|
982
1032
|
return nil if current.nil?
|
|
983
1033
|
|
|
1034
|
+
# The falsey edge keeps the optimistic mark, as {#local_truthiness_edges} explains.
|
|
1035
|
+
falsey = scope.with_ivar(node.name, narrow_falsey(current))
|
|
984
1036
|
[
|
|
985
1037
|
scope.with_ivar(node.name, narrow_truthy(current)),
|
|
986
|
-
|
|
1038
|
+
OptimisticOrigin.carry_ivar_mark(scope, falsey, node.name, nil)
|
|
987
1039
|
]
|
|
988
1040
|
end
|
|
989
1041
|
|
|
@@ -1154,15 +1206,6 @@ module Rigor
|
|
|
1154
1206
|
refinements
|
|
1155
1207
|
end
|
|
1156
1208
|
|
|
1157
|
-
# Recognised CallNode predicates:
|
|
1158
|
-
# - `recv.nil?` (Slice 6 phase 1, no args, no block)
|
|
1159
|
-
# - unary `!recv` (`name == :!`, no args, no block)
|
|
1160
|
-
# - `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` with a single
|
|
1161
|
-
# static-constant argument and no block (Slice 6 phase 2 sub-phase 1).
|
|
1162
|
-
# - `local == literal` / `literal == local` and the `!=` mirror for trusted static
|
|
1163
|
-
# literals (Slice 6 phase 2 sub-phase 2).
|
|
1164
|
-
# Anything else returns nil so the surrounding analyser falls through to the
|
|
1165
|
-
# no-narrowing fallback.
|
|
1166
1209
|
# Issue #1223 — `(x = e).nil?`, `(x ||= e).empty?`, `(@x = e).is_a?(C)`. A write's value is the binding
|
|
1167
1210
|
# it leaves, and a predicate is narrowed in the scope after its receiver ran, so the call narrows the
|
|
1168
1211
|
# variable exactly as the same call on a read of it would. Every analyser keys its receiver on a read
|
|
@@ -2440,10 +2483,10 @@ module Rigor
|
|
|
2440
2483
|
narrowed == original ? :relational : :local_binding
|
|
2441
2484
|
end
|
|
2442
2485
|
|
|
2443
|
-
# `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` narrowing. The
|
|
2444
|
-
# MUST be a `LocalVariableReadNode` (so we have a name to rebind), and the argument
|
|
2486
|
+
# `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` narrowing. The argument
|
|
2445
2487
|
# MUST be a single static constant reference (`Foo` or `Foo::Bar`) we can resolve to a
|
|
2446
|
-
# qualified class name.
|
|
2488
|
+
# qualified class name. The receiver is a local, a global, constant or instance variable
|
|
2489
|
+
# ({#receiver_slot}) or a single-hop chain; anything else falls through to "no narrowing".
|
|
2447
2490
|
def analyse_class_predicate(node, scope, exact:)
|
|
2448
2491
|
return nil if node.arguments.nil?
|
|
2449
2492
|
return nil unless node.arguments.arguments.size == 1
|
|
@@ -2512,14 +2555,13 @@ module Rigor
|
|
|
2512
2555
|
class_predicate_scopes(scope, node.receiver.name, current, class_name, exact: exact)
|
|
2513
2556
|
end
|
|
2514
2557
|
|
|
2515
|
-
# Stable single-hop method-chain narrowing
|
|
2516
|
-
# receiver narrowing across stable receivers"). When the predicate's receiver is
|
|
2558
|
+
# Stable single-hop method-chain narrowing. When the predicate's receiver is
|
|
2517
2559
|
# `<local/ivar>.<method>` with no args and no block, record the truthy / falsey
|
|
2518
2560
|
# narrowing in `Scope#method_chain_narrowings` keyed on the chain address. The
|
|
2519
2561
|
# dominated body's identical chain reads then observe the narrowed type through
|
|
2520
2562
|
# `ExpressionTyper#call_type_for`'s lookup.
|
|
2521
2563
|
#
|
|
2522
|
-
# Heuristic-by-design
|
|
2564
|
+
# Heuristic-by-design: a second call to `x.last` could in
|
|
2523
2565
|
# principle return a different value than the first. The chain is dropped on (1)
|
|
2524
2566
|
# receiver variable rebind (handled inside `Scope#with_local` / `#with_ivar`), and (2)
|
|
2525
2567
|
# any intervening call against the same root receiver (handled by
|
|
@@ -2762,10 +2804,14 @@ module Rigor
|
|
|
2762
2804
|
|
|
2763
2805
|
# Issue #1955 — the subject on the match edge of `/re/ === x` (and of `case x when /re/`). `Regexp#===`
|
|
2764
2806
|
# matches a String, a Symbol by its name, and an object that converts with `to_str`, and answers false for
|
|
2765
|
-
# anything else.
|
|
2766
|
-
#
|
|
2767
|
-
#
|
|
2768
|
-
#
|
|
2807
|
+
# anything else. A member is kept whole when its class defines `to_str` (#2002) — a String and its
|
|
2808
|
+
# subclasses, a class whose signature, ancestry or project source declares it — or when no signature
|
|
2809
|
+
# declares the class, and a `Dynamic` member is kept, since it may also be a Symbol at run time. Any other
|
|
2810
|
+
# member narrows to its String and Symbol parts, which are only what can match: a `String | nil` subject
|
|
2811
|
+
# reads `String`, a Symbol one stays `Symbol`, an Integer one drops out. An `Object`, `BasicObject` or
|
|
2812
|
+
# module member narrows too (to `String | Symbol`): its instances may be anything, and reading the edge as
|
|
2813
|
+
# the String it most often is keeps `s.length` on it from reporting. The no-match edge keeps the entry type:
|
|
2814
|
+
# a String that does not match reaches it too.
|
|
2769
2815
|
def regexp_case_equality_type(current, scope)
|
|
2770
2816
|
members = current.is_a?(Type::Union) ? current.members : [current]
|
|
2771
2817
|
Type::Combinator.union(*members.map { |member| regexp_case_equality_member(member, scope) })
|
|
@@ -2773,6 +2819,7 @@ module Rigor
|
|
|
2773
2819
|
|
|
2774
2820
|
def regexp_case_equality_member(member, scope)
|
|
2775
2821
|
return member if member.is_a?(Type::Dynamic)
|
|
2822
|
+
return member if converts_with_to_str?(member, scope)
|
|
2776
2823
|
|
|
2777
2824
|
parts = %w[String Symbol].map do |class_name|
|
|
2778
2825
|
narrow_class(member, class_name, exact: false, environment: scope.environment, scope: scope)
|
|
@@ -2780,6 +2827,36 @@ module Rigor
|
|
|
2780
2827
|
Type::Combinator.union(*parts)
|
|
2781
2828
|
end
|
|
2782
2829
|
|
|
2830
|
+
# True when a class `member` dispatches through declares `to_str`, or is declared by no signature
|
|
2831
|
+
# ({#class_lacks_method?} answers false for it). `Object`, `BasicObject` and a module String or Symbol
|
|
2832
|
+
# includes (`Kernel`, `Comparable`) are read as their String and Symbol parts instead: their instances may be
|
|
2833
|
+
# anything, and the String they most often are keeps a String method on the arm from reporting. Any other
|
|
2834
|
+
# module is kept whole, since a matching value may include it; so is one of those that declares `to_str`.
|
|
2835
|
+
def converts_with_to_str?(member, scope)
|
|
2836
|
+
targets = ProjectMethodOwnership.targets(member)
|
|
2837
|
+
return false if targets.nil? || targets.empty?
|
|
2838
|
+
|
|
2839
|
+
targets.any? { |class_name, kind| class_converts_with_to_str?(class_name, kind, scope) }
|
|
2840
|
+
end
|
|
2841
|
+
|
|
2842
|
+
# The modules String's or Symbol's own ancestry includes, read from the running Ruby.
|
|
2843
|
+
STRING_SYMBOL_MODULES = (String.ancestors + Symbol.ancestors).grep_v(Class).filter_map(&:name).uniq.freeze
|
|
2844
|
+
private_constant :STRING_SYMBOL_MODULES
|
|
2845
|
+
|
|
2846
|
+
def class_converts_with_to_str?(class_name, kind, scope)
|
|
2847
|
+
return false if kind == :instance && OPEN_SURFACE_CLASSES.include?(class_name)
|
|
2848
|
+
if kind == :instance && scope.environment.rbs_module?(class_name)
|
|
2849
|
+
return module_declares_to_str?(class_name, scope) || !STRING_SYMBOL_MODULES.include?(class_name)
|
|
2850
|
+
end
|
|
2851
|
+
|
|
2852
|
+
!class_lacks_method?(class_name, :to_str, kind, scope)
|
|
2853
|
+
end
|
|
2854
|
+
|
|
2855
|
+
def module_declares_to_str?(module_name, scope)
|
|
2856
|
+
ProjectMethodOwnership.defines?(module_name, :to_str, :instance, scope) ||
|
|
2857
|
+
!ExternalAncestorResolution.method_definition(module_name, :to_str, :instance, scope: scope).nil?
|
|
2858
|
+
end
|
|
2859
|
+
|
|
2783
2860
|
def unwrap_parens(node)
|
|
2784
2861
|
while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode) &&
|
|
2785
2862
|
node.body.body.size == 1
|
|
@@ -2803,9 +2880,7 @@ module Rigor
|
|
|
2803
2880
|
# - For each fact, the corresponding positional argument (matched by parameter name in
|
|
2804
2881
|
# the selected overload) MUST be a `Prism::LocalVariableReadNode` for narrowing to
|
|
2805
2882
|
# apply.
|
|
2806
|
-
# - When the target is `self`, narrowing applies to the receiver
|
|
2807
|
-
# not yet narrow `self` itself, so `self`-targeted facts are accepted by the merger
|
|
2808
|
-
# but currently produce no scope edits.
|
|
2883
|
+
# - When the target is `self`, narrowing applies to the receiver ({#apply_self_fact}).
|
|
2809
2884
|
def analyse_rbs_extended_contribution(node, scope)
|
|
2810
2885
|
method_def = resolve_rbs_extended_method(node, scope)
|
|
2811
2886
|
return nil if method_def.nil?
|
|
@@ -3342,8 +3417,8 @@ module Rigor
|
|
|
3342
3417
|
# extend record that is structurally incomplete: `Widget.extend(m)` at a call site and an `extend`
|
|
3343
3418
|
# written in a file outside the analysed set are both invisible to the walk behind
|
|
3344
3419
|
# {Scope#singleton_extends_of}, and the declaration-side record joined to it below is no more
|
|
3345
|
-
# complete — RBS says nothing about a class it does not declare. A shape that turned this table
|
|
3346
|
-
# `:subclass` verdict would have to be right about ABSENCE too, and it cannot be. Preserving
|
|
3420
|
+
# complete — RBS says nothing about a class it does not declare. A shape that turned this table
|
|
3421
|
+
# into a positive `:subclass` verdict would have to be right about ABSENCE too, and it cannot be. Preserving
|
|
3347
3422
|
# `Singleton[C]` would also hand the arm a receiver whose singleton method table has no `clamp` on
|
|
3348
3423
|
# it, trading the unreachable-clause false positive for an undefined-method one — the same trap
|
|
3349
3424
|
# #657 recorded on the `Constant` carrier.
|
|
@@ -3525,7 +3600,7 @@ module Rigor
|
|
|
3525
3600
|
rbs_type.is_a?(RBS::Types::Literal) && rbs_type.literal == value
|
|
3526
3601
|
end
|
|
3527
3602
|
|
|
3528
|
-
# The
|
|
3603
|
+
# The three receiver shapes with a scope binding the edges can rebind, mirroring
|
|
3529
3604
|
# `apply_self_fact`. A method chain or arbitrary expression has none, so it reads nil and
|
|
3530
3605
|
# the caller declines.
|
|
3531
3606
|
def receiver_binding_type(receiver, scope)
|
|
@@ -3635,7 +3710,7 @@ module Rigor
|
|
|
3635
3710
|
|
|
3636
3711
|
# Walks a call chain down to its `&.` link, collecting the suffix method names applied
|
|
3637
3712
|
# above it (outermost first). Declines unless the chain bottoms out in a `&.` call on a
|
|
3638
|
-
# direct local
|
|
3713
|
+
# direct local, instance-variable, global or constant read — the slots a scope can rewrite — and unless
|
|
3639
3714
|
# every suffix is a bare reader or predicate, since an argument or a block could make the
|
|
3640
3715
|
# answer depend on something other than the receiver.
|
|
3641
3716
|
#
|
|
@@ -123,9 +123,14 @@ module Rigor
|
|
|
123
123
|
# all.
|
|
124
124
|
def answer_for_entry(scope, entry, method_name)
|
|
125
125
|
unless entry.external?
|
|
126
|
-
|
|
126
|
+
defines = source_defines?(scope, entry.name, method_name) || macro_named?(scope, entry.name, method_name)
|
|
127
|
+
return defines ? [:source, entry.name] : nil
|
|
127
128
|
end
|
|
128
129
|
|
|
130
|
+
# Issue #2094 — a project module with no `def` is held as an external; a macro of its body may name it.
|
|
131
|
+
named = entry.candidates.find { |candidate| macro_named?(scope, candidate, method_name) }
|
|
132
|
+
return [:source, named] if named
|
|
133
|
+
|
|
129
134
|
entry.candidates.each do |candidate|
|
|
130
135
|
definition = Rigor::Reflection.instance_method_definition(candidate, method_name, scope: scope)
|
|
131
136
|
return [private_definition?(definition) ? :private_rbs : :rbs, candidate, definition] if definition
|
|
@@ -137,6 +142,13 @@ module Rigor
|
|
|
137
142
|
end
|
|
138
143
|
private_class_method :answer_for_entry
|
|
139
144
|
|
|
145
|
+
# Issue #2094 — whether a class-body macro of `owner` was handed `method_name` as a literal
|
|
146
|
+
# (`dsl_method :xdyn`, `def_delegators :@target, :xdyn`). The indexer files every such name as an opaque
|
|
147
|
+
# envelope, since what the macro defines under it is unknown, so the module may define the name.
|
|
148
|
+
def macro_named?(scope, owner, method_name)
|
|
149
|
+
scope.parameter_envelopes_of(owner).key?([:instance, method_name.to_sym])
|
|
150
|
+
end
|
|
151
|
+
|
|
140
152
|
def private_definition?(definition)
|
|
141
153
|
definition.respond_to?(:accessibility) && definition.accessibility == :private
|
|
142
154
|
end
|
|
@@ -18,7 +18,7 @@ module Rigor
|
|
|
18
18
|
# that happens to have been produced optimistically.
|
|
19
19
|
#
|
|
20
20
|
# Issue #286: the `if` / `unless` branch elision is one consumer of `Narrowing.predicate_certainty`; the
|
|
21
|
-
# other two are `flow.always-truthy-condition` and
|
|
21
|
+
# other two are `flow.always-truthy-condition` and `StatementEvaluator#right_operand_dead?`, and the
|
|
22
22
|
# spec passage above binds all three. This channel is what lets a certainty judgment tell the two apart.
|
|
23
23
|
#
|
|
24
24
|
# Issue #313: the mark is attached to a *value* — the call node that produced it, or the local / ivar it
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "mutation_widening"
|
|
5
|
+
require_relative "receiver_alias"
|
|
6
|
+
require_relative "../type"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
# Issue #2499 rule 3 (part of #2494) — the stdlib calls that fill a String the caller passes in. After
|
|
11
|
+
# `io.read(n, buf)`, `io.readpartial(n, buf)`, `io.sysread(n, buf)` and `io.read_nonblock(n, buf)` the buffer
|
|
12
|
+
# holds what was read, and after `StringIO.new(s)` every later write through the StringIO lands in `s`. The
|
|
13
|
+
# argument (a local, instance, class or global variable read) is widened as an in-place `replace` of it would
|
|
14
|
+
# widen it, so a `buf = +""` seed stops reading `""` after the call.
|
|
15
|
+
#
|
|
16
|
+
# A built-in catalogue keyed by method name and argument position, not a receiver-typed dispatch: a method of
|
|
17
|
+
# another class with the same name and a second positional argument loses only precision on that argument.
|
|
18
|
+
module OutBufferMutation
|
|
19
|
+
# Method name => the zero-based position of the buffer argument.
|
|
20
|
+
READERS = { read: 1, readpartial: 1, sysread: 1, read_nonblock: 1 }.freeze
|
|
21
|
+
private_constant :READERS
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# `scope` with the buffer argument of `call_node` widened, or `scope` itself when the call is no such call or the
|
|
26
|
+
# argument holds no String: `Rails.cache.read(key, opts)` passes an options Hash that keeps its shape.
|
|
27
|
+
def floor(call_node, scope)
|
|
28
|
+
buffer = buffer_argument(call_node)
|
|
29
|
+
return scope if buffer.nil?
|
|
30
|
+
|
|
31
|
+
getter, = MutationWidening::ALIAS_ACCESSORS[buffer.class]
|
|
32
|
+
return scope unless string_carrier?(scope.public_send(getter, ReceiverAlias.read_name(buffer)))
|
|
33
|
+
|
|
34
|
+
MutationWidening.widen_alias_read(:replace, buffer, scope)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# A `Constant<String>` or the `String` nominal, or a union of nothing else.
|
|
38
|
+
def string_carrier?(type)
|
|
39
|
+
case type
|
|
40
|
+
when Type::Constant then type.value.is_a?(String)
|
|
41
|
+
when Type::Nominal then type.class_name == "String"
|
|
42
|
+
when Type::Union then type.members.all? { |member| string_carrier?(member) }
|
|
43
|
+
else false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def buffer_argument(call_node)
|
|
48
|
+
arguments = call_node.arguments&.arguments
|
|
49
|
+
return nil if arguments.nil?
|
|
50
|
+
|
|
51
|
+
position = READERS[call_node.name]
|
|
52
|
+
position = 0 if position.nil? && string_io_new?(call_node)
|
|
53
|
+
return nil if position.nil?
|
|
54
|
+
|
|
55
|
+
argument = arguments[position]
|
|
56
|
+
MutationWidening::ALIAS_ACCESSORS.key?(argument.class) ? argument : nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def string_io_new?(call_node)
|
|
60
|
+
return false unless call_node.name == :new
|
|
61
|
+
|
|
62
|
+
receiver = call_node.receiver
|
|
63
|
+
case receiver
|
|
64
|
+
when Prism::ConstantReadNode then receiver.name == :StringIO
|
|
65
|
+
when Prism::ConstantPathNode then receiver.parent.nil? && receiver.name == :StringIO
|
|
66
|
+
else false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
private_class_method :buffer_argument, :string_io_new?, :string_carrier?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -95,11 +95,10 @@ module Rigor
|
|
|
95
95
|
# may type in a later round once its own argument resolves). Unresolved call sites do not
|
|
96
96
|
# contribute.
|
|
97
97
|
#
|
|
98
|
-
# What it does NOT do yet
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
# because the only consumer is the protection metric, which runs no diagnostics).
|
|
98
|
+
# What it does NOT do yet: typing keyword / optional / rest / block parameters themselves,
|
|
99
|
+
# inherited-method receivers, top-level helpers, and a rigorous closed-call-site-set proof
|
|
100
|
+
# (the union is optimistic over resolved sites; the `check` walk tolerates that because an
|
|
101
|
+
# inferred parameter is only a lower bound, which the in-body rules decline on, WD6b).
|
|
103
102
|
class ParameterInferenceCollector
|
|
104
103
|
EMPTY = {}.freeze
|
|
105
104
|
|
|
@@ -120,7 +119,7 @@ module Rigor
|
|
|
120
119
|
# @param files — project `.rb` paths to scan for call sites.
|
|
121
120
|
# @param target_ruby — Prism parse target.
|
|
122
121
|
# @param max_rounds — the WD5 fixpoint cap (1 = single-level).
|
|
123
|
-
# @return
|
|
122
|
+
# @return the inferred parameter table, frozen.
|
|
124
123
|
def self.collect(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS, workers: 0)
|
|
125
124
|
new(files: files, environment: environment, target_ruby: target_ruby,
|
|
126
125
|
max_rounds: max_rounds, workers: workers).collect
|
|
@@ -13,7 +13,7 @@ module Rigor
|
|
|
13
13
|
#
|
|
14
14
|
# ADR-17 slice 2 gave `pre_eval:` a project-wide **method** registry ({ProjectPatchedMethods}). A
|
|
15
15
|
# constant declared in the same file stayed invisible across the file boundary: `Scope#in_source_constants`
|
|
16
|
-
#
|
|
16
|
+
# was per-file and kept out of `Runner#project_scope_seed_tables`, so only class-shaped
|
|
17
17
|
# constants (a `class` / `module` declaration, or one of the four meta-new forms `ScopeIndexer`'s
|
|
18
18
|
# `record_class_new_constant_decl` promotes) ever crossed. `TIMEOUT = 30` in a listed file read as
|
|
19
19
|
# `Dynamic[top]` everywhere else.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../type"
|
|
4
4
|
require_relative "external_ancestor_resolution"
|
|
5
|
+
require_relative "../run_scoped_slots"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module Inference
|
|
@@ -19,7 +20,7 @@ module Rigor
|
|
|
19
20
|
NO_TARGETS = [].freeze
|
|
20
21
|
private_constant :NO_TARGETS
|
|
21
22
|
|
|
22
|
-
MEMO_KEY = :__rigor_project_method_ownership_memo__
|
|
23
|
+
MEMO_KEY = RunScopedSlots.register(:__rigor_project_method_ownership_memo__)
|
|
23
24
|
private_constant :MEMO_KEY
|
|
24
25
|
|
|
25
26
|
module_function
|
|
@@ -72,13 +73,15 @@ module Rigor
|
|
|
72
73
|
# `Scope` merges each file's discovery with the project pre-pass, so the slot turns over per file, and
|
|
73
74
|
# an ADR-46 dependency the first lookup recorded belongs to the file every later hit serves. Pool
|
|
74
75
|
# workers are separate processes, and a Ractor has its own `Thread.current`. The `:ancestry` bucket
|
|
75
|
-
# belongs to {ClosureEscapeAnalyzer}'s ancestry step.
|
|
76
|
+
# belongs to {ClosureEscapeAnalyzer}'s ancestry step. The `:block_runs` bucket belongs to
|
|
77
|
+
# {BlockRunCount}.
|
|
76
78
|
def memo(scope)
|
|
77
79
|
discovery = scope.discovery
|
|
78
80
|
environment = scope.environment
|
|
79
81
|
slot = Thread.current[MEMO_KEY]
|
|
80
82
|
unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
|
|
81
|
-
|
|
83
|
+
buckets = { instance: {}, singleton: {}, ancestry: {}, block_runs: {}.compare_by_identity }
|
|
84
|
+
slot = [discovery, environment, buckets]
|
|
82
85
|
Thread.current[MEMO_KEY] = slot
|
|
83
86
|
end
|
|
84
87
|
slot[2]
|
|
@@ -39,7 +39,8 @@ module Rigor
|
|
|
39
39
|
# `dependencies.source_inference:`), and no `discovery_seed:` base is merged in (the runner can start from one).
|
|
40
40
|
NOT_SEEDED = {
|
|
41
41
|
run_generation: "the identity token of a check run's return memo; these paths run no check",
|
|
42
|
-
constant_sources: "read
|
|
42
|
+
constant_sources: "read by dependency recording and the #663 pre_eval retraction; a seed carries it only " \
|
|
43
|
+
"when asked (DiscoverySeed.tables_for_buffer(constant_sources: true), the editor publish)",
|
|
43
44
|
param_inferred_types: "the parameter-inference output itself; each caller adds its own " \
|
|
44
45
|
"(DiscoverySeed.build, coverage under parameter_inference:, the collector's last round)"
|
|
45
46
|
}.freeze
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../type"
|
|
6
|
+
require_relative "block_run_count"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
# Issue #2127 — whether a call to a PROJECT method may run the block it passes more than once, read off the
|
|
11
|
+
# callee's body. {BlockRepetition.may_repeat?} answers only for the core iterators {ClosureEscapeAnalyzer}
|
|
12
|
+
# catalogues, so `def yielder = (yield; yield)` or the idiomatic wrapper `def each = @items.each { |i| yield i }`
|
|
13
|
+
# counted as running the block once, and a `return` in the block that only a later run reaches (`each { return
|
|
14
|
+
# false if (seen += 1) > 3 }`) never joined the method's return type.
|
|
15
|
+
#
|
|
16
|
+
# Only the later-run `return` pass asks {.may_run_block_repeatedly?}
|
|
17
|
+
# (`StatementEvaluator#collect_repeated_block_returns`); the block's entry scopes and the ADR-56 write-back keep
|
|
18
|
+
# {BlockRepetition}'s rule. That pass only widens the method's return type, so the rule here errs toward
|
|
19
|
+
# "repeats". The expression-position block-value pass asks {.block_value_may_repeat?} instead, whose rule
|
|
20
|
+
# ({BlockRunCount}) errs the other way. The callee repeats when its body, outside a nested `def` (a `yield` in a
|
|
21
|
+
# class body is a syntax error inside a method):
|
|
22
|
+
#
|
|
23
|
+
# - holds two or more sites that run the block (`yield`, or `call` / `yield` / `===` / `[]` on the `&blk`
|
|
24
|
+
# parameter), or one inside a loop, a block or a lambda, which may run it many times; or
|
|
25
|
+
# - hands the block on (`each(&blk)`, `each(&)`, `...` forwarding) or reads the `&blk` parameter any other way
|
|
26
|
+
# (stores it, passes it as an argument), so code the walk does not see may run it any number of times.
|
|
27
|
+
#
|
|
28
|
+
# A callee the analysis cannot resolve to a project `def` (an unknown receiver, a method defined by
|
|
29
|
+
# metaprogramming) answers false, as before.
|
|
30
|
+
module ProjectYielder
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
BLOCK_RUN_CALLS = Set[:call, :yield, :===, :[]].freeze
|
|
34
|
+
REPEATING_NODES = Set[Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::BlockNode,
|
|
35
|
+
Prism::LambdaNode].freeze
|
|
36
|
+
private_constant :BLOCK_RUN_CALLS, :REPEATING_NODES
|
|
37
|
+
|
|
38
|
+
def may_run_block_repeatedly?(call_node, receiver_type, scope)
|
|
39
|
+
def_node = callee_def(call_node, receiver_type, scope)
|
|
40
|
+
return false unless def_node.is_a?(Prism::DefNode) && def_node.body
|
|
41
|
+
|
|
42
|
+
repeats?(def_node)
|
|
43
|
+
rescue StandardError
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Issues #2301 and #2303 — whether the callee may run the block more than once on one path, for the
|
|
48
|
+
# expression-position block-value pass ({BlockRunCount}). Answers false where the callee does not resolve.
|
|
49
|
+
def block_value_may_repeat?(call_node, receiver_type, scope)
|
|
50
|
+
def_node = callee_def(call_node, receiver_type, scope)
|
|
51
|
+
return false unless def_node.is_a?(Prism::DefNode) && def_node.body
|
|
52
|
+
|
|
53
|
+
BlockRunCount.may_repeat?(def_node, scope)
|
|
54
|
+
rescue StandardError
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The `def` an instance call on a project class, a singleton call on one, or an implicit-self call to a
|
|
59
|
+
# top-level method resolves to.
|
|
60
|
+
def callee_def(call_node, receiver_type, scope)
|
|
61
|
+
name = call_node.name
|
|
62
|
+
found =
|
|
63
|
+
case receiver_type
|
|
64
|
+
when Type::Nominal then scope.user_def_through_ancestors(receiver_type.class_name, name).first
|
|
65
|
+
when Type::Singleton then scope.singleton_def_for(receiver_type.class_name, name)
|
|
66
|
+
end
|
|
67
|
+
found || (call_node.receiver.nil? ? scope.top_level_def_for(name) : nil)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def repeats?(def_node)
|
|
71
|
+
block_param = def_node.parameters&.block
|
|
72
|
+
counter = { sites: 0, repeats: false }
|
|
73
|
+
walk(def_node.body, block_param&.name, false, counter)
|
|
74
|
+
counter[:repeats] || counter[:sites] >= 2
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def walk(node, block_name, repeating, counter)
|
|
78
|
+
return if counter[:repeats] || node.is_a?(Prism::DefNode)
|
|
79
|
+
|
|
80
|
+
skipped = visit(node, block_name, repeating, counter)
|
|
81
|
+
inner = repeating || REPEATING_NODES.include?(node.class)
|
|
82
|
+
node.compact_child_nodes.each do |child|
|
|
83
|
+
walk(child, block_name, inner, counter) unless child.equal?(skipped)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Counts `node` and answers the child the walk must not visit: the `blk` receiver of a `blk.call`, which is a
|
|
88
|
+
# run, not a hand-off.
|
|
89
|
+
def visit(node, block_name, repeating, counter)
|
|
90
|
+
case node
|
|
91
|
+
when Prism::YieldNode then run_site(repeating, counter)
|
|
92
|
+
when Prism::ForwardingArgumentsNode then counter[:repeats] = true
|
|
93
|
+
when Prism::BlockArgumentNode then counter[:repeats] = true if forwards_block?(node, block_name)
|
|
94
|
+
when Prism::CallNode
|
|
95
|
+
return unless block_run_call?(node, block_name)
|
|
96
|
+
|
|
97
|
+
run_site(repeating, counter)
|
|
98
|
+
node.receiver
|
|
99
|
+
when Prism::LocalVariableReadNode then counter[:repeats] = true if block_name && node.name == block_name
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def run_site(repeating, counter)
|
|
104
|
+
counter[:sites] += 1
|
|
105
|
+
counter[:repeats] = true if repeating
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# `blk.call` and its spellings.
|
|
109
|
+
def block_run_call?(node, block_name)
|
|
110
|
+
receiver = node.receiver
|
|
111
|
+
!block_name.nil? && receiver.is_a?(Prism::LocalVariableReadNode) && receiver.name == block_name &&
|
|
112
|
+
BLOCK_RUN_CALLS.include?(node.name)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# `&blk` or an anonymous `&` handed to another call.
|
|
116
|
+
def forwards_block?(node, block_name)
|
|
117
|
+
expression = node.expression
|
|
118
|
+
expression.nil? || (expression.is_a?(Prism::LocalVariableReadNode) && expression.name == block_name)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -85,6 +85,26 @@ module Rigor
|
|
|
85
85
|
candidates(first, depth + 1) + candidates(second, depth + 1)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# Calls whose result hands its caller the receiver's own element objects (issue #2287): `h.values` the hash's
|
|
89
|
+
# values, `a.dup` / `a.clone` a shallow copy sharing them, and the blockless enumerator spellings (`a.each`,
|
|
90
|
+
# `a.map`, `a.each_with_index`, `e.with_index`) the elements themselves.
|
|
91
|
+
ELEMENT_EXPOSING_CALLS = %i[values dup clone each map each_with_index with_index].to_set.freeze
|
|
92
|
+
private_constant :ELEMENT_EXPOSING_CALLS
|
|
93
|
+
|
|
94
|
+
# The variables whose elements a loop over `receiver` is handed: {.candidates} of `receiver` once every
|
|
95
|
+
# blockless {ELEMENT_EXPOSING_CALLS} call is peeled off, so `h.values.each { |v| v << 2 }` and
|
|
96
|
+
# `a.each_with_index.map { |x, i| x << i }` name `h` and `a`. Only a loop's element widening reads it; an
|
|
97
|
+
# in-place mutation OF such a result (`h.values << 1`, `a.dup << 1`) changes a new object, not the variable.
|
|
98
|
+
def element_sources(receiver)
|
|
99
|
+
depth = 0
|
|
100
|
+
while receiver.is_a?(Prism::CallNode) && receiver.block.nil? && ELEMENT_EXPOSING_CALLS.include?(receiver.name)
|
|
101
|
+
return [] if (depth += 1) > WALK_DEPTH_CAP
|
|
102
|
+
|
|
103
|
+
receiver = receiver.receiver
|
|
104
|
+
end
|
|
105
|
+
candidates(receiver)
|
|
106
|
+
end
|
|
107
|
+
|
|
88
108
|
# The variable reads an in-place mutation of `receiver` changes: {.candidates}' locals and instance
|
|
89
109
|
# variables, or the class variable or global the receiver reads or writes directly, parenthesised or not
|
|
90
110
|
# (`(@@c ||= []) << x` mutates `@@c`, as `(buf ||= []) << x` mutates `buf`). A class variable or global counts
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../run_scoped_slots"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Inference
|
|
5
7
|
module RefinedDispatch
|
|
@@ -16,7 +18,7 @@ module Rigor
|
|
|
16
18
|
# the name itself, and no constant write of it. Any other shape answers false, as does a class a
|
|
17
19
|
# `K = Class.new(...)` write names.
|
|
18
20
|
module ConstructedClass
|
|
19
|
-
MEMO_KEY = :__rigor_constructed_class__
|
|
21
|
+
MEMO_KEY = RunScopedSlots.register(:__rigor_constructed_class__)
|
|
20
22
|
EMPTY = [].freeze
|
|
21
23
|
private_constant :MEMO_KEY, :EMPTY
|
|
22
24
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../builtins/cruby_definers"
|
|
4
4
|
require_relative "constructed_class"
|
|
5
|
+
require_relative "../../run_scoped_slots"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module Inference
|
|
@@ -317,7 +318,7 @@ module Rigor
|
|
|
317
318
|
all.include?(name) ? all - [name] : all
|
|
318
319
|
end
|
|
319
320
|
|
|
320
|
-
SUBCLASS_MEMO_KEY = :__rigor_own_method_subclasses__
|
|
321
|
+
SUBCLASS_MEMO_KEY = RunScopedSlots.register(:__rigor_own_method_subclasses__)
|
|
321
322
|
private_constant :SUBCLASS_MEMO_KEY
|
|
322
323
|
|
|
323
324
|
# One thread-local slot keyed on the scope's discovery index (a frozen `Data`) and environment, the only
|