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
|
@@ -15,6 +15,7 @@ require_relative "../cache/file_digest"
|
|
|
15
15
|
require_relative "../analysis/check_rules/published_constant_guard"
|
|
16
16
|
require_relative "anonymous_meta_class"
|
|
17
17
|
require_relative "def_handle"
|
|
18
|
+
require_relative "definers_index"
|
|
18
19
|
require_relative "fresh_frame_blocks"
|
|
19
20
|
require_relative "global_write_census"
|
|
20
21
|
require_relative "guard_rebinding"
|
|
@@ -43,6 +44,11 @@ require_relative "scope_indexer/layered_def_nestings"
|
|
|
43
44
|
require_relative "scope_indexer/mixin_accumulator"
|
|
44
45
|
require_relative "scope_indexer/certainty"
|
|
45
46
|
require_relative "scope_indexer/member_layouts_collector"
|
|
47
|
+
require_relative "scope_indexer/declared_ivar_types"
|
|
48
|
+
require_relative "scope_indexer/declared_param_writes"
|
|
49
|
+
require_relative "scope_indexer/constant_ivar_value"
|
|
50
|
+
require_relative "scope_indexer/ivar_contributions"
|
|
51
|
+
require_relative "scope_indexer/empty_seed"
|
|
46
52
|
|
|
47
53
|
module Rigor
|
|
48
54
|
module Inference
|
|
@@ -65,8 +71,8 @@ module Rigor
|
|
|
65
71
|
# ```
|
|
66
72
|
#
|
|
67
73
|
# Nodes that are not part of the program subtree (e.g. synthesised virtual nodes that the caller looks up after the
|
|
68
|
-
# fact) yield the `default_scope`. The returned Hash is mutable in principle but callers MUST treat it as
|
|
69
|
-
# the indexer itself never exposes a way to update it past construction.
|
|
74
|
+
# fact) yield the seeded `default_scope`. The returned Hash is mutable in principle but callers MUST treat it as
|
|
75
|
+
# read-only; the indexer itself never exposes a way to update it past construction.
|
|
70
76
|
# rubocop:disable-next Metrics/ModuleLength
|
|
71
77
|
module ScopeIndexer
|
|
72
78
|
# Issue #644 — the census descriptor for a constant write that publishes no value: an unfoldable or
|
|
@@ -107,12 +113,24 @@ module Rigor
|
|
|
107
113
|
# `Dynamic[top]`. Off for every other reader, so parameter inference
|
|
108
114
|
# and sig-gen observe the clause's plain entry.
|
|
109
115
|
# @return identity-comparing
|
|
110
|
-
# table whose default value is `default_scope`.
|
|
116
|
+
# table whose default value is the seeded `default_scope`.
|
|
111
117
|
# The no-op re-anchoring table (#722 residue 2): shared so the overwhelmingly common
|
|
112
118
|
# nothing-to-re-anchor walk allocates none.
|
|
113
119
|
EMPTY_RENAMES = {}.freeze
|
|
114
120
|
|
|
115
|
-
|
|
121
|
+
# No path whose contributions are all `possible` (issue #2021), the cross-file walks' default.
|
|
122
|
+
EMPTY_PATH_SET = Set.new.freeze
|
|
123
|
+
# The {IvarWriterCensus} entry kinds the project pre-pass tags with the file that records them.
|
|
124
|
+
PATH_TAGGED_KINDS = %i[own_write own_write_type].to_set.freeze
|
|
125
|
+
|
|
126
|
+
# `declared_ivar_types:` — whether an RBS-declared ivar type replaces the census's join of the writes (#1406).
|
|
127
|
+
# `rigor sig-gen` passes false: it proposes what the code says, and reading a declaration back would make a row
|
|
128
|
+
# count as generated only because it is declared.
|
|
129
|
+
# `ivar_writer_scope:` — the scope whose project tables the #1154 declared-parameter credit reads to find the
|
|
130
|
+
# ivar writers outside this file ({DeclaredParamWrites}); defaults to `default_scope`. `rigor sig-gen` indexes
|
|
131
|
+
# each file under an unseeded scope, so it passes one carrying the project's tables.
|
|
132
|
+
def index(root, default_scope:, converged_loop_recording: false, widen_clauses: false, # rubocop:disable Metrics/AbcSize
|
|
133
|
+
declared_ivar_types: true, ivar_writer_scope: nil)
|
|
116
134
|
# Slice A-declarations. Build the declaration overrides first so every scope handed to the StatementEvaluator
|
|
117
135
|
# already carries the table; structural sharing through `Scope#with_local` / `#with_fact` / `#with_self_type`
|
|
118
136
|
# propagates it across every derived scope.
|
|
@@ -134,7 +152,9 @@ module Rigor
|
|
|
134
152
|
|
|
135
153
|
# Slice 7 phase 2. Pre-pass over every class/module body to collect the per-class ivar accumulator. Seeded after
|
|
136
154
|
# declared_types so the rvalue typer in the pre-pass can see declaration overrides.
|
|
137
|
-
class_ivars =
|
|
155
|
+
class_ivars = DeclaredParamWrites.with_writer_scope(ivar_writer_scope) do
|
|
156
|
+
build_class_ivar_index(root, seeded_scope, declared_ivar_types: declared_ivar_types)
|
|
157
|
+
end
|
|
138
158
|
seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(class_ivars: class_ivars))
|
|
139
159
|
|
|
140
160
|
# Slice 7 phase 6. Same pre-pass shape for cvars (per class) and globals (program-wide). Globals are also
|
|
@@ -197,19 +217,31 @@ module Rigor
|
|
|
197
217
|
# share.
|
|
198
218
|
seeded_scope = seed_struct_fold_safe(seeded_scope, root).with_match_frame(root)
|
|
199
219
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
converged_loop_recording: converged_loop_recording,
|
|
203
|
-
widen_clauses: widen_clauses).evaluate(root)
|
|
220
|
+
evaluate = recording_evaluator(table, converged_loop_recording, widen_clauses)
|
|
221
|
+
evaluate.call(root, seeded_scope)
|
|
204
222
|
|
|
205
|
-
propagate(root, table, seeded_scope)
|
|
223
|
+
with_unentered_evaluator(evaluate) { propagate(root, table, seeded_scope) }
|
|
206
224
|
table
|
|
207
225
|
end
|
|
208
226
|
|
|
227
|
+
# The `(node, scope, excluded = nil) ->` evaluation that records every node it visits into `table`, except the
|
|
228
|
+
# nodes in `excluded`: the file's own pass, and each construct {#evaluate_unentered_flow} evaluates in the walk
|
|
229
|
+
# after it.
|
|
230
|
+
def recording_evaluator(table, converged_loop_recording, widen_clauses)
|
|
231
|
+
lambda do |node, scope, excluded = nil|
|
|
232
|
+
on_enter =
|
|
233
|
+
if excluded.nil? || excluded.empty? then ->(visited, at) { table[visited] = at }
|
|
234
|
+
else ->(visited, at) { table[visited] = at unless excluded.include?(visited) }
|
|
235
|
+
end
|
|
236
|
+
StatementEvaluator.new(scope: scope, on_enter: on_enter, converged_loop_recording: converged_loop_recording,
|
|
237
|
+
widen_clauses: widen_clauses).evaluate(node)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
209
241
|
# Runs the combined methods/def-nodes descent (one walk of the file), seeds the discovered-methods existence table
|
|
210
|
-
# onto `seeded_scope` (merged UNDER the cross-file pre-pass seed `default_scope` carries), and returns
|
|
211
|
-
# file_def_nodes, file_envelopes]` so the caller can thread the def-node and issue #992
|
|
212
|
-
# {#merge_project_method_indexes} without walking the file a second time.
|
|
242
|
+
# onto `seeded_scope` (merged UNDER the cross-file pre-pass seed `default_scope` carries), and returns
|
|
243
|
+
# `[scope, [file_def_nodes, file_envelopes, file_siblings]]` so the caller can thread the def-node and issue #992
|
|
244
|
+
# envelope tables into {#merge_project_method_indexes} without walking the file a second time.
|
|
213
245
|
#
|
|
214
246
|
# ADR-119 WD3 — the file's `possible_discovered_methods` folds over the seed's by {#fold_possible_methods}, so a
|
|
215
247
|
# name the file defines certainly is no longer possible and a name only the file's possible contribution
|
|
@@ -280,8 +312,8 @@ module Rigor
|
|
|
280
312
|
siblings = per_file_siblings(seeded_scope.discovery.sibling_tables, file_tables[0], file_tables[2],
|
|
281
313
|
file_singleton, file_visibilities)
|
|
282
314
|
extends, methods_table, unpositioned =
|
|
283
|
-
merge_and_fold_extends(default_scope, root, def_nodes,
|
|
284
|
-
mixin_unpositioned)
|
|
315
|
+
merge_and_fold_extends(default_scope, root, [def_nodes, file_tables[0]],
|
|
316
|
+
[singleton_def_nodes, file_singleton[0]], seeded_scope, siblings, mixin_unpositioned)
|
|
285
317
|
|
|
286
318
|
seeded_scope.with_discovery(
|
|
287
319
|
seeded_scope.discovery.with(
|
|
@@ -395,13 +427,23 @@ module Rigor
|
|
|
395
427
|
# ADR-119 WD1 — the unpositioned union (the seed's, the mixin walks' and this file's extend side) is settled
|
|
396
428
|
# first, because the fold reads it to tell a certain extend edge from a listed one. `siblings` is written by
|
|
397
429
|
# the fold.
|
|
398
|
-
|
|
430
|
+
# `def_pair` and `singleton_pair` are `[merged table, this file's own]`; the merged tables are frozen once the
|
|
431
|
+
# fold, their last in-place writer, is done ({#freeze_def_tables}).
|
|
432
|
+
def merge_and_fold_extends(default_scope, root, def_pair, singleton_pair, seeded_scope, siblings,
|
|
399
433
|
mixin_unpositioned)
|
|
434
|
+
def_nodes = def_pair.first
|
|
435
|
+
singleton_def_nodes = singleton_pair.first
|
|
400
436
|
file_extends, extends, extend_unpositioned = merge_extend_tables(default_scope, root)
|
|
401
437
|
unpositioned = union_unpositioned(default_scope.discovery.unpositioned_mixins, *mixin_unpositioned,
|
|
402
438
|
extend_unpositioned)
|
|
403
439
|
methods_table = fold_per_file_extends(file_extends, def_nodes, singleton_def_nodes, seeded_scope, siblings,
|
|
404
440
|
unpositioned)
|
|
441
|
+
# The fold also writes the singleton hash of each class this file `extend`s, declared there or not.
|
|
442
|
+
file_extends.each_key { |owner| singleton_def_nodes[owner]&.freeze }
|
|
443
|
+
freeze_def_tables(def_pair, singleton_pair)
|
|
444
|
+
# #2300 — the override gate's definers index then inverts the seed once per run, not once per file.
|
|
445
|
+
DefinersIndex.record_layers(def_nodes, default_scope.discovered_def_nodes)
|
|
446
|
+
DefinersIndex.record_layers(singleton_def_nodes, default_scope.discovered_singleton_def_nodes)
|
|
405
447
|
[extends, methods_table, unpositioned]
|
|
406
448
|
end
|
|
407
449
|
|
|
@@ -432,6 +474,20 @@ module Rigor
|
|
|
432
474
|
walked.fetch(:header_nestings)).freeze]
|
|
433
475
|
end
|
|
434
476
|
|
|
477
|
+
# Issue #2330 — the per-file def table is read by identity: the callee-chain census memo is keyed by the
|
|
478
|
+
# {Scope::DiscoveryIndex} that carries it, which is only sound while the table is never written in place.
|
|
479
|
+
# Frozen after the extends fold, the last in-place writer; the seed's per-class hashes are frozen already.
|
|
480
|
+
#
|
|
481
|
+
# Only the per-class hashes of the classes this file declares are frozen here (the merge made or took
|
|
482
|
+
# them); a seed-only class's hash was frozen by `finalize_def_index`, so walking the whole table per file is
|
|
483
|
+
# needless.
|
|
484
|
+
def freeze_def_tables(*pairs)
|
|
485
|
+
pairs.each do |table, file_table|
|
|
486
|
+
file_table.each_key { |owner| table[owner].freeze }
|
|
487
|
+
table.freeze
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
435
491
|
# The instance-side def-node table and its issue #681 nesting twin, each merged over the cross-file seed.
|
|
436
492
|
# Returned as a pair so the two stay written together: a node the merge keeps must be the same object the
|
|
437
493
|
# nesting table keys, and they are only that if both halves take the same file's walk.
|
|
@@ -498,25 +554,35 @@ module Rigor
|
|
|
498
554
|
# (singleton vs instance). The rvalue is typed with NO local bindings — the pre-pass lacks statement-level
|
|
499
555
|
# threading — so `@x = 1` records `Constant[1]` but `@x = some_local + 1` records `Dynamic[Top]` (since
|
|
500
556
|
# `some_local` is unbound at pre-pass time). Multiple writes to the same ivar union via `Type::Combinator.union`.
|
|
501
|
-
def build_class_ivar_index(root, default_scope)
|
|
557
|
+
def build_class_ivar_index(root, default_scope, declared_ivar_types: true)
|
|
502
558
|
accumulator = {}
|
|
503
559
|
mutated_ivars = {}
|
|
504
560
|
read_before_write = {}
|
|
505
561
|
init_writes = {}
|
|
506
|
-
# WD3 — per
|
|
507
|
-
#
|
|
508
|
-
#
|
|
509
|
-
# by class.
|
|
562
|
+
# WD3 — per class, then per method, the ivars the method definitely assigns non-nil on every completing path,
|
|
563
|
+
# consulted by `dead_transient_nil_writes` so a ctor that reassigns `@x` indirectly through an unconditional
|
|
564
|
+
# same-class method call (`mask!`) credits the overwrite. Built once per program here, memoised by class.
|
|
510
565
|
method_assign_effects = build_method_assign_effects(root)
|
|
566
|
+
# #2291 — an `@x = LIT` write reads the file's constant table, built on the first such write.
|
|
567
|
+
accumulator[CONSTANT_IVAR_VALUES] = ConstantIvarValue.new(root, default_scope)
|
|
511
568
|
walk_class_ivars(root, [], default_scope, accumulator, mutated_ivars,
|
|
512
569
|
read_before_write, init_writes, method_assign_effects)
|
|
570
|
+
accumulator.delete(CONSTANT_IVAR_VALUES)
|
|
513
571
|
reflective_opaque = accumulator.delete(REFLECTIVE_OPAQUE_CLASSES)
|
|
514
572
|
exempt_class_body_writes!(init_writes, accumulator.delete(DEFINE_METHOD_WRITES) || {})
|
|
515
573
|
merge_held_ivar_writes!(accumulator)
|
|
516
574
|
record_aliased_ivar_mutations!(root, mutated_ivars)
|
|
517
575
|
widen_mutated_ivar_entries!(accumulator, mutated_ivars)
|
|
576
|
+
# #2170 — an ancestor's writes as this file's census types them, before its read-before-write `nil`.
|
|
577
|
+
writes = accumulator.transform_values(&:dup)
|
|
518
578
|
contribute_read_before_write_nil!(accumulator, read_before_write, init_writes)
|
|
519
579
|
make_reflective_classes_gradual!(accumulator, reflective_opaque)
|
|
580
|
+
# #2079 / #2170 — another file's writes, and the chain's modules' and superclasses', join existing entries.
|
|
581
|
+
IvarContributions.join!(accumulator, writes, default_scope)
|
|
582
|
+
# An in-place mutation the file observes widens a joined value too (a same-file ancestor's String literal).
|
|
583
|
+
widen_mutated_ivar_entries!(accumulator, mutated_ivars)
|
|
584
|
+
# #1406 — an RBS `@x: T` or `attr_* x: T` declaration replaces the census's join of the writes.
|
|
585
|
+
DeclaredIvarTypes.apply!(accumulator, default_scope) if declared_ivar_types
|
|
520
586
|
accumulator.transform_values(&:freeze).freeze
|
|
521
587
|
end
|
|
522
588
|
|
|
@@ -600,8 +666,8 @@ module Rigor
|
|
|
600
666
|
end
|
|
601
667
|
end
|
|
602
668
|
|
|
603
|
-
#
|
|
604
|
-
#
|
|
669
|
+
# Per class, then per method, the ivars each method hands back, dropping the empty sets so the caller's
|
|
670
|
+
# `next if per_class.nil?` skips a whole class in one lookup.
|
|
605
671
|
def returned_ivars_by_method(defs)
|
|
606
672
|
defs.each_with_object({}) do |(class_name, methods), acc|
|
|
607
673
|
methods.each do |method_name, def_node|
|
|
@@ -690,9 +756,8 @@ module Rigor
|
|
|
690
756
|
# The widening uses `MutationWidening.widen_for_mutator` — the same primitive
|
|
691
757
|
# `Inference::StatementEvaluator#eval_call` applies for per-method-body widening on a local / ivar receiver. The
|
|
692
758
|
# class-level pass extends that primitive's reach so a `Tuple`-seeded ivar in `initialize` is observed as
|
|
693
|
-
# `Nominal[Array]` at the entry of every OTHER method body in the class — closing the cross-method gap
|
|
694
|
-
#
|
|
695
|
-
# mutation"; Redmine 6.1.2 `Redmine::Views::Builders::Structure` is the canonical worked site).
|
|
759
|
+
# `Nominal[Array]` at the entry of every OTHER method body in the class — closing the cross-method gap
|
|
760
|
+
# (Redmine 6.1.2 `Redmine::Views::Builders::Structure` is the canonical worked site).
|
|
696
761
|
#
|
|
697
762
|
# Always-safe: the widening can only LOSE precision; the underlying nominal (`Array` / `Hash`) and the element
|
|
698
763
|
# union are preserved.
|
|
@@ -737,18 +802,27 @@ module Rigor
|
|
|
737
802
|
end
|
|
738
803
|
|
|
739
804
|
widen_member_for_lookup_mutators(member, observed_methods)
|
|
740
|
-
when Type::Constant
|
|
741
|
-
# `@s = +"ab"` in one method and `@s << "c"` in another: without this arm the seed stayed pinned at every
|
|
742
|
-
# other method's entry, and `@s == "ab"` folded always-truthy on a receiver that holds `"abc"`.
|
|
743
|
-
return member unless StringMutation.constant?(member) &&
|
|
744
|
-
observed_methods.any? { |m| StringMutation::MUTATORS.include?(m) }
|
|
745
|
-
|
|
746
|
-
Type::Combinator.nominal_of("String")
|
|
805
|
+
when Type::Constant then widen_constant_for_observed_mutators(member, observed_methods)
|
|
747
806
|
else
|
|
748
807
|
member
|
|
749
808
|
end
|
|
750
809
|
end
|
|
751
810
|
|
|
811
|
+
# `@s = +"ab"` in one method and `@s << "c"` in another: without this arm the seed stayed pinned at every other
|
|
812
|
+
# method's entry, and `@s == "ab"` folded always-truthy on a receiver that holds `"abc"`. A constant-folded Set
|
|
813
|
+
# (`@s = Set["a"]`, then `@s.clear; @s << 1` elsewhere — issue #2294) takes the untyped floor the Tuple arm takes.
|
|
814
|
+
def widen_constant_for_observed_mutators(member, observed_methods)
|
|
815
|
+
if SetMutation.constant?(member)
|
|
816
|
+
return member unless observed_methods.any? { |m| SetMutation::MUTATORS.include?(m) }
|
|
817
|
+
|
|
818
|
+
return Type::Combinator.nominal_of("Set", type_args: [Type::Combinator.untyped])
|
|
819
|
+
end
|
|
820
|
+
return member unless StringMutation.constant?(member) &&
|
|
821
|
+
observed_methods.any? { |m| StringMutation::MUTATORS.include?(m) }
|
|
822
|
+
|
|
823
|
+
Type::Combinator.nominal_of("String")
|
|
824
|
+
end
|
|
825
|
+
|
|
752
826
|
# A `HashShape` ivar seed some method gave a default, a default proc or identity keys, and no method stored
|
|
753
827
|
# into, removed from or rewrote: its pairs are the seed's own, so it takes the per-method {HashLookupMutation}
|
|
754
828
|
# widening rather than the untyped floor above — a present key keeps its value across methods, a missing one
|
|
@@ -1024,8 +1098,11 @@ module Rigor
|
|
|
1024
1098
|
# nodes to skip; soundness is post-domination at the top statement level, so dropping the nil never hides a real
|
|
1025
1099
|
# runtime-nil read.
|
|
1026
1100
|
dead_writes = dead_transient_nil_writes(def_node.body, class_name, method_assign_effects)
|
|
1101
|
+
# #1154 — an `@x = x` from a declared parameter records the parameter's declared type.
|
|
1102
|
+
declared = DeclaredParamWrites::EMPTY
|
|
1103
|
+
declared = DeclaredParamWrites.collect(def_node, class_name, body_scope) unless singleton
|
|
1027
1104
|
gather_ivar_writes(def_node.body, body_scope, class_name, accumulator,
|
|
1028
|
-
EMPTY_GUARDED_IVARS, mutated_ivars, dead_writes)
|
|
1105
|
+
EMPTY_GUARDED_IVARS, mutated_ivars, dead_writes, declared)
|
|
1029
1106
|
|
|
1030
1107
|
# B2.3 — collect per-method evidence for the read-before- write nil contribution. The accumulator-level decision
|
|
1031
1108
|
# ("is this ivar truly read-before-write across the class lifetime?") is finalised at
|
|
@@ -1285,9 +1362,9 @@ module Rigor
|
|
|
1285
1362
|
# not `InstanceVariableWriteNode`s, so the generic descent below never records them as writes. Left unhandled,
|
|
1286
1363
|
# an ivar written only via massign in `initialize` stays absent from `init_writes`, and
|
|
1287
1364
|
# `contribute_read_before_write_nil!` then unions a spurious `nil` into its class-ivar seed — masking a
|
|
1288
|
-
# genuinely-typed `@m` (e.g. `Tuple[]`) as `T | nil` at every sibling read. The RHS runs before any
|
|
1289
|
-
# committed, so descend into `value` FIRST (an ivar read there is read-before-write), then mark every
|
|
1290
|
-
# target as written.
|
|
1365
|
+
# genuinely-typed `@m` (e.g. `Tuple[]`) as `T | nil` at every sibling read. The RHS runs before any
|
|
1366
|
+
# target is committed, so descend into `value` FIRST (an ivar read there is read-before-write), then mark every
|
|
1367
|
+
# ivar target as written.
|
|
1291
1368
|
if node.is_a?(Prism::MultiWriteNode)
|
|
1292
1369
|
detect_read_before_write(node.value, seen_writes, read_first) if node.value
|
|
1293
1370
|
detect_multi_write_target_writes(node, seen_writes, read_first)
|
|
@@ -1341,14 +1418,14 @@ module Rigor
|
|
|
1341
1418
|
private_constant :TAIL_VALUE_DEPTH_CAP
|
|
1342
1419
|
private_constant :EMPTY_GUARDED_IVARS
|
|
1343
1420
|
|
|
1344
|
-
def gather_ivar_writes(node, scope, class_name, accumulator, guarded_ivars = EMPTY_GUARDED_IVARS,
|
|
1345
|
-
mutated_ivars = nil, dead_writes = nil)
|
|
1421
|
+
def gather_ivar_writes(node, scope, class_name, accumulator, guarded_ivars = EMPTY_GUARDED_IVARS, # rubocop:disable Metrics/ParameterLists
|
|
1422
|
+
mutated_ivars = nil, dead_writes = nil, declared = DeclaredParamWrites::EMPTY)
|
|
1346
1423
|
return unless node.is_a?(Prism::Node)
|
|
1347
1424
|
|
|
1348
1425
|
if node.is_a?(Prism::InstanceVariableWriteNode)
|
|
1349
1426
|
unless dead_writes&.include?(node.object_id)
|
|
1350
1427
|
record_ivar_write(node, scope, class_name, accumulator,
|
|
1351
|
-
guarded: guarded_ivars.include?(node.name))
|
|
1428
|
+
guarded: guarded_ivars.include?(node.name), declared: declared[node])
|
|
1352
1429
|
end
|
|
1353
1430
|
else
|
|
1354
1431
|
record_compound_ivar_write(node, scope, class_name, accumulator)
|
|
@@ -1371,12 +1448,12 @@ module Rigor
|
|
|
1371
1448
|
|
|
1372
1449
|
if node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
|
|
1373
1450
|
walk_conditional_ivar_writes(node, scope, class_name, accumulator, guarded_ivars,
|
|
1374
|
-
mutated_ivars, dead_writes)
|
|
1451
|
+
mutated_ivars, dead_writes, declared)
|
|
1375
1452
|
return
|
|
1376
1453
|
end
|
|
1377
1454
|
|
|
1378
1455
|
node.rigor_each_child do |c|
|
|
1379
|
-
gather_ivar_writes(c, scope, class_name, accumulator, guarded_ivars, mutated_ivars, dead_writes)
|
|
1456
|
+
gather_ivar_writes(c, scope, class_name, accumulator, guarded_ivars, mutated_ivars, dead_writes, declared)
|
|
1380
1457
|
end
|
|
1381
1458
|
end
|
|
1382
1459
|
|
|
@@ -1414,13 +1491,21 @@ module Rigor
|
|
|
1414
1491
|
method_name, receiver = mutation_target(node)
|
|
1415
1492
|
return if method_name.nil?
|
|
1416
1493
|
return unless receiver.is_a?(Prism::InstanceVariableReadNode)
|
|
1417
|
-
return unless
|
|
1494
|
+
return unless ivar_census_mutator?(method_name)
|
|
1418
1495
|
|
|
1419
1496
|
per_class = (mutated_ivars[class_name] ||= {})
|
|
1420
1497
|
per_ivar = (per_class[receiver.name] ||= Set.new)
|
|
1421
1498
|
per_ivar << method_name
|
|
1422
1499
|
end
|
|
1423
1500
|
|
|
1501
|
+
# The Set-only names (`add`, `merge`, … — issue #2294) are recorded here rather than in `SHAPE_MUTATORS`, which
|
|
1502
|
+
# every scan reads by name alone: there `merge` would make `Hash#merge`, which returns a new Hash, a mutation of
|
|
1503
|
+
# its receiver. The census is safe to over-collect, since {#widen_member_for_observed_mutators} reads each name
|
|
1504
|
+
# against the table of the carrier it widens.
|
|
1505
|
+
def ivar_census_mutator?(method_name)
|
|
1506
|
+
MutationWidening::SHAPE_MUTATORS.include?(method_name) || SetMutation::MUTATORS.include?(method_name)
|
|
1507
|
+
end
|
|
1508
|
+
|
|
1424
1509
|
# `[mutator name, receiver node]` for a node that mutates its receiver, `nil` otherwise.
|
|
1425
1510
|
#
|
|
1426
1511
|
# `@h[k] ||= v` and its `&&=` / `+=` siblings store through `[]=` but are not `[]=` CallNodes; missing them left
|
|
@@ -1452,22 +1537,22 @@ module Rigor
|
|
|
1452
1537
|
# init; end` would otherwise be marked too — but that pattern (write @x in the else of `if @x`) is a separate
|
|
1453
1538
|
# idiom whose surrounding reads of `@x` would then surface a nil-receiver FP. The ELSE branch is left ungarded so
|
|
1454
1539
|
# those reads continue to type as they did before this fix.
|
|
1455
|
-
def walk_conditional_ivar_writes(node, scope, class_name, accumulator, guarded_ivars,
|
|
1456
|
-
mutated_ivars = nil, dead_writes = nil)
|
|
1540
|
+
def walk_conditional_ivar_writes(node, scope, class_name, accumulator, guarded_ivars, # rubocop:disable Metrics/ParameterLists
|
|
1541
|
+
mutated_ivars = nil, dead_writes = nil, declared = DeclaredParamWrites::EMPTY)
|
|
1457
1542
|
then_guards = then_body_guarded_ivars(node)
|
|
1458
1543
|
then_guarded = then_guards.empty? ? guarded_ivars : (guarded_ivars | then_guards)
|
|
1459
1544
|
|
|
1460
1545
|
gather_ivar_writes(node.predicate, scope, class_name, accumulator, guarded_ivars,
|
|
1461
|
-
mutated_ivars, dead_writes)
|
|
1546
|
+
mutated_ivars, dead_writes, declared)
|
|
1462
1547
|
if node.statements
|
|
1463
1548
|
gather_ivar_writes(node.statements, scope, class_name, accumulator, then_guarded,
|
|
1464
|
-
mutated_ivars, dead_writes)
|
|
1549
|
+
mutated_ivars, dead_writes, declared)
|
|
1465
1550
|
end
|
|
1466
1551
|
branch = node.is_a?(Prism::IfNode) ? node.subsequent : node.else_clause
|
|
1467
1552
|
return unless branch
|
|
1468
1553
|
|
|
1469
1554
|
gather_ivar_writes(branch, scope, class_name, accumulator, guarded_ivars,
|
|
1470
|
-
mutated_ivars, dead_writes)
|
|
1555
|
+
mutated_ivars, dead_writes, declared)
|
|
1471
1556
|
end
|
|
1472
1557
|
|
|
1473
1558
|
# Returns the set of ivar names that, in the THEN body of this conditional, are statically known to be in a nil /
|
|
@@ -1555,10 +1640,9 @@ module Rigor
|
|
|
1555
1640
|
SAME_CLASS_CALL_DEPTH_CAP = 3
|
|
1556
1641
|
private_constant :SAME_CLASS_CALL_DEPTH_CAP
|
|
1557
1642
|
|
|
1558
|
-
# WD3 — builds the per-class definite-assignment summary
|
|
1559
|
-
#
|
|
1560
|
-
#
|
|
1561
|
-
# `mask!`).
|
|
1643
|
+
# WD3 — builds the per-class definite-assignment summary: per class, then per method, the ivars assigned non-nil
|
|
1644
|
+
# on every completing path. Used so a ctor's `dead_transient_nil_writes` can credit an indirect overwrite through
|
|
1645
|
+
# an unconditionally-called same-class method (ipaddr's `initialize` reassigns `@mask_addr` via `mask!`).
|
|
1562
1646
|
#
|
|
1563
1647
|
# Each method's set is computed by the same suffix definite-assignment analysis used for the ctor seed, run from
|
|
1564
1648
|
# the method body's first statement for every ivar the method writes anywhere. Same-class calls inside a method
|
|
@@ -1944,11 +2028,12 @@ module Rigor
|
|
|
1944
2028
|
assigns.include?(target)
|
|
1945
2029
|
end
|
|
1946
2030
|
|
|
1947
|
-
def record_ivar_write(node, scope, class_name, accumulator, guarded: false)
|
|
1948
|
-
rvalue_type =
|
|
2031
|
+
def record_ivar_write(node, scope, class_name, accumulator, guarded: false, declared: nil)
|
|
2032
|
+
rvalue_type = declared || accumulator[CONSTANT_IVAR_VALUES]&.type_of(node.value, scope) ||
|
|
2033
|
+
scope.type_of(node.value)
|
|
1949
2034
|
|
|
1950
2035
|
# `@x = nil unless @x` / `@y = false unless @y` — follow-up to the polarity-aware defensive-init guard fix
|
|
1951
|
-
# (
|
|
2036
|
+
# (defensive ivar-init with a nil / false rvalue). When the rvalue is itself a falsey
|
|
1952
2037
|
# Constant, `union(rvalue, Constant[nil])` collapses (for `nil`) or doesn't widen the type's truthiness profile
|
|
1953
2038
|
# (for `false`) — the predicate `unless @x` then folds to a single `Constant[nil]` / `Constant[false]` and the
|
|
1954
2039
|
# `flow.always-truthy-condition` / `-always-falsey-` rule false-fires on the no-op-but-documented-default idiom.
|
|
@@ -1958,6 +2043,8 @@ module Rigor
|
|
|
1958
2043
|
# `lib/tdiary/configuration.rb:157` for the worked site.
|
|
1959
2044
|
return if guarded && falsey_constant?(rvalue_type)
|
|
1960
2045
|
|
|
2046
|
+
rvalue_type = EmptySeed.erase(node.value, rvalue_type)
|
|
2047
|
+
|
|
1961
2048
|
rvalue_type = Type::Combinator.union(rvalue_type, Type::Combinator.constant_of(nil)) if guarded
|
|
1962
2049
|
accumulate_ivar_type(accumulator, class_name, node.name, rvalue_type)
|
|
1963
2050
|
end
|
|
@@ -1987,7 +2074,7 @@ module Rigor
|
|
|
1987
2074
|
# `nil`, the guard bound its own ivar, and `ExpressionTyper#compound_write_value` read
|
|
1988
2075
|
# `truthy(nil) | bot` — an instance method that sig-gen declared `-> bot`.
|
|
1989
2076
|
def record_ivar_or_write(node, scope, class_name, accumulator)
|
|
1990
|
-
rvalue_type = scope.type_of(node.value)
|
|
2077
|
+
rvalue_type = EmptySeed.erase(node.value, scope.type_of(node.value))
|
|
1991
2078
|
return if Narrowing.narrow_truthy(rvalue_type).is_a?(Type::Bot)
|
|
1992
2079
|
|
|
1993
2080
|
accumulate_ivar_type(accumulator, class_name, node.name,
|
|
@@ -2151,7 +2238,9 @@ module Rigor
|
|
|
2151
2238
|
CLASS_BODY_WRITES = :class_body_writes
|
|
2152
2239
|
# The classes a computed-name `instance_variable_set` may write any ivar of ({#record_reflective_ivar_write}).
|
|
2153
2240
|
REFLECTIVE_OPAQUE_CLASSES = :reflective_opaque_classes
|
|
2154
|
-
|
|
2241
|
+
# The census's {ConstantIvarValue}, which {#record_ivar_write} reads.
|
|
2242
|
+
CONSTANT_IVAR_VALUES = :constant_ivar_values
|
|
2243
|
+
private_constant :DEFINE_METHOD_WRITES, :CLASS_BODY_WRITES, :REFLECTIVE_OPAQUE_CLASSES, :CONSTANT_IVAR_VALUES
|
|
2155
2244
|
|
|
2156
2245
|
# Unions `type` into the class-ivar accumulator for `(class_name, ivar_name)`. Shared by the single-write and
|
|
2157
2246
|
# multi-write (parallel-assignment) collectors.
|
|
@@ -2184,7 +2273,11 @@ module Rigor
|
|
|
2184
2273
|
|
|
2185
2274
|
rhs_type = scope.type_of(node.value)
|
|
2186
2275
|
ivars = MultiTargetBinder.bind_marked(node, rhs_type, scope: scope, soften_slots: false).ivars
|
|
2187
|
-
|
|
2276
|
+
# #2499 — `@a, @b = {}, []` erases each empty seed as a single write does.
|
|
2277
|
+
values = EmptySeed.multi_write_values(node)
|
|
2278
|
+
ivars.each do |name, type|
|
|
2279
|
+
accumulate_ivar_type(accumulator, class_name, name, EmptySeed.erase(values[name], type))
|
|
2280
|
+
end
|
|
2188
2281
|
end
|
|
2189
2282
|
|
|
2190
2283
|
def falsey_constant?(type)
|
|
@@ -2351,7 +2444,7 @@ module Rigor
|
|
|
2351
2444
|
end
|
|
2352
2445
|
|
|
2353
2446
|
def record_cvar_write(node, scope, class_name, accumulator)
|
|
2354
|
-
rvalue_type = scope.type_of(node.value)
|
|
2447
|
+
rvalue_type = EmptySeed.erase(node.value, scope.type_of(node.value))
|
|
2355
2448
|
accumulator[class_name] ||= {}
|
|
2356
2449
|
existing = accumulator[class_name][node.name]
|
|
2357
2450
|
accumulator[class_name][node.name] =
|
|
@@ -2473,7 +2566,7 @@ module Rigor
|
|
|
2473
2566
|
end
|
|
2474
2567
|
|
|
2475
2568
|
def record_global_write(node, scope, accumulator)
|
|
2476
|
-
rvalue_type = scope.type_of(node.value)
|
|
2569
|
+
rvalue_type = EmptySeed.erase(node.value, scope.type_of(node.value))
|
|
2477
2570
|
existing = accumulator[node.name]
|
|
2478
2571
|
accumulator[node.name] =
|
|
2479
2572
|
existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
|
|
@@ -3238,7 +3331,7 @@ module Rigor
|
|
|
3238
3331
|
unless owner.empty?
|
|
3239
3332
|
body_scope = census_body_scope(default_scope, enclosing_prefix, Type::Combinator.singleton_of(owner))
|
|
3240
3333
|
end
|
|
3241
|
-
rvalue_type = meta_new_constant_type(node, full) || body_scope.type_of(node.value)
|
|
3334
|
+
rvalue_type = meta_new_constant_type(node, full) || EmptySeed.erase(node.value, body_scope.type_of(node.value))
|
|
3242
3335
|
existing = accumulator[full]
|
|
3243
3336
|
accumulator[full] = existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
|
|
3244
3337
|
end
|
|
@@ -3433,7 +3526,17 @@ module Rigor
|
|
|
3433
3526
|
# walk worked out for it ({#refine_context}).
|
|
3434
3527
|
MethodTables = Struct.new(:existence, :envelopes, :refinements, :certainty, :certain, :possible,
|
|
3435
3528
|
:contested_envelopes, :contested_def_nodes, :root, :in_effect, :census,
|
|
3436
|
-
:refine_text, :literal_seen, :source_bytes, :loose_constants, :block_spans
|
|
3529
|
+
:refine_text, :literal_seen, :source_bytes, :loose_constants, :block_spans,
|
|
3530
|
+
:definer_history) do
|
|
3531
|
+
# Issue #2006 — records, in walk order, that a definer other than an alias wrote the instance method `name`
|
|
3532
|
+
# of `class_name` at `node`: `def_node` for a `def`, nil for a `define_method(:x)` or an `attr_*` reader or
|
|
3533
|
+
# writer, and whether the write is {#certain}. {ScopeIndexer#apply_alias_def_nodes} reads the history to copy
|
|
3534
|
+
# what an alias's old name held at the alias, and to tell an alias a later definer replaced.
|
|
3535
|
+
def wrote_definer(class_name, name, node, def_node)
|
|
3536
|
+
entry = [node.location.start_offset, def_node, certain ? true : false].freeze
|
|
3537
|
+
((self.definer_history ||= {})[[class_name, name]] ||= []) << entry
|
|
3538
|
+
end
|
|
3539
|
+
|
|
3437
3540
|
# {ScopeIndexer.module_refine_target} against this tree's {InEffectRefinements}.
|
|
3438
3541
|
def refine_target_of(node)
|
|
3439
3542
|
return nil if ScopeIndexer.refine_target(node).nil?
|
|
@@ -3526,7 +3629,8 @@ module Rigor
|
|
|
3526
3629
|
REFINEMENT_WILDCARD = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
|
|
3527
3630
|
REFINE_WORD = /\brefine\b/
|
|
3528
3631
|
HEREDOC_OPENER = /<<[~-]?["'`A-Za-z_]/
|
|
3529
|
-
|
|
3632
|
+
COMPARISON_OPERATORS = Set[:==, :!=, :===].freeze
|
|
3633
|
+
private_constant :EMPTY_REFINEMENTS, :REFINEMENT_WILDCARD, :REFINE_WORD, :HEREDOC_OPENER, :COMPARISON_OPERATORS
|
|
3530
3634
|
|
|
3531
3635
|
NO_OFFSETS = [].freeze
|
|
3532
3636
|
private_constant :NO_OFFSETS
|
|
@@ -3619,7 +3723,8 @@ module Rigor
|
|
|
3619
3723
|
# `self`, charged to its module or the wildcard), `:literal` (A3's `:refine` / eval-string literal), `:nested` (a
|
|
3620
3724
|
# `refine` in a refine body, or in a singleton method of one), `:dsl` (where `self` is a class, #1689) and
|
|
3621
3725
|
# `:raises` (no block, a Proc as the block, another receiver, or a computed target where `self` is `main`), and
|
|
3622
|
-
# `:data` (a `:refine` literal where `self` is a plain class or one of its instances)
|
|
3726
|
+
# `:data` (a `:refine` literal where `self` is a plain class or one of its instances), and `:inert` (issue #2203:
|
|
3727
|
+
# a `:refine` Symbol that is a hash key or a comparison operand, which no call reads as a method name).
|
|
3623
3728
|
def refine_census(root)
|
|
3624
3729
|
census = []
|
|
3625
3730
|
refinements = build_methods_and_def_nodes(root, census: census)[3]
|
|
@@ -3798,6 +3903,8 @@ module Rigor
|
|
|
3798
3903
|
end
|
|
3799
3904
|
|
|
3800
3905
|
census_call(node, qualified_prefix, tables, refine_body)
|
|
3906
|
+
when Prism::AssocNode
|
|
3907
|
+
census_assoc_key(node, tables)
|
|
3801
3908
|
when Prism::SymbolNode
|
|
3802
3909
|
census_symbol(node, tables)
|
|
3803
3910
|
end
|
|
@@ -3853,6 +3960,8 @@ module Rigor
|
|
|
3853
3960
|
arguments = node.arguments&.arguments
|
|
3854
3961
|
return if arguments.nil?
|
|
3855
3962
|
|
|
3963
|
+
return census_compared_literal(arguments.first, tables) if COMPARISON_OPERATORS.include?(node.name)
|
|
3964
|
+
|
|
3856
3965
|
strings = RefineCensus.string_literal_call?(node)
|
|
3857
3966
|
arguments.each do |argument|
|
|
3858
3967
|
next unless RefineCensus.refine_symbol?(argument) || (strings && RefineCensus.refine_string?(argument))
|
|
@@ -3887,6 +3996,26 @@ module Rigor
|
|
|
3887
3996
|
|
|
3888
3997
|
def constant_shaped?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
3889
3998
|
|
|
3999
|
+
# Issue #2203 — a `:refine` Symbol a comparison reads (`node.name == :refine`, `!=`, `===`) is a value compared,
|
|
4000
|
+
# never a method name a call runs: it is inert (`:inert`), and records no row.
|
|
4001
|
+
def census_compared_literal(argument, tables)
|
|
4002
|
+
return unless RefineCensus.refine_symbol?(argument) && !tables.literal_seen?(argument)
|
|
4003
|
+
|
|
4004
|
+
tables.see_literal(argument)
|
|
4005
|
+
tables.account(argument, :inert)
|
|
4006
|
+
end
|
|
4007
|
+
|
|
4008
|
+
# Issue #2203 — a `:refine` Symbol that is a hash key (`h(x, refine: true)`, `{ refine: 1 }`, `{ :refine => 1 }`)
|
|
4009
|
+
# names an option, not `Module#refine`: it is inert (`:inert`), and records no row. A `:refine` value stays a
|
|
4010
|
+
# literal.
|
|
4011
|
+
def census_assoc_key(node, tables)
|
|
4012
|
+
key = node.key
|
|
4013
|
+
return unless RefineCensus.refine_symbol?(key) && !tables.literal_seen?(key)
|
|
4014
|
+
|
|
4015
|
+
tables.see_literal(key)
|
|
4016
|
+
tables.account(key, :inert)
|
|
4017
|
+
end
|
|
4018
|
+
|
|
3890
4019
|
# A3 — a `:refine` Symbol anywhere else (`alias my_refine refine`, `%i[refine]`, a hash) names `Module#refine`
|
|
3891
4020
|
# for code the walk cannot follow, run on the module the in-effect walk says `self` is there; where `self` is a
|
|
3892
4021
|
# plain class or one of its instances it is data.
|
|
@@ -4158,6 +4287,8 @@ module Rigor
|
|
|
4158
4287
|
end
|
|
4159
4288
|
end
|
|
4160
4289
|
return
|
|
4290
|
+
when Prism::AssocNode
|
|
4291
|
+
census_assoc_key(node, methods_acc) if methods_acc.mentions_refine?
|
|
4161
4292
|
when Prism::SymbolNode
|
|
4162
4293
|
census_symbol(node, methods_acc) if methods_acc.mentions_refine?
|
|
4163
4294
|
when Prism::CallNode
|
|
@@ -4171,7 +4302,9 @@ module Rigor
|
|
|
4171
4302
|
anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc.at(node),
|
|
4172
4303
|
source_path)
|
|
4173
4304
|
unless in_singleton_class
|
|
4174
|
-
|
|
4305
|
+
superseded = call_superseded_names(node)
|
|
4306
|
+
supersede_def_nodes(superseded, owner_prefix, def_nodes_acc, methods_acc.at(node))
|
|
4307
|
+
note_call_definers(node, superseded, owner_prefix, methods_acc.at(node))
|
|
4175
4308
|
end
|
|
4176
4309
|
tables = methods_acc.at(node)
|
|
4177
4310
|
record_singleton_prepend(node, qualified_prefix, tables,
|
|
@@ -4811,6 +4944,7 @@ module Rigor
|
|
|
4811
4944
|
accumulator[class_name] ||= {}
|
|
4812
4945
|
accumulator[class_name][def_node.name] = def_node
|
|
4813
4946
|
follow_slot_writer(tables, :contested_def_nodes, [class_name, def_node.name], tables.nil? || tables.certain)
|
|
4947
|
+
tables&.wrote_definer(class_name, def_node.name, def_node, def_node)
|
|
4814
4948
|
record_anonymous_body_def_as_toplevel(def_node, qualified_prefix, accumulator, tables)
|
|
4815
4949
|
end
|
|
4816
4950
|
|
|
@@ -4870,6 +5004,16 @@ module Rigor
|
|
|
4870
5004
|
end
|
|
4871
5005
|
end
|
|
4872
5006
|
|
|
5007
|
+
# Issue #2006 — a `define_method(:x)` or `attr_*` call is a definer the alias pass reads in source order
|
|
5008
|
+
# ({#apply_alias_def_nodes}). An `alias_method` call, whose new name {#call_superseded_names} also lists, is
|
|
5009
|
+
# itself an alias: the pass records its copy, so the history must not hold it as a definer with no `def`.
|
|
5010
|
+
def note_call_definers(call_node, names, qualified_prefix, tables)
|
|
5011
|
+
return if names.empty? || qualified_prefix.empty? || call_node.name == :alias_method
|
|
5012
|
+
|
|
5013
|
+
class_name = qualified_prefix.join("::")
|
|
5014
|
+
names.each { |name| tables.wrote_definer(class_name, name, call_node, nil) }
|
|
5015
|
+
end
|
|
5016
|
+
|
|
4873
5017
|
# #319 — a `def` inside an anonymous `Class.new` / `Module.new` body ALSO stays in the `<toplevel>` table.
|
|
4874
5018
|
# Before the anonymous class had a name the body was walked with an empty prefix, so every such `def`
|
|
4875
5019
|
# landed there; that is the same leniency this key already grants a `def` nested in any other DSL block
|
|
@@ -5035,7 +5179,7 @@ module Rigor
|
|
|
5035
5179
|
# nested `def` or block inside its body is still a range the containment half needs.
|
|
5036
5180
|
# `def_owner_prefix` is the eval-body override described on {#walk_methods_and_def_nodes}:
|
|
5037
5181
|
# an explicit owner for def rows while declarations stay lexical (`[]` = ownerless).
|
|
5038
|
-
def walk_deferred_ranges(node, qualified_prefix, in_singleton_class, inside_deferred, # rubocop:disable Metrics/ParameterLists
|
|
5182
|
+
def walk_deferred_ranges(node, qualified_prefix, in_singleton_class, inside_deferred, # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
|
5039
5183
|
mf_offsets, ranges, def_owner_prefix = nil, singleton_cref: false,
|
|
5040
5184
|
defs_singleton: false)
|
|
5041
5185
|
return unless node.is_a?(Prism::Node)
|
|
@@ -5071,11 +5215,34 @@ module Rigor
|
|
|
5071
5215
|
singleton_cref: singleton_cref)
|
|
5072
5216
|
end
|
|
5073
5217
|
|
|
5218
|
+
record_definer_name_row(node, ranges) if node.is_a?(Prism::CallNode)
|
|
5074
5219
|
walk_deferred_children(node, qualified_prefix, in_singleton_class, inside_deferred,
|
|
5075
5220
|
mf_offsets, ranges, def_owner_prefix, singleton_cref: singleton_cref,
|
|
5076
5221
|
defs_singleton: defs_singleton)
|
|
5077
5222
|
end
|
|
5078
5223
|
|
|
5224
|
+
DEFINER_NAME_CALLS = Set[:define_method, :define_singleton_method, :alias_method].freeze
|
|
5225
|
+
DEFINER_SEND_CALLS = Set[:send, :__send__, :public_send].freeze
|
|
5226
|
+
private_constant :DEFINER_NAME_CALLS, :DEFINER_SEND_CALLS
|
|
5227
|
+
|
|
5228
|
+
# Issue #2184 — a `define_method` / `define_singleton_method` / `alias_method` call with a literal (new) name,
|
|
5229
|
+
# on any receiver and through `send` too, records an ownerless, kindless row over the name literal: no owner or
|
|
5230
|
+
# side is claimed, so no ordering scan matches it and the literal contains no call. It only tells a by-name
|
|
5231
|
+
# reader that the project gives the name a body no def table holds. A computed name records nothing.
|
|
5232
|
+
def record_definer_name_row(node, ranges)
|
|
5233
|
+
arguments = node.arguments&.arguments || []
|
|
5234
|
+
if DEFINER_SEND_CALLS.include?(node.name)
|
|
5235
|
+
return unless DEFINER_NAME_CALLS.include?(literal_method_name(arguments.first))
|
|
5236
|
+
|
|
5237
|
+
arguments = arguments.drop(1)
|
|
5238
|
+
end
|
|
5239
|
+
return unless DEFINER_NAME_CALLS.include?(node.name) || DEFINER_SEND_CALLS.include?(node.name)
|
|
5240
|
+
|
|
5241
|
+
argument = arguments.first
|
|
5242
|
+
name = literal_method_name(argument)
|
|
5243
|
+
ranges << [argument.location.start_offset, argument.location.end_offset, name, nil, nil] if name
|
|
5244
|
+
end
|
|
5245
|
+
|
|
5079
5246
|
# The `def` arm of {#walk_deferred_ranges}: records the range row — singleton-side when the
|
|
5080
5247
|
# def sits in a `class <<` body or an `instance_eval` block (`defs_singleton`) — then walks
|
|
5081
5248
|
# the body as a deferred range.
|
|
@@ -5586,7 +5753,9 @@ module Rigor
|
|
|
5586
5753
|
mf_offsets, ranges)
|
|
5587
5754
|
start = def_node.location.start_offset
|
|
5588
5755
|
if inside_deferred
|
|
5589
|
-
|
|
5756
|
+
# Issue #2184 — the def installs at invocation under a `self` the walk does not know, so the row names the
|
|
5757
|
+
# method but claims no owner or side.
|
|
5758
|
+
ranges << [start, def_node.location.end_offset, def_node.name, nil, nil]
|
|
5590
5759
|
return
|
|
5591
5760
|
end
|
|
5592
5761
|
|
|
@@ -7564,7 +7733,8 @@ module Rigor
|
|
|
7564
7733
|
return record_module_function(node, current_class, accumulator, in_singleton) if node.name == :module_function
|
|
7565
7734
|
|
|
7566
7735
|
taint_opaque_eval(node, current_class, accumulator)
|
|
7567
|
-
return if singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator
|
|
7736
|
+
return if singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator,
|
|
7737
|
+
in_singleton: in_singleton)
|
|
7568
7738
|
|
|
7569
7739
|
kind, _arguments, via_send = mixin_call_view(node)
|
|
7570
7740
|
return if kind.nil?
|
|
@@ -7572,7 +7742,8 @@ module Rigor
|
|
|
7572
7742
|
effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: in_singleton)
|
|
7573
7743
|
.select { |_owner, side| side == :extend || in_singleton }
|
|
7574
7744
|
return if effects.empty?
|
|
7575
|
-
return if !via_send && singleton_mixin_recorded?(node, kind, current_class, accumulator,
|
|
7745
|
+
return if !via_send && singleton_mixin_recorded?(node, kind, qualified_prefix, current_class, accumulator,
|
|
7746
|
+
in_singleton)
|
|
7576
7747
|
|
|
7577
7748
|
effects.each { |owner, side| accumulator.taint(owner, side) }
|
|
7578
7749
|
end
|
|
@@ -7587,7 +7758,7 @@ module Rigor
|
|
|
7587
7758
|
# but only where it is a direct statement (a top-level statement, or one of a direct declaration body or of a
|
|
7588
7759
|
# block that runs there once): there the methods walk records those marks under the same owner. In a method
|
|
7589
7760
|
# body, a hook or a conditional it may run on a receiver or at a time the marks cannot place, so it still taints.
|
|
7590
|
-
def singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator)
|
|
7761
|
+
def singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator, in_singleton: false)
|
|
7591
7762
|
receiver = node.receiver
|
|
7592
7763
|
return false unless singleton_class_of_receiver?(receiver) && surface_rewriting_call?(node)
|
|
7593
7764
|
return false if singleton_class_named_call?(node) && accumulator.direct?(node)
|
|
@@ -7595,10 +7766,33 @@ module Rigor
|
|
|
7595
7766
|
owner = singleton_class_owner(node, receiver.receiver, qualified_prefix, current_class)
|
|
7596
7767
|
return false if owner.nil?
|
|
7597
7768
|
|
|
7769
|
+
# Inside `class << self` a bare `singleton_class` is the singleton's own singleton, one level up from the
|
|
7770
|
+
# owner's extend side, so that call stays a taint.
|
|
7771
|
+
up_level = in_singleton && lexical_self_receiver?(receiver.receiver)
|
|
7772
|
+
return true if !up_level && singleton_class_include_recorded?(node, owner, accumulator)
|
|
7773
|
+
|
|
7598
7774
|
accumulator.taint(owner, :extend)
|
|
7599
7775
|
true
|
|
7600
7776
|
end
|
|
7601
7777
|
|
|
7778
|
+
# Issue #2094 — `Float.singleton_class.include(Ext)` (or `prepend`, which the extend table cannot order, so the
|
|
7779
|
+
# edge is unpositioned as every receiver-form edge is) puts `Ext` on the singleton side as `Float.extend(Ext)`
|
|
7780
|
+
# does, the fold #915 gives `class << self; include M`. A `send` spelling or an argument that is not a constant
|
|
7781
|
+
# stays a taint.
|
|
7782
|
+
def singleton_class_include_recorded?(node, owner, accumulator)
|
|
7783
|
+
return false unless %i[include prepend].include?(node.name)
|
|
7784
|
+
|
|
7785
|
+
arguments = node.arguments&.arguments || []
|
|
7786
|
+
return false if arguments.empty? || arguments.any? { |arg| Source::ConstantPath.qualified_name(arg).nil? }
|
|
7787
|
+
|
|
7788
|
+
!record_extend_targets(node, owner, accumulator).empty?
|
|
7789
|
+
end
|
|
7790
|
+
|
|
7791
|
+
# A bare or `self.` receiver: the lexical `self`.
|
|
7792
|
+
def lexical_self_receiver?(receiver)
|
|
7793
|
+
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
7794
|
+
end
|
|
7795
|
+
|
|
7602
7796
|
# A bare, `self.` or constant receiver's `singleton_class` call.
|
|
7603
7797
|
def singleton_class_of_receiver?(receiver)
|
|
7604
7798
|
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil?
|
|
@@ -7657,13 +7851,29 @@ module Rigor
|
|
|
7657
7851
|
|
|
7658
7852
|
# Records the singleton-side edges of a receiverless `extend` (or, inside `class << self`, `include` /
|
|
7659
7853
|
# `prepend`). False when it recorded nothing.
|
|
7660
|
-
def singleton_mixin_recorded?(node, kind, current_class, accumulator, in_singleton)
|
|
7661
|
-
return
|
|
7854
|
+
def singleton_mixin_recorded?(node, kind, qualified_prefix, current_class, accumulator, in_singleton)
|
|
7855
|
+
return receiver_extend_recorded?(node, kind, qualified_prefix, accumulator) if node.receiver
|
|
7856
|
+
return false unless current_class
|
|
7662
7857
|
return false unless in_singleton ? MIXIN_CALL_NAMES.include?(kind) : kind == :extend
|
|
7663
7858
|
|
|
7664
7859
|
!record_extend_targets(node, current_class, accumulator).empty?
|
|
7665
7860
|
end
|
|
7666
7861
|
|
|
7862
|
+
# Issue #2094 — the receiver form, `Float.extend(Ext)`: the singleton twin of #1722's `Integer.include(Ext)`,
|
|
7863
|
+
# filed under the class the constant receiver names ({#prepend_call_receiver}) and, like that form,
|
|
7864
|
+
# unpositioned wherever it is written ({MixinAccumulator#note}). A `self` argument names the receiver, not the
|
|
7865
|
+
# enclosing class, so a call with one stays a taint. A constant receiver names the same class inside
|
|
7866
|
+
# `class << self`, so the walk's singleton flag does not matter here.
|
|
7867
|
+
def receiver_extend_recorded?(node, kind, qualified_prefix, accumulator)
|
|
7868
|
+
return false if kind != :extend
|
|
7869
|
+
return false if (node.arguments&.arguments || []).any?(Prism::SelfNode)
|
|
7870
|
+
|
|
7871
|
+
owner = prepend_call_receiver(node, qualified_prefix)
|
|
7872
|
+
return false if owner.nil?
|
|
7873
|
+
|
|
7874
|
+
!record_extend_targets(node, owner, accumulator).empty?
|
|
7875
|
+
end
|
|
7876
|
+
|
|
7667
7877
|
# The table stores search order, not call order: a later `extend` statement prepends its module
|
|
7668
7878
|
# (`extend A; extend B` → singleton ancestors `[B, A]`) while the arguments of a single
|
|
7669
7879
|
# `extend A, B` keep call order (`[A, B]`). Prepending each call's argument list preserves both,
|
|
@@ -7895,6 +8105,16 @@ module Rigor
|
|
|
7895
8105
|
end
|
|
7896
8106
|
end
|
|
7897
8107
|
|
|
8108
|
+
# Issue #2013 — contests the instance slot of `name`, recorded or not, and marks the contest as a
|
|
8109
|
+
# replacement's ({ScopeIndexer.replaced_visibility_mark}): a `define_method(:name)` or an alias gave the name
|
|
8110
|
+
# a visibility the walk does not read, and another file may record the slot. A later certain write in this
|
|
8111
|
+
# walk clears the contest ({#wrote}) and leaves the mark, which then only keeps a contest another file puts on
|
|
8112
|
+
# the slot: the fold declines more, never reports more, and file order is not load order there either.
|
|
8113
|
+
def contest_replaced(class_name, name)
|
|
8114
|
+
contest_slot(class_name, name)
|
|
8115
|
+
contest_slot(class_name, ScopeIndexer.replaced_visibility_mark(name))
|
|
8116
|
+
end
|
|
8117
|
+
|
|
7898
8118
|
# Contests the `[class_name, name]` slot, recorded or not: a visibility call changed it in a way the walk
|
|
7899
8119
|
# cannot read (`private def self.x` reaching an instance `x`).
|
|
7900
8120
|
def contest_slot(class_name, name)
|
|
@@ -8231,6 +8451,12 @@ module Rigor
|
|
|
8231
8451
|
# them public (`K.public_class_method(*names)`, `K.singleton_class.class_eval { … }`).
|
|
8232
8452
|
SINGLETON_WILDCARD_SLOT = %i[singleton *].freeze
|
|
8233
8453
|
|
|
8454
|
+
# Issue #2013 — the mark a contested instance slot `name` carries in `contested_discovered_method_visibilities`
|
|
8455
|
+
# when a `define_method(:name)` or an alias replaced the method: `[:replaced, name]`. No reader asks for it; it
|
|
8456
|
+
# keeps the slot's contest when another file's write of the slot folds later ({#fold_visibility_contests}),
|
|
8457
|
+
# since file order is not load order.
|
|
8458
|
+
def self.replaced_visibility_mark(name) = [:replaced, name].freeze
|
|
8459
|
+
|
|
8234
8460
|
# Issue #1984 — a class method visibility call written on a constant (`K.public_class_method(:x)`,
|
|
8235
8461
|
# `K.send(:public_class_method, :x)`), anywhere: each literal name is recorded public on every class the
|
|
8236
8462
|
# constant may name, so a reopening's or another file's `private_class_method` disagrees with it and is
|
|
@@ -8501,19 +8727,21 @@ module Rigor
|
|
|
8501
8727
|
Source::AliasNames.alias_method_call_names(call_node)&.first(1) || []
|
|
8502
8728
|
end
|
|
8503
8729
|
|
|
8504
|
-
# Issue #1989 — a `define_method` or an alias that replaces a method
|
|
8505
|
-
#
|
|
8506
|
-
#
|
|
8507
|
-
#
|
|
8508
|
-
#
|
|
8730
|
+
# Issue #1989 — a `define_method` or an alias that replaces a method gives the name a visibility the walk
|
|
8731
|
+
# does not read (the running default for `define_method`, the old name's for an alias), so the slot is
|
|
8732
|
+
# contested rather than left asserting the replaced `def`'s. Issue #1984 — the slot is contested whether or
|
|
8733
|
+
# not this file recorded it, since another file may have; issue #2013 — on the instance side too, with the
|
|
8734
|
+
# mark that keeps the contest when that file's write folds later ({VisibilityTable#contest_replaced}).
|
|
8509
8735
|
def contest_replaced_visibility(names, qualified_prefix, accumulator, side: :instance)
|
|
8510
8736
|
return if names.empty? || qualified_prefix.empty?
|
|
8511
8737
|
|
|
8512
8738
|
class_name = qualified_prefix.join("::")
|
|
8513
|
-
recorded = accumulator[class_name]
|
|
8514
8739
|
names.each do |name|
|
|
8515
|
-
|
|
8516
|
-
|
|
8740
|
+
if side == :singleton
|
|
8741
|
+
accumulator.contest_slot(class_name, visibility_slot(name, side))
|
|
8742
|
+
else
|
|
8743
|
+
accumulator.contest_replaced(class_name, name)
|
|
8744
|
+
end
|
|
8517
8745
|
end
|
|
8518
8746
|
end
|
|
8519
8747
|
|
|
@@ -8754,48 +8982,76 @@ module Rigor
|
|
|
8754
8982
|
#
|
|
8755
8983
|
# ADR-119 WD3 — the alias is the slot's writer: the new name's slot is contested when the alias is possible
|
|
8756
8984
|
# or the slot it copies is contested, and cleared otherwise.
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8985
|
+
#
|
|
8986
|
+
# Issue #2006 — in source order. An alias copies what its old name held AT the alias, read from the walk's
|
|
8987
|
+
# definer history ({MethodTables#wrote_definer}) and the copies earlier aliases made: a `def` of the old name
|
|
8988
|
+
# written after the alias (`alias old_x x; def x = 1`) is not what `old_x` runs, and nothing before the alias
|
|
8989
|
+
# copies nothing. The new name's slot then follows its last writer: a later `def` keeps the slot the walk
|
|
8990
|
+
# gave it, and a later `define_method` / `attr_*` leaves the copy contested, as {#supersede_def_nodes} leaves
|
|
8991
|
+
# a replaced `def`.
|
|
8992
|
+
def apply_alias_def_nodes(root, accumulator, tables)
|
|
8993
|
+
alias_map = collect_class_alias_map(root, [], AliasMap.new.track(tables.certainty))
|
|
8994
|
+
history = (tables.definer_history || {}).transform_values(&:dup)
|
|
8995
|
+
alias_map.copy_into(history).each { |key| settle_alias_slot(accumulator, tables, key, history[key]) }
|
|
8996
|
+
end
|
|
8997
|
+
|
|
8998
|
+
# Writes the new name's slot from its last writer `writes.last` ({#apply_alias_def_nodes}): an alias copy or a
|
|
8999
|
+
# later `def` writes its `def` with its certainty (for the `def`, what the walk wrote already). A last writer
|
|
9000
|
+
# with no `def` this file holds — a later `define_method` / `attr_*`, or an alias that copies nothing this file
|
|
9001
|
+
# defines (an inherited or core method, an attr reader, a name it cannot read) — contests the slot, which keeps
|
|
9002
|
+
# what it holds or else the latest copy, so no sibling file's `def` fills it.
|
|
9003
|
+
def settle_alias_slot(accumulator, tables, (class_name, new_name), writes)
|
|
9004
|
+
last = writes.last
|
|
9005
|
+
slot = (accumulator[class_name] ||= {})
|
|
9006
|
+
if last[1]
|
|
9007
|
+
slot[new_name] = last[1]
|
|
9008
|
+
follow_slot_writer(tables, :contested_def_nodes, [class_name, new_name], last[2])
|
|
9009
|
+
else
|
|
9010
|
+
copy = writes.rfind { |write| write[1] }
|
|
9011
|
+
slot[new_name] ||= copy[1] if copy
|
|
9012
|
+
follow_slot_writer(tables, :contested_def_nodes, [class_name, new_name], false)
|
|
8774
9013
|
end
|
|
8775
9014
|
end
|
|
8776
9015
|
|
|
8777
|
-
# The `{class => {new name => old name}}` map {#collect_class_alias_map} fills, which also
|
|
8778
|
-
#
|
|
9016
|
+
# The `{class => {new name => old name}}` map {#collect_class_alias_map} fills, which also lists every alias it
|
|
9017
|
+
# met as an event `[class, new name, old name, start offset, possible?]` ({Certainty}).
|
|
8779
9018
|
class AliasMap < Hash
|
|
9019
|
+
attr_reader :events
|
|
9020
|
+
|
|
8780
9021
|
def track(certainty)
|
|
8781
9022
|
@certainty = certainty || Certainty::EMPTY
|
|
8782
|
-
@
|
|
9023
|
+
@events = []
|
|
8783
9024
|
self
|
|
8784
9025
|
end
|
|
8785
9026
|
|
|
8786
|
-
# Records that `node` wrote the `[class_name, new_name]` entry
|
|
8787
|
-
def wrote(class_name, new_name, node)
|
|
9027
|
+
# Records that `node` wrote the `[class_name, new_name]` entry, aliasing `old_name`.
|
|
9028
|
+
def wrote(class_name, new_name, old_name, node)
|
|
8788
9029
|
return if @certainty.nil?
|
|
8789
9030
|
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
9031
|
+
@events << [class_name, new_name, old_name, node.location.start_offset, Certainty.possible?(@certainty, node)]
|
|
9032
|
+
end
|
|
9033
|
+
|
|
9034
|
+
# Issue #2006 — adds every alias to `history` (`{[class, name] => [[offset, def node or nil, certain,
|
|
9035
|
+
# (:alias)], ...]}`, ascending), in source order. An alias whose old name held a `def` at the alias copies it,
|
|
9036
|
+
# certain when that write and the alias are. Any other alias — an old name with no `def` of this file before
|
|
9037
|
+
# it (inherited, core, an attr reader, a sibling file's), or one it cannot read — adds a write with no `def`,
|
|
9038
|
+
# which leaves the slot contested ({ScopeIndexer#settle_alias_slot}). An alias whose new name it cannot read
|
|
9039
|
+
# adds one to every name of the class written before it. Returns the `[class, name]` keys it wrote.
|
|
9040
|
+
def copy_into(history)
|
|
9041
|
+
@events.sort_by { |event| event[3] }.flat_map do |class_name, new_name, old_name, at, possible|
|
|
9042
|
+
source = old_name && history[[class_name, old_name]]&.rfind { |write| write.first < at }
|
|
9043
|
+
write = if source&.[](1) then [at, source[1], source[2] && !possible, :alias].freeze
|
|
9044
|
+
else [at, nil, false, :alias].freeze
|
|
9045
|
+
end
|
|
9046
|
+
keys = new_name ? [[class_name, new_name]] : names_before(history, class_name, at)
|
|
9047
|
+
keys.each { |key| (history[key] ||= []).push(write).sort_by!(&:first) }
|
|
9048
|
+
end.uniq
|
|
8795
9049
|
end
|
|
8796
9050
|
|
|
8797
|
-
|
|
8798
|
-
|
|
9051
|
+
private
|
|
9052
|
+
|
|
9053
|
+
def names_before(history, class_name, at)
|
|
9054
|
+
history.filter_map { |key, writes| key if key.first == class_name && writes.first.first < at }
|
|
8799
9055
|
end
|
|
8800
9056
|
end
|
|
8801
9057
|
|
|
@@ -8857,12 +9113,19 @@ module Rigor
|
|
|
8857
9113
|
# `t` alias was the corpus case). Unlike AliasMethodNode a call's children can carry further
|
|
8858
9114
|
# class bodies (`Class.new do … end`), so the walk continues below it either way.
|
|
8859
9115
|
names = Source::AliasNames.alias_method_call_names(node)
|
|
8860
|
-
if names && !rec_prefix.empty? && !(singleton_cref && leaf_owner.nil?)
|
|
9116
|
+
if (names || unread_alias_method?(node)) && !rec_prefix.empty? && !(singleton_cref && leaf_owner.nil?)
|
|
8861
9117
|
record_alias_call_entry(accumulator, rec_prefix.join("::"), names, node)
|
|
8862
9118
|
end
|
|
8863
9119
|
false
|
|
8864
9120
|
end
|
|
8865
9121
|
|
|
9122
|
+
# Issue #2006 — an `alias_method` call on no receiver or `self` whose names {Source::AliasNames} cannot read
|
|
9123
|
+
# (`alias_method n, :to_s`, `self.alias_method :x, :y`, a splat): it still replaces a method, which
|
|
9124
|
+
# {AliasMap#copy_into} contests.
|
|
9125
|
+
def unread_alias_method?(node)
|
|
9126
|
+
node.name == :alias_method && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
9127
|
+
end
|
|
9128
|
+
|
|
8866
9129
|
# The rebinding-block call arm of {#collect_class_alias_map}: an anonymous meta-new call
|
|
8867
9130
|
# (`Class.new { … }` no write names) files its block's aliases nowhere, and an eval-family
|
|
8868
9131
|
# call binds them on the receiver — a nameable receiver re-anchors the owner while a
|
|
@@ -8953,20 +9216,32 @@ module Rigor
|
|
|
8953
9216
|
end
|
|
8954
9217
|
|
|
8955
9218
|
def record_alias_call_entry(accumulator, class_name, names, node)
|
|
9219
|
+
if names.nil?
|
|
9220
|
+
# Issue #2006 — an unread `alias_method` maps nothing, but still replaces a method ({AliasMap#copy_into}).
|
|
9221
|
+
new_name = Source::AliasNames.literal_name(node.arguments&.arguments&.first)
|
|
9222
|
+
accumulator.wrote(class_name, new_name, nil, node) if accumulator.is_a?(AliasMap)
|
|
9223
|
+
return
|
|
9224
|
+
end
|
|
9225
|
+
|
|
8956
9226
|
(accumulator[class_name] ||= {})[names.first] = names.last
|
|
8957
|
-
accumulator.wrote(class_name, names.first, node) if accumulator.is_a?(AliasMap)
|
|
9227
|
+
accumulator.wrote(class_name, names.first, names.last, node) if accumulator.is_a?(AliasMap)
|
|
8958
9228
|
end
|
|
8959
9229
|
|
|
8960
9230
|
def record_alias_map_entry(alias_node, qualified_prefix, accumulator)
|
|
8961
9231
|
return if qualified_prefix.empty?
|
|
8962
9232
|
|
|
8963
9233
|
names = Source::AliasNames.keyword_names(alias_node)
|
|
8964
|
-
return if names.nil?
|
|
8965
|
-
|
|
8966
9234
|
class_name = qualified_prefix.join("::")
|
|
9235
|
+
if names.nil?
|
|
9236
|
+
# Issue #2006 — an `alias` whose names are not plain symbols still replaces a method ({AliasMap#copy_into}).
|
|
9237
|
+
new_name = Source::AliasNames.symbol_name(alias_node.new_name)
|
|
9238
|
+
accumulator.wrote(class_name, new_name, nil, alias_node) if accumulator.is_a?(AliasMap)
|
|
9239
|
+
return
|
|
9240
|
+
end
|
|
9241
|
+
|
|
8967
9242
|
new_name, old_name = names
|
|
8968
9243
|
(accumulator[class_name] ||= {})[new_name] = old_name
|
|
8969
|
-
accumulator.wrote(class_name, new_name, alias_node) if accumulator.is_a?(AliasMap)
|
|
9244
|
+
accumulator.wrote(class_name, new_name, old_name, alias_node) if accumulator.is_a?(AliasMap)
|
|
8970
9245
|
end
|
|
8971
9246
|
|
|
8972
9247
|
def record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
@@ -9153,14 +9428,15 @@ module Rigor
|
|
|
9153
9428
|
# {#discovered_project_index_incremental}'s changed-file branch) yields the live def nodes the signature
|
|
9154
9429
|
# reads their parameter structure from. Issue #1120 — and each file's own refinement table, which the merged
|
|
9155
9430
|
# def-index cannot attribute back to a file, so the session can diff it against the file's seed bundle.
|
|
9156
|
-
# Issue #1459 — and each file's {#defined_method_names}, diffed the same way.
|
|
9157
|
-
#
|
|
9431
|
+
# Issue #1459 — and each file's {#defined_method_names}, diffed the same way. Issue #2134 — and each file's
|
|
9432
|
+
# {#visibility_entries}. Issue #2158 — and each file's `global_write_census`.
|
|
9433
|
+
# @return `{ def_index:, code_fingerprints:, declaration_signatures:, refinements:, defined_method_names:,
|
|
9434
|
+
# visibility_entries:, global_write_census: }`.
|
|
9158
9435
|
def scan_summary_for_paths(paths, buffer: nil)
|
|
9159
9436
|
acc = new_def_index_accumulator
|
|
9160
9437
|
code_fingerprints = {}
|
|
9161
9438
|
declaration_signatures = {}
|
|
9162
|
-
|
|
9163
|
-
defined_method_names = {}
|
|
9439
|
+
per_file = { refinements: {}, defined_method_names: {}, visibility_entries: {}, global_write_census: {} }
|
|
9164
9440
|
paths.each do |path|
|
|
9165
9441
|
physical = buffer ? buffer.resolve(path) : path
|
|
9166
9442
|
source = File.read(physical)
|
|
@@ -9169,16 +9445,21 @@ module Rigor
|
|
|
9169
9445
|
fold_file_index(acc, file_index)
|
|
9170
9446
|
code_fingerprints[path] = code_fingerprint(source, parsed.comments)
|
|
9171
9447
|
declaration_signatures[path] = declaration_signature(file_index)
|
|
9172
|
-
|
|
9173
|
-
defined_method_names[path] = defined_method_names(file_index)
|
|
9448
|
+
file_summary(file_index).each { |key, table| per_file[key][path] = table unless table.nil? }
|
|
9174
9449
|
rescue DeclarationWalk::ContractError
|
|
9175
9450
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
9176
9451
|
rescue StandardError
|
|
9177
9452
|
next
|
|
9178
9453
|
end
|
|
9179
9454
|
{ def_index: finalize_def_index(acc), code_fingerprints: code_fingerprints,
|
|
9180
|
-
declaration_signatures: declaration_signatures,
|
|
9181
|
-
|
|
9455
|
+
declaration_signatures: declaration_signatures, **per_file }
|
|
9456
|
+
end
|
|
9457
|
+
|
|
9458
|
+
# One file's tables {#scan_summary_for_paths} hands the session to diff against the file's seed bundle (nil for
|
|
9459
|
+
# a table the file does not have).
|
|
9460
|
+
def file_summary(file_index)
|
|
9461
|
+
{ refinements: file_index[:refinements], defined_method_names: defined_method_names(file_index),
|
|
9462
|
+
visibility_entries: visibility_entries(file_index), global_write_census: file_index[:global_write_census] }
|
|
9182
9463
|
end
|
|
9183
9464
|
|
|
9184
9465
|
# Issue #1459 — every method name one file's index defines, on any class or module and either side: the keys of
|
|
@@ -9192,9 +9473,39 @@ module Rigor
|
|
|
9192
9473
|
%i[methods def_nodes singleton_def_nodes].each do |key|
|
|
9193
9474
|
file_index[key]&.each_value { |table| names.merge(table.keys) }
|
|
9194
9475
|
end
|
|
9476
|
+
# Issue #2184 — a named deferred-range row with no side (a `def` in a block, a `define_singleton_method`) is a
|
|
9477
|
+
# definition no table above holds.
|
|
9478
|
+
file_index[:deferred_ranges]&.each_value { |rows| rows.each { |row| names << row[2] if row[2] && row[3].nil? } }
|
|
9195
9479
|
names.freeze
|
|
9196
9480
|
end
|
|
9197
9481
|
|
|
9482
|
+
# Issue #2134 — one file's contributions to the visibility table and its contested sibling, as a Set of
|
|
9483
|
+
# `[class, slot, visibility]` entries and `[:contested, class, slot]` contests, where a slot is a method name or
|
|
9484
|
+
# `[:singleton, name]`. Takes a live file index or a seed bundle, which hold the same keys.
|
|
9485
|
+
#
|
|
9486
|
+
# Issue #2161 — and a `[:module_function, owner, name]` entry for each of the file's deferred-range rows that
|
|
9487
|
+
# `DefinerResolution.module_function_row?` counts (a named def row whose kind is not `:instance`): a module
|
|
9488
|
+
# function's singleton copy is public whatever its instance slot says, so the row is a visibility fact the
|
|
9489
|
+
# table itself does not hold.
|
|
9490
|
+
def visibility_entries(file_index)
|
|
9491
|
+
entries = Set.new
|
|
9492
|
+
file_index[:method_visibilities]&.each do |class_name, table|
|
|
9493
|
+
table.each { |slot, visibility| entries << [class_name, slot, visibility] }
|
|
9494
|
+
end
|
|
9495
|
+
contested = (file_index[:siblings] || {})[:contested_discovered_method_visibilities]
|
|
9496
|
+
contested&.each { |(class_name, slot)| entries << [:contested, class_name, slot] }
|
|
9497
|
+
add_module_function_entries(file_index[:deferred_ranges], entries)
|
|
9498
|
+
entries.freeze
|
|
9499
|
+
end
|
|
9500
|
+
|
|
9501
|
+
def add_module_function_entries(deferred_ranges, entries)
|
|
9502
|
+
deferred_ranges&.each_value do |rows|
|
|
9503
|
+
rows.each do |(_start, _finish, name, kind, owner)|
|
|
9504
|
+
entries << [:module_function, owner, name] if name && owner && kind != :instance
|
|
9505
|
+
end
|
|
9506
|
+
end
|
|
9507
|
+
end
|
|
9508
|
+
|
|
9198
9509
|
# ADR-89 WD1 — a per-file digest of every cross-file DECLARATION surface an ancestry / file-level
|
|
9199
9510
|
# dependent consumes, EXCLUDING method bodies (which only symbol dependents consume, via the ADR-46
|
|
9200
9511
|
# symbol edges the change-detection already fingerprints per method). A body edit that touches no
|
|
@@ -9412,14 +9723,21 @@ module Rigor
|
|
|
9412
9723
|
# Error degradation is identical to the two independent loops it replaces: a read / parse failure (the
|
|
9413
9724
|
# rescue's real target) contributes nothing to either table. The subset-scoped callers
|
|
9414
9725
|
# ({IncrementalSession}, `coverage --protection`) keep calling the individual methods unchanged.
|
|
9415
|
-
|
|
9726
|
+
#
|
|
9727
|
+
# @param uncertain — issue #2021: the paths whose every method contribution is `possible`
|
|
9728
|
+
# ({Certainty.every_contribution}).
|
|
9729
|
+
def discovered_project_index_for_paths(paths, buffer: nil, uncertain: EMPTY_PATH_SET)
|
|
9416
9730
|
classes = {}
|
|
9417
9731
|
acc = new_def_index_accumulator
|
|
9418
9732
|
paths.each do |path|
|
|
9419
9733
|
physical = buffer ? buffer.resolve(path) : path
|
|
9420
9734
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
9421
9735
|
collect_class_decls(root, [], classes)
|
|
9422
|
-
|
|
9736
|
+
if uncertain.include?(path)
|
|
9737
|
+
fold_file_index(acc, without_written_slots(build_file_index(path, root, uncertain: true), acc))
|
|
9738
|
+
else
|
|
9739
|
+
accumulate_project_index(acc, path, root)
|
|
9740
|
+
end
|
|
9423
9741
|
rescue DeclarationWalk::ContractError
|
|
9424
9742
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
9425
9743
|
rescue StandardError
|
|
@@ -9467,14 +9785,19 @@ module Rigor
|
|
|
9467
9785
|
# @param paths — project file paths, in canonical order.
|
|
9468
9786
|
# @param seed_bundles — the prior run's per-file bundles, keyed by logical path.
|
|
9469
9787
|
# @return `{ classes:, def_index:, bundles: }`.
|
|
9470
|
-
|
|
9788
|
+
# @param uncertain — issue #2021: as {#discovered_project_index_for_paths}. Such a file is always re-walked and
|
|
9789
|
+
# keeps no bundle, since a bundle does not record that its contributions were all possible.
|
|
9790
|
+
def discovered_project_index_incremental(paths, seed_bundles:, buffer: nil, uncertain: EMPTY_PATH_SET) # rubocop:disable Metrics/AbcSize
|
|
9471
9791
|
classes = {}
|
|
9472
9792
|
acc = new_def_index_accumulator
|
|
9473
9793
|
bundles = {}
|
|
9474
9794
|
paths.each do |path|
|
|
9475
9795
|
physical = buffer ? buffer.resolve(path) : path
|
|
9476
9796
|
digest = Cache::FileDigest.hexdigest(physical)
|
|
9477
|
-
|
|
9797
|
+
walked = uncertain.include?(path)
|
|
9798
|
+
# A walked file neither reuses nor stores a bundle. Both are guards no spec separates: no bundle is ever
|
|
9799
|
+
# stored for a walked file, so the reuse guard matters only if a path changes role within one session.
|
|
9800
|
+
cached = walked ? nil : seed_bundles[path]
|
|
9478
9801
|
if cached && cached[:digest] == digest
|
|
9479
9802
|
bundles[path] = cached
|
|
9480
9803
|
classes.merge!(cached[:classes])
|
|
@@ -9483,13 +9806,13 @@ module Rigor
|
|
|
9483
9806
|
source = File.read(physical)
|
|
9484
9807
|
parsed = Prism.parse(source, filepath: path)
|
|
9485
9808
|
root = parsed.value
|
|
9486
|
-
file_index = build_file_index(path, root)
|
|
9809
|
+
file_index = build_file_index(path, root, uncertain: walked)
|
|
9487
9810
|
file_classes = {}
|
|
9488
9811
|
collect_class_decls(root, [], file_classes)
|
|
9489
|
-
|
|
9490
|
-
|
|
9812
|
+
fingerprint = code_fingerprint(source, parsed.comments)
|
|
9813
|
+
bundles[path] = build_seed_bundle(file_index, file_classes, digest, fingerprint) unless walked
|
|
9491
9814
|
classes.merge!(file_classes)
|
|
9492
|
-
fold_file_index(acc, file_index)
|
|
9815
|
+
fold_file_index(acc, walked ? without_written_slots(file_index, acc) : file_index)
|
|
9493
9816
|
end
|
|
9494
9817
|
rescue DeclarationWalk::ContractError
|
|
9495
9818
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
@@ -9503,12 +9826,43 @@ module Rigor
|
|
|
9503
9826
|
# Builds ONE file's isolated def-index contribution (live `Prism::DefNode`s) by folding it into a fresh
|
|
9504
9827
|
# accumulator — recon Q2(a)'s "constructible in isolation" property. Shared by the cold-baseline bundle
|
|
9505
9828
|
# build and the incremental changed-file re-walk.
|
|
9506
|
-
def build_file_index(path, root)
|
|
9829
|
+
def build_file_index(path, root, uncertain: false)
|
|
9507
9830
|
file_acc = new_def_index_accumulator
|
|
9508
|
-
accumulate_project_index(file_acc, path, root)
|
|
9831
|
+
accumulate_project_index(file_acc, path, root, uncertain: uncertain)
|
|
9509
9832
|
file_acc
|
|
9510
9833
|
end
|
|
9511
9834
|
|
|
9835
|
+
# Issue #2021 — the single-valued slot tables an `uncertain` file writes, each with its contest sibling.
|
|
9836
|
+
UNCERTAIN_SLOT_TABLES = {
|
|
9837
|
+
def_nodes: :contested_discovered_def_nodes,
|
|
9838
|
+
singleton_def_nodes: :contested_discovered_singleton_def_nodes,
|
|
9839
|
+
method_visibilities: :contested_discovered_method_visibilities,
|
|
9840
|
+
parameter_envelopes: :contested_discovered_parameter_envelopes
|
|
9841
|
+
}.freeze
|
|
9842
|
+
|
|
9843
|
+
# Issue #2021 — an `uncertain` file's isolated index without the slots `acc` already holds. The walked files fold
|
|
9844
|
+
# after every other discovered file, so `acc` holds what the configured (certain) files wrote: a walked reopening
|
|
9845
|
+
# of such a class adds the names it defines (existence) and leaves each written def node, visibility and
|
|
9846
|
+
# envelope, and its contest, as the certain writer left it. Without this, the walked file's possible write was
|
|
9847
|
+
# the slot's last and contested it, and `rigor check lib/user.rb plug/x.rb` withheld what `rigor check
|
|
9848
|
+
# lib/user.rb` reports. A slot no earlier file wrote is taken, contested.
|
|
9849
|
+
def without_written_slots(file_index, acc)
|
|
9850
|
+
siblings = (file_index[:siblings] || {}).dup
|
|
9851
|
+
tables = UNCERTAIN_SLOT_TABLES.to_h do |table, sibling|
|
|
9852
|
+
written = acc[table]
|
|
9853
|
+
kept = {}
|
|
9854
|
+
(file_index[table] || {}).each do |class_name, entries|
|
|
9855
|
+
sitting = written[class_name]
|
|
9856
|
+
entries = entries.reject { |key, _| sitting.key?(key) } if sitting
|
|
9857
|
+
kept[class_name] = entries unless entries.empty?
|
|
9858
|
+
end
|
|
9859
|
+
contests = siblings[sibling]
|
|
9860
|
+
siblings[sibling] = contests.select { |class_name, key| kept[class_name]&.key?(key) }.to_set if contests
|
|
9861
|
+
[table, kept]
|
|
9862
|
+
end
|
|
9863
|
+
file_index.merge(tables).merge(siblings: siblings)
|
|
9864
|
+
end
|
|
9865
|
+
|
|
9512
9866
|
# ADR-85 WD2 — folds a single file's isolated def-index contribution into the cross-file accumulator,
|
|
9513
9867
|
# applying EXACTLY the merge semantics {#accumulate_project_index} applies incrementally (def_nodes
|
|
9514
9868
|
# later-wins, def_sources first-wins, includes / class_sources accumulate, everything else later-wins).
|
|
@@ -9645,9 +9999,14 @@ module Rigor
|
|
|
9645
9999
|
# Issue #1984 — a singleton slot (`[:singleton, name]`) does not follow its writer: file order is not load
|
|
9646
10000
|
# order, so two files that give it different visibilities (`private_class_method :x` here,
|
|
9647
10001
|
# `class << self; public :x; end` in a reopening) leave it contested, and a contest on it stays whichever
|
|
9648
|
-
# file folds later. The instance slots fold as before
|
|
10002
|
+
# file folds later. The instance slots fold as before, except (issue #2013) one whose contest carries a
|
|
10003
|
+
# replacement's mark ({ScopeIndexer.replaced_visibility_mark}): a `define_method` or an alias in one file and a
|
|
10004
|
+
# `private def` in another leave the visibility to load order, so that contest stays too.
|
|
9649
10005
|
def fold_visibility_contests(sitting, recorded, file_slots, file_contested)
|
|
9650
|
-
stale = sitting.select
|
|
10006
|
+
stale = sitting.select do |class_name, name|
|
|
10007
|
+
!name.is_a?(Array) && file_slots[class_name]&.key?(name) &&
|
|
10008
|
+
!sitting.include?([class_name, ScopeIndexer.replaced_visibility_mark(name)])
|
|
10009
|
+
end
|
|
9651
10010
|
disagreeing = singleton_visibility_disagreements(recorded, file_slots)
|
|
9652
10011
|
return sitting if stale.empty? && file_contested.empty? && disagreeing.empty?
|
|
9653
10012
|
|
|
@@ -10070,12 +10429,12 @@ module Rigor
|
|
|
10070
10429
|
# Folds one file's class-keyed indexes into the cross-file accumulator. `method_visibilities` (ADR-35) is
|
|
10071
10430
|
# collected here so the override-visibility-reduced rule can read an ancestor's visibility declared in a sibling
|
|
10072
10431
|
# file.
|
|
10073
|
-
def accumulate_project_index(acc, path, root)
|
|
10432
|
+
def accumulate_project_index(acc, path, root, uncertain: false)
|
|
10074
10433
|
# One combined descent yields both the methods existence table and the def-node table; the latter is also
|
|
10075
10434
|
# consumed by `record_class_sources`, so a def-dense file is walked once here instead of three times (methods +
|
|
10076
10435
|
# def-nodes ×2). See {#build_methods_and_def_nodes}.
|
|
10077
10436
|
file_methods, file_def_nodes, file_envelopes, file_refinements, file_singleton_def_nodes, certainty =
|
|
10078
|
-
walk_def_contributions(acc, path, root)
|
|
10437
|
+
walk_def_contributions(acc, path, root, uncertain: uncertain)
|
|
10079
10438
|
merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
|
|
10080
10439
|
fold_parameter_envelopes(acc, file_envelopes)
|
|
10081
10440
|
fold_refinements(acc, file_refinements)
|
|
@@ -10102,8 +10461,8 @@ module Rigor
|
|
|
10102
10461
|
# file's siblings folded into the accumulator BEFORE its members join it (ADR-119 WD3: the `possible` rule
|
|
10103
10462
|
# reads the accumulator's member as it stood). Returns `[methods, def_nodes, envelopes, refinements,
|
|
10104
10463
|
# singleton_def_nodes, certainty]`, the classification passed on for the visibility walk.
|
|
10105
|
-
def walk_def_contributions(acc, path, root)
|
|
10106
|
-
certainty = Certainty.possible_nodes(root)
|
|
10464
|
+
def walk_def_contributions(acc, path, root, uncertain: false)
|
|
10465
|
+
certainty = uncertain ? Certainty.every_contribution(root) : Certainty.possible_nodes(root)
|
|
10107
10466
|
file_methods, file_def_nodes, file_envelopes, file_refinements, file_siblings =
|
|
10108
10467
|
build_methods_and_def_nodes(root, path, certainty: certainty)
|
|
10109
10468
|
file_singleton_def_nodes, file_singleton_contested = build_singleton_def_tables(root, certainty: certainty)
|
|
@@ -10149,7 +10508,16 @@ module Rigor
|
|
|
10149
10508
|
census.writes.each do |name, descriptor|
|
|
10150
10509
|
(acc[:constant_writes][name] ||= {})[path] = descriptor
|
|
10151
10510
|
end
|
|
10152
|
-
acc[:global_write_census].merge(census.write_census.census)
|
|
10511
|
+
acc[:global_write_census].merge(attribute_own_writes(census.write_census.census, path))
|
|
10512
|
+
end
|
|
10513
|
+
|
|
10514
|
+
# #1154 — tags each `[:own_write, class, ivar]` entry with the file that records it, so a reader of the merged
|
|
10515
|
+
# census can tell a second file writing the same ivar of the same class (`UnseenIvarWriters`).
|
|
10516
|
+
# Issue #2079 — and each `[:own_write_type, class, ivar, value]` entry, which {IvarContributions} reads.
|
|
10517
|
+
def attribute_own_writes(census, path)
|
|
10518
|
+
return census unless census.any? { |entry| PATH_TAGGED_KINDS.include?(entry.first) }
|
|
10519
|
+
|
|
10520
|
+
census.to_set { |entry| PATH_TAGGED_KINDS.include?(entry.first) ? [*entry, path].freeze : entry }
|
|
10153
10521
|
end
|
|
10154
10522
|
|
|
10155
10523
|
# Folds one file's Data + Struct member-layout tables into the cross-file accumulator (kept out of
|
|
@@ -10272,8 +10640,12 @@ module Rigor
|
|
|
10272
10640
|
singleton_cref: false)
|
|
10273
10641
|
return unless node.is_a?(Prism::Node)
|
|
10274
10642
|
|
|
10643
|
+
# #1154 — the class whose instance `def`s the ivar-writer census credits; none under `class << self`.
|
|
10644
|
+
ivar_owner = census_ivar_owner(qualified_prefix, self_owner, singleton_cref)
|
|
10275
10645
|
case node
|
|
10276
10646
|
when Prism::SingletonClassNode
|
|
10647
|
+
# #1154 — a `class << self` inside an instance `def` opens the instance's singleton class.
|
|
10648
|
+
tables.write_census.visit(node, top_level: qualified_prefix.empty?, owner: ivar_owner)
|
|
10277
10649
|
return walk_census_singleton_class(node, qualified_prefix, tables, self_owner,
|
|
10278
10650
|
meta_owner, singleton_cref)
|
|
10279
10651
|
when Prism::ClassNode, Prism::ModuleNode
|
|
@@ -10281,7 +10653,7 @@ module Rigor
|
|
|
10281
10653
|
singleton_cref)
|
|
10282
10654
|
else
|
|
10283
10655
|
census_constant_write(node, qualified_prefix, tables, self_owner, singleton_cref: singleton_cref)
|
|
10284
|
-
tables.write_census.visit(node, top_level: qualified_prefix.empty
|
|
10656
|
+
tables.write_census.visit(node, top_level: qualified_prefix.empty?, owner: ivar_owner)
|
|
10285
10657
|
end
|
|
10286
10658
|
|
|
10287
10659
|
rebound = rebound_block_self(node, qualified_prefix, nil, meta_owner, self_owner)
|
|
@@ -10313,6 +10685,16 @@ module Rigor
|
|
|
10313
10685
|
end
|
|
10314
10686
|
end
|
|
10315
10687
|
|
|
10688
|
+
# #1154 — the class whose instance `def`s the ivar-writer census credits: the class a `Struct.new` /
|
|
10689
|
+
# `Data.define` / `Class.new` constant or a `K.class_eval` block opens (`self_owner`), else the lexical class;
|
|
10690
|
+
# none under `class << self` or an opaque block. The prefix Array is joined only when an entry is recorded.
|
|
10691
|
+
def census_ivar_owner(qualified_prefix, self_owner, singleton_cref)
|
|
10692
|
+
return self_owner if self_owner.is_a?(String)
|
|
10693
|
+
return nil if self_owner || singleton_cref || qualified_prefix.empty?
|
|
10694
|
+
|
|
10695
|
+
qualified_prefix
|
|
10696
|
+
end
|
|
10697
|
+
|
|
10316
10698
|
# Under an unnameable cref a bare/`self::` header pushes `#<singleton>::Name` — still
|
|
10317
10699
|
# unnameable — so the body keeps the OPAQUE self and the cref flag; nameable
|
|
10318
10700
|
# headers re-anchor at a real cref.
|
|
@@ -10568,7 +10950,7 @@ module Rigor
|
|
|
10568
10950
|
# A {DYNAMIC_TARGET_PREFIX} key is not a name and never publishes; it is expanded by
|
|
10569
10951
|
# {#apply_dynamic_target_writes} into a retraction over the names it could have written.
|
|
10570
10952
|
#
|
|
10571
|
-
# @return
|
|
10953
|
+
# @return the value table and the table of source paths per name.
|
|
10572
10954
|
def finalize_constant_writes(writes)
|
|
10573
10955
|
values = {}
|
|
10574
10956
|
sources = {}
|
|
@@ -11400,6 +11782,7 @@ module Rigor
|
|
|
11400
11782
|
table[node] = parent_scope
|
|
11401
11783
|
parent_scope
|
|
11402
11784
|
end
|
|
11785
|
+
evaluate_unentered_flow(node, table, current_scope)
|
|
11403
11786
|
|
|
11404
11787
|
case node
|
|
11405
11788
|
when Prism::IfNode
|
|
@@ -11415,7 +11798,7 @@ module Rigor
|
|
|
11415
11798
|
child_scope = entered ? current_scope : closure_scope(node, current_scope)
|
|
11416
11799
|
node.rigor_each_child { |child| propagate(child, table, child_scope) }
|
|
11417
11800
|
when Prism::CallNode
|
|
11418
|
-
propagate_call(node, table, current_scope)
|
|
11801
|
+
propagate_call(node, table, current_scope, recorded: recorded)
|
|
11419
11802
|
when Prism::RescueNode, Prism::EnsureNode
|
|
11420
11803
|
# Issue #1360 — a clause the evaluator did not enter (a `begin` in a value position) reads `$!`, `$@` and
|
|
11421
11804
|
# `$?` unbound, not the enclosing clause's; one it entered keeps what it recorded.
|
|
@@ -11429,6 +11812,84 @@ module Rigor
|
|
|
11429
11812
|
end
|
|
11430
11813
|
end
|
|
11431
11814
|
|
|
11815
|
+
# Issues #2196 and #2056 — the constructs whose parts run under a narrowing their own condition or left operand
|
|
11816
|
+
# makes, keyed to the part {StatementEvaluator} records first when it enters one. Issue #2214 — a `&&=` on a
|
|
11817
|
+
# variable runs its value under the variable's truthy narrowing (`[c &&= c + 1]`).
|
|
11818
|
+
UNENTERED_FLOW_ENTRY = {
|
|
11819
|
+
Prism::AndNode => :left, Prism::OrNode => :left,
|
|
11820
|
+
Prism::IfNode => :predicate, Prism::UnlessNode => :predicate,
|
|
11821
|
+
Prism::WhileNode => :predicate, Prism::UntilNode => :predicate,
|
|
11822
|
+
Prism::LocalVariableAndWriteNode => :value, Prism::InstanceVariableAndWriteNode => :value,
|
|
11823
|
+
Prism::ClassVariableAndWriteNode => :value, Prism::GlobalVariableAndWriteNode => :value,
|
|
11824
|
+
Prism::CaseNode => :predicate
|
|
11825
|
+
}.freeze
|
|
11826
|
+
private_constant :UNENTERED_FLOW_ENTRY
|
|
11827
|
+
|
|
11828
|
+
UNENTERED_EVALUATOR_KEY = :rigor_scope_index_unentered_evaluator
|
|
11829
|
+
private_constant :UNENTERED_EVALUATOR_KEY
|
|
11830
|
+
|
|
11831
|
+
# Issues #2196 and #2056 — an `&&` / `||` / `and` / `or`, an `if` / `unless` / ternary or a `while` / `until`
|
|
11832
|
+
# in a value position (an array element, a hash value, a call argument, an interpolation, a multi-value
|
|
11833
|
+
# `return`) is typed by `ExpressionTyper` and never entered, so no part of it was recorded and this walk handed
|
|
11834
|
+
# every part one scope. A walk that narrows by the condition's edges cannot follow a write inside the narrowed
|
|
11835
|
+
# part (`[c && (c = c.size; c.even?)]`, `[x || (set!; @x.upcase)]`) nor one inside a block in an earlier
|
|
11836
|
+
# conjunct (`[x.nil? && [1].each { x = "d" } && x.upcase]`). Such a construct is evaluated here instead, from
|
|
11837
|
+
# the scope this walk reached it with, under the index's own recorder, so each part is recorded with the scope
|
|
11838
|
+
# the statement form records it with. What it does not visit (a dead arm) is walked as before.
|
|
11839
|
+
def evaluate_unentered_flow(node, table, current_scope)
|
|
11840
|
+
entry = UNENTERED_FLOW_ENTRY[node.class]
|
|
11841
|
+
return if entry.nil?
|
|
11842
|
+
|
|
11843
|
+
# An entered construct is already recorded; skipping it is a performance guard, not a behaviour. A `case`
|
|
11844
|
+
# without a subject records its first `when` test first.
|
|
11845
|
+
part = node.public_send(entry) || (node.conditions.first&.conditions&.first if node.is_a?(Prism::CaseNode))
|
|
11846
|
+
return if part.nil? || table.key?(part)
|
|
11847
|
+
|
|
11848
|
+
evaluate = Thread.current[UNENTERED_EVALUATOR_KEY]
|
|
11849
|
+
evaluate&.call(node, current_scope, code_body_nodes(node, current_scope.source_path))
|
|
11850
|
+
end
|
|
11851
|
+
|
|
11852
|
+
# The evaluation of an unentered construct starts with no class frame and no lexical nesting, which only a code
|
|
11853
|
+
# body observes: a `def`, a `class` / `module` / `class << …` body, and the block of a class-creating call
|
|
11854
|
+
# (`Class.new { … }`, `Struct.new`, `Data.define`, `Module.new`) or of a `refine`, whose `def`s and constants
|
|
11855
|
+
# resolve against the class the walk tracks and the evaluation does not. Every node in one is left out of the
|
|
11856
|
+
# recording, so the walk reaches it unrecorded and handles it as it did before #2196 ({#unentered_block_entry},
|
|
11857
|
+
# {#record_class_body_defs}).
|
|
11858
|
+
# Answers nil, allocating nothing, for a construct holding no code body: the common case.
|
|
11859
|
+
def code_body_nodes(node, source_path, acc = nil, inside: false)
|
|
11860
|
+
inside ||= CODE_BODY_NODES.include?(node.class)
|
|
11861
|
+
(acc ||= Set.new.compare_by_identity) << node if inside
|
|
11862
|
+
if !inside && node.is_a?(Prism::CallNode) && class_body_block_call?(node, source_path)
|
|
11863
|
+
acc = code_body_nodes(node.block, source_path, acc, inside: true)
|
|
11864
|
+
node.rigor_each_child do |child|
|
|
11865
|
+
acc = code_body_nodes(child, source_path, acc) unless child.equal?(node.block)
|
|
11866
|
+
end
|
|
11867
|
+
return acc
|
|
11868
|
+
end
|
|
11869
|
+
node.rigor_each_child { |child| acc = code_body_nodes(child, source_path, acc, inside: inside) }
|
|
11870
|
+
acc
|
|
11871
|
+
end
|
|
11872
|
+
|
|
11873
|
+
CODE_BODY_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
11874
|
+
private_constant :CODE_BODY_NODES
|
|
11875
|
+
|
|
11876
|
+
def class_body_block_call?(node, source_path)
|
|
11877
|
+
node.block.is_a?(Prism::BlockNode) &&
|
|
11878
|
+
(!AnonymousMetaClass.name_for(node, source_path).nil? || !refine_target(node).nil?)
|
|
11879
|
+
end
|
|
11880
|
+
|
|
11881
|
+
# The evaluation {#evaluate_unentered_flow} runs for the walk of one {#index}, installed for that walk only.
|
|
11882
|
+
# Index walks nest (a rule may re-index a file while another file's walk is suspended), so the outer one is
|
|
11883
|
+
# restored on exit. A `propagate` called directly while an outer index walk is running would record into the
|
|
11884
|
+
# OUTER index's table through it; nothing outside {#index} calls `propagate`.
|
|
11885
|
+
def with_unentered_evaluator(evaluate)
|
|
11886
|
+
outer = Thread.current[UNENTERED_EVALUATOR_KEY]
|
|
11887
|
+
Thread.current[UNENTERED_EVALUATOR_KEY] = evaluate
|
|
11888
|
+
yield
|
|
11889
|
+
ensure
|
|
11890
|
+
Thread.current[UNENTERED_EVALUATOR_KEY] = outer
|
|
11891
|
+
end
|
|
11892
|
+
|
|
11432
11893
|
SELF_OPENING_BODIES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
11433
11894
|
private_constant :SELF_OPENING_BODIES
|
|
11434
11895
|
|
|
@@ -11463,11 +11924,11 @@ module Rigor
|
|
|
11463
11924
|
# argument one scope and threads none, so arguments that write keep the call's: an earlier one may rebind the
|
|
11464
11925
|
# receiver a later one reads (`return y&.concat((y = nil).to_s, y.upcase)`, where the evaluator threads no
|
|
11465
11926
|
# operand).
|
|
11466
|
-
def propagate_call(node, table, current_scope)
|
|
11927
|
+
def propagate_call(node, table, current_scope, recorded: false)
|
|
11467
11928
|
block = node.block
|
|
11468
11929
|
operands =
|
|
11469
11930
|
OperandEffects.any?(node.arguments) ? current_scope : Narrowing.safe_navigation_scope(node, current_scope)
|
|
11470
|
-
entry = unentered_block_entry(node, block, table, current_scope)
|
|
11931
|
+
entry = unentered_block_entry(node, block, table, current_scope, call_recorded: recorded)
|
|
11471
11932
|
if entry.equal?(current_scope) && operands.equal?(current_scope)
|
|
11472
11933
|
node.rigor_each_child { |child| propagate(child, table, current_scope) }
|
|
11473
11934
|
return
|
|
@@ -11491,7 +11952,7 @@ module Rigor
|
|
|
11491
11952
|
node.rigor_each_child { |child| propagate(child, table, child_scope) }
|
|
11492
11953
|
end
|
|
11493
11954
|
|
|
11494
|
-
def unentered_block_entry(node, block, table, current_scope)
|
|
11955
|
+
def unentered_block_entry(node, block, table, current_scope, call_recorded: false)
|
|
11495
11956
|
return current_scope unless block.is_a?(Prism::BlockNode) && !table.key?(block)
|
|
11496
11957
|
|
|
11497
11958
|
narrowing = unentered_block_narrowing(node, current_scope)
|
|
@@ -11504,6 +11965,12 @@ module Rigor
|
|
|
11504
11965
|
else
|
|
11505
11966
|
LastLine.block_entry(FreshFrameBlocks.closure_entry(current_scope, block, node), block, node)
|
|
11506
11967
|
end
|
|
11968
|
+
# Issue #2196 — a class-body block whose call the evaluator recorded but did not enter (one inside a construct
|
|
11969
|
+
# {#evaluate_unentered_flow} evaluated, or a later operand) starts from the scope the call was evaluated in,
|
|
11970
|
+
# which may narrow a captured local the body writes; this walk runs the body's statements from one scope, so
|
|
11971
|
+
# such a local reads `Dynamic[top]` in it: `[c && Class.new { c = c.size; c.even? }]`. A block whose call was
|
|
11972
|
+
# not recorded keeps the enclosing scope as before.
|
|
11973
|
+
entry = widen_condition_writes(block, entry) if call_recorded && class_body_block_call?(node, entry.source_path)
|
|
11507
11974
|
anonymous = AnonymousMetaClass.name_for(node, current_scope.source_path)
|
|
11508
11975
|
return unentered_meta_class_body_entry(block, entry, anonymous, table) if anonymous
|
|
11509
11976
|
|
|
@@ -11690,7 +12157,8 @@ module Rigor
|
|
|
11690
12157
|
# A test that calls a method may rebind an instance-variable or global subject before the body reads it
|
|
11691
12158
|
# (`when String then 1 when reset! then @x.upcase`, where `reset!` writes `@x`), and the walk cannot tell. From
|
|
11692
12159
|
# the first such test on, the subject reads `Dynamic[top]` on both edges ({#forget_called_subject}). The
|
|
11693
|
-
# evaluator
|
|
12160
|
+
# evaluator forgets a global subject there too (issue #2211). A `case` / `when` the evaluator did not enter is
|
|
12161
|
+
# evaluated ({#evaluate_unentered_flow}); this walk handles only a clause that evaluation does not visit.
|
|
11694
12162
|
def propagate_subject_case(node, table, current_scope)
|
|
11695
12163
|
subject = node.predicate
|
|
11696
12164
|
propagate(subject, table, current_scope)
|