rigortype 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -2
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
- data/docs/handbook/01-getting-started.md +6 -3
- data/docs/handbook/11-sig-gen.md +85 -3
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +455 -113
- data/docs/manual/03-configuration.md +76 -5
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +21 -2
- data/docs/manual/10-mcp-server.md +38 -2
- data/docs/manual/12-caching.md +4 -3
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +32 -15
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/baseline.rb +24 -27
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
- data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
- data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules.rb +720 -141
- data/lib/rigor/analysis/crash_signature.rb +45 -3
- data/lib/rigor/analysis/dependency_recorder.rb +237 -80
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/edit_replay.rb +93 -6
- data/lib/rigor/analysis/fact_store.rb +3 -3
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
- data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
- data/lib/rigor/analysis/incremental_session.rb +306 -53
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +39 -3
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
- data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
- data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
- data/lib/rigor/analysis/runner.rb +298 -109
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
- data/lib/rigor/analysis/template_unit_positions.rb +2 -3
- data/lib/rigor/analysis/template_units.rb +9 -1
- data/lib/rigor/analysis/worker_session.rb +65 -71
- data/lib/rigor/bleeding_edge.rb +22 -10
- data/lib/rigor/builtins/imported_refinements.rb +6 -8
- data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
- data/lib/rigor/builtins/regex_refinement.rb +4 -6
- data/lib/rigor/cache/descriptor.rb +6 -5
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +95 -17
- data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
- data/lib/rigor/cache/rbs_constant_table.rb +1 -1
- data/lib/rigor/cache/rbs_descriptor.rb +90 -4
- data/lib/rigor/cache/rbs_environment.rb +8 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
- data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
- data/lib/rigor/cache/store.rb +11 -10
- data/lib/rigor/ci_detector.rb +1 -1
- data/lib/rigor/cli/annotate_command.rb +24 -19
- data/lib/rigor/cli/baseline_command.rb +160 -57
- data/lib/rigor/cli/catalogue.rb +256 -0
- data/lib/rigor/cli/check_command.rb +112 -104
- data/lib/rigor/cli/check_runner_factory.rb +2 -2
- data/lib/rigor/cli/command.rb +15 -1
- data/lib/rigor/cli/coverage_command.rb +21 -8
- data/lib/rigor/cli/coverage_mutation.rb +5 -7
- data/lib/rigor/cli/coverage_renderer.rb +2 -1
- data/lib/rigor/cli/coverage_scan.rb +3 -5
- data/lib/rigor/cli/diff_command.rb +25 -20
- data/lib/rigor/cli/docs_command.rb +1 -2
- data/lib/rigor/cli/doctor_command.rb +12 -12
- data/lib/rigor/cli/effects_command.rb +50 -40
- data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
- data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
- data/lib/rigor/cli/effects_renderer.rb +2 -1
- data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
- data/lib/rigor/cli/explain_command.rb +18 -8
- data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
- data/lib/rigor/cli/json_document.rb +41 -0
- data/lib/rigor/cli/lens_command.rb +29 -10
- data/lib/rigor/cli/lsp_command.rb +250 -23
- data/lib/rigor/cli/mcp_command.rb +12 -7
- data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
- data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
- data/lib/rigor/cli/options.rb +40 -1
- data/lib/rigor/cli/plugin_command.rb +93 -46
- data/lib/rigor/cli/plugins_command.rb +17 -18
- data/lib/rigor/cli/plugins_renderer.rb +10 -6
- data/lib/rigor/cli/protection_fork_scan.rb +2 -2
- data/lib/rigor/cli/protection_renderer.rb +2 -1
- data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
- data/lib/rigor/cli/sig_gen_command.rb +92 -27
- data/lib/rigor/cli/skill_command.rb +32 -3
- data/lib/rigor/cli/skill_deep_probe.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +14 -9
- data/lib/rigor/cli/triage_command.rb +10 -11
- data/lib/rigor/cli/triage_renderer.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +16 -11
- data/lib/rigor/cli/type_of_project_scope.rb +1 -4
- data/lib/rigor/cli/type_of_renderer.rb +4 -1
- data/lib/rigor/cli/type_scan_command.rb +14 -9
- data/lib/rigor/cli/type_scan_renderer.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +27 -17
- data/lib/rigor/cli/upgrade_command.rb +5 -0
- data/lib/rigor/cli.rb +214 -56
- data/lib/rigor/config_audit.rb +33 -19
- data/lib/rigor/configuration/severity_profile.rb +14 -5
- data/lib/rigor/configuration.rb +104 -35
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/effects/definition_context.rb +0 -2
- data/lib/rigor/effects/envelope.rb +1 -1
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/mutation_classifier.rb +4 -7
- data/lib/rigor/effects/propagator.rb +1 -1
- data/lib/rigor/effects/registry.rb +2 -2
- data/lib/rigor/effects/snapshot.rb +1 -1
- data/lib/rigor/effects/taint_cause.rb +0 -2
- data/lib/rigor/effects/unit_scan.rb +9 -13
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
- data/lib/rigor/environment/class_registry.rb +3 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/core_root.rb +77 -0
- data/lib/rigor/environment/default_libraries.rb +18 -1
- data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
- data/lib/rigor/environment/gated_signature_guard.rb +1 -1
- data/lib/rigor/environment/gated_signature_plan.rb +5 -2
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/lockfile_resolver.rb +28 -12
- data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
- data/lib/rigor/environment/member_consistency.rb +56 -7
- data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
- data/lib/rigor/environment/project_path_gems.rb +124 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
- data/lib/rigor/environment/rbs_loader.rb +772 -110
- data/lib/rigor/environment/reflection.rb +7 -7
- data/lib/rigor/environment/required_features.rb +0 -3
- data/lib/rigor/environment/ruby_library_roots.rb +1 -1
- data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
- data/lib/rigor/environment.rb +169 -42
- data/lib/rigor/flow_contribution/merger.rb +4 -4
- data/lib/rigor/inference/acceptance.rb +136 -40
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/block_run_count.rb +185 -0
- data/lib/rigor/inference/budget_trace.rb +8 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
- data/lib/rigor/inference/callee_write_census.rb +815 -0
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +258 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/deep_erasure.rb +39 -7
- data/lib/rigor/inference/def_handle.rb +1 -1
- data/lib/rigor/inference/def_node_resolver.rb +33 -15
- data/lib/rigor/inference/def_return_typer.rb +47 -5
- data/lib/rigor/inference/definer_resolution.rb +19 -9
- data/lib/rigor/inference/definers_index.rb +162 -0
- data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
- data/lib/rigor/inference/element_read_widening.rb +49 -3
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1559 -320
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +81 -5
- data/lib/rigor/inference/global_writer_reach.rb +260 -0
- data/lib/rigor/inference/hkt_body.rb +1 -2
- data/lib/rigor/inference/hkt_body_parser.rb +3 -5
- data/lib/rigor/inference/hkt_reducer.rb +2 -2
- data/lib/rigor/inference/hkt_registry.rb +3 -5
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +61 -15
- data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
- data/lib/rigor/inference/ivar_writer_census.rb +154 -8
- data/lib/rigor/inference/key_presence_guard.rb +51 -24
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +66 -14
- data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
- data/lib/rigor/inference/match_rebinding.rb +28 -5
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +123 -25
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +24 -3
- data/lib/rigor/inference/multi_target_binder.rb +2 -2
- data/lib/rigor/inference/mutation_rejoin.rb +240 -4
- data/lib/rigor/inference/mutation_widening.rb +199 -29
- data/lib/rigor/inference/narrowing.rb +238 -50
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +134 -9
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
- data/lib/rigor/inference/pre_eval_constants.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +30 -9
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/project_yielder.rb +11 -17
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_alias.rb +20 -0
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +287 -0
- data/lib/rigor/inference/remover_narrowing.rb +224 -0
- data/lib/rigor/inference/rewrite_mutation.rb +53 -2
- data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
- data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
- data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2213 -226
- data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/set_mutation.rb +189 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +3268 -358
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
- data/lib/rigor/inference/toplevel_def_census.rb +67 -0
- data/lib/rigor/inference/unknown_store_widening.rb +103 -4
- data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -9
- data/lib/rigor/language_server/buffer_table.rb +3 -3
- data/lib/rigor/language_server/completion_provider.rb +26 -20
- data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -2
- data/lib/rigor/language_server/hover_provider.rb +1 -7
- data/lib/rigor/language_server/hover_renderer.rb +6 -8
- data/lib/rigor/language_server/project_context.rb +39 -1
- data/lib/rigor/language_server/publish_batcher.rb +3 -4
- data/lib/rigor/language_server/selection_range_provider.rb +2 -4
- data/lib/rigor/language_server/server.rb +7 -3
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/plugins.rb +1 -1
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/mcp/server.rb +73 -2
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/bundled_catalog.rb +1 -12
- data/lib/rigor/plugin/io_boundary.rb +26 -4
- data/lib/rigor/plugin/loader.rb +25 -8
- data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
- data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
- data/lib/rigor/plugin/manifest.rb +16 -16
- data/lib/rigor/plugin/registry.rb +16 -5
- data/lib/rigor/plugin/services.rb +2 -3
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
- data/lib/rigor/plugin/trust_policy.rb +48 -2
- data/lib/rigor/plugin/type_node_resolver.rb +2 -4
- data/lib/rigor/plugin.rb +6 -8
- data/lib/rigor/plugin_gap_advisory.rb +5 -1
- data/lib/rigor/project_environment.rb +16 -2
- data/lib/rigor/protection/analysis_guard.rb +1 -1
- data/lib/rigor/protection/discovery_seed.rb +44 -5
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
- data/lib/rigor/rbs_extended/reporter.rb +27 -3
- data/lib/rigor/rbs_extended.rb +2 -3
- data/lib/rigor/reflection/constant_ancestors.rb +81 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +32 -1
- data/lib/rigor/run_scoped_slots.rb +38 -0
- data/lib/rigor/runtime/jit.rb +1 -1
- data/lib/rigor/scope/discovery_index.rb +168 -8
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +901 -54
- data/lib/rigor/scope.rb +644 -37
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +18 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
- data/lib/rigor/sig_gen/generator.rb +588 -66
- data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
- data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
- data/lib/rigor/sig_gen/layout_index.rb +3 -3
- data/lib/rigor/sig_gen/method_candidate.rb +37 -4
- data/lib/rigor/sig_gen/observation_collector.rb +14 -7
- data/lib/rigor/sig_gen/observed_call.rb +1 -1
- data/lib/rigor/sig_gen/path_mapper.rb +6 -0
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/renderer.rb +15 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
- data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
- data/lib/rigor/sig_gen/write_reporter.rb +63 -14
- data/lib/rigor/sig_gen/write_result.rb +23 -12
- data/lib/rigor/sig_gen/writer.rb +238 -37
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/acceptance_router.rb +2 -2
- data/lib/rigor/type/accepts_result.rb +1 -1
- data/lib/rigor/type/app.rb +4 -6
- data/lib/rigor/type/combinator.rb +173 -21
- data/lib/rigor/type/constant.rb +4 -5
- data/lib/rigor/type/hash_shape.rb +21 -40
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type/union.rb +1 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/identifier.rb +1 -2
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node/string_literal.rb +1 -2
- data/lib/rigor/type_node/symbol_literal.rb +1 -2
- data/lib/rigor/type_node/union.rb +3 -6
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/value_semantics.rb +1 -10
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
- data/sig/rigor/analysis/baseline.rbs +0 -3
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -2
- data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/cli/diff_command.rbs +1 -1
- data/sig/rigor/cli/explain_command.rbs +1 -1
- data/sig/rigor/effects/registry.rbs +2 -2
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +53 -18
- data/sig/rigor/plugin/effect_attribution.rbs +3 -3
- data/sig/rigor/plugin/effect_edge.rbs +1 -1
- data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +9 -9
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/plugin/protocol_contract.rbs +3 -7
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +43 -4
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-plugin-author/SKILL.md +4 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +82 -3
|
@@ -5,6 +5,7 @@ require "prism"
|
|
|
5
5
|
require_relative "../scope"
|
|
6
6
|
require_relative "../type"
|
|
7
7
|
require_relative "../source/alias_names"
|
|
8
|
+
require_relative "../source/forwardable_delegators"
|
|
8
9
|
require_relative "../source/literals"
|
|
9
10
|
require_relative "../source/constant_path"
|
|
10
11
|
require_relative "object_mixins"
|
|
@@ -12,9 +13,13 @@ require_relative "../source/node_children"
|
|
|
12
13
|
require_relative "../source/node_walker"
|
|
13
14
|
require_relative "../source/parameter_envelope"
|
|
14
15
|
require_relative "../cache/file_digest"
|
|
16
|
+
require_relative "../typing_state"
|
|
17
|
+
require_relative "../memo_audit"
|
|
15
18
|
require_relative "../analysis/check_rules/published_constant_guard"
|
|
16
19
|
require_relative "anonymous_meta_class"
|
|
20
|
+
require_relative "census_foreign_block"
|
|
17
21
|
require_relative "def_handle"
|
|
22
|
+
require_relative "definers_index"
|
|
18
23
|
require_relative "fresh_frame_blocks"
|
|
19
24
|
require_relative "global_write_census"
|
|
20
25
|
require_relative "guard_rebinding"
|
|
@@ -25,6 +30,7 @@ require_relative "hash_lookup_mutation"
|
|
|
25
30
|
require_relative "in_effect_refinements"
|
|
26
31
|
require_relative "refine_census"
|
|
27
32
|
require_relative "refine_self"
|
|
33
|
+
require_relative "receiver_writer_call"
|
|
28
34
|
require_relative "reflective_ivar_write"
|
|
29
35
|
require_relative "index_write_widening"
|
|
30
36
|
require_relative "module_function_state"
|
|
@@ -45,6 +51,9 @@ require_relative "scope_indexer/certainty"
|
|
|
45
51
|
require_relative "scope_indexer/member_layouts_collector"
|
|
46
52
|
require_relative "scope_indexer/declared_ivar_types"
|
|
47
53
|
require_relative "scope_indexer/declared_param_writes"
|
|
54
|
+
require_relative "scope_indexer/constant_ivar_value"
|
|
55
|
+
require_relative "scope_indexer/ivar_contributions"
|
|
56
|
+
require_relative "scope_indexer/empty_seed"
|
|
48
57
|
|
|
49
58
|
module Rigor
|
|
50
59
|
module Inference
|
|
@@ -67,8 +76,8 @@ module Rigor
|
|
|
67
76
|
# ```
|
|
68
77
|
#
|
|
69
78
|
# Nodes that are not part of the program subtree (e.g. synthesised virtual nodes that the caller looks up after the
|
|
70
|
-
# fact) yield the `default_scope`. The returned Hash is mutable in principle but callers MUST treat it as
|
|
71
|
-
# the indexer itself never exposes a way to update it past construction.
|
|
79
|
+
# fact) yield the seeded `default_scope`. The returned Hash is mutable in principle but callers MUST treat it as
|
|
80
|
+
# read-only; the indexer itself never exposes a way to update it past construction.
|
|
72
81
|
# rubocop:disable-next Metrics/ModuleLength
|
|
73
82
|
module ScopeIndexer
|
|
74
83
|
# Issue #644 — the census descriptor for a constant write that publishes no value: an unfoldable or
|
|
@@ -109,13 +118,15 @@ module Rigor
|
|
|
109
118
|
# `Dynamic[top]`. Off for every other reader, so parameter inference
|
|
110
119
|
# and sig-gen observe the clause's plain entry.
|
|
111
120
|
# @return identity-comparing
|
|
112
|
-
# table whose default value is `default_scope`.
|
|
121
|
+
# table whose default value is the seeded `default_scope`.
|
|
113
122
|
# The no-op re-anchoring table (#722 residue 2): shared so the overwhelmingly common
|
|
114
123
|
# nothing-to-re-anchor walk allocates none.
|
|
115
124
|
EMPTY_RENAMES = {}.freeze
|
|
116
125
|
|
|
117
126
|
# No path whose contributions are all `possible` (issue #2021), the cross-file walks' default.
|
|
118
127
|
EMPTY_PATH_SET = Set.new.freeze
|
|
128
|
+
# The {IvarWriterCensus} entry kinds the project pre-pass tags with the file that records them.
|
|
129
|
+
PATH_TAGGED_KINDS = %i[own_write own_write_type].to_set.freeze
|
|
119
130
|
|
|
120
131
|
# `declared_ivar_types:` — whether an RBS-declared ivar type replaces the census's join of the writes (#1406).
|
|
121
132
|
# `rigor sig-gen` passes false: it proposes what the code says, and reading a declaration back would make a row
|
|
@@ -222,20 +233,21 @@ module Rigor
|
|
|
222
233
|
# nodes in `excluded`: the file's own pass, and each construct {#evaluate_unentered_flow} evaluates in the walk
|
|
223
234
|
# after it.
|
|
224
235
|
def recording_evaluator(table, converged_loop_recording, widen_clauses)
|
|
225
|
-
lambda do |node, scope, excluded = nil|
|
|
236
|
+
lambda do |node, scope, excluded = nil, block_only: false|
|
|
226
237
|
on_enter =
|
|
227
238
|
if excluded.nil? || excluded.empty? then ->(visited, at) { table[visited] = at }
|
|
228
239
|
else ->(visited, at) { table[visited] = at unless excluded.include?(visited) }
|
|
229
240
|
end
|
|
230
|
-
StatementEvaluator.new(scope: scope, on_enter: on_enter,
|
|
231
|
-
|
|
241
|
+
evaluator = StatementEvaluator.new(scope: scope, on_enter: on_enter, widen_clauses: widen_clauses,
|
|
242
|
+
converged_loop_recording: converged_loop_recording)
|
|
243
|
+
block_only ? evaluator.evaluate_block_of(node) : evaluator.evaluate(node)
|
|
232
244
|
end
|
|
233
245
|
end
|
|
234
246
|
|
|
235
247
|
# Runs the combined methods/def-nodes descent (one walk of the file), seeds the discovered-methods existence table
|
|
236
|
-
# onto `seeded_scope` (merged UNDER the cross-file pre-pass seed `default_scope` carries), and returns
|
|
237
|
-
# file_def_nodes, file_envelopes]` so the caller can thread the def-node and issue #992
|
|
238
|
-
# {#merge_project_method_indexes} without walking the file a second time.
|
|
248
|
+
# onto `seeded_scope` (merged UNDER the cross-file pre-pass seed `default_scope` carries), and returns
|
|
249
|
+
# `[scope, [file_def_nodes, file_envelopes, file_siblings]]` so the caller can thread the def-node and issue #992
|
|
250
|
+
# envelope tables into {#merge_project_method_indexes} without walking the file a second time.
|
|
239
251
|
#
|
|
240
252
|
# ADR-119 WD3 — the file's `possible_discovered_methods` folds over the seed's by {#fold_possible_methods}, so a
|
|
241
253
|
# name the file defines certainly is no longer possible and a name only the file's possible contribution
|
|
@@ -306,8 +318,8 @@ module Rigor
|
|
|
306
318
|
siblings = per_file_siblings(seeded_scope.discovery.sibling_tables, file_tables[0], file_tables[2],
|
|
307
319
|
file_singleton, file_visibilities)
|
|
308
320
|
extends, methods_table, unpositioned =
|
|
309
|
-
merge_and_fold_extends(default_scope, root, def_nodes,
|
|
310
|
-
mixin_unpositioned)
|
|
321
|
+
merge_and_fold_extends(default_scope, root, [def_nodes, file_tables[0]],
|
|
322
|
+
[singleton_def_nodes, file_singleton[0]], seeded_scope, siblings, mixin_unpositioned)
|
|
311
323
|
|
|
312
324
|
seeded_scope.with_discovery(
|
|
313
325
|
seeded_scope.discovery.with(
|
|
@@ -387,8 +399,9 @@ module Rigor
|
|
|
387
399
|
default_scope.discovered_includes.merge(file[:includes]) do |_class, cross_file, per_file|
|
|
388
400
|
(per_file + cross_file).uniq
|
|
389
401
|
end,
|
|
402
|
+
# ADR-129 G4 — each merged list is frozen as the per-file walk's lists are ({#freeze_mixin_lists}).
|
|
390
403
|
default_scope.discovered_prepends.merge(file[:prepends]) do |_class, cross_file, per_file|
|
|
391
|
-
(per_file + cross_file).uniq
|
|
404
|
+
(per_file + cross_file).uniq.freeze
|
|
392
405
|
end,
|
|
393
406
|
file[:unpositioned]
|
|
394
407
|
]
|
|
@@ -421,13 +434,23 @@ module Rigor
|
|
|
421
434
|
# ADR-119 WD1 — the unpositioned union (the seed's, the mixin walks' and this file's extend side) is settled
|
|
422
435
|
# first, because the fold reads it to tell a certain extend edge from a listed one. `siblings` is written by
|
|
423
436
|
# the fold.
|
|
424
|
-
|
|
437
|
+
# `def_pair` and `singleton_pair` are `[merged table, this file's own]`; the merged tables are frozen once the
|
|
438
|
+
# fold, their last in-place writer, is done ({#freeze_def_tables}).
|
|
439
|
+
def merge_and_fold_extends(default_scope, root, def_pair, singleton_pair, seeded_scope, siblings,
|
|
425
440
|
mixin_unpositioned)
|
|
441
|
+
def_nodes = def_pair.first
|
|
442
|
+
singleton_def_nodes = singleton_pair.first
|
|
426
443
|
file_extends, extends, extend_unpositioned = merge_extend_tables(default_scope, root)
|
|
427
444
|
unpositioned = union_unpositioned(default_scope.discovery.unpositioned_mixins, *mixin_unpositioned,
|
|
428
445
|
extend_unpositioned)
|
|
429
446
|
methods_table = fold_per_file_extends(file_extends, def_nodes, singleton_def_nodes, seeded_scope, siblings,
|
|
430
447
|
unpositioned)
|
|
448
|
+
# The fold also writes the singleton hash of each class this file `extend`s, declared there or not.
|
|
449
|
+
file_extends.each_key { |owner| singleton_def_nodes[owner]&.freeze }
|
|
450
|
+
freeze_def_tables(def_pair, singleton_pair)
|
|
451
|
+
# #2300 — the override gate's definers index then inverts the seed once per run, not once per file.
|
|
452
|
+
DefinersIndex.record_layers(def_nodes, default_scope.discovered_def_nodes)
|
|
453
|
+
DefinersIndex.record_layers(singleton_def_nodes, default_scope.discovered_singleton_def_nodes)
|
|
431
454
|
[extends, methods_table, unpositioned]
|
|
432
455
|
end
|
|
433
456
|
|
|
@@ -458,6 +481,20 @@ module Rigor
|
|
|
458
481
|
walked.fetch(:header_nestings)).freeze]
|
|
459
482
|
end
|
|
460
483
|
|
|
484
|
+
# Issue #2330 — the per-file def table is read by identity: the callee-chain census memo is keyed by the
|
|
485
|
+
# {Scope::DiscoveryIndex} that carries it, which is only sound while the table is never written in place.
|
|
486
|
+
# Frozen after the extends fold, the last in-place writer; the seed's per-class hashes are frozen already.
|
|
487
|
+
#
|
|
488
|
+
# Only the per-class hashes of the classes this file declares are frozen here (the merge made or took
|
|
489
|
+
# them); a seed-only class's hash was frozen by `finalize_def_index`, so walking the whole table per file is
|
|
490
|
+
# needless.
|
|
491
|
+
def freeze_def_tables(*pairs)
|
|
492
|
+
pairs.each do |table, file_table|
|
|
493
|
+
file_table.each_key { |owner| table[owner].freeze }
|
|
494
|
+
table.freeze
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
461
498
|
# The instance-side def-node table and its issue #681 nesting twin, each merged over the cross-file seed.
|
|
462
499
|
# Returned as a pair so the two stay written together: a node the merge keeps must be the same object the
|
|
463
500
|
# nesting table keys, and they are only that if both halves take the same file's walk.
|
|
@@ -529,25 +566,45 @@ module Rigor
|
|
|
529
566
|
mutated_ivars = {}
|
|
530
567
|
read_before_write = {}
|
|
531
568
|
init_writes = {}
|
|
532
|
-
# WD3 — per
|
|
533
|
-
#
|
|
534
|
-
#
|
|
535
|
-
# by class.
|
|
569
|
+
# WD3 — per class, then per method, the ivars the method definitely assigns non-nil on every completing path,
|
|
570
|
+
# consulted by `dead_transient_nil_writes` so a ctor that reassigns `@x` indirectly through an unconditional
|
|
571
|
+
# same-class method call (`mask!`) credits the overwrite. Built once per program here, memoised by class.
|
|
536
572
|
method_assign_effects = build_method_assign_effects(root)
|
|
573
|
+
# #2291 — an `@x = LIT` write reads the file's constant table, built on the first such write.
|
|
574
|
+
accumulator[CONSTANT_IVAR_VALUES] = ConstantIvarValue.new(root, default_scope)
|
|
537
575
|
walk_class_ivars(root, [], default_scope, accumulator, mutated_ivars,
|
|
538
576
|
read_before_write, init_writes, method_assign_effects)
|
|
577
|
+
accumulator.delete(CONSTANT_IVAR_VALUES)
|
|
539
578
|
reflective_opaque = accumulator.delete(REFLECTIVE_OPAQUE_CLASSES)
|
|
579
|
+
singleton_writes = accumulator.delete(SINGLETON_IVAR_WRITES)
|
|
540
580
|
exempt_class_body_writes!(init_writes, accumulator.delete(DEFINE_METHOD_WRITES) || {})
|
|
541
581
|
merge_held_ivar_writes!(accumulator)
|
|
542
582
|
record_aliased_ivar_mutations!(root, mutated_ivars)
|
|
543
583
|
widen_mutated_ivar_entries!(accumulator, mutated_ivars)
|
|
584
|
+
# #2170 — an ancestor's writes as this file's census types them, before its read-before-write `nil`.
|
|
585
|
+
writes = instance_side_writes(accumulator, singleton_writes)
|
|
544
586
|
contribute_read_before_write_nil!(accumulator, read_before_write, init_writes)
|
|
545
587
|
make_reflective_classes_gradual!(accumulator, reflective_opaque)
|
|
588
|
+
# #2079 / #2170 — another file's writes, and the chain's modules' and superclasses', join existing entries.
|
|
589
|
+
IvarContributions.join!(accumulator, writes, default_scope)
|
|
590
|
+
# An in-place mutation the file observes widens a joined value too (a same-file ancestor's String literal).
|
|
591
|
+
widen_mutated_ivar_entries!(accumulator, mutated_ivars)
|
|
546
592
|
# #1406 — an RBS `@x: T` or `attr_* x: T` declaration replaces the census's join of the writes.
|
|
547
593
|
DeclaredIvarTypes.apply!(accumulator, default_scope) if declared_ivar_types
|
|
548
594
|
accumulator.transform_values(&:freeze).freeze
|
|
549
595
|
end
|
|
550
596
|
|
|
597
|
+
# #2170 — a copy of the census entries for {IvarContributions} to join as an ancestor's own. #2931 — an ivar a
|
|
598
|
+
# singleton-side `def` of the class writes is left out: its entry joins that write, which runs on the module or
|
|
599
|
+
# class object and never on an includer's instance, so the join reads the file's instance-side census entries
|
|
600
|
+
# for it, as another file's reader does.
|
|
601
|
+
def instance_side_writes(accumulator, singleton_writes)
|
|
602
|
+
accumulator.to_h do |class_name, entries|
|
|
603
|
+
skipped = singleton_writes&.[](class_name)
|
|
604
|
+
[class_name, skipped ? entries.except(*skipped) : entries.dup]
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
|
|
551
608
|
# Folds the class-body writes {#collect_class_body_ivar_writes} held under {CLASS_BODY_WRITES} into the
|
|
552
609
|
# `init_writes` exemption, except for an ivar a `define_method` block, or a `block_as_methods:` block bound to the
|
|
553
610
|
# receiver's instance ({#method_body_block_call?}), of the class writes (#1768). Such an ivar had no census entry
|
|
@@ -628,8 +685,8 @@ module Rigor
|
|
|
628
685
|
end
|
|
629
686
|
end
|
|
630
687
|
|
|
631
|
-
#
|
|
632
|
-
#
|
|
688
|
+
# Per class, then per method, the ivars each method hands back, dropping the empty sets so the caller's
|
|
689
|
+
# `next if per_class.nil?` skips a whole class in one lookup.
|
|
633
690
|
def returned_ivars_by_method(defs)
|
|
634
691
|
defs.each_with_object({}) do |(class_name, methods), acc|
|
|
635
692
|
methods.each do |method_name, def_node|
|
|
@@ -718,9 +775,8 @@ module Rigor
|
|
|
718
775
|
# The widening uses `MutationWidening.widen_for_mutator` — the same primitive
|
|
719
776
|
# `Inference::StatementEvaluator#eval_call` applies for per-method-body widening on a local / ivar receiver. The
|
|
720
777
|
# class-level pass extends that primitive's reach so a `Tuple`-seeded ivar in `initialize` is observed as
|
|
721
|
-
# `Nominal[Array]` at the entry of every OTHER method body in the class — closing the cross-method gap
|
|
722
|
-
#
|
|
723
|
-
# mutation"; Redmine 6.1.2 `Redmine::Views::Builders::Structure` is the canonical worked site).
|
|
778
|
+
# `Nominal[Array]` at the entry of every OTHER method body in the class — closing the cross-method gap
|
|
779
|
+
# (Redmine 6.1.2 `Redmine::Views::Builders::Structure` is the canonical worked site).
|
|
724
780
|
#
|
|
725
781
|
# Always-safe: the widening can only LOSE precision; the underlying nominal (`Array` / `Hash`) and the element
|
|
726
782
|
# union are preserved.
|
|
@@ -765,18 +821,27 @@ module Rigor
|
|
|
765
821
|
end
|
|
766
822
|
|
|
767
823
|
widen_member_for_lookup_mutators(member, observed_methods)
|
|
768
|
-
when Type::Constant
|
|
769
|
-
# `@s = +"ab"` in one method and `@s << "c"` in another: without this arm the seed stayed pinned at every
|
|
770
|
-
# other method's entry, and `@s == "ab"` folded always-truthy on a receiver that holds `"abc"`.
|
|
771
|
-
return member unless StringMutation.constant?(member) &&
|
|
772
|
-
observed_methods.any? { |m| StringMutation::MUTATORS.include?(m) }
|
|
773
|
-
|
|
774
|
-
Type::Combinator.nominal_of("String")
|
|
824
|
+
when Type::Constant then widen_constant_for_observed_mutators(member, observed_methods)
|
|
775
825
|
else
|
|
776
826
|
member
|
|
777
827
|
end
|
|
778
828
|
end
|
|
779
829
|
|
|
830
|
+
# `@s = +"ab"` in one method and `@s << "c"` in another: without this arm the seed stayed pinned at every other
|
|
831
|
+
# method's entry, and `@s == "ab"` folded always-truthy on a receiver that holds `"abc"`. A constant-folded Set
|
|
832
|
+
# (`@s = Set["a"]`, then `@s.clear; @s << 1` elsewhere — issue #2294) takes the untyped floor the Tuple arm takes.
|
|
833
|
+
def widen_constant_for_observed_mutators(member, observed_methods)
|
|
834
|
+
if SetMutation.constant?(member)
|
|
835
|
+
return member unless observed_methods.any? { |m| SetMutation::MUTATORS.include?(m) }
|
|
836
|
+
|
|
837
|
+
return Type::Combinator.nominal_of("Set", type_args: [Type::Combinator.untyped])
|
|
838
|
+
end
|
|
839
|
+
return member unless StringMutation.constant?(member) &&
|
|
840
|
+
observed_methods.any? { |m| StringMutation::MUTATORS.include?(m) }
|
|
841
|
+
|
|
842
|
+
Type::Combinator.nominal_of("String")
|
|
843
|
+
end
|
|
844
|
+
|
|
780
845
|
# A `HashShape` ivar seed some method gave a default, a default proc or identity keys, and no method stored
|
|
781
846
|
# into, removed from or rewrote: its pairs are the seed's own, so it takes the per-method {HashLookupMutation}
|
|
782
847
|
# widening rather than the untyped floor above — a present key keeps its value across methods, a missing one
|
|
@@ -903,6 +968,7 @@ module Rigor
|
|
|
903
968
|
collect_initializer_block_ivars(node, owner, default_scope, accumulator, mutated_ivars, init_writes)
|
|
904
969
|
record_attr_writer_ivars(node, owner, accumulator) if ATTR_WRITER_MACROS.include?(attr_macro(node))
|
|
905
970
|
collect_define_method_block_ivars(node, owner, default_scope, accumulator, mutated_ivars)
|
|
971
|
+
collect_receiver_writer_ivars(node, owner, qualified_prefix, default_scope, accumulator, mutated_ivars)
|
|
906
972
|
false
|
|
907
973
|
else
|
|
908
974
|
walk_ivars_meta_new?(node, qualified_prefix, default_scope, accumulator,
|
|
@@ -1031,6 +1097,8 @@ module Rigor
|
|
|
1031
1097
|
read_before_write = nil, init_writes = nil, method_assign_effects = nil,
|
|
1032
1098
|
singleton_self: false)
|
|
1033
1099
|
return if def_node.body.nil? || qualified_prefix.empty?
|
|
1100
|
+
# Issue #3155 — `def String.q` writes the ivars of another object.
|
|
1101
|
+
return if foreign_def?(def_node, qualified_prefix, false)
|
|
1034
1102
|
|
|
1035
1103
|
class_name = qualified_prefix.join("::")
|
|
1036
1104
|
singleton = singleton_self || def_node.receiver.is_a?(Prism::SelfNode) ||
|
|
@@ -1057,6 +1125,10 @@ module Rigor
|
|
|
1057
1125
|
declared = DeclaredParamWrites.collect(def_node, class_name, body_scope) unless singleton
|
|
1058
1126
|
gather_ivar_writes(def_node.body, body_scope, class_name, accumulator,
|
|
1059
1127
|
EMPTY_GUARDED_IVARS, mutated_ivars, dead_writes, declared)
|
|
1128
|
+
if singleton
|
|
1129
|
+
singleton_written = (accumulator[SINGLETON_IVAR_WRITES] ||= {})[class_name] ||= Set.new
|
|
1130
|
+
define_method_written_ivars(def_node.body, singleton_written)
|
|
1131
|
+
end
|
|
1060
1132
|
|
|
1061
1133
|
# B2.3 — collect per-method evidence for the read-before- write nil contribution. The accumulator-level decision
|
|
1062
1134
|
# ("is this ivar truly read-before-write across the class lifetime?") is finalised at
|
|
@@ -1083,7 +1155,7 @@ module Rigor
|
|
|
1083
1155
|
|
|
1084
1156
|
seen_writes = Set.new
|
|
1085
1157
|
read_first = Set.new
|
|
1086
|
-
detect_read_before_write(block_node.body, seen_writes, read_first)
|
|
1158
|
+
detect_read_before_write(block_node.body, seen_writes, read_first, class_name)
|
|
1087
1159
|
init_set = (init_writes[class_name] ||= Set.new)
|
|
1088
1160
|
seen_writes.each { |name| init_set << name }
|
|
1089
1161
|
end
|
|
@@ -1146,6 +1218,29 @@ module Rigor
|
|
|
1146
1218
|
define_method_written_ivars(block.body, written)
|
|
1147
1219
|
end
|
|
1148
1220
|
|
|
1221
|
+
# Issue #2488 — the explicit-receiver arm of {#walk_class_ivars}: `E.attr_writer :w` (or through `send`) records
|
|
1222
|
+
# `@w` as `Dynamic[top]` on `E`, as an `attr_writer` in `E`'s body does ({#record_attr_writer_ivars}), and an
|
|
1223
|
+
# `E.define_method(:m) { … }` block is walked as an instance body of `E`, as one in `E`'s body is
|
|
1224
|
+
# ({#collect_define_method_block_ivars}). `E` resolves as a `class_eval` receiver does; an unnameable one
|
|
1225
|
+
# contributes nothing. A call inside a `def` is that body's to walk.
|
|
1226
|
+
def collect_receiver_writer_ivars(node, owner, qualified_prefix, default_scope, accumulator, mutated_ivars)
|
|
1227
|
+
return if ReceiverWriterCall.receiver(node).nil?
|
|
1228
|
+
|
|
1229
|
+
target = eval_receiver_prefix(node, owner, qualified_prefix, unnameable_self: owner.empty?)
|
|
1230
|
+
return if target.nil? || target.empty?
|
|
1231
|
+
|
|
1232
|
+
class_name = target.join("::")
|
|
1233
|
+
ReceiverWriterCall.writer_ivars(node).each do |ivar|
|
|
1234
|
+
accumulate_ivar_type(accumulator, class_name, ivar, Type::Combinator.untyped)
|
|
1235
|
+
end
|
|
1236
|
+
body = ReceiverWriterCall.define_method_block(node)&.body
|
|
1237
|
+
return if body.nil?
|
|
1238
|
+
|
|
1239
|
+
body_scope = census_body_scope(default_scope, target, Type::Combinator.nominal_of(class_name))
|
|
1240
|
+
gather_ivar_writes(body, body_scope, class_name, accumulator, EMPTY_GUARDED_IVARS, mutated_ivars)
|
|
1241
|
+
define_method_written_ivars(body, (accumulator[DEFINE_METHOD_WRITES] ||= {})[class_name] ||= Set.new)
|
|
1242
|
+
end
|
|
1243
|
+
|
|
1149
1244
|
# The ivars a `define_method` block body writes, in any write form, outside a nested `def` / `class` / `module`.
|
|
1150
1245
|
def define_method_written_ivars(node, acc)
|
|
1151
1246
|
return unless node.is_a?(Prism::Node)
|
|
@@ -1191,7 +1286,7 @@ module Rigor
|
|
|
1191
1286
|
|
|
1192
1287
|
seen_writes = Set.new
|
|
1193
1288
|
read_first = Set.new
|
|
1194
|
-
detect_read_before_write(def_node.body, seen_writes, read_first)
|
|
1289
|
+
detect_read_before_write(def_node.body, seen_writes, read_first, class_name)
|
|
1195
1290
|
|
|
1196
1291
|
# ADR-38 — `initialize` is the built-in initializer gate; a plugin may declare additional `def`-form initializer
|
|
1197
1292
|
# methods (minitest `setup`, Rails `after_initialize`, DI setters) on a constrained class. Both fold their
|
|
@@ -1306,7 +1401,7 @@ module Rigor
|
|
|
1306
1401
|
)
|
|
1307
1402
|
end
|
|
1308
1403
|
|
|
1309
|
-
def detect_read_before_write(node, seen_writes, read_first)
|
|
1404
|
+
def detect_read_before_write(node, seen_writes, read_first, class_name = nil)
|
|
1310
1405
|
return unless node.is_a?(Prism::Node)
|
|
1311
1406
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
1312
1407
|
|
|
@@ -1316,20 +1411,21 @@ module Rigor
|
|
|
1316
1411
|
# not `InstanceVariableWriteNode`s, so the generic descent below never records them as writes. Left unhandled,
|
|
1317
1412
|
# an ivar written only via massign in `initialize` stays absent from `init_writes`, and
|
|
1318
1413
|
# `contribute_read_before_write_nil!` then unions a spurious `nil` into its class-ivar seed — masking a
|
|
1319
|
-
# genuinely-typed `@m` (e.g. `Tuple[]`) as `T | nil` at every sibling read. The RHS runs before any
|
|
1320
|
-
# committed, so descend into `value` FIRST (an ivar read there is read-before-write), then mark every
|
|
1321
|
-
# target as written.
|
|
1414
|
+
# genuinely-typed `@m` (e.g. `Tuple[]`) as `T | nil` at every sibling read. The RHS runs before any
|
|
1415
|
+
# target is committed, so descend into `value` FIRST (an ivar read there is read-before-write), then mark every
|
|
1416
|
+
# ivar target as written.
|
|
1322
1417
|
if node.is_a?(Prism::MultiWriteNode)
|
|
1323
|
-
detect_read_before_write(node.value, seen_writes, read_first) if node.value
|
|
1324
|
-
detect_multi_write_target_writes(node, seen_writes, read_first)
|
|
1418
|
+
detect_read_before_write(node.value, seen_writes, read_first, class_name) if node.value
|
|
1419
|
+
detect_multi_write_target_writes(node, seen_writes, read_first, class_name)
|
|
1325
1420
|
return
|
|
1326
1421
|
end
|
|
1327
1422
|
|
|
1328
1423
|
# Descend BEFORE recording a write — `@x = @x + 1`'s RHS is an `InstanceVariableReadNode` that runs before the
|
|
1329
1424
|
# write is committed; the read is therefore read-before-write semantically. `each_child` yields the value
|
|
1330
1425
|
# child before the lvalue target (`compact_child_nodes` field order), matching this order.
|
|
1331
|
-
|
|
1332
|
-
|
|
1426
|
+
# #2490 — what a block whose `self` is another object reads or writes is that object's.
|
|
1427
|
+
each_census_child(node, class_name) do |c|
|
|
1428
|
+
detect_read_before_write(c, seen_writes, read_first, class_name)
|
|
1333
1429
|
end
|
|
1334
1430
|
|
|
1335
1431
|
seen_writes << node.name if IVAR_WRITE_NODES.any? { |klass| node.is_a?(klass) }
|
|
@@ -1341,19 +1437,19 @@ module Rigor
|
|
|
1341
1437
|
# Records each ivar target of a `MultiWriteNode` / nested `MultiTargetNode` into `seen_writes`, and descends into
|
|
1342
1438
|
# any non-ivar target (a `CallTargetNode` / `IndexTargetNode` receiver such as `@obj.x, @y = …`) so an ivar read
|
|
1343
1439
|
# inside a target receiver still counts as read-before-write.
|
|
1344
|
-
def detect_multi_write_target_writes(node, seen_writes, read_first)
|
|
1440
|
+
def detect_multi_write_target_writes(node, seen_writes, read_first, class_name = nil)
|
|
1345
1441
|
targets = (node.lefts || []) + [node.rest].compact + (node.rights || [])
|
|
1346
1442
|
targets.each do |target|
|
|
1347
1443
|
case target
|
|
1348
1444
|
when Prism::InstanceVariableTargetNode
|
|
1349
1445
|
seen_writes << target.name
|
|
1350
1446
|
when Prism::MultiTargetNode
|
|
1351
|
-
detect_multi_write_target_writes(target, seen_writes, read_first)
|
|
1447
|
+
detect_multi_write_target_writes(target, seen_writes, read_first, class_name)
|
|
1352
1448
|
when Prism::SplatNode
|
|
1353
1449
|
inner = target.expression
|
|
1354
1450
|
seen_writes << inner.name if inner.is_a?(Prism::InstanceVariableTargetNode)
|
|
1355
1451
|
else
|
|
1356
|
-
detect_read_before_write(target, seen_writes, read_first)
|
|
1452
|
+
detect_read_before_write(target, seen_writes, read_first, class_name)
|
|
1357
1453
|
end
|
|
1358
1454
|
end
|
|
1359
1455
|
end
|
|
@@ -1361,6 +1457,21 @@ module Rigor
|
|
|
1361
1457
|
IVAR_BARRIER_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].freeze
|
|
1362
1458
|
private_constant :IVAR_BARRIER_NODES
|
|
1363
1459
|
|
|
1460
|
+
# Issue #2490 — yields the children of `node` a census walk for `class_name` descends into: all of them, or only
|
|
1461
|
+
# those {CensusForeignBlock.census_children} keeps when `node` runs code whose `self` is another object. A nil
|
|
1462
|
+
# `class_name` keeps them all. A reentered `def` (#2941) is yielded as itself, so a walk that stops at a `def`
|
|
1463
|
+
# skips it; `def_bodies:` yields its body instead, for a walk that counts its writes. `reentry: false` (a
|
|
1464
|
+
# singleton-side `def`) keeps none of the reentered code.
|
|
1465
|
+
def each_census_child(node, class_name, def_bodies: false, reentry: true, &)
|
|
1466
|
+
outside = class_name && CensusForeignBlock.census_children(node, class_name, reentry: reentry)
|
|
1467
|
+
return node.rigor_each_child(&) unless outside
|
|
1468
|
+
|
|
1469
|
+
outside.each do |child|
|
|
1470
|
+
child = child.body if def_bodies && child.is_a?(Prism::DefNode)
|
|
1471
|
+
yield child if child
|
|
1472
|
+
end
|
|
1473
|
+
end
|
|
1474
|
+
|
|
1364
1475
|
EMPTY_GUARDED_IVARS = Set.new.freeze
|
|
1365
1476
|
|
|
1366
1477
|
# Shared empty result for a def that returns no ivar — the overwhelmingly common case.
|
|
@@ -1394,6 +1505,7 @@ module Rigor
|
|
|
1394
1505
|
# array/tuple-shaped, else the unanalyzable floor (the same `Dynamic[top]` a single write to an unknown RHS
|
|
1395
1506
|
# records — an unanalyzable multi-write means unknown, not nil).
|
|
1396
1507
|
record_multi_write_ivars(node, scope, class_name, accumulator)
|
|
1508
|
+
record_binding_target_ivars(node, scope, class_name, accumulator)
|
|
1397
1509
|
|
|
1398
1510
|
record_ivar_mutator_call(node, class_name, mutated_ivars) if mutated_ivars
|
|
1399
1511
|
|
|
@@ -1406,7 +1518,10 @@ module Rigor
|
|
|
1406
1518
|
return
|
|
1407
1519
|
end
|
|
1408
1520
|
|
|
1409
|
-
|
|
1521
|
+
# #2490 — a block or `class <<` body whose `self` is another object writes that object's ivars. #2941 — a `def`
|
|
1522
|
+
# in it that runs on a `class_name` instance again counts its writes.
|
|
1523
|
+
# The reentry is for code that runs on an instance again, so a singleton-side `def` takes none.
|
|
1524
|
+
each_census_child(node, class_name, def_bodies: true, reentry: !scope.self_type.is_a?(Type::Singleton)) do |c|
|
|
1410
1525
|
gather_ivar_writes(c, scope, class_name, accumulator, guarded_ivars, mutated_ivars, dead_writes, declared)
|
|
1411
1526
|
end
|
|
1412
1527
|
end
|
|
@@ -1445,13 +1560,21 @@ module Rigor
|
|
|
1445
1560
|
method_name, receiver = mutation_target(node)
|
|
1446
1561
|
return if method_name.nil?
|
|
1447
1562
|
return unless receiver.is_a?(Prism::InstanceVariableReadNode)
|
|
1448
|
-
return unless
|
|
1563
|
+
return unless ivar_census_mutator?(method_name)
|
|
1449
1564
|
|
|
1450
1565
|
per_class = (mutated_ivars[class_name] ||= {})
|
|
1451
1566
|
per_ivar = (per_class[receiver.name] ||= Set.new)
|
|
1452
1567
|
per_ivar << method_name
|
|
1453
1568
|
end
|
|
1454
1569
|
|
|
1570
|
+
# The Set-only names (`add`, `merge`, … — issue #2294) are recorded here rather than in `SHAPE_MUTATORS`, which
|
|
1571
|
+
# every scan reads by name alone: there `merge` would make `Hash#merge`, which returns a new Hash, a mutation of
|
|
1572
|
+
# its receiver. The census is safe to over-collect, since {#widen_member_for_observed_mutators} reads each name
|
|
1573
|
+
# against the table of the carrier it widens.
|
|
1574
|
+
def ivar_census_mutator?(method_name)
|
|
1575
|
+
MutationWidening::SHAPE_MUTATORS.include?(method_name) || SetMutation::MUTATORS.include?(method_name)
|
|
1576
|
+
end
|
|
1577
|
+
|
|
1455
1578
|
# `[mutator name, receiver node]` for a node that mutates its receiver, `nil` otherwise.
|
|
1456
1579
|
#
|
|
1457
1580
|
# `@h[k] ||= v` and its `&&=` / `+=` siblings store through `[]=` but are not `[]=` CallNodes; missing them left
|
|
@@ -1586,10 +1709,9 @@ module Rigor
|
|
|
1586
1709
|
SAME_CLASS_CALL_DEPTH_CAP = 3
|
|
1587
1710
|
private_constant :SAME_CLASS_CALL_DEPTH_CAP
|
|
1588
1711
|
|
|
1589
|
-
# WD3 — builds the per-class definite-assignment summary
|
|
1590
|
-
#
|
|
1591
|
-
#
|
|
1592
|
-
# `mask!`).
|
|
1712
|
+
# WD3 — builds the per-class definite-assignment summary: per class, then per method, the ivars assigned non-nil
|
|
1713
|
+
# on every completing path. Used so a ctor's `dead_transient_nil_writes` can credit an indirect overwrite through
|
|
1714
|
+
# an unconditionally-called same-class method (ipaddr's `initialize` reassigns `@mask_addr` via `mask!`).
|
|
1593
1715
|
#
|
|
1594
1716
|
# Each method's set is computed by the same suffix definite-assignment analysis used for the ctor seed, run from
|
|
1595
1717
|
# the method body's first statement for every ivar the method writes anywhere. Same-class calls inside a method
|
|
@@ -1976,10 +2098,11 @@ module Rigor
|
|
|
1976
2098
|
end
|
|
1977
2099
|
|
|
1978
2100
|
def record_ivar_write(node, scope, class_name, accumulator, guarded: false, declared: nil)
|
|
1979
|
-
rvalue_type = declared ||
|
|
2101
|
+
rvalue_type = declared || accumulator[CONSTANT_IVAR_VALUES]&.type_of(node.value, scope) ||
|
|
2102
|
+
scope.type_of(node.value)
|
|
1980
2103
|
|
|
1981
2104
|
# `@x = nil unless @x` / `@y = false unless @y` — follow-up to the polarity-aware defensive-init guard fix
|
|
1982
|
-
# (
|
|
2105
|
+
# (defensive ivar-init with a nil / false rvalue). When the rvalue is itself a falsey
|
|
1983
2106
|
# Constant, `union(rvalue, Constant[nil])` collapses (for `nil`) or doesn't widen the type's truthiness profile
|
|
1984
2107
|
# (for `false`) — the predicate `unless @x` then folds to a single `Constant[nil]` / `Constant[false]` and the
|
|
1985
2108
|
# `flow.always-truthy-condition` / `-always-falsey-` rule false-fires on the no-op-but-documented-default idiom.
|
|
@@ -1989,6 +2112,8 @@ module Rigor
|
|
|
1989
2112
|
# `lib/tdiary/configuration.rb:157` for the worked site.
|
|
1990
2113
|
return if guarded && falsey_constant?(rvalue_type)
|
|
1991
2114
|
|
|
2115
|
+
rvalue_type = EmptySeed.erase(node.value, rvalue_type)
|
|
2116
|
+
|
|
1992
2117
|
rvalue_type = Type::Combinator.union(rvalue_type, Type::Combinator.constant_of(nil)) if guarded
|
|
1993
2118
|
accumulate_ivar_type(accumulator, class_name, node.name, rvalue_type)
|
|
1994
2119
|
end
|
|
@@ -2018,7 +2143,7 @@ module Rigor
|
|
|
2018
2143
|
# `nil`, the guard bound its own ivar, and `ExpressionTyper#compound_write_value` read
|
|
2019
2144
|
# `truthy(nil) | bot` — an instance method that sig-gen declared `-> bot`.
|
|
2020
2145
|
def record_ivar_or_write(node, scope, class_name, accumulator)
|
|
2021
|
-
rvalue_type = scope.type_of(node.value)
|
|
2146
|
+
rvalue_type = EmptySeed.erase(node.value, scope.type_of(node.value))
|
|
2022
2147
|
return if Narrowing.narrow_truthy(rvalue_type).is_a?(Type::Bot)
|
|
2023
2148
|
|
|
2024
2149
|
accumulate_ivar_type(accumulator, class_name, node.name,
|
|
@@ -2182,7 +2307,13 @@ module Rigor
|
|
|
2182
2307
|
CLASS_BODY_WRITES = :class_body_writes
|
|
2183
2308
|
# The classes a computed-name `instance_variable_set` may write any ivar of ({#record_reflective_ivar_write}).
|
|
2184
2309
|
REFLECTIVE_OPAQUE_CLASSES = :reflective_opaque_classes
|
|
2185
|
-
|
|
2310
|
+
# The census's {ConstantIvarValue}, which {#record_ivar_write} reads.
|
|
2311
|
+
CONSTANT_IVAR_VALUES = :constant_ivar_values
|
|
2312
|
+
# Issue #2931 — per class, the ivars a singleton-side `def` (`def self.x`) of it writes, which the census files
|
|
2313
|
+
# under the class too (#1958).
|
|
2314
|
+
SINGLETON_IVAR_WRITES = :singleton_ivar_writes
|
|
2315
|
+
private_constant :DEFINE_METHOD_WRITES, :CLASS_BODY_WRITES, :REFLECTIVE_OPAQUE_CLASSES, :CONSTANT_IVAR_VALUES,
|
|
2316
|
+
:SINGLETON_IVAR_WRITES
|
|
2186
2317
|
|
|
2187
2318
|
# Unions `type` into the class-ivar accumulator for `(class_name, ivar_name)`. Shared by the single-write and
|
|
2188
2319
|
# multi-write (parallel-assignment) collectors.
|
|
@@ -2215,9 +2346,65 @@ module Rigor
|
|
|
2215
2346
|
|
|
2216
2347
|
rhs_type = scope.type_of(node.value)
|
|
2217
2348
|
ivars = MultiTargetBinder.bind_marked(node, rhs_type, scope: scope, soften_slots: false).ivars
|
|
2218
|
-
|
|
2349
|
+
# #2499 — `@a, @b = {}, []` erases each empty seed as a single write does.
|
|
2350
|
+
values = EmptySeed.multi_write_values(node)
|
|
2351
|
+
ivars.each do |name, type|
|
|
2352
|
+
accumulate_ivar_type(accumulator, class_name, name, EmptySeed.erase(values[name], type))
|
|
2353
|
+
end
|
|
2354
|
+
end
|
|
2355
|
+
|
|
2356
|
+
# Issue #1217 — `rescue => @err` and `for @x in xs` (with `for *@x in`, `for @x, y in`) write the ivar through a
|
|
2357
|
+
# target node, not an `InstanceVariableWriteNode`, so the census missed them: a sibling method read the
|
|
2358
|
+
# constructor's `@err = nil` alone and folded `if @err` always-falsey. A rescue reference contributes the
|
|
2359
|
+
# exception instance type the clause names; a `for` index the `Dynamic[top]` floor, since the census does not
|
|
2360
|
+
# type the element, and an unknown value is honest where a missing one is not.
|
|
2361
|
+
def record_binding_target_ivars(node, scope, class_name, accumulator)
|
|
2362
|
+
names = BindingTargets.names(node, Prism::InstanceVariableTargetNode)
|
|
2363
|
+
return if names.empty?
|
|
2364
|
+
|
|
2365
|
+
type = BindingTargets.value_type(node, scope)
|
|
2366
|
+
names.each { |name| accumulate_ivar_type(accumulator, class_name, name, type) }
|
|
2219
2367
|
end
|
|
2220
2368
|
|
|
2369
|
+
# The names a `rescue =>` reference or a `for` index binds through a target of `target_class`, looking through
|
|
2370
|
+
# a splat and a multi-target index.
|
|
2371
|
+
module BindingTargets
|
|
2372
|
+
EMPTY = [].freeze
|
|
2373
|
+
private_constant :EMPTY
|
|
2374
|
+
|
|
2375
|
+
module_function
|
|
2376
|
+
|
|
2377
|
+
def names(node, target_class)
|
|
2378
|
+
root =
|
|
2379
|
+
case node
|
|
2380
|
+
when Prism::RescueNode then node.reference
|
|
2381
|
+
when Prism::ForNode then node.index
|
|
2382
|
+
end
|
|
2383
|
+
return EMPTY if root.nil?
|
|
2384
|
+
|
|
2385
|
+
collect(root, target_class, [])
|
|
2386
|
+
end
|
|
2387
|
+
|
|
2388
|
+
# What an ivar target is written with: a rescue reference's exception instance, as the in-method binding
|
|
2389
|
+
# types it ({ErrorInfo.rescue_exception_type}), a `for` index's `Dynamic[top]` floor.
|
|
2390
|
+
def value_type(node, scope)
|
|
2391
|
+
return Type::Combinator.untyped unless node.is_a?(Prism::RescueNode)
|
|
2392
|
+
|
|
2393
|
+
ErrorInfo.rescue_exception_type(node) { |exception_node| scope.type_of(exception_node) }
|
|
2394
|
+
end
|
|
2395
|
+
|
|
2396
|
+
def collect(target, target_class, acc)
|
|
2397
|
+
case target
|
|
2398
|
+
when target_class then acc << target.name
|
|
2399
|
+
when Prism::SplatNode then collect(target.expression, target_class, acc) if target.expression
|
|
2400
|
+
when Prism::MultiTargetNode
|
|
2401
|
+
[*target.lefts, target.rest, *target.rights].compact.each { |t| collect(t, target_class, acc) }
|
|
2402
|
+
end
|
|
2403
|
+
acc
|
|
2404
|
+
end
|
|
2405
|
+
end
|
|
2406
|
+
private_constant :BindingTargets
|
|
2407
|
+
|
|
2221
2408
|
def falsey_constant?(type)
|
|
2222
2409
|
type.is_a?(Type::Constant) && (type.value.nil? || type.value == false)
|
|
2223
2410
|
end
|
|
@@ -2242,7 +2429,7 @@ module Rigor
|
|
|
2242
2429
|
def legacy_class_cvar_index(root, default_scope)
|
|
2243
2430
|
accumulator = {}
|
|
2244
2431
|
walk_class_cvars(root, [], default_scope, accumulator)
|
|
2245
|
-
accumulator
|
|
2432
|
+
finished_cvar_census(accumulator)
|
|
2246
2433
|
end
|
|
2247
2434
|
|
|
2248
2435
|
# `def_owner` names the class a `def` leaf's cvar writes belong to when `self` is
|
|
@@ -2320,6 +2507,8 @@ module Rigor
|
|
|
2320
2507
|
_self_decl, child_prefix, child_cref = ctx
|
|
2321
2508
|
return true unless node.body
|
|
2322
2509
|
|
|
2510
|
+
collect_body_cvar_writes(node.body, child_cref ? [] : child_prefix,
|
|
2511
|
+
scope_entering_declaration(default_scope, node.constant_path), accumulator)
|
|
2323
2512
|
walk_class_cvars(node.body, child_cref ? [] : child_prefix,
|
|
2324
2513
|
scope_entering_declaration(default_scope, node.constant_path), accumulator,
|
|
2325
2514
|
singleton_cref: child_cref)
|
|
@@ -2369,24 +2558,89 @@ module Rigor
|
|
|
2369
2558
|
class_name = qualified_prefix.join("::")
|
|
2370
2559
|
body_scope = census_body_scope(default_scope, qualified_prefix,
|
|
2371
2560
|
Type::Combinator.nominal_of(class_name))
|
|
2372
|
-
gather_cvar_writes(def_node.body, body_scope, class_name, accumulator)
|
|
2561
|
+
gather_cvar_writes(def_node.body, body_scope, class_name, accumulator, join_only: false)
|
|
2562
|
+
end
|
|
2563
|
+
|
|
2564
|
+
# Issue #3204 — the writes a `class` / `module` body makes outside its `def`s (`@@x = nil` at the top of
|
|
2565
|
+
# the body, in a block or a `class << self` there): Ruby resolves each through the body's lexical cref,
|
|
2566
|
+
# so it is the same cvar a sibling `def` reads. Such a write only joins an entry a `def` body's write of
|
|
2567
|
+
# the same class creates: the census misses most of the other ways a cvar is written
|
|
2568
|
+
# (`||=`, `+=`, an including class, a subclass, another file, `class_variable_set` from outside), so a
|
|
2569
|
+
# body-only seed would be precise where Ruby's value is not, and stays unbound. The rvalue is typed with
|
|
2570
|
+
# the class itself as `self`. The gather stops at `def`, `class` and `module`, which their own events enter.
|
|
2571
|
+
def collect_body_cvar_writes(body, qualified_prefix, default_scope, accumulator)
|
|
2572
|
+
return if body.nil? || qualified_prefix.empty?
|
|
2573
|
+
|
|
2574
|
+
class_name = qualified_prefix.join("::")
|
|
2575
|
+
body_scope = census_body_scope(default_scope, qualified_prefix,
|
|
2576
|
+
Type::Combinator.singleton_of(class_name))
|
|
2577
|
+
gather_cvar_writes(body, body_scope, class_name, accumulator, join_only: true)
|
|
2373
2578
|
end
|
|
2374
2579
|
|
|
2375
|
-
def gather_cvar_writes(node, scope, class_name, accumulator)
|
|
2580
|
+
def gather_cvar_writes(node, scope, class_name, accumulator, join_only:)
|
|
2376
2581
|
return unless node.is_a?(Prism::Node)
|
|
2377
2582
|
|
|
2378
|
-
|
|
2583
|
+
if node.is_a?(Prism::ClassVariableWriteNode)
|
|
2584
|
+
record_cvar_write(accumulator, class_name, node.name, EmptySeed.erase(node.value, scope.type_of(node.value)),
|
|
2585
|
+
join_only: join_only)
|
|
2586
|
+
end
|
|
2587
|
+
record_reflective_cvar_write(node, scope, class_name, accumulator) if node.is_a?(Prism::CallNode)
|
|
2588
|
+
# Issue #1217 — `rescue => @@c` / `for @@c in xs` write through a target node. Each enters as the
|
|
2589
|
+
# `Dynamic[top]` floor, the rescue reference too: the census keys each class alone, so a superclass's
|
|
2590
|
+
# `@@c = nil` is not in a subclass's entry, and a typed exception entry would read as non-nil there.
|
|
2591
|
+
BindingTargets.names(node, Prism::ClassVariableTargetNode).each do |name|
|
|
2592
|
+
record_cvar_write(accumulator, class_name, name, Type::Combinator.untyped, join_only: join_only)
|
|
2593
|
+
end
|
|
2379
2594
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
2380
2595
|
|
|
2381
|
-
node.rigor_each_child { |c| gather_cvar_writes(c, scope, class_name, accumulator) }
|
|
2596
|
+
node.rigor_each_child { |c| gather_cvar_writes(c, scope, class_name, accumulator, join_only: join_only) }
|
|
2382
2597
|
end
|
|
2383
2598
|
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2599
|
+
# Issue #3204 — `class_variable_set(:@@x, v)` on an implicit or `self` receiver writes the cvar a sibling
|
|
2600
|
+
# `def` reads, as `@@x = v` does. It only joins, like a class-body write: a dynamic name, a `send`, an
|
|
2601
|
+
# explicit receiver or another file may write the same cvar unseen.
|
|
2602
|
+
def record_reflective_cvar_write(node, scope, class_name, accumulator)
|
|
2603
|
+
return unless node.name == :class_variable_set && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
2604
|
+
|
|
2605
|
+
args = node.arguments&.arguments
|
|
2606
|
+
return unless args&.size == 2
|
|
2607
|
+
|
|
2608
|
+
name_node, value = args
|
|
2609
|
+
# A name that is not a cvar's never meets a `def` write's entry, so the join drops it.
|
|
2610
|
+
name = name_node.unescaped if name_node.is_a?(Prism::SymbolNode) || name_node.is_a?(Prism::StringNode)
|
|
2611
|
+
return unless name
|
|
2612
|
+
|
|
2613
|
+
record_cvar_write(accumulator, class_name, name.to_sym, EmptySeed.erase(value, scope.type_of(value)),
|
|
2614
|
+
join_only: true)
|
|
2615
|
+
end
|
|
2616
|
+
|
|
2617
|
+
# A class-body or `class_variable_set` write no `def` body's write has met yet (issue #3204): it joins an
|
|
2618
|
+
# entry a creating write makes later, and {#finished_cvar_census} drops it when none does.
|
|
2619
|
+
JoinOnlyCvarWrite = Data.define(:type)
|
|
2620
|
+
private_constant :JoinOnlyCvarWrite
|
|
2621
|
+
|
|
2622
|
+
# Every census write unions with what the entry already holds, so the seed does not depend on the order of
|
|
2623
|
+
# the writes. A `join_only` write (a class or module body's, or `class_variable_set`) never creates an entry
|
|
2624
|
+
# on its own: it is held as a {JoinOnlyCvarWrite} until a `def` body's write of the same cvar arrives.
|
|
2625
|
+
def record_cvar_write(accumulator, class_name, name, type, join_only:)
|
|
2626
|
+
entries = (accumulator[class_name] ||= {})
|
|
2627
|
+
existing = entries[name]
|
|
2628
|
+
entries[name] =
|
|
2629
|
+
if existing.nil? then join_only ? JoinOnlyCvarWrite.new(type) : type
|
|
2630
|
+
elsif existing.is_a?(JoinOnlyCvarWrite)
|
|
2631
|
+
joined = Type::Combinator.union(existing.type, type)
|
|
2632
|
+
join_only ? JoinOnlyCvarWrite.new(joined) : joined
|
|
2633
|
+
else Type::Combinator.union(existing, type)
|
|
2634
|
+
end
|
|
2635
|
+
end
|
|
2636
|
+
|
|
2637
|
+
# The census as the discovery index holds it: each cvar only joining writes reached dropped, each class's
|
|
2638
|
+
# entry frozen, then the whole.
|
|
2639
|
+
def finished_cvar_census(accumulator)
|
|
2640
|
+
accumulator.filter_map do |class_name, entries|
|
|
2641
|
+
kept = entries.reject { |_, type| type.is_a?(JoinOnlyCvarWrite) }
|
|
2642
|
+
[class_name, kept.freeze] unless kept.empty?
|
|
2643
|
+
end.to_h.freeze
|
|
2390
2644
|
end
|
|
2391
2645
|
|
|
2392
2646
|
# The program-global pre-pass's tables on the seeded scope's discovery index, and each global materialised into
|
|
@@ -2504,7 +2758,7 @@ module Rigor
|
|
|
2504
2758
|
end
|
|
2505
2759
|
|
|
2506
2760
|
def record_global_write(node, scope, accumulator)
|
|
2507
|
-
rvalue_type = scope.type_of(node.value)
|
|
2761
|
+
rvalue_type = EmptySeed.erase(node.value, scope.type_of(node.value))
|
|
2508
2762
|
existing = accumulator[node.name]
|
|
2509
2763
|
accumulator[node.name] =
|
|
2510
2764
|
existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
|
|
@@ -3269,7 +3523,7 @@ module Rigor
|
|
|
3269
3523
|
unless owner.empty?
|
|
3270
3524
|
body_scope = census_body_scope(default_scope, enclosing_prefix, Type::Combinator.singleton_of(owner))
|
|
3271
3525
|
end
|
|
3272
|
-
rvalue_type = meta_new_constant_type(node, full) || body_scope.type_of(node.value)
|
|
3526
|
+
rvalue_type = meta_new_constant_type(node, full) || EmptySeed.erase(node.value, body_scope.type_of(node.value))
|
|
3273
3527
|
existing = accumulator[full]
|
|
3274
3528
|
accumulator[full] = existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
|
|
3275
3529
|
end
|
|
@@ -3443,6 +3697,31 @@ module Rigor
|
|
|
3443
3697
|
tables.siblings]
|
|
3444
3698
|
end
|
|
3445
3699
|
|
|
3700
|
+
# Issue #3015 — the bodies of a concern's `included` / `prepended` / `class_methods` blocks, each with the prefix
|
|
3701
|
+
# of the module whose body writes it ({ScopeIndexer#concern_hook_blocks}). Ruby runs such a block on the includer
|
|
3702
|
+
# (or on `Owner::ClassMethods`), so a `def self.x` or `class << self` def in it is not the module's own singleton
|
|
3703
|
+
# method, and the walks that file one ask {#hook_block?} first. Mixed into an accumulator with a
|
|
3704
|
+
# `hook_block_spans` slot.
|
|
3705
|
+
module HookBlockSpans
|
|
3706
|
+
def mark_hook_blocks(blocks, owner)
|
|
3707
|
+
blocks.each do |block|
|
|
3708
|
+
(self.hook_block_spans ||= []) << [block.location.start_offset, block.location.end_offset, owner]
|
|
3709
|
+
end
|
|
3710
|
+
end
|
|
3711
|
+
|
|
3712
|
+
# Whether `node` sits in a block marked for the module `owner` (a prefix).
|
|
3713
|
+
def hook_block?(node, owner)
|
|
3714
|
+
offset = node.location.start_offset
|
|
3715
|
+
!hook_block_spans.nil? &&
|
|
3716
|
+
hook_block_spans.any? { |start, stop, marked| offset >= start && offset < stop && marked == owner }
|
|
3717
|
+
end
|
|
3718
|
+
|
|
3719
|
+
# Whether `def_node` is a `def self.x` in a block marked for the module `owner`.
|
|
3720
|
+
def hook_block_self_def?(def_node, owner)
|
|
3721
|
+
def_node.receiver.is_a?(Prism::SelfNode) && hook_block?(def_node, owner)
|
|
3722
|
+
end
|
|
3723
|
+
end
|
|
3724
|
+
|
|
3446
3725
|
# The accumulator {#walk_methods_and_def_nodes} threads: the existence table and its issue #992
|
|
3447
3726
|
# envelope twin, which only {#record_method} and {#record_surface_mark} write, and the issue #1120
|
|
3448
3727
|
# refinement table, which only {#record_refinement_defs} writes. That last one stays nil until a
|
|
@@ -3465,7 +3744,9 @@ module Rigor
|
|
|
3465
3744
|
MethodTables = Struct.new(:existence, :envelopes, :refinements, :certainty, :certain, :possible,
|
|
3466
3745
|
:contested_envelopes, :contested_def_nodes, :root, :in_effect, :census,
|
|
3467
3746
|
:refine_text, :literal_seen, :source_bytes, :loose_constants, :block_spans,
|
|
3468
|
-
:definer_history) do
|
|
3747
|
+
:definer_history, :forwardable_extends, :hook_block_spans) do
|
|
3748
|
+
include HookBlockSpans
|
|
3749
|
+
|
|
3469
3750
|
# Issue #2006 — records, in walk order, that a definer other than an alias wrote the instance method `name`
|
|
3470
3751
|
# of `class_name` at `node`: `def_node` for a `def`, nil for a `define_method(:x)` or an `attr_*` reader or
|
|
3471
3752
|
# writer, and whether the write is {#certain}. {ScopeIndexer#apply_alias_def_nodes} reads the history to copy
|
|
@@ -3567,7 +3848,8 @@ module Rigor
|
|
|
3567
3848
|
REFINEMENT_WILDCARD = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
|
|
3568
3849
|
REFINE_WORD = /\brefine\b/
|
|
3569
3850
|
HEREDOC_OPENER = /<<[~-]?["'`A-Za-z_]/
|
|
3570
|
-
|
|
3851
|
+
COMPARISON_OPERATORS = Set[:==, :!=, :===].freeze
|
|
3852
|
+
private_constant :EMPTY_REFINEMENTS, :REFINEMENT_WILDCARD, :REFINE_WORD, :HEREDOC_OPENER, :COMPARISON_OPERATORS
|
|
3571
3853
|
|
|
3572
3854
|
NO_OFFSETS = [].freeze
|
|
3573
3855
|
private_constant :NO_OFFSETS
|
|
@@ -3660,7 +3942,8 @@ module Rigor
|
|
|
3660
3942
|
# `self`, charged to its module or the wildcard), `:literal` (A3's `:refine` / eval-string literal), `:nested` (a
|
|
3661
3943
|
# `refine` in a refine body, or in a singleton method of one), `:dsl` (where `self` is a class, #1689) and
|
|
3662
3944
|
# `:raises` (no block, a Proc as the block, another receiver, or a computed target where `self` is `main`), and
|
|
3663
|
-
# `:data` (a `:refine` literal where `self` is a plain class or one of its instances)
|
|
3945
|
+
# `:data` (a `:refine` literal where `self` is a plain class or one of its instances), and `:inert` (issue #2203:
|
|
3946
|
+
# a `:refine` Symbol that is a hash key or a comparison operand, which no call reads as a method name).
|
|
3664
3947
|
def refine_census(root)
|
|
3665
3948
|
census = []
|
|
3666
3949
|
refinements = build_methods_and_def_nodes(root, census: census)[3]
|
|
@@ -3839,6 +4122,8 @@ module Rigor
|
|
|
3839
4122
|
end
|
|
3840
4123
|
|
|
3841
4124
|
census_call(node, qualified_prefix, tables, refine_body)
|
|
4125
|
+
when Prism::AssocNode
|
|
4126
|
+
census_assoc_key(node, tables)
|
|
3842
4127
|
when Prism::SymbolNode
|
|
3843
4128
|
census_symbol(node, tables)
|
|
3844
4129
|
end
|
|
@@ -3894,6 +4179,8 @@ module Rigor
|
|
|
3894
4179
|
arguments = node.arguments&.arguments
|
|
3895
4180
|
return if arguments.nil?
|
|
3896
4181
|
|
|
4182
|
+
return census_compared_literal(arguments.first, tables) if COMPARISON_OPERATORS.include?(node.name)
|
|
4183
|
+
|
|
3897
4184
|
strings = RefineCensus.string_literal_call?(node)
|
|
3898
4185
|
arguments.each do |argument|
|
|
3899
4186
|
next unless RefineCensus.refine_symbol?(argument) || (strings && RefineCensus.refine_string?(argument))
|
|
@@ -3928,6 +4215,26 @@ module Rigor
|
|
|
3928
4215
|
|
|
3929
4216
|
def constant_shaped?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
3930
4217
|
|
|
4218
|
+
# Issue #2203 — a `:refine` Symbol a comparison reads (`node.name == :refine`, `!=`, `===`) is a value compared,
|
|
4219
|
+
# never a method name a call runs: it is inert (`:inert`), and records no row.
|
|
4220
|
+
def census_compared_literal(argument, tables)
|
|
4221
|
+
return unless RefineCensus.refine_symbol?(argument) && !tables.literal_seen?(argument)
|
|
4222
|
+
|
|
4223
|
+
tables.see_literal(argument)
|
|
4224
|
+
tables.account(argument, :inert)
|
|
4225
|
+
end
|
|
4226
|
+
|
|
4227
|
+
# Issue #2203 — a `:refine` Symbol that is a hash key (`h(x, refine: true)`, `{ refine: 1 }`, `{ :refine => 1 }`)
|
|
4228
|
+
# names an option, not `Module#refine`: it is inert (`:inert`), and records no row. A `:refine` value stays a
|
|
4229
|
+
# literal.
|
|
4230
|
+
def census_assoc_key(node, tables)
|
|
4231
|
+
key = node.key
|
|
4232
|
+
return unless RefineCensus.refine_symbol?(key) && !tables.literal_seen?(key)
|
|
4233
|
+
|
|
4234
|
+
tables.see_literal(key)
|
|
4235
|
+
tables.account(key, :inert)
|
|
4236
|
+
end
|
|
4237
|
+
|
|
3931
4238
|
# A3 — a `:refine` Symbol anywhere else (`alias my_refine refine`, `%i[refine]`, a hash) names `Module#refine`
|
|
3932
4239
|
# for code the walk cannot follow, run on the module the in-effect walk says `self` is there; where `self` is a
|
|
3933
4240
|
# plain class or one of its instances it is data.
|
|
@@ -4101,6 +4408,11 @@ module Rigor
|
|
|
4101
4408
|
census_scan(node.superclass, qualified_prefix, methods_acc)
|
|
4102
4409
|
end
|
|
4103
4410
|
record_declaration_facts(node, child_prefix, methods_acc.at(node)) unless child_cref
|
|
4411
|
+
unless child_cref
|
|
4412
|
+
methods_acc.mark_hook_blocks(concern_hook_blocks(node, child_prefix), child_prefix)
|
|
4413
|
+
record_concern_class_methods(node, child_prefix, methods_acc, def_nodes_acc)
|
|
4414
|
+
record_hook_definer_calls(node, child_prefix, methods_acc)
|
|
4415
|
+
end
|
|
4104
4416
|
body_prefix = child_cref ? [] : child_prefix
|
|
4105
4417
|
if node.body
|
|
4106
4418
|
walk_methods_and_def_nodes(node.body, body_prefix, false, methods_acc, def_nodes_acc,
|
|
@@ -4120,11 +4432,12 @@ module Rigor
|
|
|
4120
4432
|
if !node.expression.is_a?(Prism::SelfNode) || tables.in_block?(node)
|
|
4121
4433
|
record_singleton_body_prepends(node, qualified_prefix, tables)
|
|
4122
4434
|
end
|
|
4435
|
+
record_constant_singleton_prepends(node, qualified_prefix, tables)
|
|
4123
4436
|
walk_methods_and_def_nodes(node.expression, qualified_prefix, in_singleton_class,
|
|
4124
4437
|
methods_acc, def_nodes_acc, source_path, def_owner_prefix,
|
|
4125
4438
|
singleton_cref: singleton_cref,
|
|
4126
4439
|
defs_singleton: defs_singleton)
|
|
4127
|
-
if node.body
|
|
4440
|
+
if node.body && !hook_block_singleton?(node, in_singleton_class, owner_prefix, methods_acc)
|
|
4128
4441
|
# The body's lexical cref is unnameable AND its qualified prefix stays the
|
|
4129
4442
|
# ENCLOSING lexical one — `class C3::CD` below `class <<` resolves `C3`
|
|
4130
4443
|
# lexically, never against the singleton's name. `singleton_prefix` rides
|
|
@@ -4138,6 +4451,7 @@ module Rigor
|
|
|
4138
4451
|
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
|
|
4139
4452
|
Prism::ConstantPathOrWriteNode
|
|
4140
4453
|
record_meta_superclass(node, qualified_prefix, def_owner_prefix, methods_acc.at(node))
|
|
4454
|
+
record_struct_same_class(node, qualified_prefix, def_owner_prefix, methods_acc.at(node))
|
|
4141
4455
|
if (split = meta_new_block_split(node, qualified_prefix, def_owner_prefix, singleton_cref))
|
|
4142
4456
|
enclosing, body, body_self = split
|
|
4143
4457
|
# A meta-new block rebinds only `self` — `Module.nesting` stays lexical — so
|
|
@@ -4179,11 +4493,13 @@ module Rigor
|
|
|
4179
4493
|
node.rigor_each_child { |child| census_scan(child, qualified_prefix, methods_acc) }
|
|
4180
4494
|
end
|
|
4181
4495
|
record_foreign_singleton_def(node, qualified_prefix, owner_prefix, methods_acc.at(node))
|
|
4496
|
+
return if methods_acc.hook_block_self_def?(node, owner_prefix)
|
|
4497
|
+
|
|
4182
4498
|
unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
|
|
4183
4499
|
(singleton_cref && owner_prefix.empty?)
|
|
4184
|
-
singleton_def = in_singleton_class
|
|
4500
|
+
singleton_def = def_context(in_singleton_class, defs_singleton)
|
|
4185
4501
|
record_def_method(node, owner_prefix, singleton_def, methods_acc.at(node))
|
|
4186
|
-
record_def_body_evidence(node, owner_prefix, methods_acc)
|
|
4502
|
+
record_def_body_evidence(node, owner_prefix, methods_acc, singleton_def ? :singleton : :instance)
|
|
4187
4503
|
record_def_node(node, owner_prefix, singleton_def, def_nodes_acc, methods_acc)
|
|
4188
4504
|
end
|
|
4189
4505
|
return
|
|
@@ -4199,11 +4515,15 @@ module Rigor
|
|
|
4199
4515
|
end
|
|
4200
4516
|
end
|
|
4201
4517
|
return
|
|
4518
|
+
when Prism::AssocNode
|
|
4519
|
+
census_assoc_key(node, methods_acc) if methods_acc.mentions_refine?
|
|
4202
4520
|
when Prism::SymbolNode
|
|
4203
4521
|
census_symbol(node, methods_acc) if methods_acc.mentions_refine?
|
|
4204
4522
|
when Prism::CallNode
|
|
4205
4523
|
census_literal_arguments(node, qualified_prefix, methods_acc) if methods_acc.mentions_refine?
|
|
4206
4524
|
if receiver_eval_call?(node)
|
|
4525
|
+
record_singleton_eval_definers(node, owner_prefix, in_singleton_class, methods_acc.at(node))
|
|
4526
|
+
record_constant_singleton_eval_defs(node, qualified_prefix, methods_acc.at(node))
|
|
4207
4527
|
return walk_eval_methods_and_defs(node, qualified_prefix, in_singleton_class, methods_acc,
|
|
4208
4528
|
def_nodes_acc, source_path, def_owner_prefix,
|
|
4209
4529
|
singleton_cref: singleton_cref,
|
|
@@ -4212,13 +4532,15 @@ module Rigor
|
|
|
4212
4532
|
anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc.at(node),
|
|
4213
4533
|
source_path)
|
|
4214
4534
|
unless in_singleton_class
|
|
4215
|
-
superseded = call_superseded_names(node)
|
|
4535
|
+
superseded = call_superseded_names(node, owner_prefix, methods_acc)
|
|
4216
4536
|
supersede_def_nodes(superseded, owner_prefix, def_nodes_acc, methods_acc.at(node))
|
|
4217
4537
|
note_call_definers(node, superseded, owner_prefix, methods_acc.at(node))
|
|
4218
4538
|
end
|
|
4219
4539
|
tables = methods_acc.at(node)
|
|
4220
4540
|
record_singleton_prepend(node, qualified_prefix, tables,
|
|
4221
4541
|
self_ok: node.name == :prepend && tables.in_block?(node))
|
|
4542
|
+
own_prefix = def_owner_prefix&.empty? || defs_singleton == :unnameable ? EMPTY_PREFIX : owner_prefix
|
|
4543
|
+
record_own_singleton_prepend(node, [own_prefix, qualified_prefix], in_singleton_class, tables)
|
|
4222
4544
|
# Issue #1120 — a refine body's defs go to the refinement table and nowhere else.
|
|
4223
4545
|
if (target = methods_acc.refine_target_of(node))
|
|
4224
4546
|
return record_literal_target_refine(node, target, qualified_prefix, owner_prefix, methods_acc)
|
|
@@ -4332,6 +4654,27 @@ module Rigor
|
|
|
4332
4654
|
record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
|
|
4333
4655
|
end
|
|
4334
4656
|
|
|
4657
|
+
# Issue #2979 — `K = Struct.new("Named", :a)`, with or without a block, binds `K` and `Struct::Named` to one class
|
|
4658
|
+
# object, so each name's bucket takes a `[ENVELOPE_SAME_CLASS_KIND, other]` entry and the method chain of either
|
|
4659
|
+
# reads the other's tables (`Scope::ResolutionChain`). Only the bare factory call qualifies: under a wrapping
|
|
4660
|
+
# `Class.new(Struct.new("Named", :a))` the constant names a SUBCLASS of `Struct::Named`, which already reaches it
|
|
4661
|
+
# as an ancestor only one way.
|
|
4662
|
+
def record_struct_same_class(node, qualified_prefix, self_base, tables)
|
|
4663
|
+
rvalue = meta_new_rvalue(node)
|
|
4664
|
+
return unless struct_new_call?(rvalue)
|
|
4665
|
+
|
|
4666
|
+
named = AnonymousMetaClass.struct_class_name(rvalue)
|
|
4667
|
+
return if named.nil?
|
|
4668
|
+
|
|
4669
|
+
child_prefix = meta_new_child_prefix(node, qualified_prefix, self_base)
|
|
4670
|
+
return if child_prefix.nil? || child_prefix.empty?
|
|
4671
|
+
|
|
4672
|
+
class_name = child_prefix.join("::")
|
|
4673
|
+
kind = Scope::DiscoveryIndex::ENVELOPE_SAME_CLASS_KIND
|
|
4674
|
+
record_envelope(tables.envelopes, class_name, [kind, named], Source::ParameterEnvelope::OPAQUE)
|
|
4675
|
+
record_envelope(tables.envelopes, named, [kind, class_name], Source::ParameterEnvelope::OPAQUE)
|
|
4676
|
+
end
|
|
4677
|
+
|
|
4335
4678
|
def record_meta_new_facts(rvalue, child_prefix, tables)
|
|
4336
4679
|
record_meta_members(rvalue, child_prefix, tables)
|
|
4337
4680
|
# `Module.new(&BODY)`: the body runs on the new module but cannot be read, so its surface is unknown (#1816).
|
|
@@ -4359,11 +4702,111 @@ module Rigor
|
|
|
4359
4702
|
record_define_method(node, qualified_prefix, in_singleton_class, methods_acc) if node.name == :define_method
|
|
4360
4703
|
record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc) if attr_macro(node)
|
|
4361
4704
|
record_module_attr_methods(node, qualified_prefix, methods_acc) if MODULE_ATTR_MACROS.key?(node.name)
|
|
4705
|
+
record_forwardable_methods(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
4362
4706
|
record_alias_method_call(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
4363
4707
|
record_surface_evidence(node, qualified_prefix, methods_acc)
|
|
4708
|
+
record_self_definer_call(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
4364
4709
|
AnonymousMetaClass.name_for(node, source_path)
|
|
4365
4710
|
end
|
|
4366
4711
|
|
|
4712
|
+
# Issue #2901 — the class-body calls on `self` that open the class to `call.undefined-method` without a name
|
|
4713
|
+
# this walk can read: `send(:define_method, m)` (the instance side, or the singleton side in a `class << self`
|
|
4714
|
+
# body), `define_singleton_method(m)` (the singleton side, outside `class << self`, where it would define on the
|
|
4715
|
+
# singleton's own singleton), and a call of any other name, filed as a
|
|
4716
|
+
# {Scope::DiscoveryIndex::ENVELOPE_BODY_CALL_KIND} entry for the reader to resolve against the class's macros.
|
|
4717
|
+
def record_self_definer_call(node, qualified_prefix, in_singleton_class, accumulator)
|
|
4718
|
+
return if qualified_prefix.empty? || !(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
4719
|
+
|
|
4720
|
+
class_name = qualified_prefix.join("::")
|
|
4721
|
+
if node.name == :define_singleton_method
|
|
4722
|
+
record_open_singleton_definer(node, class_name, in_singleton_class, accumulator)
|
|
4723
|
+
record_literal_self_definer(node, class_name, in_singleton_class, accumulator)
|
|
4724
|
+
elsif SURFACE_SEND_CALLS.include?(node.name)
|
|
4725
|
+
record_sent_define_method(node, class_name, in_singleton_class, accumulator)
|
|
4726
|
+
record_literal_self_definer(node, class_name, in_singleton_class, accumulator)
|
|
4727
|
+
elsif MODULE_NEW_MIXIN_CALLS.include?(node.name)
|
|
4728
|
+
record_module_new_mixin(node, class_name, in_singleton_class, accumulator)
|
|
4729
|
+
elsif !in_singleton_class && !BODY_CALL_EXCLUDED.include?(node.name)
|
|
4730
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_BODY_CALL_KIND, node.name]
|
|
4731
|
+
record_envelope(accumulator.envelopes, class_name, key, Source::ParameterEnvelope::OPAQUE)
|
|
4732
|
+
end
|
|
4733
|
+
end
|
|
4734
|
+
|
|
4735
|
+
# The class-body calls this walk reads as definers of their own, or that core's `Module` answers: a project
|
|
4736
|
+
# macro of one of these names is not looked for.
|
|
4737
|
+
BODY_CALL_EXCLUDED = %i[
|
|
4738
|
+
define_method define_singleton_method alias_method attr_reader attr_writer attr_accessor include extend prepend
|
|
4739
|
+
private public protected module_function private_class_method public_class_method private_constant
|
|
4740
|
+
public_constant require require_relative autoload autoload_relative
|
|
4741
|
+
].to_set.freeze
|
|
4742
|
+
private_constant :BODY_CALL_EXCLUDED
|
|
4743
|
+
|
|
4744
|
+
def record_open_singleton_definer(node, class_name, in_singleton_class, accumulator)
|
|
4745
|
+
return if in_singleton_class || !computed_first_name?(node.arguments&.arguments)
|
|
4746
|
+
|
|
4747
|
+
record_surface_mark(accumulator, class_name, Scope::DiscoveryIndex::ENVELOPE_OPEN_SINGLETON_NAMES_MARK)
|
|
4748
|
+
end
|
|
4749
|
+
|
|
4750
|
+
def record_sent_define_method(node, class_name, in_singleton_class, accumulator)
|
|
4751
|
+
arguments = node.arguments&.arguments
|
|
4752
|
+
return unless arguments && literal_method_name(arguments.first) == :define_method
|
|
4753
|
+
return unless computed_first_name?(arguments.drop(1))
|
|
4754
|
+
|
|
4755
|
+
record_surface_mark(accumulator, class_name, open_names_mark(in_singleton_class))
|
|
4756
|
+
end
|
|
4757
|
+
|
|
4758
|
+
# Issue #2975 — `include(Module.new { %i[a b].each { |x| define_method(x) {} } })` mixes in a module whose body
|
|
4759
|
+
# runs a computed `define_method` on its own `self`, so the side the mixin lands on (the instance side, or the
|
|
4760
|
+
# singleton side in a `class << self` body) is open as a direct `define_method(m)` there would leave it.
|
|
4761
|
+
def record_module_new_mixin(node, class_name, in_singleton_class, accumulator)
|
|
4762
|
+
return unless module_new_definer_argument?(node)
|
|
4763
|
+
|
|
4764
|
+
record_surface_mark(accumulator, class_name, open_names_mark(in_singleton_class))
|
|
4765
|
+
end
|
|
4766
|
+
|
|
4767
|
+
MODULE_NEW_MIXIN_CALLS = %i[include prepend].to_set.freeze
|
|
4768
|
+
private_constant :MODULE_NEW_MIXIN_CALLS
|
|
4769
|
+
|
|
4770
|
+
# Whether a mixin call's argument is a `Module.new { … }` whose body runs a computed `define_method` on the
|
|
4771
|
+
# module ({#runs_computed_self_definer?}).
|
|
4772
|
+
def module_new_definer_argument?(node)
|
|
4773
|
+
(node.arguments&.arguments || []).any? do |argument|
|
|
4774
|
+
module_new_call?(argument) && argument.block.is_a?(Prism::BlockNode) &&
|
|
4775
|
+
runs_computed_self_definer?(argument.block.body)
|
|
4776
|
+
end
|
|
4777
|
+
end
|
|
4778
|
+
|
|
4779
|
+
# Issue #2975 — `singleton_class.class_eval { def my_attr(*n) = n.each { |x| define_method(x) {} } }` in a class
|
|
4780
|
+
# body, or the same on `Const.singleton_class`, defines a class macro the eval-body walk files under no owner: the
|
|
4781
|
+
# method takes the singleton-side definer entry {#record_computed_definer} gives a `def self.my_attr`.
|
|
4782
|
+
def record_singleton_eval_definers(node, qualified_prefix, in_singleton_class, tables)
|
|
4783
|
+
return if in_singleton_class || !CLASS_EVAL_BLOCK_CALLS.include?(node.name)
|
|
4784
|
+
|
|
4785
|
+
receiver = node.receiver
|
|
4786
|
+
return unless singleton_class_of_receiver?(receiver)
|
|
4787
|
+
|
|
4788
|
+
current = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
|
|
4789
|
+
owner = singleton_class_owner(node, receiver.receiver, qualified_prefix, current)
|
|
4790
|
+
return if owner.nil?
|
|
4791
|
+
|
|
4792
|
+
body = node.block.body
|
|
4793
|
+
return unless body.is_a?(Prism::StatementsNode)
|
|
4794
|
+
|
|
4795
|
+
body.body.each do |statement|
|
|
4796
|
+
next unless statement.is_a?(Prism::DefNode) && statement.receiver.nil?
|
|
4797
|
+
|
|
4798
|
+
record_computed_definer(statement, owner.split("::"), tables, :singleton)
|
|
4799
|
+
end
|
|
4800
|
+
end
|
|
4801
|
+
|
|
4802
|
+
CLASS_EVAL_BLOCK_CALLS = %i[class_eval module_eval class_exec module_exec].to_set.freeze
|
|
4803
|
+
private_constant :CLASS_EVAL_BLOCK_CALLS
|
|
4804
|
+
|
|
4805
|
+
# Whether a definer's argument list starts with a name that is not a Symbol or String literal.
|
|
4806
|
+
def computed_first_name?(arguments)
|
|
4807
|
+
!arguments.nil? && !arguments.empty? && literal_method_name(arguments.first).nil?
|
|
4808
|
+
end
|
|
4809
|
+
|
|
4367
4810
|
# The `alias_method :new, :old` CallNode twin of {#record_alias_method} (#533): the NEW name joins
|
|
4368
4811
|
# the discovered-methods table so calls to it stop reading undefined, and
|
|
4369
4812
|
# {#apply_alias_def_nodes}'s map walk gives it the ORIGINAL def node for return inference.
|
|
@@ -4664,17 +5107,27 @@ module Rigor
|
|
|
4664
5107
|
# (#590). Their bodies are entered as class bodies, so a member read inside one dispatches on the struct's
|
|
4665
5108
|
# own class and must find the reader there: a member that shadows a `Kernel` private (`lambda`) otherwise
|
|
4666
5109
|
# falls through to `Kernel#lambda`, and `lambda.upcase` reports an undefined method on `Proc` — a wrong-type
|
|
4667
|
-
# diagnostic on correct code. No-op for `Class.new` / `Module.new
|
|
4668
|
-
#
|
|
5110
|
+
# diagnostic on correct code. No-op for `Class.new` / `Module.new`. A splatted member list registers its literal
|
|
5111
|
+
# members and opens the instance side ({#record_meta_member_names}).
|
|
4669
5112
|
def record_meta_members(factory_call, qualified_prefix, accumulator, allow_outer_block: true)
|
|
4670
5113
|
factory_call = resolve_meta_factory_call(factory_call, allow_outer_block: allow_outer_block)
|
|
4671
5114
|
return unless factory_call
|
|
4672
5115
|
|
|
4673
|
-
|
|
4674
|
-
|
|
5116
|
+
# Issue #2968 — `K = Struct.new("Named", :a)` makes one class under two names, `K` and `Struct::Named`; the
|
|
5117
|
+
# members are credited on both.
|
|
5118
|
+
named = AnonymousMetaClass.struct_class_name(factory_call) if struct_new_call?(factory_call)
|
|
5119
|
+
[qualified_prefix.join("::"), named].compact.uniq.each do |class_name|
|
|
5120
|
+
record_meta_member_names(factory_call, class_name, accumulator)
|
|
5121
|
+
end
|
|
5122
|
+
end
|
|
4675
5123
|
|
|
4676
|
-
|
|
4677
|
-
|
|
5124
|
+
# Issue #2968 — a splatted member list (`Struct.new(*FIELDS)`) names members this walk cannot read, so the
|
|
5125
|
+
# instance side opens: no name is provably missing there.
|
|
5126
|
+
def record_meta_member_names(factory_call, class_name, accumulator)
|
|
5127
|
+
if meta_members_open?(factory_call)
|
|
5128
|
+
record_surface_mark(accumulator, class_name, Scope::DiscoveryIndex::ENVELOPE_OPEN_INSTANCE_NAMES_MARK)
|
|
5129
|
+
end
|
|
5130
|
+
meta_member_names(factory_call).each { |member| record_method(accumulator, class_name, member, :instance) }
|
|
4678
5131
|
end
|
|
4679
5132
|
|
|
4680
5133
|
# Unwinds nested single-parent `Class.new(...)` calls to a root `Struct.new(...)` / `Data.define(...)`.
|
|
@@ -4705,22 +5158,33 @@ module Rigor
|
|
|
4705
5158
|
data_define_call?(call_node) || struct_new_call?(call_node)
|
|
4706
5159
|
end
|
|
4707
5160
|
|
|
4708
|
-
# The Symbol member names of a `Data.define
|
|
4709
|
-
#
|
|
4710
|
-
# String class name is NOT stripped, so `Struct.new("Name", :a) do` registers no members and the body
|
|
4711
|
-
# is entered under the anonymous name (consistent between passes). `Data.define` args are all Symbols.
|
|
5161
|
+
# The literal Symbol member names of a `Data.define` / `Struct.new` call ({#meta_member_positionals}). A splatted
|
|
5162
|
+
# argument contributes none; {#meta_members_open?} says the list is then incomplete.
|
|
4712
5163
|
def meta_member_names(call_node)
|
|
5164
|
+
meta_member_positionals(call_node).filter_map { |arg| arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) }
|
|
5165
|
+
end
|
|
5166
|
+
|
|
5167
|
+
# Issue #2968 — whether a `Data.define` / `Struct.new` call's member list holds a splat, so its members are not
|
|
5168
|
+
# all literal and the reader set is unknown. A layout table or a closed reader set must not be built from it.
|
|
5169
|
+
def meta_members_open?(call_node)
|
|
5170
|
+
meta_member_positionals(call_node).any?(Prism::SplatNode)
|
|
5171
|
+
end
|
|
5172
|
+
|
|
5173
|
+
# The member arguments of a `Data.define` / `Struct.new` call. For `Struct.new` the trailing `keyword_init:` hash
|
|
5174
|
+
# is stripped ({#struct_new_positionals}). A leading class-name String (`Struct.new("Named", :a)`) is neither a
|
|
5175
|
+
# Symbol nor a splat, so it is no member and does not open the list.
|
|
5176
|
+
def meta_member_positionals(call_node)
|
|
4713
5177
|
raw = call_node.arguments&.arguments || []
|
|
4714
|
-
|
|
4715
|
-
symbols.filter_map { |arg| arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) }
|
|
5178
|
+
struct_new_call?(call_node) ? struct_new_positionals(raw) : raw
|
|
4716
5179
|
end
|
|
4717
5180
|
|
|
4718
5181
|
def record_def_method(def_node, qualified_prefix, in_singleton_class, accumulator)
|
|
4719
5182
|
return if qualified_prefix.empty?
|
|
4720
5183
|
|
|
4721
5184
|
class_name = qualified_prefix.join("::")
|
|
4722
|
-
|
|
4723
|
-
|
|
5185
|
+
kind = def_side(def_node, qualified_prefix, in_singleton_class)
|
|
5186
|
+
return if kind.nil?
|
|
5187
|
+
|
|
4724
5188
|
record_method(accumulator, class_name, def_node.name, kind, Source::ParameterEnvelope.of(def_node))
|
|
4725
5189
|
end
|
|
4726
5190
|
|
|
@@ -4729,12 +5193,20 @@ module Rigor
|
|
|
4729
5193
|
# `def decorate(obj) = obj.extend(Decorator)`. A class-body macro records the same evidence in
|
|
4730
5194
|
# {#record_surface_evidence}; `send` is left out here because inside a body it is an ordinary call far
|
|
4731
5195
|
# more often than a definition.
|
|
4732
|
-
def
|
|
5196
|
+
# The side a `def`'s body evidence is filed on: a `def self.x`'s singleton side, unless the caller files it as an
|
|
5197
|
+
# instance method elsewhere (`receiver_side: false`, #3005's `Owner::<included>`).
|
|
5198
|
+
def evidence_side(def_node, side, receiver_side) = receiver_side && def_node.receiver ? :singleton : side
|
|
5199
|
+
|
|
5200
|
+
def record_def_body_evidence(def_node, qualified_prefix, tables, side, receiver_side: true)
|
|
4733
5201
|
return if def_node.body.nil?
|
|
4734
5202
|
|
|
5203
|
+
side = evidence_side(def_node, side, receiver_side)
|
|
5204
|
+
|
|
5205
|
+
definer = false
|
|
4735
5206
|
Source::NodeWalker.each(def_node.body) do |node|
|
|
4736
5207
|
if node.is_a?(Prism::SingletonClassNode)
|
|
4737
5208
|
record_singleton_body_prepends(node, qualified_prefix, tables)
|
|
5209
|
+
record_constant_singleton_prepends(node, qualified_prefix, tables, owner: true)
|
|
4738
5210
|
next
|
|
4739
5211
|
end
|
|
4740
5212
|
next unless node.is_a?(Prism::CallNode)
|
|
@@ -4742,13 +5214,629 @@ module Rigor
|
|
|
4742
5214
|
if node.name == :extend
|
|
4743
5215
|
record_object_extension(node, qualified_prefix, tables)
|
|
4744
5216
|
elsif SURFACE_EVAL_CALLS.include?(node.name) || SURFACE_NAMING_CALLS.include?(node.name)
|
|
5217
|
+
definer ||= computed_self_define_method?(node)
|
|
4745
5218
|
record_body_rewrite(node, qualified_prefix, tables)
|
|
4746
5219
|
else
|
|
4747
5220
|
record_singleton_prepend(node, qualified_prefix, tables, self_ok: true)
|
|
4748
5221
|
end
|
|
4749
5222
|
end
|
|
5223
|
+
record_computed_definer(def_node, qualified_prefix, tables, side) if definer
|
|
5224
|
+
record_hook_extends(def_node, qualified_prefix, tables, side)
|
|
5225
|
+
end
|
|
5226
|
+
|
|
5227
|
+
# Issue #2975 — an ActiveSupport::Concern-style hook, `def self.included(base) = base.extend(ClassMethods)`, puts
|
|
5228
|
+
# `ClassMethods` on every includer's singleton chain, which no `extend` edge of the includer records. The module
|
|
5229
|
+
# takes a `[ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods"]` entry per constant extended on the hook's parameter, as
|
|
5230
|
+
# written, so `call.undefined-method` can resolve an includer's class-body macro calls against it.
|
|
5231
|
+
#
|
|
5232
|
+
# Issue #1592 — the entry also names the hook (`[ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods", :included]`), so the
|
|
5233
|
+
# includer's singleton resolution chain draws the module only for the edge whose hook runs. Only a direct
|
|
5234
|
+
# statement of the hook body with constant arguments and no `return` / `next` / `break` in an earlier
|
|
5235
|
+
# statement is a fact the chain can place. Any other extend of the parameter anywhere in the body (nested,
|
|
5236
|
+
# conditional, after an early exit, or with an argument that is not a constant), a `base.send(:extend, …)`, a
|
|
5237
|
+
# `base.singleton_class.include` / `prepend` and a `class_eval`-family block on the parameter take a
|
|
5238
|
+
# `[ENVELOPE_HOOK_EXTEND_OPEN_KIND, hook]` entry instead of, or beside, the named one. Each definition also takes
|
|
5239
|
+
# a `[ENVELOPE_HOOK_DEF_KIND, hook, source]` entry, so a module that defines the hook twice (in two files, or
|
|
5240
|
+
# twice in one) is read as open: which definition Ruby keeps is load order.
|
|
5241
|
+
def record_hook_extends(def_node, qualified_prefix, tables, side)
|
|
5242
|
+
return unless side == :singleton && HOOK_EXTEND_DEFS.include?(def_node.name) && !qualified_prefix.empty?
|
|
5243
|
+
|
|
5244
|
+
owner = qualified_prefix.join("::")
|
|
5245
|
+
hook = def_node.name
|
|
5246
|
+
def_key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_DEF_KIND, hook, def_node.slice.split.join(" ")]
|
|
5247
|
+
record_envelope(tables.envelopes, owner, def_key, Source::ParameterEnvelope::OPAQUE)
|
|
5248
|
+
param = def_node.parameters&.requireds&.first
|
|
5249
|
+
return unless param.is_a?(Prism::RequiredParameterNode)
|
|
5250
|
+
|
|
5251
|
+
target = ->(call) { hook_param_receiver?(call.receiver, param.name) }
|
|
5252
|
+
scan_hook_body(def_node.body, target, owner, tables, hook) do |node|
|
|
5253
|
+
hook_singleton_reshape?(node, param.name)
|
|
5254
|
+
end
|
|
5255
|
+
end
|
|
5256
|
+
|
|
5257
|
+
# The shared walk of a hook body or a concern's hook block: `target` says whether a call's receiver is the
|
|
5258
|
+
# includer, the block says whether a call reshapes the includer's singleton some other way.
|
|
5259
|
+
def scan_hook_body(body, target, owner, tables, hook)
|
|
5260
|
+
placeable = placeable_hook_statements(body)
|
|
5261
|
+
Source::NodeWalker.each(body) do |node|
|
|
5262
|
+
if node.is_a?(Prism::CallNode) && node.name == :extend && target.call(node)
|
|
5263
|
+
complete = record_hook_extend_arguments(node, owner, tables, hook)
|
|
5264
|
+
record_hook_extend_open(owner, tables, hook) unless complete && placeable.include?(node)
|
|
5265
|
+
elsif (node.is_a?(Prism::CallNode) && yield(node)) || singleton_mixin_body?(node)
|
|
5266
|
+
record_hook_extend_open(owner, tables, hook)
|
|
5267
|
+
end
|
|
5268
|
+
end
|
|
5269
|
+
end
|
|
5270
|
+
|
|
5271
|
+
# The direct statements of a body that run unless an earlier statement raised: those before the first
|
|
5272
|
+
# statement holding a `return`, `next` or `break` anywhere in it. An endless `def` has its one expression.
|
|
5273
|
+
def placeable_hook_statements(body)
|
|
5274
|
+
statements = body.is_a?(Prism::StatementsNode) ? body.body : [body].compact
|
|
5275
|
+
placeable = Set.new.compare_by_identity
|
|
5276
|
+
statements.each do |statement|
|
|
5277
|
+
break if exits_early?(statement)
|
|
5278
|
+
|
|
5279
|
+
placeable << statement
|
|
5280
|
+
end
|
|
5281
|
+
placeable
|
|
5282
|
+
end
|
|
5283
|
+
|
|
5284
|
+
# A `next` or `break` inside a block or lambda leaves only that block, and a lambda's `return` only the lambda;
|
|
5285
|
+
# a `return` inside a block leaves the hook.
|
|
5286
|
+
def exits_early?(node, in_block: false)
|
|
5287
|
+
case node
|
|
5288
|
+
when Prism::ReturnNode then true
|
|
5289
|
+
when Prism::NextNode, Prism::BreakNode then !in_block
|
|
5290
|
+
when Prism::LambdaNode then false
|
|
5291
|
+
else
|
|
5292
|
+
inner = in_block || node.is_a?(Prism::BlockNode)
|
|
5293
|
+
node.rigor_each_child { |child| return true if exits_early?(child, in_block: inner) }
|
|
5294
|
+
false
|
|
5295
|
+
end
|
|
5296
|
+
end
|
|
5297
|
+
|
|
5298
|
+
# A `class << self` body that mixes in, anywhere under a hook body or block.
|
|
5299
|
+
def singleton_mixin_body?(node)
|
|
5300
|
+
return false unless node.is_a?(Prism::SingletonClassNode) && node.expression.is_a?(Prism::SelfNode)
|
|
5301
|
+
|
|
5302
|
+
found = false
|
|
5303
|
+
Source::NodeWalker.each(node.body) do |child|
|
|
5304
|
+
found ||= child.is_a?(Prism::CallNode) && MIXIN_SELF_CALLS.include?(child.name) && child.receiver.nil?
|
|
5305
|
+
end
|
|
5306
|
+
found
|
|
5307
|
+
end
|
|
5308
|
+
|
|
5309
|
+
MIXIN_SELF_CALLS = %i[include prepend extend].freeze
|
|
5310
|
+
private_constant :MIXIN_SELF_CALLS
|
|
5311
|
+
|
|
5312
|
+
def hook_param_receiver?(receiver, name)
|
|
5313
|
+
receiver.is_a?(Prism::LocalVariableReadNode) && receiver.name == name
|
|
5314
|
+
end
|
|
5315
|
+
|
|
5316
|
+
# A hook-body call that may mix into the parameter's singleton class without a plain `base.extend(…)`:
|
|
5317
|
+
# `base.send(:extend, M)` (or `__send__` / `public_send`), `base.singleton_class.include(M)` / `prepend(M)`, and a
|
|
5318
|
+
# `class_eval` / `instance_eval` / `module_eval`-family call on the parameter, whose block runs with it as `self`.
|
|
5319
|
+
def hook_singleton_reshape?(node, name)
|
|
5320
|
+
receiver = node.receiver
|
|
5321
|
+
if HOOK_SEND_CALLS.include?(node.name)
|
|
5322
|
+
send_extend?(node) && hook_param_receiver?(receiver, name)
|
|
5323
|
+
elsif HOOK_EVAL_CALLS.include?(node.name)
|
|
5324
|
+
hook_param_receiver?(receiver, name)
|
|
5325
|
+
else
|
|
5326
|
+
%i[include prepend].include?(node.name) && receiver.is_a?(Prism::CallNode) &&
|
|
5327
|
+
receiver.name == :singleton_class && hook_param_receiver?(receiver.receiver, name)
|
|
5328
|
+
end
|
|
5329
|
+
end
|
|
5330
|
+
|
|
5331
|
+
HOOK_EVAL_CALLS = %i[class_eval class_exec module_eval module_exec instance_eval instance_exec].freeze
|
|
5332
|
+
private_constant :HOOK_EVAL_CALLS
|
|
5333
|
+
|
|
5334
|
+
# `send(:extend, …)`, `__send__` or `public_send`, on whatever receiver.
|
|
5335
|
+
def send_extend?(node)
|
|
5336
|
+
first = node.arguments&.arguments&.first
|
|
5337
|
+
HOOK_SEND_CALLS.include?(node.name) && first.is_a?(Prism::SymbolNode) && first.unescaped == "extend"
|
|
5338
|
+
end
|
|
5339
|
+
|
|
5340
|
+
HOOK_SEND_CALLS = %i[send __send__ public_send].freeze
|
|
5341
|
+
private_constant :HOOK_SEND_CALLS
|
|
5342
|
+
|
|
5343
|
+
# Records each constant argument as written and answers whether every argument was one.
|
|
5344
|
+
def record_hook_extend_arguments(node, owner, tables, hook)
|
|
5345
|
+
arguments = node.arguments&.arguments || []
|
|
5346
|
+
complete = !arguments.empty?
|
|
5347
|
+
arguments.each do |argument|
|
|
5348
|
+
written = Source::ConstantPath.qualified_name_or_nil(argument)
|
|
5349
|
+
next complete = false if written.nil?
|
|
5350
|
+
|
|
5351
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND, written, hook]
|
|
5352
|
+
record_envelope(tables.envelopes, owner, key, Source::ParameterEnvelope::OPAQUE)
|
|
5353
|
+
end
|
|
5354
|
+
complete
|
|
5355
|
+
end
|
|
5356
|
+
|
|
5357
|
+
def record_hook_extend_open(owner, tables, hook)
|
|
5358
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_EXTEND_OPEN_KIND, hook]
|
|
5359
|
+
record_envelope(tables.envelopes, owner, key, Source::ParameterEnvelope::OPAQUE)
|
|
5360
|
+
end
|
|
5361
|
+
|
|
5362
|
+
HOOK_EXTEND_DEFS = %i[included prepended].to_set.freeze
|
|
5363
|
+
private_constant :HOOK_EXTEND_DEFS
|
|
5364
|
+
|
|
5365
|
+
# Issue #2997 — a module's `included` / `prepended` hook defined by a call rather than `def self.included`:
|
|
5366
|
+
# `define_singleton_method(:included) { |base| base.extend(X) }` written as a statement of the module body is
|
|
5367
|
+
# read as {#record_hook_extends} reads the `def` (a `[ENVELOPE_HOOK_DEF_KIND, hook, source]` entry, and the
|
|
5368
|
+
# block body scanned with its first parameter as the includer). (`class << self; def included(base)` already
|
|
5369
|
+
# reaches {#record_hook_extends} as a singleton `def`.) Any other call that may define a hook takes a
|
|
5370
|
+
# `[ENVELOPE_HOOK_EXTEND_OPEN_KIND, hook]` entry, for each hook a non-literal name may spell: one nested in
|
|
5371
|
+
# a block, method or condition, one without a block of one plain parameter, `send(:define_singleton_method,
|
|
5372
|
+
# …)`, `singleton_class.define_method(…)`, and `define_method` in the body of `class << self`.
|
|
5373
|
+
def record_hook_definer_calls(node, child_prefix, methods_acc)
|
|
5374
|
+
return unless node.is_a?(Prism::ModuleNode) && node.body.is_a?(Prism::StatementsNode)
|
|
5375
|
+
|
|
5376
|
+
owner = child_prefix.join("::")
|
|
5377
|
+
direct = node.body.body
|
|
5378
|
+
each_hook_definer(node.body, false) do |call, hooks|
|
|
5379
|
+
tables = methods_acc.at(call)
|
|
5380
|
+
param = readable_hook_parameter(call) if direct.any? { |statement| statement.equal?(call) }
|
|
5381
|
+
if param
|
|
5382
|
+
record_hook_block(call, param, owner, tables, hooks.first)
|
|
5383
|
+
else
|
|
5384
|
+
hooks.each { |hook| record_hook_extend_open(owner, tables, hook) }
|
|
5385
|
+
end
|
|
5386
|
+
end
|
|
5387
|
+
end
|
|
5388
|
+
|
|
5389
|
+
# The entries of one readable `define_singleton_method(:hook) { |base| … }`.
|
|
5390
|
+
def record_hook_block(call, param, owner, tables, hook)
|
|
5391
|
+
def_key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_DEF_KIND, hook, call.slice.split.join(" ")]
|
|
5392
|
+
record_envelope(tables.envelopes, owner, def_key, Source::ParameterEnvelope::OPAQUE)
|
|
5393
|
+
body = call.block&.body
|
|
5394
|
+
return if body.nil?
|
|
5395
|
+
|
|
5396
|
+
target = ->(extend_call) { hook_param_receiver?(extend_call.receiver, param.name) }
|
|
5397
|
+
scan_hook_body(body, target, owner, tables, hook) { |inner| hook_singleton_reshape?(inner, param.name) }
|
|
5398
|
+
end
|
|
5399
|
+
|
|
5400
|
+
# Yields each call under a module body that may define the module's `included` / `prepended`, with the hooks
|
|
5401
|
+
# it may define. A nested class or module body has its own self, and so has an instance `def`'s body; in a
|
|
5402
|
+
# `class << self` body `define_method` defines on the module and `define_singleton_method` on its singleton
|
|
5403
|
+
# class, and in a singleton `def`'s body `self` is the module again.
|
|
5404
|
+
def each_hook_definer(node, in_singleton, &)
|
|
5405
|
+
case node
|
|
5406
|
+
when Prism::ClassNode, Prism::ModuleNode then nil
|
|
5407
|
+
when Prism::SingletonClassNode
|
|
5408
|
+
each_hook_definer(node.expression, in_singleton, &)
|
|
5409
|
+
each_hook_definer(node.body, !in_singleton && node.expression.is_a?(Prism::SelfNode), &)
|
|
5410
|
+
when Prism::DefNode
|
|
5411
|
+
each_hook_definer(node.body, false, &) if in_singleton || node.receiver.is_a?(Prism::SelfNode)
|
|
5412
|
+
else
|
|
5413
|
+
if node.is_a?(Prism::CallNode)
|
|
5414
|
+
hooks = hook_definer_names(node, in_singleton)
|
|
5415
|
+
yield node, hooks unless hooks.nil?
|
|
5416
|
+
end
|
|
5417
|
+
node&.rigor_each_child { |child| each_hook_definer(child, in_singleton, &) }
|
|
5418
|
+
end
|
|
5419
|
+
end
|
|
5420
|
+
|
|
5421
|
+
# The hooks a call may define on the module whose body runs it, or nil for a call that defines none.
|
|
5422
|
+
def hook_definer_names(node, in_singleton)
|
|
5423
|
+
name_node = hook_definer_name_node(node, in_singleton)
|
|
5424
|
+
return nil if name_node.nil?
|
|
5425
|
+
|
|
5426
|
+
name = literal_method_name(name_node)
|
|
5427
|
+
return HOOK_EXTEND_DEFS.include?(name) ? [name] : nil if name
|
|
5428
|
+
|
|
5429
|
+
hooks = HOOK_EXTEND_DEFS.select { |hook| hook.start_with?(literal_name_prefix(name_node)) }
|
|
5430
|
+
hooks.empty? ? nil : hooks
|
|
5431
|
+
end
|
|
5432
|
+
|
|
5433
|
+
# The literal text an interpolated name starts with (`"is_"` for `"is_#{n}"`), or `""` for a name computed any
|
|
5434
|
+
# other way: only a hook whose name starts with it may be the one defined.
|
|
5435
|
+
def literal_name_prefix(node)
|
|
5436
|
+
return "" unless node.is_a?(Prism::InterpolatedSymbolNode) || node.is_a?(Prism::InterpolatedStringNode)
|
|
5437
|
+
|
|
5438
|
+
first = node.parts.first
|
|
5439
|
+
first.is_a?(Prism::StringNode) ? first.unescaped : ""
|
|
5440
|
+
end
|
|
5441
|
+
|
|
5442
|
+
# The argument naming the method a call that may define a singleton method of the module defines, or nil.
|
|
5443
|
+
def hook_definer_name_node(node, in_singleton)
|
|
5444
|
+
arguments = node.arguments&.arguments || []
|
|
5445
|
+
if HOOK_SEND_CALLS.include?(node.name) && self_receiver?(node.receiver)
|
|
5446
|
+
definer = literal_method_name(arguments.first)
|
|
5447
|
+
arguments[1] if definer == :define_singleton_method || (in_singleton && definer == :define_method)
|
|
5448
|
+
elsif singleton_definer_call?(node, in_singleton)
|
|
5449
|
+
arguments.first
|
|
5450
|
+
end
|
|
5451
|
+
end
|
|
5452
|
+
|
|
5453
|
+
def singleton_definer_call?(node, in_singleton)
|
|
5454
|
+
receiver = node.receiver
|
|
5455
|
+
case node.name
|
|
5456
|
+
when :define_singleton_method then !in_singleton && self_receiver?(receiver)
|
|
5457
|
+
when :define_method
|
|
5458
|
+
(in_singleton && self_receiver?(receiver)) ||
|
|
5459
|
+
(receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && self_receiver?(receiver.receiver))
|
|
5460
|
+
else false
|
|
5461
|
+
end
|
|
5462
|
+
end
|
|
5463
|
+
|
|
5464
|
+
def self_receiver?(receiver) = receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
5465
|
+
|
|
5466
|
+
# The includer parameter of a `define_singleton_method(:hook) { |base| … }` the hook read can place: one literal
|
|
5467
|
+
# hook name, a literal block, and a plain first parameter. Nil for any other shape.
|
|
5468
|
+
def readable_hook_parameter(call)
|
|
5469
|
+
arguments = call.arguments&.arguments || []
|
|
5470
|
+
return nil unless call.name == :define_singleton_method && arguments.size == 1
|
|
5471
|
+
return nil unless literal_method_name(arguments.first) && call.block.is_a?(Prism::BlockNode)
|
|
5472
|
+
|
|
5473
|
+
params = call.block.parameters
|
|
5474
|
+
param = params.parameters&.requireds&.first if params.is_a?(Prism::BlockParametersNode)
|
|
5475
|
+
param.is_a?(Prism::RequiredParameterNode) ? param : nil
|
|
5476
|
+
end
|
|
5477
|
+
|
|
5478
|
+
# Issue #2986 — a module whose body runs `extend ActiveSupport::Concern` has `Concern#append_features` (or
|
|
5479
|
+
# `prepend_features`) extend each direct includer with `const_get(:ClassMethods)`, as #2975's hook does with its
|
|
5480
|
+
# constant, so the module takes a `[ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods", :const_get]` entry. A
|
|
5481
|
+
# `class_methods do … end` block of that body is a `module_eval` of `Owner::ClassMethods`: each `def` in it is
|
|
5482
|
+
# also recorded on that module, with its body's evidence (a computed definer's entry among it), so the hook
|
|
5483
|
+
# reader resolves and reads it. The walk keeps recording the same `def` on the module itself as before. A call on
|
|
5484
|
+
# `self` in an `included do … end` or `prepended do … end` block takes a
|
|
5485
|
+
# `[ENVELOPE_HOOK_BODY_CALL_KIND, hook, name]` entry, since Ruby runs that block in the includer's class body.
|
|
5486
|
+
#
|
|
5487
|
+
# Issue #1592 — the `def` node is filed on `Owner::ClassMethods` too, so the includer's singleton resolution
|
|
5488
|
+
# chain, which holds that module, answers a class-side call with the `def`'s own return type.
|
|
5489
|
+
#
|
|
5490
|
+
# Issue #2997 — a `class_methods` block is read in every body of the module, not only one that runs `extend
|
|
5491
|
+
# ActiveSupport::Concern` itself: a reopening in another file runs it on a module that is already a concern, and
|
|
5492
|
+
# where it loads first Ruby raises `NoMethodError` at load, so no running program has the includer without the
|
|
5493
|
+
# method. The module is a concern, and its includers draw on `Owner::ClassMethods`, only when some body records
|
|
5494
|
+
# the concern entry.
|
|
5495
|
+
def record_concern_class_methods(node, child_prefix, methods_acc, def_nodes_acc)
|
|
5496
|
+
return unless node.is_a?(Prism::ModuleNode) && node.body.is_a?(Prism::StatementsNode)
|
|
5497
|
+
|
|
5498
|
+
concern = concern_body?(node.body, child_prefix)
|
|
5499
|
+
owner = child_prefix.join("::")
|
|
5500
|
+
if concern
|
|
5501
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods", :const_get]
|
|
5502
|
+
record_envelope(methods_acc.at(node).envelopes, owner, key, Source::ParameterEnvelope::OPAQUE)
|
|
5503
|
+
record_concern_dependencies(node.body.body, child_prefix, owner, methods_acc)
|
|
5504
|
+
end
|
|
5505
|
+
node.body.body.each do |statement|
|
|
5506
|
+
next unless statement.is_a?(Prism::CallNode) && block_statement?(statement)
|
|
5507
|
+
|
|
5508
|
+
if statement.name == :class_methods
|
|
5509
|
+
record_class_methods_block(statement, [*child_prefix, "ClassMethods"], methods_acc, def_nodes_acc)
|
|
5510
|
+
elsif CONCERN_HOOK_BLOCKS.include?(statement.name)
|
|
5511
|
+
hook = concern ? statement.name : Scope::DiscoveryIndex::CONCERN_BLOCK_HOOKS.fetch(statement.name)
|
|
5512
|
+
record_concern_hook_block(statement, hook, owner, methods_acc)
|
|
5513
|
+
record_hook_block_singleton_defs(statement, hook, child_prefix, methods_acc, def_nodes_acc)
|
|
5514
|
+
end
|
|
5515
|
+
end
|
|
5516
|
+
end
|
|
5517
|
+
|
|
5518
|
+
# Issue #3005 — a `def self.x` in an `included do … end` (or `prepended do … end`) block defines `x` on the
|
|
5519
|
+
# includer's singleton class, since Ruby runs the block in the includer's class body. Each such `def` is filed
|
|
5520
|
+
# as an instance method of the module's `Owner::<included>` (`<prepended>`), a name no program can spell, and
|
|
5521
|
+
# the module takes a `[ENVELOPE_HOOK_EXTEND_KIND, "<included>", hook]` entry, so the includer's singleton
|
|
5522
|
+
# resolution chain holds that module where the hook's own extends go: after the includer's singleton and any
|
|
5523
|
+
# module prepended to it, ahead of the concern's `ClassMethods`.
|
|
5524
|
+
#
|
|
5525
|
+
# Issue #3015 — a receiverless `def` written as a statement of a `class << self` body in the block (found with
|
|
5526
|
+
# {#each_class_methods_def}'s stops) is filed the same way. A `class << self` body with a statement-level
|
|
5527
|
+
# visibility call (`private`, `private :x`, `private def x`) may make a `def` non-public, which the module does
|
|
5528
|
+
# not record, so it files none of its `def`s and opens the hook instead ({#record_hook_extend_open}).
|
|
5529
|
+
def record_hook_block_singleton_defs(statement, hook, child_prefix, methods_acc, def_nodes_acc)
|
|
5530
|
+
segment = "<#{statement.name}>"
|
|
5531
|
+
prefix = [*child_prefix, segment]
|
|
5532
|
+
name = prefix.join("::")
|
|
5533
|
+
owner = child_prefix.join("::")
|
|
5534
|
+
defs = hook_block_singleton_defs(statement.block.body, owner, hook, methods_acc)
|
|
5535
|
+
return if defs.empty?
|
|
5536
|
+
|
|
5537
|
+
defs.each do |def_node|
|
|
5538
|
+
record_method(methods_acc.at(def_node), name, def_node.name, :instance, Source::ParameterEnvelope.of(def_node))
|
|
5539
|
+
(def_nodes_acc[name] ||= {})[def_node.name] = def_node
|
|
5540
|
+
record_def_body_evidence(def_node, prefix, methods_acc, :instance, receiver_side: false)
|
|
5541
|
+
end
|
|
5542
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND, segment, hook]
|
|
5543
|
+
record_envelope(methods_acc.at(statement).envelopes, owner, key, Source::ParameterEnvelope::OPAQUE)
|
|
5544
|
+
end
|
|
5545
|
+
|
|
5546
|
+
# The `def`s {#record_hook_block_singleton_defs} files, opening the hook of `owner` for each `class << self` body
|
|
5547
|
+
# with a visibility call.
|
|
5548
|
+
def hook_block_singleton_defs(body, owner, hook, methods_acc)
|
|
5549
|
+
defs = []
|
|
5550
|
+
each_class_methods_def(body) { |def_node| defs << def_node if def_node.receiver.is_a?(Prism::SelfNode) }
|
|
5551
|
+
each_hook_block_singleton_class(body) do |singleton|
|
|
5552
|
+
statements = statements_of(singleton.body)
|
|
5553
|
+
if statements.any? { |child| singleton_body_visibility_call?(child) }
|
|
5554
|
+
record_hook_extend_open(owner, methods_acc.at(singleton), hook)
|
|
5555
|
+
else
|
|
5556
|
+
statements.each { |child| defs << child if child.is_a?(Prism::DefNode) && child.receiver.nil? }
|
|
5557
|
+
end
|
|
5558
|
+
end
|
|
5559
|
+
defs
|
|
5560
|
+
end
|
|
5561
|
+
|
|
5562
|
+
# Issue #3015 — the `class << self` bodies of an `included` / `prepended` block, with {#each_class_methods_def}'s
|
|
5563
|
+
# stops.
|
|
5564
|
+
def each_hook_block_singleton_class(node, &)
|
|
5565
|
+
case node
|
|
5566
|
+
when Prism::SingletonClassNode then yield node if node.expression.is_a?(Prism::SelfNode)
|
|
5567
|
+
when *CONCERN_BLOCK_STOPS then nil
|
|
5568
|
+
else node&.rigor_each_child { |child| each_hook_block_singleton_class(child, &) }
|
|
5569
|
+
end
|
|
5570
|
+
end
|
|
5571
|
+
|
|
5572
|
+
SINGLETON_BODY_VISIBILITY_CALLS = %i[private protected public private_class_method public_class_method].freeze
|
|
5573
|
+
private_constant :SINGLETON_BODY_VISIBILITY_CALLS
|
|
5574
|
+
|
|
5575
|
+
def singleton_body_visibility_call?(statement)
|
|
5576
|
+
statement.is_a?(Prism::CallNode) && statement.receiver.nil? &&
|
|
5577
|
+
SINGLETON_BODY_VISIBILITY_CALLS.include?(statement.name)
|
|
5578
|
+
end
|
|
5579
|
+
|
|
5580
|
+
def record_class_methods_block(statement, class_methods_prefix, methods_acc, def_nodes_acc)
|
|
5581
|
+
each_class_methods_def(statement.block&.body) do |def_node|
|
|
5582
|
+
record_def_method(def_node, class_methods_prefix, false, methods_acc.at(def_node))
|
|
5583
|
+
record_def_node(def_node, class_methods_prefix, false, def_nodes_acc)
|
|
5584
|
+
record_def_body_evidence(def_node, class_methods_prefix, methods_acc, :instance)
|
|
5585
|
+
end
|
|
5586
|
+
end
|
|
5587
|
+
|
|
5588
|
+
# Issue #2992 — `Concern#append_features` (or `prepend_features`) of a concern `include`d (or `prepend`ed) into a
|
|
5589
|
+
# module that has already run `extend ActiveSupport::Concern` records it as a dependency of that module and
|
|
5590
|
+
# includes nothing; the module's final includer includes it. Only a constant argument of an `include` /
|
|
5591
|
+
# `prepend` on the module's own `self` ({#concern_dependency_arguments}) written as a statement of the body
|
|
5592
|
+
# AFTER the body's own `extend` is provably such a dependency: one written ahead of it, or in a reopening
|
|
5593
|
+
# without it, may run first and is an ordinary include.
|
|
5594
|
+
# Each such constant takes a `[ENVELOPE_CONCERN_DEPENDENCY_KIND, written]` entry; whether it names a concern is
|
|
5595
|
+
# the reader's question. One statement's constants are recorded last first: `include A, B` runs
|
|
5596
|
+
# `B.append_features` first, so the dependencies list it ahead, and a reader that reverses the entries to put a
|
|
5597
|
+
# later statement's nearer keeps one statement's in argument order, as Ruby searches them.
|
|
5598
|
+
def record_concern_dependencies(statements, child_prefix, owner, methods_acc)
|
|
5599
|
+
extend_at = statements.index { |statement| concern_extend?(statement, child_prefix) }
|
|
5600
|
+
statements.drop(extend_at + 1).each do |statement|
|
|
5601
|
+
arguments = concern_dependency_arguments(statement)
|
|
5602
|
+
next if arguments.nil?
|
|
5603
|
+
|
|
5604
|
+
arguments.reverse_each do |argument|
|
|
5605
|
+
written = Source::ConstantPath.qualified_name_or_nil(argument)
|
|
5606
|
+
next if written.nil?
|
|
5607
|
+
|
|
5608
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_CONCERN_DEPENDENCY_KIND, written]
|
|
5609
|
+
record_envelope(methods_acc.at(statement).envelopes, owner, key, Source::ParameterEnvelope::OPAQUE)
|
|
5610
|
+
end
|
|
5611
|
+
end
|
|
5612
|
+
end
|
|
5613
|
+
|
|
5614
|
+
# The arguments of a statement that `include`s or `prepend`s on the module's own `self`, or nil. Issue #2995 —
|
|
5615
|
+
# `self.include Dep` and a receiverless or `self.` `send(:include, Dep)` run the same `append_features` as a
|
|
5616
|
+
# bare `include Dep`, and the include walk records them as it does.
|
|
5617
|
+
def concern_dependency_arguments(statement)
|
|
5618
|
+
return nil unless statement.is_a?(Prism::CallNode) && statement.block.nil? &&
|
|
5619
|
+
lexical_self_receiver?(statement.receiver)
|
|
5620
|
+
|
|
5621
|
+
kind, arguments, = mixin_call_view(statement)
|
|
5622
|
+
arguments if MIXIN_CALL_NAMES.include?(kind)
|
|
5623
|
+
end
|
|
5624
|
+
|
|
5625
|
+
# Issue #1592 — an `extend` on `self` in the block extends the includer as `base.extend` in a hook does, so it
|
|
5626
|
+
# records the same entries, named by the block's hook ({#record_hook_extends}), and any `extend` nested in the
|
|
5627
|
+
# block, a `class << self` mixin or a `send(:extend, …)` there is open.
|
|
5628
|
+
#
|
|
5629
|
+
# Issue #3005 — read in every body of the module, as a `class_methods` block is: in a body that does not run
|
|
5630
|
+
# `extend ActiveSupport::Concern` itself the entries name the block's gated hook
|
|
5631
|
+
# ({Scope::DiscoveryIndex::CONCERN_BLOCK_HOOKS}), which a reader takes as the plain hook only for a concern.
|
|
5632
|
+
def record_concern_hook_block(statement, hook, owner, methods_acc)
|
|
5633
|
+
body = statement.block.body
|
|
5634
|
+
each_self_call(body) do |call|
|
|
5635
|
+
entry = [Scope::DiscoveryIndex::ENVELOPE_HOOK_BODY_CALL_KIND, hook, call.name]
|
|
5636
|
+
record_envelope(methods_acc.at(call).envelopes, owner, entry, Source::ParameterEnvelope::OPAQUE)
|
|
5637
|
+
end
|
|
5638
|
+
return if body.nil?
|
|
5639
|
+
|
|
5640
|
+
target = ->(call) { call.receiver.nil? || call.receiver.is_a?(Prism::SelfNode) }
|
|
5641
|
+
scan_hook_body(body, target, owner, methods_acc.at(statement), hook) { |call| send_extend?(call) }
|
|
5642
|
+
end
|
|
5643
|
+
|
|
5644
|
+
CONCERN_HOOK_BLOCKS = %i[included prepended].to_set.freeze
|
|
5645
|
+
HOOK_BLOCK_CALLS = (CONCERN_HOOK_BLOCKS | [:class_methods]).freeze
|
|
5646
|
+
|
|
5647
|
+
# Issue #3015 — the blocks of the `included` / `prepended` / `class_methods` statements of a module body that
|
|
5648
|
+
# runs `extend ActiveSupport::Concern` itself; none for any other body, whose `included` may be its own DSL. A
|
|
5649
|
+
# reopening without the `extend` is not read.
|
|
5650
|
+
def concern_hook_blocks(node, child_prefix)
|
|
5651
|
+
return [] unless node.is_a?(Prism::ModuleNode) && concern_body?(node.body, child_prefix)
|
|
5652
|
+
|
|
5653
|
+
node.body.body.filter_map do |statement|
|
|
5654
|
+
statement.block if block_statement?(statement) && HOOK_BLOCK_CALLS.include?(statement.name)
|
|
5655
|
+
end
|
|
5656
|
+
end
|
|
5657
|
+
|
|
5658
|
+
# Issue #3015 — whether `node` is a `class << self` in a block {HookBlockSpans#mark_hook_blocks} marked for the
|
|
5659
|
+
# module the walk is in: the defs of its body are not the module's own singleton methods, and the walks do not
|
|
5660
|
+
# file them there.
|
|
5661
|
+
def hook_block_singleton?(node, in_singleton_class, owner_prefix, accumulator)
|
|
5662
|
+
!in_singleton_class && node.expression.is_a?(Prism::SelfNode) && accumulator.hook_block?(node, owner_prefix)
|
|
5663
|
+
end
|
|
5664
|
+
private_constant :CONCERN_HOOK_BLOCKS, :HOOK_BLOCK_CALLS
|
|
5665
|
+
|
|
5666
|
+
# Whether a module body runs `extend ActiveSupport::Concern` as one of its own statements (`extend Concern`
|
|
5667
|
+
# in a module nested in `module ActiveSupport`).
|
|
5668
|
+
def concern_body?(body, child_prefix)
|
|
5669
|
+
return false unless body.is_a?(Prism::StatementsNode)
|
|
5670
|
+
|
|
5671
|
+
body.body.any? { |statement| concern_extend?(statement, child_prefix) }
|
|
5672
|
+
end
|
|
5673
|
+
|
|
5674
|
+
#
|
|
5675
|
+
# Issue #3005 — `self.extend ActiveSupport::Concern` and a receiverless or `self.` `send` / `public_send` /
|
|
5676
|
+
# `__send__(:extend, ActiveSupport::Concern)` run the same `extend` on the module ({#mixin_call_view}).
|
|
5677
|
+
def concern_extend?(statement, child_prefix)
|
|
5678
|
+
return false unless statement.is_a?(Prism::CallNode) && lexical_self_receiver?(statement.receiver)
|
|
5679
|
+
|
|
5680
|
+
kind, arguments, = mixin_call_view(statement)
|
|
5681
|
+
return false unless kind == :extend
|
|
5682
|
+
|
|
5683
|
+
arguments.any? do |argument|
|
|
5684
|
+
written = Source::ConstantPath.qualified_name_or_nil(argument)&.delete_prefix("::")
|
|
5685
|
+
written == "ActiveSupport::Concern" || (written == "Concern" && child_prefix.first == "ActiveSupport")
|
|
5686
|
+
end
|
|
5687
|
+
end
|
|
5688
|
+
|
|
5689
|
+
# A receiverless block call (`class_methods do … end`, `included do … end`). Concern's methods take no
|
|
5690
|
+
# positional argument here (`class_methods` raises on one), so arguments are not looked at.
|
|
5691
|
+
def block_statement?(node)
|
|
5692
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? && node.block.is_a?(Prism::BlockNode)
|
|
5693
|
+
end
|
|
5694
|
+
|
|
5695
|
+
# The `def`s a `class_methods` block body writes, directly or wrapped in a call (`private def x`), not those in
|
|
5696
|
+
# a nested `def`, class, module, singleton-class, block or lambda body, which define elsewhere or later.
|
|
5697
|
+
def each_class_methods_def(node, &)
|
|
5698
|
+
case node
|
|
5699
|
+
when Prism::DefNode then yield node
|
|
5700
|
+
when *CONCERN_BLOCK_STOPS then nil
|
|
5701
|
+
else node&.rigor_each_child { |child| each_class_methods_def(child, &) }
|
|
5702
|
+
end
|
|
5703
|
+
end
|
|
5704
|
+
|
|
5705
|
+
# The calls on `self` (no receiver, or `self.`) an `included` / `prepended` block body makes, not those in a
|
|
5706
|
+
# nested `def`, class, module, singleton-class or lambda body.
|
|
5707
|
+
#
|
|
5708
|
+
# Issue #2992 — a block that keeps `self` and runs as the block body runs (`[1].each { my_c :x }`) makes its
|
|
5709
|
+
# calls on the includer too, so the walk enters it ({#self_call_block?}). Any other block stays out; its call's
|
|
5710
|
+
# receiver and arguments are still read.
|
|
5711
|
+
def each_self_call(node, &)
|
|
5712
|
+
case node
|
|
5713
|
+
when *SELF_CALL_STOPS then nil
|
|
5714
|
+
when Prism::CallNode
|
|
5715
|
+
yield node if node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
5716
|
+
if node.block.is_a?(Prism::BlockNode) && !self_call_block?(node)
|
|
5717
|
+
[node.receiver, node.arguments].each { |part| each_self_call(part, &) }
|
|
5718
|
+
else
|
|
5719
|
+
node.rigor_each_child { |child| each_self_call(child, &) }
|
|
5720
|
+
end
|
|
5721
|
+
else node&.rigor_each_child { |child| each_self_call(child, &) }
|
|
5722
|
+
end
|
|
5723
|
+
end
|
|
5724
|
+
|
|
5725
|
+
# Whether a call's block may run on the includer while the hook block runs. Every block may, since leaving one
|
|
5726
|
+
# out would report the macro's names, except one that rebinds `self` ({#rebinds_self_block?}) and the calls
|
|
5727
|
+
# known to keep a block for later: a receiverless `lambda` / `proc` and `Proc.new`, which run it only when
|
|
5728
|
+
# called, and `Kernel.lambda` / `Kernel.proc`; `on_load` on any receiver, which runs it when a framework
|
|
5729
|
+
# component loads; and a receiverless framework callback macro (`after_save { }`), which runs it on an instance.
|
|
5730
|
+
#
|
|
5731
|
+
# Issue #3000 — the callback stop is a list of the Rails callback macros, not a `before_` / `after_` /
|
|
5732
|
+
# `around_` prefix: a project macro so named may yield at once (`def self.after_foo = yield`), and entering a
|
|
5733
|
+
# block that runs later only loses a report.
|
|
5734
|
+
def self_call_block?(node)
|
|
5735
|
+
return false if rebinds_self_block?(node)
|
|
5736
|
+
return false if node.name == :on_load
|
|
5737
|
+
|
|
5738
|
+
receiver = node.receiver
|
|
5739
|
+
if receiver.nil?
|
|
5740
|
+
!DEFERRED_BLOCK_CALLS.include?(node.name) && !CALLBACK_MACROS.include?(node.name)
|
|
5741
|
+
elsif DEFERRED_BLOCK_CALLS.include?(node.name)
|
|
5742
|
+
!constant_named?(receiver, "Kernel")
|
|
5743
|
+
else
|
|
5744
|
+
!(node.name == :new && constant_named?(receiver, "Proc"))
|
|
5745
|
+
end
|
|
5746
|
+
end
|
|
5747
|
+
|
|
5748
|
+
def constant_named?(node, name)
|
|
5749
|
+
Source::ConstantPath.qualified_name_or_nil(node)&.delete_prefix("::") == name
|
|
5750
|
+
end
|
|
5751
|
+
|
|
5752
|
+
DEFERRED_BLOCK_CALLS = %i[lambda proc].freeze
|
|
5753
|
+
# The callback macros of ActiveModel (validations), ActiveRecord (`Callbacks::CALLBACKS`, transactions),
|
|
5754
|
+
# ActiveJob (callbacks, `after_discard`), AbstractController (with its `prepend_` / `append_` forms),
|
|
5755
|
+
# ActionMailer, ActionMailbox, ActionCable (channel, connection), ActiveSupport::CurrentAttributes and
|
|
5756
|
+
# ActiveSupport::Reloader, read from Rails 8.1.3.
|
|
5757
|
+
CALLBACK_MACROS = %i[
|
|
5758
|
+
before_validation after_validation
|
|
5759
|
+
after_initialize after_find after_touch
|
|
5760
|
+
before_save around_save after_save before_create around_create after_create
|
|
5761
|
+
before_update around_update after_update before_destroy around_destroy after_destroy
|
|
5762
|
+
before_commit after_commit after_rollback
|
|
5763
|
+
after_save_commit after_create_commit after_update_commit after_destroy_commit
|
|
5764
|
+
before_perform around_perform after_perform before_enqueue around_enqueue after_enqueue after_discard
|
|
5765
|
+
before_action around_action after_action
|
|
5766
|
+
prepend_before_action prepend_around_action prepend_after_action
|
|
5767
|
+
append_before_action append_around_action append_after_action
|
|
5768
|
+
before_processing around_processing after_processing
|
|
5769
|
+
before_reset after_reset before_class_unload after_class_unload
|
|
5770
|
+
before_deliver around_deliver after_deliver
|
|
5771
|
+
before_subscribe after_subscribe before_unsubscribe after_unsubscribe
|
|
5772
|
+
before_command around_command after_command
|
|
5773
|
+
].to_set.freeze
|
|
5774
|
+
private_constant :DEFERRED_BLOCK_CALLS, :CALLBACK_MACROS
|
|
5775
|
+
|
|
5776
|
+
CONCERN_BLOCK_STOPS = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode,
|
|
5777
|
+
Prism::BlockNode, Prism::LambdaNode].freeze
|
|
5778
|
+
SELF_CALL_STOPS = (CONCERN_BLOCK_STOPS - [Prism::BlockNode]).freeze
|
|
5779
|
+
private_constant :CONCERN_BLOCK_STOPS, :SELF_CALL_STOPS
|
|
5780
|
+
|
|
5781
|
+
# Issue #2901 — a method whose body runs a `define_method` on its own `self` with a computed name, directly or in
|
|
5782
|
+
# a block that keeps `self` (`def self.my_attr(*n) = n.each { |x| define_method(x) {} }`), is a class macro: a
|
|
5783
|
+
# class body that calls it gains members no table names. The method takes a
|
|
5784
|
+
# `[ENVELOPE_COMPUTED_DEFINER_KIND, side, name]` entry, which `call.undefined-method` resolves a class's body
|
|
5785
|
+
# calls against. A `def Other.x` (another object's method) records none, and neither does a `define_method` inside
|
|
5786
|
+
# a nested `def`, a class body or a block that rebinds `self` ({#rebinds_self_block?}).
|
|
5787
|
+
#
|
|
5788
|
+
# Issue #2975 — the entry carries the side the method is defined on (`:singleton` for `def self.x` or a
|
|
5789
|
+
# `class << self` body, `:instance` otherwise), so a body call resolves only against a macro on the side its
|
|
5790
|
+
# singleton chain reaches.
|
|
5791
|
+
def record_computed_definer(def_node, qualified_prefix, tables, side)
|
|
5792
|
+
return if qualified_prefix.empty?
|
|
5793
|
+
return unless def_node.receiver.nil? || def_node.receiver.is_a?(Prism::SelfNode)
|
|
5794
|
+
return unless runs_computed_self_definer?(def_node.body)
|
|
5795
|
+
|
|
5796
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_COMPUTED_DEFINER_KIND, side, def_node.name]
|
|
5797
|
+
record_envelope(tables.envelopes, qualified_prefix.join("::"), key, Source::ParameterEnvelope::OPAQUE)
|
|
5798
|
+
end
|
|
5799
|
+
|
|
5800
|
+
def computed_self_define_method?(node)
|
|
5801
|
+
node.name == :define_method && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
|
|
5802
|
+
computed_first_name?(node.arguments&.arguments)
|
|
4750
5803
|
end
|
|
4751
5804
|
|
|
5805
|
+
def runs_computed_self_definer?(node)
|
|
5806
|
+
case node
|
|
5807
|
+
when Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
5808
|
+
return false
|
|
5809
|
+
when Prism::CallNode
|
|
5810
|
+
return true if computed_self_define_method?(node)
|
|
5811
|
+
# Issue #2975 — `include(Module.new { define_method(x) {} })` on `self` puts the computed method on `self`'s
|
|
5812
|
+
# instance side, as a direct `define_method(x)` would.
|
|
5813
|
+
return true if MODULE_NEW_MIXIN_CALLS.include?(node.name) &&
|
|
5814
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
|
|
5815
|
+
module_new_definer_argument?(node)
|
|
5816
|
+
if rebinds_self_block?(node)
|
|
5817
|
+
return [node.receiver, node.arguments].any? do |part|
|
|
5818
|
+
runs_computed_self_definer?(part)
|
|
5819
|
+
end
|
|
5820
|
+
end
|
|
5821
|
+
end
|
|
5822
|
+
return false unless node.is_a?(Prism::Node)
|
|
5823
|
+
|
|
5824
|
+
node.rigor_each_child { |child| return true if runs_computed_self_definer?(child) }
|
|
5825
|
+
false
|
|
5826
|
+
end
|
|
5827
|
+
|
|
5828
|
+
# A call whose block runs with another `self`: a class factory (`Class.new { }`), a definer's own body
|
|
5829
|
+
# (`define_method(:x) { }` runs on an instance), or an eval on a receiver other than `self`.
|
|
5830
|
+
def rebinds_self_block?(node)
|
|
5831
|
+
return false unless node.block.is_a?(Prism::BlockNode)
|
|
5832
|
+
return true if meta_new_constant_rvalue?(node) || SELF_DEFINER_BLOCK_CALLS.include?(node.name)
|
|
5833
|
+
|
|
5834
|
+
RECEIVER_EVAL_CALLS.include?(node.name) && !(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
5835
|
+
end
|
|
5836
|
+
|
|
5837
|
+
SELF_DEFINER_BLOCK_CALLS = %i[define_method define_singleton_method].freeze
|
|
5838
|
+
private_constant :SELF_DEFINER_BLOCK_CALLS
|
|
5839
|
+
|
|
4752
5840
|
# Issue #1820 — `obj.singleton_class.prepend(M)`, or `prepend M` in a `class << obj` body, puts `M` ahead of
|
|
4753
5841
|
# `obj`'s class even when the class already includes it (`prepend` re-inserts it; `extend` would not), so
|
|
4754
5842
|
# `M`'s refinement answers first for that object. `M` takes {Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK},
|
|
@@ -4766,6 +5854,56 @@ module Rigor
|
|
|
4766
5854
|
record_object_extension(node, qualified_prefix, tables) if singleton
|
|
4767
5855
|
end
|
|
4768
5856
|
|
|
5857
|
+
# Issue #2998 — a prepend to the class object's own singleton class, `prepend P` in a `class << self` body or
|
|
5858
|
+
# `singleton_class.prepend P` / `singleton_class.send(:prepend, P)` in the class body, files a
|
|
5859
|
+
# `[ENVELOPE_SINGLETON_PREPEND_KIND, "P"]` entry per constant argument, as the extends walk spells it, under the
|
|
5860
|
+
# owner the walk passes in. The extends walk records the same edge in `discovered_extends`, and the singleton
|
|
5861
|
+
# resolution chain reads the entries that match one of those edges as prepends.
|
|
5862
|
+
#
|
|
5863
|
+
# A constant receiver's `Const.singleton_class.prepend P` (or its `send` spelling) files the entry on every name
|
|
5864
|
+
# the constant can denote from the call site, as {#record_body_rewrite} marks it; the extends walk records that
|
|
5865
|
+
# edge unpositioned, so the chain's reading of it is master's and the typing read declines where they differ.
|
|
5866
|
+
# `prefixes` is `[owner_prefix, qualified_prefix]`: the class `self` names (empty where it names none) and the
|
|
5867
|
+
# lexical prefix a constant resolves against.
|
|
5868
|
+
#
|
|
5869
|
+
# Issue #3020 — an argument that names no module (`prepend(*MODS)`, `prepend helper`; {#opaque_mixin_argument?})
|
|
5870
|
+
# files `[ENVELOPE_SINGLETON_PREPEND_KIND, "?"]`: the extends walk lists that edge as {MixinAccumulator::UNNAMED},
|
|
5871
|
+
# which cannot say a prepend from an `extend`, and the chain reads the entry as a prepend region it cannot name.
|
|
5872
|
+
def record_own_singleton_prepend(node, prefixes, in_singleton_class, tables)
|
|
5873
|
+
kind, arguments, = mixin_call_view(node)
|
|
5874
|
+
return unless kind == :prepend
|
|
5875
|
+
|
|
5876
|
+
owners = singleton_prepend_owners(node.receiver, *prefixes, in_singleton_class)
|
|
5877
|
+
arguments.each do |argument|
|
|
5878
|
+
name = Source::ConstantPath.qualified_name(argument)
|
|
5879
|
+
name ||= MixinAccumulator::UNNAMED if opaque_mixin_argument?(argument)
|
|
5880
|
+
next unless name
|
|
5881
|
+
|
|
5882
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_SINGLETON_PREPEND_KIND, name]
|
|
5883
|
+
owners.each { |owner| record_envelope(tables.envelopes, owner, key, Source::ParameterEnvelope::OPAQUE) }
|
|
5884
|
+
end
|
|
5885
|
+
end
|
|
5886
|
+
|
|
5887
|
+
# The classes whose singleton a prepend call on `receiver` reaches: the enclosing class for `prepend` in its
|
|
5888
|
+
# `class << self` body or `singleton_class.prepend` on a bare or `self.` receiver, and every name a constant
|
|
5889
|
+
# receiver's `Const.singleton_class` can denote.
|
|
5890
|
+
def singleton_prepend_owners(receiver, owner_prefix, qualified_prefix, in_singleton_class)
|
|
5891
|
+
own = owner_prefix.empty? ? EMPTY_PREFIX : [owner_prefix.join("::")]
|
|
5892
|
+
return own if in_singleton_class && lexical_self_receiver?(receiver)
|
|
5893
|
+
return EMPTY_PREFIX unless singleton_class_of_receiver?(receiver)
|
|
5894
|
+
|
|
5895
|
+
object = receiver.receiver
|
|
5896
|
+
return in_singleton_class ? EMPTY_PREFIX : own if lexical_self_receiver?(object)
|
|
5897
|
+
return EMPTY_PREFIX unless object.is_a?(Prism::ConstantReadNode) || object.is_a?(Prism::ConstantPathNode)
|
|
5898
|
+
|
|
5899
|
+
constant_receiver_candidates(object, qualified_prefix)
|
|
5900
|
+
end
|
|
5901
|
+
|
|
5902
|
+
# A bare or `self.` receiver's `singleton_class` call: `singleton_class` or `self.singleton_class`.
|
|
5903
|
+
def self_singleton_class?(receiver)
|
|
5904
|
+
singleton_class_of_receiver?(receiver) && lexical_self_receiver?(receiver.receiver)
|
|
5905
|
+
end
|
|
5906
|
+
|
|
4769
5907
|
def singleton_class_call?(receiver, self_ok)
|
|
4770
5908
|
return false unless receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class
|
|
4771
5909
|
return false unless receiver.arguments.nil?
|
|
@@ -4774,6 +5912,28 @@ module Rigor
|
|
|
4774
5912
|
self_ok || !(object.nil? || object.is_a?(Prism::SelfNode))
|
|
4775
5913
|
end
|
|
4776
5914
|
|
|
5915
|
+
# Issue #3010 — the singleton-prepend entries of a `class << Const` body's prepends on the other names the
|
|
5916
|
+
# constant can denote ({#constant_singleton_owners}); the body's own walk files them under the owner. The
|
|
5917
|
+
# extends walk records the matching edges, unpositioned ({#record_constant_singleton_candidates}). In a method
|
|
5918
|
+
# body, which the methods walk does not enter, `owner:` files them under the owner too.
|
|
5919
|
+
def record_constant_singleton_prepends(node, qualified_prefix, tables, owner: false)
|
|
5920
|
+
return unless constant_singleton_class?(node)
|
|
5921
|
+
|
|
5922
|
+
resolved, others = constant_singleton_owners(node, qualified_prefix)
|
|
5923
|
+
others = [resolved, *others] if owner
|
|
5924
|
+
return if others.empty? || node.body.nil?
|
|
5925
|
+
|
|
5926
|
+
each_singleton_body_prepend(node.body) do |call|
|
|
5927
|
+
mixin_call_view(call)[1].each do |argument|
|
|
5928
|
+
name = Source::ConstantPath.qualified_name(argument)
|
|
5929
|
+
next unless name
|
|
5930
|
+
|
|
5931
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_SINGLETON_PREPEND_KIND, name]
|
|
5932
|
+
others.each { |owner| record_envelope(tables.envelopes, owner, key, Source::ParameterEnvelope::OPAQUE) }
|
|
5933
|
+
end
|
|
5934
|
+
end
|
|
5935
|
+
end
|
|
5936
|
+
|
|
4777
5937
|
def record_singleton_body_prepends(node, qualified_prefix, tables)
|
|
4778
5938
|
return if node.body.nil?
|
|
4779
5939
|
|
|
@@ -4805,17 +5965,44 @@ module Rigor
|
|
|
4805
5965
|
end
|
|
4806
5966
|
end
|
|
4807
5967
|
|
|
4808
|
-
#
|
|
4809
|
-
#
|
|
4810
|
-
# the form so the method registers as singleton on the enclosing class.
|
|
5968
|
+
# The side of the enclosing class `qualified_prefix` a `def` defines its method on: `:instance`, `:singleton`, or
|
|
5969
|
+
# nil when the def defines no method of that class.
|
|
4811
5970
|
#
|
|
4812
|
-
#
|
|
4813
|
-
#
|
|
4814
|
-
#
|
|
5971
|
+
# `in_singleton_class` is true in a `class << self` body and `:eval` in an `instance_eval` block on the class
|
|
5972
|
+
# ({#def_context}), whose `self` is the class itself.
|
|
5973
|
+
#
|
|
5974
|
+
# - A receiverless def lands on the instance side, or on the singleton side in either body.
|
|
5975
|
+
# - `def self.x` is a singleton method. `def Foo.x` inside `module Foo` (or `def Meta.init` inside `module Meta`)
|
|
5976
|
+
# is the same: at the def-site the constant `Foo` is the module itself (== `self`).
|
|
5977
|
+
# - Issue #3155 — any other receiver (`def String.q`, `def A.z` in `A::B`, `def obj.q` on a local or a constant
|
|
5978
|
+
# holding an object) is a singleton method of that object, not the enclosing class, and `def self.x` in a
|
|
5979
|
+
# `class << self` body is a method of the singleton class's own singleton: nil. The def's body is still
|
|
5980
|
+
# walked; only the method is not filed on the enclosing class. A constant receiver's own singleton is
|
|
5981
|
+
# {#record_foreign_singleton_def}'s.
|
|
5982
|
+
def def_side(def_node, qualified_prefix, in_singleton_class)
|
|
5983
|
+
receiver = def_node.receiver
|
|
5984
|
+
if receiver.nil?
|
|
5985
|
+
in_singleton_class ? :singleton : :instance
|
|
5986
|
+
elsif receiver.is_a?(Prism::SelfNode)
|
|
5987
|
+
in_singleton_class == true ? nil : :singleton
|
|
5988
|
+
elsif def_receiver_targets_lexical_self?(receiver, qualified_prefix)
|
|
5989
|
+
:singleton
|
|
5990
|
+
end
|
|
5991
|
+
end
|
|
5992
|
+
|
|
5993
|
+
# The `in_singleton_class` {#def_side} reads for a def under a walk's two flags: an `instance_eval` block's
|
|
5994
|
+
# `defs_singleton` (`self` is the receiver, so `def self.x` is its singleton method) unless the def is also in
|
|
5995
|
+
# a `class << self` body.
|
|
5996
|
+
def def_context(in_singleton_class, defs_singleton)
|
|
5997
|
+
in_singleton_class || (defs_singleton ? :eval : false)
|
|
5998
|
+
end
|
|
5999
|
+
|
|
4815
6000
|
def def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
4816
|
-
|
|
6001
|
+
def_side(def_node, qualified_prefix, in_singleton_class) == :singleton
|
|
6002
|
+
end
|
|
4817
6003
|
|
|
4818
|
-
|
|
6004
|
+
def foreign_def?(def_node, qualified_prefix, in_singleton_class)
|
|
6005
|
+
def_side(def_node, qualified_prefix, in_singleton_class).nil?
|
|
4819
6006
|
end
|
|
4820
6007
|
|
|
4821
6008
|
# Only `Prism::ConstantReadNode` is observed in real Ruby — Prism mis-parses `def C::P.method` as `def C.P` (Ruby
|
|
@@ -4848,7 +6035,7 @@ module Rigor
|
|
|
4848
6035
|
# ADR-119 WD3 — the slot follows its writer (later-wins): a certain write clears the key from
|
|
4849
6036
|
# `contested_discovered_def_nodes` and a possible one adds it ({MethodTables#certain}, set for `def_node`).
|
|
4850
6037
|
def record_def_node(def_node, qualified_prefix, in_singleton_class, accumulator, tables = nil)
|
|
4851
|
-
return
|
|
6038
|
+
return unless def_side(def_node, qualified_prefix, in_singleton_class) == :instance
|
|
4852
6039
|
|
|
4853
6040
|
class_name = qualified_prefix.empty? ? TOP_LEVEL_DEF_KEY : qualified_prefix.join("::")
|
|
4854
6041
|
accumulator[class_name] ||= {}
|
|
@@ -4873,8 +6060,9 @@ module Rigor
|
|
|
4873
6060
|
|
|
4874
6061
|
# Issue #1989 — the instance method names a non-`def` definer written on the class body's self replaces:
|
|
4875
6062
|
# each reader (and `x=` writer) an `attr_*` call declares, a literal `define_method(:x)`, and the new name of
|
|
4876
|
-
# an `alias_method :x, :y` call
|
|
4877
|
-
|
|
6063
|
+
# an `alias_method :x, :y` call, and each instance-side name of a Forwardable delegator (#2793). A non-literal
|
|
6064
|
+
# name replaces nothing Rigor can name.
|
|
6065
|
+
def call_superseded_names(call_node, qualified_prefix, accumulator)
|
|
4878
6066
|
if (macro = attr_macro(call_node))
|
|
4879
6067
|
names = []
|
|
4880
6068
|
each_attr_macro_name(call_node) do |base|
|
|
@@ -4884,6 +6072,8 @@ module Rigor
|
|
|
4884
6072
|
names
|
|
4885
6073
|
elsif call_node.name == :define_method
|
|
4886
6074
|
define_method_superseded_name(call_node)
|
|
6075
|
+
elsif (forwarded = forwardable_superseded_names(call_node, qualified_prefix, accumulator)).any?
|
|
6076
|
+
forwarded
|
|
4887
6077
|
else
|
|
4888
6078
|
Source::AliasNames.alias_method_call_names(call_node)&.first(1) || []
|
|
4889
6079
|
end
|
|
@@ -4969,6 +6159,10 @@ module Rigor
|
|
|
4969
6159
|
# The singleton walk's accumulator: the `{class => {name => DefNode}}` table, plus the contested slots, which
|
|
4970
6160
|
# follow each slot's writer ({#follow_slot_writer}'s rule).
|
|
4971
6161
|
class SingletonDefTable < Hash
|
|
6162
|
+
include HookBlockSpans
|
|
6163
|
+
|
|
6164
|
+
attr_accessor :hook_block_spans
|
|
6165
|
+
|
|
4972
6166
|
# Issue #1936 — contests the slots of `names` (every slot of `class_name` for `:all`) that an earlier write
|
|
4973
6167
|
# filled: a later non-`def` definer replaced the `def` they hold. The slot keeps the `def`, as
|
|
4974
6168
|
# {ScopeIndexer#supersede_def_nodes} keeps the instance one, and a later write clears the contest.
|
|
@@ -5160,7 +6354,7 @@ module Rigor
|
|
|
5160
6354
|
mf_offsets, ranges, def_owner_prefix, singleton_cref,
|
|
5161
6355
|
defs_singleton)
|
|
5162
6356
|
owner = defs_singleton == :unnameable ? EMPTY_PREFIX : (def_owner_prefix || qualified_prefix)
|
|
5163
|
-
record_deferred_def(node, owner, in_singleton_class
|
|
6357
|
+
record_deferred_def(node, owner, def_context(in_singleton_class, defs_singleton), inside_deferred,
|
|
5164
6358
|
mf_offsets, ranges)
|
|
5165
6359
|
walk_deferred_children(node, qualified_prefix, in_singleton_class, true, mf_offsets,
|
|
5166
6360
|
ranges, def_owner_prefix, singleton_cref: singleton_cref,
|
|
@@ -5428,18 +6622,35 @@ module Rigor
|
|
|
5428
6622
|
# proves `S::A` exists). Memoized on the `Prism::Source` every node's
|
|
5429
6623
|
# location shares — threading the set through the dozen eval-consuming
|
|
5430
6624
|
# walks would dwarf the resolution it feeds, so the one re-parse this
|
|
5431
|
-
# needs happens once per file. The memo
|
|
5432
|
-
#
|
|
5433
|
-
#
|
|
5434
|
-
# the same text gets its own source
|
|
6625
|
+
# needs happens once per file. The memo is a {TypingState} Memo, a
|
|
6626
|
+
# WeakKeyMap keyed on the source: the entry's lifetime is the parse's own
|
|
6627
|
+
# (the map holds the key weakly), the set is a pure function of the
|
|
6628
|
+
# source's text, and a worker parsing the same text gets its own source
|
|
6629
|
+
# and its own set. Not run-scoped: an entry holds no run's tables.
|
|
6630
|
+
DECLARED_NAMES = TypingState.memo(
|
|
6631
|
+
:scope_index_declared_names,
|
|
6632
|
+
audit: "scope index declared names",
|
|
6633
|
+
reason: "a WeakKeyMap keyed on the Prism::Source; the frozen set is a pure function of the source's text"
|
|
6634
|
+
)
|
|
6635
|
+
private_constant :DECLARED_NAMES
|
|
6636
|
+
|
|
5435
6637
|
def eval_file_declared_names(node)
|
|
5436
6638
|
source = node.location.send(:source)
|
|
5437
|
-
source
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
6639
|
+
return scan_declared_names(source) if MemoAudit.enabled? && MemoAudit.suspended?
|
|
6640
|
+
|
|
6641
|
+
table = TypingState.current.weak_key_map(DECLARED_NAMES)
|
|
6642
|
+
stored = table[source]
|
|
6643
|
+
if stored.nil?
|
|
6644
|
+
return table[source] = MemoAudit.key_audit("scope index declared names") { scan_declared_names(source) }
|
|
6645
|
+
end
|
|
6646
|
+
|
|
6647
|
+
MemoAudit.check("scope index declared names", stored, source.source.bytesize) { scan_declared_names(source) }
|
|
6648
|
+
end
|
|
6649
|
+
|
|
6650
|
+
def scan_declared_names(source)
|
|
6651
|
+
names = Set.new
|
|
6652
|
+
collect_declared_constant_names(Prism.parse(source.source).value, [], names)
|
|
6653
|
+
names.freeze
|
|
5443
6654
|
end
|
|
5444
6655
|
|
|
5445
6656
|
# The walk behind {eval_file_declared_names}: every qualified constant
|
|
@@ -5669,7 +6880,11 @@ module Rigor
|
|
|
5669
6880
|
return
|
|
5670
6881
|
end
|
|
5671
6882
|
|
|
5672
|
-
|
|
6883
|
+
side = def_side(def_node, qualified_prefix, in_singleton_class)
|
|
6884
|
+
# Issue #3155 — a def on another object's singleton claims no owner or side, as a deferred one.
|
|
6885
|
+
return ranges << [start, def_node.location.end_offset, def_node.name, nil, nil] if side.nil?
|
|
6886
|
+
|
|
6887
|
+
kind = if side == :singleton
|
|
5673
6888
|
:singleton
|
|
5674
6889
|
elsif ModuleFunctionState.deferred_module_function?(mf_offsets, start)
|
|
5675
6890
|
:both
|
|
@@ -5684,7 +6899,7 @@ module Rigor
|
|
|
5684
6899
|
# `module_function` toggle threads correctly across the body's *sibling* statements (a child-by-child recursion
|
|
5685
6900
|
# would reset it). At the top level / inside an arbitrary node there is no `module_function` state to carry, so
|
|
5686
6901
|
# descent is a plain per-child walk.
|
|
5687
|
-
def walk_singleton_def_nodes(node, qualified_prefix, in_singleton_class, accumulator, # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
6902
|
+
def walk_singleton_def_nodes(node, qualified_prefix, in_singleton_class, accumulator, # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
5688
6903
|
def_owner_prefix = nil, singleton_cref: false,
|
|
5689
6904
|
defs_singleton: false)
|
|
5690
6905
|
return unless node.is_a?(Prism::Node)
|
|
@@ -5708,6 +6923,7 @@ module Rigor
|
|
|
5708
6923
|
# ownerless like `class << <non-constant>`; nameable headers re-anchor.
|
|
5709
6924
|
child_cref = unnameable_decl?(node, self_decl, singleton_cref)
|
|
5710
6925
|
body_prefix = child_cref ? [] : child_prefix
|
|
6926
|
+
accumulator.mark_hook_blocks(concern_hook_blocks(node, child_prefix), child_prefix) unless child_cref
|
|
5711
6927
|
if node.body
|
|
5712
6928
|
walk_singleton_body(node.body, body_prefix, false, accumulator, nil,
|
|
5713
6929
|
singleton_cref: child_cref)
|
|
@@ -5723,7 +6939,8 @@ module Rigor
|
|
|
5723
6939
|
walk_singleton_def_nodes(node.expression, qualified_prefix, in_singleton_class,
|
|
5724
6940
|
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
5725
6941
|
defs_singleton: defs_singleton)
|
|
5726
|
-
if node.body
|
|
6942
|
+
if node.body && !hook_block_singleton?(node, in_singleton_class, def_owner_prefix || qualified_prefix,
|
|
6943
|
+
accumulator)
|
|
5727
6944
|
walk_singleton_body(node.body, qualified_prefix, true, accumulator, singleton_prefix,
|
|
5728
6945
|
singleton_cref: true)
|
|
5729
6946
|
end
|
|
@@ -5735,9 +6952,11 @@ module Rigor
|
|
|
5735
6952
|
when Prism::DefNode
|
|
5736
6953
|
# `:unnameable` is the `class <<` + `instance_eval` definee — the singleton's own
|
|
5737
6954
|
# singleton — which this table cannot name either.
|
|
6955
|
+
owner = def_owner_prefix || qualified_prefix
|
|
6956
|
+
return if accumulator.hook_block_self_def?(node, owner)
|
|
6957
|
+
|
|
5738
6958
|
unless defs_singleton == :unnameable
|
|
5739
|
-
record_singleton_def_node(node,
|
|
5740
|
-
in_singleton_class || defs_singleton, false, accumulator)
|
|
6959
|
+
record_singleton_def_node(node, owner, def_context(in_singleton_class, defs_singleton), false, accumulator)
|
|
5741
6960
|
end
|
|
5742
6961
|
return
|
|
5743
6962
|
end
|
|
@@ -5995,7 +7214,7 @@ module Rigor
|
|
|
5995
7214
|
# `:unnameable` is the `class <<` + `instance_eval` definee — the singleton's own
|
|
5996
7215
|
# singleton — which this table cannot name either.
|
|
5997
7216
|
unless defs_singleton == :unnameable
|
|
5998
|
-
record_singleton_def_node(stmt, owner_prefix, in_singleton_class
|
|
7217
|
+
record_singleton_def_node(stmt, owner_prefix, def_context(in_singleton_class, defs_singleton),
|
|
5999
7218
|
module_function_on, accumulator)
|
|
6000
7219
|
end
|
|
6001
7220
|
next
|
|
@@ -6037,8 +7256,9 @@ module Rigor
|
|
|
6037
7256
|
# function's copy as possible, so the slot holds the later def, contested, rather than the def the
|
|
6038
7257
|
# slot held before (#1968).
|
|
6039
7258
|
def record_singleton_def_node(def_node, qualified_prefix, in_singleton_class, module_function_on, accumulator)
|
|
6040
|
-
|
|
6041
|
-
|
|
7259
|
+
side = def_side(def_node, qualified_prefix, in_singleton_class)
|
|
7260
|
+
declared = side == :singleton
|
|
7261
|
+
return unless declared || (module_function_on && side == :instance)
|
|
6042
7262
|
return if qualified_prefix.empty?
|
|
6043
7263
|
|
|
6044
7264
|
class_name = qualified_prefix.join("::")
|
|
@@ -6634,6 +7854,7 @@ module Rigor
|
|
|
6634
7854
|
def record_data_member_layout(accumulator, qualified_parts, expr, allow_outer_block: true)
|
|
6635
7855
|
expr = resolve_meta_factory_call(expr, allow_outer_block: allow_outer_block)
|
|
6636
7856
|
return unless expr && data_define_call?(expr)
|
|
7857
|
+
return if meta_members_open?(expr)
|
|
6637
7858
|
|
|
6638
7859
|
members = meta_member_names(expr)
|
|
6639
7860
|
return if members.empty?
|
|
@@ -6756,6 +7977,7 @@ module Rigor
|
|
|
6756
7977
|
def record_struct_member_layout(accumulator, qualified_parts, expr, allow_outer_block: true)
|
|
6757
7978
|
expr = resolve_meta_factory_call(expr, allow_outer_block: allow_outer_block)
|
|
6758
7979
|
return unless expr && struct_new_call?(expr)
|
|
7980
|
+
return if meta_members_open?(expr)
|
|
6759
7981
|
|
|
6760
7982
|
members = meta_member_names(expr)
|
|
6761
7983
|
return if members.empty?
|
|
@@ -6924,6 +8146,7 @@ module Rigor
|
|
|
6924
8146
|
|
|
6925
8147
|
_self_decl, child_prefix, child_cref = ctx
|
|
6926
8148
|
accumulator.direct_body(node.body) if accumulator.direct?(node)
|
|
8149
|
+
accumulator.class_body(node.body)
|
|
6927
8150
|
walk_class_includes(node.body, child_cref ? [] : child_prefix,
|
|
6928
8151
|
child_cref ? nil : child_prefix.join("::"), accumulator,
|
|
6929
8152
|
singleton_cref: child_cref)
|
|
@@ -7020,7 +8243,8 @@ module Rigor
|
|
|
7020
8243
|
def record_mixin_call(node, qualified_prefix, current_class, accumulator)
|
|
7021
8244
|
taint_opaque_eval(node, current_class, accumulator)
|
|
7022
8245
|
view = mixin_call_view(node)
|
|
7023
|
-
return
|
|
8246
|
+
return taint_computed_mixin_send(node, current_class, accumulator, INSTANCE_MIXIN_NAMES, :include) if view.nil?
|
|
8247
|
+
return if view.first.nil?
|
|
7024
8248
|
|
|
7025
8249
|
kind = view.first
|
|
7026
8250
|
current_class ||= toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
|
|
@@ -7030,20 +8254,68 @@ module Rigor
|
|
|
7030
8254
|
return if instance_mixin_recorded?(node, view, qualified_prefix, current_class, accumulator)
|
|
7031
8255
|
|
|
7032
8256
|
# A mixin call the tables cannot hold still reshapes the ancestry: taint the whole owner side.
|
|
7033
|
-
|
|
8257
|
+
taint_unrecorded_mixin(node, kind, view[1], effects, accumulator)
|
|
8258
|
+
end
|
|
8259
|
+
|
|
8260
|
+
# Issue #2991 — an unrecorded `include` / `prepend` that reaches the instance side (its receiver is not a
|
|
8261
|
+
# `singleton_class` call) leaves that side's module SET unknown when an argument is not a constant
|
|
8262
|
+
# (`include(*MODS)`, `include helper`) or the receiver is a hook parameter (`base.include M`), which lists
|
|
8263
|
+
# {MixinAccumulator::UNNAMED}. A `Module.new { … }` argument keeps its #2975 reading. A hook's `base.extend M` or
|
|
8264
|
+
# `base.singleton_class.include M` taints the instance side's order alone: it mixes nothing into it.
|
|
8265
|
+
#
|
|
8266
|
+
# Issue #2995 — an instance-side call whose arguments are all constants is recorded wherever the walk names its
|
|
8267
|
+
# owner (`self.include M` and a receiverless `send(:include, M)` included), so none reaches here with a module
|
|
8268
|
+
# the tables do not hold. The extends walk's taint of `Const.include M` inside `class << self` is the one
|
|
8269
|
+
# constant-argument call that does, and the include walk records that edge under `Const`.
|
|
8270
|
+
#
|
|
8271
|
+
# Issue #2994 — the singleton side lists {MixinAccumulator::UNNAMED} too when an unrecorded call reaching it has
|
|
8272
|
+
# an argument that is not a constant: `extend(*EMODS)`, `extend helper`, `include(*MODS)` inside `class << self`,
|
|
8273
|
+
# or a `self` argument. A hook parameter's `base.extend(*X)` extends the includer, not the module the hook is
|
|
8274
|
+
# written in, so it leaves that module's singleton side unnamed-free.
|
|
8275
|
+
def taint_unrecorded_mixin(node, kind, arguments, effects, accumulator)
|
|
8276
|
+
opaque = arguments.any? { |arg| opaque_mixin_argument?(arg) }
|
|
8277
|
+
hook = hook_parameter_receiver?(node.receiver, accumulator)
|
|
8278
|
+
unnamed = INSTANCE_MIXIN_NAMES.include?(kind) && !singleton_class_eval_receiver?(node.receiver) &&
|
|
8279
|
+
(hook || opaque)
|
|
8280
|
+
effects.each do |owner, side|
|
|
8281
|
+
accumulator.taint(owner, side, unnamed: side == :include ? unnamed : opaque && !hook)
|
|
8282
|
+
end
|
|
7034
8283
|
end
|
|
7035
8284
|
|
|
7036
|
-
#
|
|
7037
|
-
#
|
|
7038
|
-
|
|
8285
|
+
# A `self` argument counts as opaque, as the instance side reads it: a `Target.send(:extend, self)` written in a
|
|
8286
|
+
# module body reaches here with the module no table records under `Target`.
|
|
8287
|
+
def opaque_mixin_argument?(arg)
|
|
8288
|
+
Source::ConstantPath.qualified_name(arg).nil? && !module_new_call?(arg)
|
|
8289
|
+
end
|
|
8290
|
+
|
|
8291
|
+
# A hook parameter itself (`base` in `def self.included(base)`): its class is the includer, which the walk cannot
|
|
8292
|
+
# name, so a mixin on it is read as unnamed rather than by its constant (#2991).
|
|
8293
|
+
def hook_parameter_receiver?(receiver, accumulator)
|
|
8294
|
+
receiver.is_a?(Prism::LocalVariableReadNode) && accumulator.hook_local?(receiver.name)
|
|
8295
|
+
end
|
|
8296
|
+
|
|
8297
|
+
# Records the instance-side edges of an `include` / `prepend` call. False when it recorded nothing — the caller
|
|
8298
|
+
# then taints the owner instead. `view` is {#mixin_call_view}'s, so `Integer.send(:include, Ext)` records `Ext` as
|
|
8299
|
+
# `Integer.include(Ext)` does (issue #1722).
|
|
8300
|
+
#
|
|
8301
|
+
# A receiverless `include` / `prepend` needs an enclosing declaration to file the edge under, and the RECEIVER
|
|
8302
|
+
# form (`Base.prepend(Loud)`, issue #1123; `Integer.include(Ext)` and `Integer.send(:include, Ext)`, issue
|
|
8303
|
+
# #1722) files it under the class the receiver names. A receiver-form edge is unpositioned wherever it is written
|
|
8304
|
+
# ({MixinAccumulator#note}), so a reader that depends on ORDER declines on it, while the set-shaped readers
|
|
8305
|
+
# (undefined-method suppression, arity, visibility) see the module as they see an in-body `include`.
|
|
8306
|
+
#
|
|
8307
|
+
# Issue #2995 — `self.include M`, `self.prepend M` and a receiverless or `self.` `send` / `public_send` /
|
|
8308
|
+
# `__send__(:include | :prepend, M)` are the same call on the same `self` as a bare `include M`, so they file
|
|
8309
|
+
# the edge under the enclosing class as it does, positioned where it would be ({MixinAccumulator#note}). The
|
|
8310
|
+
# walk names that `self` exactly where it names a bare `include`'s: `current_class` is nil wherever it cannot.
|
|
7039
8311
|
def instance_mixin_recorded?(node, view, qualified_prefix, current_class, accumulator)
|
|
7040
|
-
kind, arguments,
|
|
7041
|
-
return false unless mixin_call_recorded?(
|
|
8312
|
+
kind, arguments, = view
|
|
8313
|
+
return false unless mixin_call_recorded?(kind)
|
|
7042
8314
|
|
|
7043
8315
|
targets = arguments.filter_map { |arg| Source::ConstantPath.qualified_name(arg) }
|
|
7044
8316
|
return false if targets.empty?
|
|
7045
8317
|
|
|
7046
|
-
owner = node.receiver
|
|
8318
|
+
owner = lexical_self_receiver?(node.receiver) ? current_class : prepend_call_receiver(node, qualified_prefix)
|
|
7047
8319
|
return false if owner.nil?
|
|
7048
8320
|
|
|
7049
8321
|
write_mixin_targets(accumulator, owner, targets, prepend: kind == :prepend)
|
|
@@ -7053,7 +8325,234 @@ module Rigor
|
|
|
7053
8325
|
|
|
7054
8326
|
MIXIN_KINDS = %i[include prepend extend].freeze
|
|
7055
8327
|
SEND_CALL_NAMES = %i[send public_send __send__].freeze
|
|
7056
|
-
|
|
8328
|
+
EXTEND_MIXIN_NAMES = %i[extend].freeze
|
|
8329
|
+
private_constant :MIXIN_KINDS, :SEND_CALL_NAMES, :EXTEND_MIXIN_NAMES
|
|
8330
|
+
|
|
8331
|
+
# Issue #3006 — a receiverless or `self.` `send` / `public_send` / `__send__` written in a class body
|
|
8332
|
+
# ({MixinAccumulator#class_body_scope}) whose name is computed and which passes at least one further argument
|
|
8333
|
+
# may be an `include`, `prepend` or `extend` the walk cannot record: it lists {MixinAccumulator::UNNAMED} on
|
|
8334
|
+
# `side` of the enclosing class when the name may be one of `kinds`. A name that is a local of the call's scope
|
|
8335
|
+
# bound only to name literals is read as those names ({#local_send_names}), so `m = :attr_reader; send(m, :x)`
|
|
8336
|
+
# marks nothing. A hook parameter's `base.send(meth, M)` is read wherever the hook is, as
|
|
8337
|
+
# {#taint_unrecorded_mixin} reads `base.send(:include, M)`.
|
|
8338
|
+
def taint_computed_mixin_send(node, current_class, accumulator, kinds, side, in_singleton: false)
|
|
8339
|
+
return unless current_class
|
|
8340
|
+
|
|
8341
|
+
name = computed_send_name(node, accumulator)
|
|
8342
|
+
return if name.nil?
|
|
8343
|
+
|
|
8344
|
+
scope = accumulator.class_body_scope(node)
|
|
8345
|
+
hook = hook_parameter_receiver?(node.receiver, accumulator)
|
|
8346
|
+
return if scope.nil? && !hook
|
|
8347
|
+
|
|
8348
|
+
names = scope && local_send_names(name, scope)
|
|
8349
|
+
# Inside `class << self` the instance walk is ownerless, so this walk keeps the hook's instance-side taint too,
|
|
8350
|
+
# as {#record_extend_call} does for `base.send(:include, M)`.
|
|
8351
|
+
sides = in_singleton ? [side, :include] : [side]
|
|
8352
|
+
return taint_computed_hook_send(current_class, accumulator, names, sides) if hook
|
|
8353
|
+
return if names&.none? { |literal| kinds.any? { |kind| kind.name == literal } }
|
|
8354
|
+
|
|
8355
|
+
accumulator.taint(current_class, side, unnamed: true)
|
|
8356
|
+
end
|
|
8357
|
+
|
|
8358
|
+
# A hook parameter's computed `send`, read as {#taint_unrecorded_mixin} reads the literal one: it taints the
|
|
8359
|
+
# enclosing module's `sides` when the name may be any mixin's, and lists the unnamed marker on the instance side
|
|
8360
|
+
# alone, when the name may be `include` or `prepend`.
|
|
8361
|
+
def taint_computed_hook_send(current_class, accumulator, names, sides)
|
|
8362
|
+
return if names&.none? { |literal| MIXIN_KINDS.any? { |kind| kind.name == literal } }
|
|
8363
|
+
|
|
8364
|
+
instance = names.nil? || names.any? { |literal| INSTANCE_MIXIN_NAMES.any? { |kind| kind.name == literal } }
|
|
8365
|
+
sides.each { |side| accumulator.taint(current_class, side, unnamed: side == :include && instance) }
|
|
8366
|
+
end
|
|
8367
|
+
|
|
8368
|
+
# The name argument of a receiverless, `self.` or hook-parameter `send`-family call that passes at least one
|
|
8369
|
+
# further argument, when no literal spells it; else nil.
|
|
8370
|
+
def computed_send_name(node, accumulator)
|
|
8371
|
+
return unless SEND_CALL_NAMES.include?(node.name)
|
|
8372
|
+
return unless lexical_self_receiver?(node.receiver) || hook_parameter_receiver?(node.receiver, accumulator)
|
|
8373
|
+
|
|
8374
|
+
arguments = node.arguments&.arguments
|
|
8375
|
+
return if arguments.nil? || arguments.size < 2
|
|
8376
|
+
|
|
8377
|
+
first = arguments.first
|
|
8378
|
+
first unless literal_send_name(first)
|
|
8379
|
+
end
|
|
8380
|
+
|
|
8381
|
+
LOCAL_SCOPE_BOUNDARIES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
8382
|
+
private_constant :LOCAL_SCOPE_BOUNDARIES
|
|
8383
|
+
|
|
8384
|
+
# The names a local `send` argument can hold: every literal a `local = :name` / `local = "name"` in `scope`
|
|
8385
|
+
# writes, or nil when the argument is not a local, or when the local has no such write or any other binding in
|
|
8386
|
+
# `scope` — a parameter (the `def`'s or a block's, which may shadow it), a multiple or operator write, a pattern
|
|
8387
|
+
# or a regexp capture. Blocks share their scope's locals, so they are searched; a nested `def` or declaration
|
|
8388
|
+
# opens a scope of its own, so it is not.
|
|
8389
|
+
#
|
|
8390
|
+
# Issue #3024 — one block parameter is not opaque: as the FIRST parameter of `each` / `each_with_index` /
|
|
8391
|
+
# `map` over a literal array (or a constant the same scope assigns a literal array), it holds each element in
|
|
8392
|
+
# turn, so the elements are read as the candidate names ({#iterated_element_source}). An element that is not
|
|
8393
|
+
# a name literal still means "may be anything".
|
|
8394
|
+
def local_send_names(argument, scope)
|
|
8395
|
+
return nil unless argument.is_a?(Prism::LocalVariableReadNode)
|
|
8396
|
+
|
|
8397
|
+
names = []
|
|
8398
|
+
root = scope.is_a?(Prism::DefNode) ? scope.compact_child_nodes : [scope]
|
|
8399
|
+
return nil unless collect_local_send_names(root, argument.name, names, root)
|
|
8400
|
+
|
|
8401
|
+
names.empty? ? nil : names
|
|
8402
|
+
end
|
|
8403
|
+
|
|
8404
|
+
def collect_local_send_names(nodes, local, names, scope_nodes)
|
|
8405
|
+
nodes.all? do |node|
|
|
8406
|
+
next true if LOCAL_SCOPE_BOUNDARIES.any? { |kind| node.is_a?(kind) }
|
|
8407
|
+
|
|
8408
|
+
iterated = iterated_element_names(node, local, scope_nodes)
|
|
8409
|
+
next false if iterated == false
|
|
8410
|
+
|
|
8411
|
+
if iterated
|
|
8412
|
+
names.concat(iterated)
|
|
8413
|
+
next collect_local_send_names(iterated_element_children(node), local, names, scope_nodes)
|
|
8414
|
+
end
|
|
8415
|
+
|
|
8416
|
+
literal = local_send_name_literal(node, local)
|
|
8417
|
+
next false if literal == :clash
|
|
8418
|
+
|
|
8419
|
+
names << literal if literal
|
|
8420
|
+
|
|
8421
|
+
collect_local_send_names(node.compact_child_nodes, local, names, scope_nodes)
|
|
8422
|
+
end
|
|
8423
|
+
end
|
|
8424
|
+
|
|
8425
|
+
# Issue #3024 — the `each` / `each_with_index` / `map` arm of {#collect_local_send_names}: when `node` is
|
|
8426
|
+
# such an iterator whose first block parameter is `local`, the iterated array's elements are `local`'s
|
|
8427
|
+
# candidate names ({#iterated_element_source}), each read as {#literal_send_name}. False when an element
|
|
8428
|
+
# may spell any name; nil when `node` is not that iterator, so the caller's plain-write and binding checks
|
|
8429
|
+
# apply.
|
|
8430
|
+
def iterated_element_names(node, local, scope_nodes)
|
|
8431
|
+
elements = iterated_element_source(node, local, scope_nodes)
|
|
8432
|
+
return if elements.nil?
|
|
8433
|
+
return false if elements == :unknown
|
|
8434
|
+
|
|
8435
|
+
names = elements.map { |element| literal_send_name(element) }
|
|
8436
|
+
return false if names.any?(&:nil?)
|
|
8437
|
+
|
|
8438
|
+
names
|
|
8439
|
+
end
|
|
8440
|
+
|
|
8441
|
+
# The name literal a `local = :name` / `local = "name"` write binds `local` to for
|
|
8442
|
+
# {#collect_local_send_names}, `:clash` when `node` binds `local` to anything else — a non-literal write,
|
|
8443
|
+
# a target, an operator write, a parameter — or nil when `node` does not bind `local` at all.
|
|
8444
|
+
def local_send_name_literal(node, local)
|
|
8445
|
+
if node.is_a?(Prism::LocalVariableWriteNode) && node.name == local
|
|
8446
|
+
name = literal_send_name(node.value)
|
|
8447
|
+
return :clash if name.nil?
|
|
8448
|
+
|
|
8449
|
+
return name
|
|
8450
|
+
end
|
|
8451
|
+
:clash if local_binding?(node, local)
|
|
8452
|
+
end
|
|
8453
|
+
|
|
8454
|
+
# Issue #3024 — the array elements `node` iterates when it is an `each` / `each_with_index` / `map` call
|
|
8455
|
+
# whose FIRST block parameter is `local`: a literal `ArrayNode` receiver's elements, or a bare constant the
|
|
8456
|
+
# same scope assigns a literal array exactly once ({#constant_array_elements}). `:unknown` when the call is
|
|
8457
|
+
# such an iterator on `local` but its elements cannot be read — a non-literal receiver, or a constant that
|
|
8458
|
+
# is unwritten, multiply written or not array-valued — so the parameter may hold anything; nil when `node`
|
|
8459
|
+
# is not the iterator or `local` is not its first parameter.
|
|
8460
|
+
def iterated_element_source(node, local, scope_nodes)
|
|
8461
|
+
return unless node.is_a?(Prism::CallNode) && ARRAY_ITERATORS.include?(node.name)
|
|
8462
|
+
|
|
8463
|
+
block = node.block
|
|
8464
|
+
return unless block.is_a?(Prism::BlockNode)
|
|
8465
|
+
return unless iterated_first_parameter?(block, local)
|
|
8466
|
+
|
|
8467
|
+
iterated_receiver_elements(node.receiver, scope_nodes) || :unknown
|
|
8468
|
+
end
|
|
8469
|
+
|
|
8470
|
+
# Whether `local` is the first parameter of `block` and no other parameter binds it too (`|m, m = x|` — the
|
|
8471
|
+
# later binding wins, so the first is not the element).
|
|
8472
|
+
def iterated_first_parameter?(block, local)
|
|
8473
|
+
declared = block.parameters
|
|
8474
|
+
return false unless declared.is_a?(Prism::BlockParametersNode)
|
|
8475
|
+
|
|
8476
|
+
parameters = declared.parameters
|
|
8477
|
+
first = parameters&.requireds&.first
|
|
8478
|
+
return false unless first.is_a?(Prism::RequiredParameterNode) && first.name == local
|
|
8479
|
+
|
|
8480
|
+
Source::NodeWalker.each(parameters).none? do |param|
|
|
8481
|
+
!param.equal?(first) && local_binding?(param, local)
|
|
8482
|
+
end
|
|
8483
|
+
end
|
|
8484
|
+
|
|
8485
|
+
# The elements of the array `receiver` denotes for {#iterated_element_source}: a literal array's own, or a
|
|
8486
|
+
# bare constant's when the same scope assigns it a literal array exactly once. Nil when it denotes anything
|
|
8487
|
+
# else — a qualified or rooted constant, a local, a call — so the iterated value may be anything.
|
|
8488
|
+
def iterated_receiver_elements(receiver, scope_nodes)
|
|
8489
|
+
return receiver.elements if receiver.is_a?(Prism::ArrayNode)
|
|
8490
|
+
return unless receiver.is_a?(Prism::ConstantReadNode)
|
|
8491
|
+
|
|
8492
|
+
constant_array_elements(receiver.name, scope_nodes)
|
|
8493
|
+
end
|
|
8494
|
+
|
|
8495
|
+
# The elements of the literal array `name` is assigned in `nodes`' scope, or nil when it is written any
|
|
8496
|
+
# other number of times or its value is not an array literal — the constant may then hold anything. Only
|
|
8497
|
+
# a single plain `NAME = […]` write closes; in-place mutation (`KINDS << :include`) is unmodeled.
|
|
8498
|
+
def constant_array_elements(name, nodes)
|
|
8499
|
+
writes = constant_write_nodes(nodes, name)
|
|
8500
|
+
return unless writes.one? && writes.first.is_a?(Prism::ConstantWriteNode)
|
|
8501
|
+
|
|
8502
|
+
value = writes.first.value
|
|
8503
|
+
value.elements if value.is_a?(Prism::ArrayNode)
|
|
8504
|
+
end
|
|
8505
|
+
|
|
8506
|
+
# Every write binding the bare constant `name` in `nodes`' scope — a plain `NAME = …`, a multi-assign
|
|
8507
|
+
# target or an operator / `&&=` / `||=` write, mirroring {#local_binding?} — stopping where
|
|
8508
|
+
# {#collect_local_send_names} stops, since a nested `def` or declaration binds its own constants — so
|
|
8509
|
+
# {#constant_array_elements} can tell one literal-array assignment from none or several.
|
|
8510
|
+
def constant_write_nodes(nodes, name)
|
|
8511
|
+
nodes.flat_map do |node|
|
|
8512
|
+
next [] if LOCAL_SCOPE_BOUNDARIES.any? { |kind| node.is_a?(kind) }
|
|
8513
|
+
|
|
8514
|
+
own = constant_binding?(node, name) ? [node] : []
|
|
8515
|
+
own + constant_write_nodes(node.compact_child_nodes, name)
|
|
8516
|
+
end
|
|
8517
|
+
end
|
|
8518
|
+
|
|
8519
|
+
# A node that binds the bare constant `name`: every `Prism::Constant*` binding class — write, target,
|
|
8520
|
+
# operator, `&&=` and `||=` — except the `ConstantReadNode` and `ConstantPath*` spellings, which read
|
|
8521
|
+
# `name` or bind a qualified `A::NAME` rather than `name` itself.
|
|
8522
|
+
def constant_binding?(node, name)
|
|
8523
|
+
return false unless node.respond_to?(:name) && node.name == name
|
|
8524
|
+
|
|
8525
|
+
klass = node.class.name
|
|
8526
|
+
klass.start_with?("Prism::Constant") && !klass.start_with?("Prism::ConstantPath") &&
|
|
8527
|
+
!node.is_a?(Prism::ConstantReadNode)
|
|
8528
|
+
end
|
|
8529
|
+
|
|
8530
|
+
# The children of an iterator call {#iterated_element_source} matched, minus its block's parameters — the
|
|
8531
|
+
# first is `local` bound to the elements and the rest bind other names — so the usual scan still sees every
|
|
8532
|
+
# other binding of `local` in the receiver, arguments and body.
|
|
8533
|
+
def iterated_element_children(node)
|
|
8534
|
+
[node.receiver, node.arguments, node.block.body].compact
|
|
8535
|
+
end
|
|
8536
|
+
|
|
8537
|
+
# The method name a `send` argument spells: a name literal's, or `""` — no mixin's — for an interpolation with a
|
|
8538
|
+
# literal part no mixin name contains (`"#{name}_from_params"`). Nil when it may spell any name.
|
|
8539
|
+
def literal_send_name(value)
|
|
8540
|
+
return value.unescaped if value.is_a?(Prism::SymbolNode) || value.is_a?(Prism::StringNode)
|
|
8541
|
+
return nil unless value.is_a?(Prism::InterpolatedStringNode) || value.is_a?(Prism::InterpolatedSymbolNode)
|
|
8542
|
+
|
|
8543
|
+
excluded = value.parts.any? do |part|
|
|
8544
|
+
part.is_a?(Prism::StringNode) && MIXIN_KINDS.none? { |kind| kind.name.include?(part.unescaped) }
|
|
8545
|
+
end
|
|
8546
|
+
excluded ? "" : nil
|
|
8547
|
+
end
|
|
8548
|
+
|
|
8549
|
+
# A node other than a plain write that binds `local`: a target, an operator write or a parameter.
|
|
8550
|
+
def local_binding?(node, local)
|
|
8551
|
+
return false if node.is_a?(Prism::LocalVariableReadNode) || !node.respond_to?(:name) || node.name != local
|
|
8552
|
+
|
|
8553
|
+
node.class.name.start_with?("Prism::LocalVariable") || node.class.name.end_with?("ParameterNode") ||
|
|
8554
|
+
node.is_a?(Prism::BlockLocalVariableNode)
|
|
8555
|
+
end
|
|
7057
8556
|
|
|
7058
8557
|
# `[kind, arguments, via_send]` for a call that mixes modules in — `include` / `prepend` / `extend`, or
|
|
7059
8558
|
# `send(:include, M)` and its `public_send` / `__send__` twins with a literal first argument — else nil.
|
|
@@ -7112,8 +8611,9 @@ module Rigor
|
|
|
7112
8611
|
end
|
|
7113
8612
|
|
|
7114
8613
|
HOOK_DEFS = %i[included extended prepended inherited].freeze
|
|
8614
|
+
INSTANCE_MIXIN_NAMES = %i[include prepend].freeze
|
|
7115
8615
|
MIXIN_EVAL_CALLS = %i[class_eval module_eval class_exec module_exec instance_eval instance_exec].freeze
|
|
7116
|
-
private_constant :HOOK_DEFS, :MIXIN_EVAL_CALLS
|
|
8616
|
+
private_constant :HOOK_DEFS, :MIXIN_EVAL_CALLS, :INSTANCE_MIXIN_NAMES
|
|
7117
8617
|
|
|
7118
8618
|
# The parameter names of a `def included(base)`-style hook (a hook is written `def self.included`, or
|
|
7119
8619
|
# `def included` inside `class << self`), else nil.
|
|
@@ -7148,9 +8648,14 @@ module Rigor
|
|
|
7148
8648
|
receiver = node.receiver
|
|
7149
8649
|
return if receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
7150
8650
|
return if Source::ConstantPath.qualified_name_or_nil(receiver)
|
|
7151
|
-
return unless hook_receiver?(receiver, accumulator) || block_mixes_in?(node.block)
|
|
7152
8651
|
|
|
7153
|
-
|
|
8652
|
+
hook = hook_receiver?(receiver, accumulator)
|
|
8653
|
+
return unless hook || block_mixes_in?(node.block)
|
|
8654
|
+
|
|
8655
|
+
# Issue #2991 — the set is unknown only where the block may mix into the class itself: a hook's `base`, or a
|
|
8656
|
+
# receiverless `include` / `prepend` in the block of a receiver other than a `singleton_class`.
|
|
8657
|
+
unnamed = hook || (!singleton_class_eval_receiver?(receiver) && block_mixes_into_self?(node.block))
|
|
8658
|
+
accumulator.taint(current_class, :include, unnamed: unnamed)
|
|
7154
8659
|
accumulator.taint(current_class, :extend)
|
|
7155
8660
|
end
|
|
7156
8661
|
|
|
@@ -7162,6 +8667,22 @@ module Rigor
|
|
|
7162
8667
|
found
|
|
7163
8668
|
end
|
|
7164
8669
|
|
|
8670
|
+
# A receiverless or `self.` `include` / `prepend` (or its `send` spelling) anywhere in `node`.
|
|
8671
|
+
def block_mixes_into_self?(node)
|
|
8672
|
+
if node.is_a?(Prism::CallNode) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
8673
|
+
view = mixin_call_view(node)
|
|
8674
|
+
return true if view && INSTANCE_MIXIN_NAMES.include?(view.first)
|
|
8675
|
+
end
|
|
8676
|
+
|
|
8677
|
+
found = false
|
|
8678
|
+
node.rigor_each_child { |child| found ||= block_mixes_into_self?(child) }
|
|
8679
|
+
found
|
|
8680
|
+
end
|
|
8681
|
+
|
|
8682
|
+
def singleton_class_eval_receiver?(receiver)
|
|
8683
|
+
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class
|
|
8684
|
+
end
|
|
8685
|
+
|
|
7165
8686
|
def singleton_class_receiver?(receiver)
|
|
7166
8687
|
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil? &&
|
|
7167
8688
|
(receiver.receiver.nil? || receiver.receiver.is_a?(Prism::SelfNode))
|
|
@@ -7193,19 +8714,8 @@ module Rigor
|
|
|
7193
8714
|
list.unshift(*(prepend ? targets : targets.reject { |target| list.include?(target) }))
|
|
7194
8715
|
end
|
|
7195
8716
|
|
|
7196
|
-
# Whether a mixin call
|
|
7197
|
-
|
|
7198
|
-
# `Integer.include(Ext)` and `Integer.send(:include, Ext)`, issue #1722) files it under the class the
|
|
7199
|
-
# receiver names. A receiver-form edge is unpositioned wherever it is written ({MixinAccumulator#note}), so
|
|
7200
|
-
# a reader that depends on ORDER declines on it, while the set-shaped readers (undefined-method
|
|
7201
|
-
# suppression, arity, visibility) see the module as they see an in-body `include`. A receiverless `send`
|
|
7202
|
-
# stays a taint: the walk does not resolve its `self`.
|
|
7203
|
-
def mixin_call_recorded?(node, kind, via_send, current_class)
|
|
7204
|
-
return false unless MIXIN_CALL_NAMES.include?(kind)
|
|
7205
|
-
return !via_send && !current_class.nil? if node.receiver.nil?
|
|
7206
|
-
|
|
7207
|
-
true
|
|
7208
|
-
end
|
|
8717
|
+
# Whether a mixin call's kind reaches the instance-side tables: `include` and `prepend`.
|
|
8718
|
+
def mixin_call_recorded?(kind) = MIXIN_CALL_NAMES.include?(kind)
|
|
7209
8719
|
|
|
7210
8720
|
# Issue #1697 — a receiverless `include M` written as a statement of the file's own top level is
|
|
7211
8721
|
# `main.include`, which is `Object.include`: it mixes M into `Object`. It is recorded under its own
|
|
@@ -7214,14 +8724,22 @@ module Rigor
|
|
|
7214
8724
|
# walk that types a call reaches it: what Ruby reaches first for a name such an include supplies
|
|
7215
8725
|
# depends on more than the chain shows (a block that rebinds `self`, an `extend` or a singleton `def`
|
|
7216
8726
|
# on `main`), so only the diagnostic silences read it (`Inference::ObjectMixins`), and typing is
|
|
7217
|
-
# issue #1715's.
|
|
7218
|
-
#
|
|
7219
|
-
#
|
|
7220
|
-
#
|
|
7221
|
-
#
|
|
8727
|
+
# issue #1715's. `main` has no `prepend`, and a top-level `extend M` reaches `main`'s singleton alone, so
|
|
8728
|
+
# neither is recorded.
|
|
8729
|
+
#
|
|
8730
|
+
# Issue #3007 — the call counts wherever {MixinAccumulator::MainSelfCalls} takes its `self` to be `main`: a
|
|
8731
|
+
# program statement, or a call under top-level control flow (`if`, `begin`, `case`, …) or in a top-level
|
|
8732
|
+
# `def` body. Such a call may not run, so its edge is in the set with no position ({MixinAccumulator#note}),
|
|
8733
|
+
# as ADR-119 WD7 records an `include` in a method body. The same call inside a top-level block stays
|
|
8734
|
+
# unrecorded: its `self` may not be `main` (a `describe do include M end` or `class_exec` block rebinds it,
|
|
8735
|
+
# while `[1].each do include M end` does not, and the walk cannot tell them apart).
|
|
8736
|
+
#
|
|
8737
|
+
# Issue #2995 — `self.include M`, a receiverless or `self.` `send(:include, M)` and its `__send__` twin are the
|
|
8738
|
+
# same `main.include`. `public_send(:include, M)` is not: `main`'s `include` is private, so it raises.
|
|
7222
8739
|
def toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
|
|
7223
8740
|
return nil unless current_class.nil? && qualified_prefix.empty?
|
|
7224
|
-
return nil unless node.receiver
|
|
8741
|
+
return nil unless lexical_self_receiver?(node.receiver) && node.name != :public_send
|
|
8742
|
+
return nil unless mixin_call_view(node)&.first == :include
|
|
7225
8743
|
|
|
7226
8744
|
accumulator.program_statement?(node) ? ObjectMixins::TOPLEVEL_INCLUDE_KEY : nil
|
|
7227
8745
|
end
|
|
@@ -7283,8 +8801,8 @@ module Rigor
|
|
|
7283
8801
|
singleton_cref)
|
|
7284
8802
|
when Prism::SingletonClassNode
|
|
7285
8803
|
# Issue #915 — `class << self` opens the enclosing declaration's OWN singleton, so an `include`
|
|
7286
|
-
# written in it is the same singleton ancestor an `extend` in the class body would add.
|
|
7287
|
-
# `
|
|
8804
|
+
# written in it is the same singleton ancestor an `extend` in the class body would add. Issue #3010 —
|
|
8805
|
+
# `class << Const` is followed too; `class << obj` names something this walk cannot resolve to a class.
|
|
7288
8806
|
# Nested declarations inside the body take the `ClassNode` branch above and reset the flag with
|
|
7289
8807
|
# their own owner, so the marker cannot leak past the singleton body it belongs to.
|
|
7290
8808
|
return walk_extends_singleton_class(node, qualified_prefix, current_class, accumulator,
|
|
@@ -7337,7 +8855,8 @@ module Rigor
|
|
|
7337
8855
|
|
|
7338
8856
|
# The `class <<` arm of {#walk_class_extends}: the expression evaluates in the enclosing
|
|
7339
8857
|
# context while the body is the singleton's — `class << self` keeps the enclosing owner
|
|
7340
|
-
# (its `include` is the singleton ancestor #915 folds),
|
|
8858
|
+
# (its `include` is the singleton ancestor #915 folds), `class << Const` takes the class the
|
|
8859
|
+
# constant names (#3010), other operands walk ownerless.
|
|
7341
8860
|
# `self` inside is the singleton — `self::` receivers decline — and the lexical cref
|
|
7342
8861
|
# below is unnameable in every case.
|
|
7343
8862
|
def walk_extends_singleton_class(node, qualified_prefix, current_class, accumulator,
|
|
@@ -7346,17 +8865,114 @@ module Rigor
|
|
|
7346
8865
|
in_singleton: in_singleton, singleton_self: singleton_self,
|
|
7347
8866
|
singleton_cref: singleton_cref)
|
|
7348
8867
|
return unless node.body
|
|
8868
|
+
return walk_extends_constant_singleton(node, qualified_prefix, accumulator) if constant_singleton_class?(node)
|
|
7349
8869
|
|
|
7350
8870
|
# `class << self` keeps the owner only at the FIRST level — nested inside a
|
|
7351
8871
|
# singleton body, `self` IS the singleton and `class << self` opens the
|
|
7352
8872
|
# singleton's own singleton (`#<Class:#<Class:C>>`), which nothing names.
|
|
7353
8873
|
opens_self = node.expression.is_a?(Prism::SelfNode) && !in_singleton
|
|
7354
8874
|
accumulator.direct_body(node.body) if opens_self && accumulator.direct?(node)
|
|
8875
|
+
accumulator.class_body(node.body) if opens_self
|
|
7355
8876
|
walk_class_extends(node.body, qualified_prefix, opens_self ? current_class : nil,
|
|
7356
8877
|
accumulator, in_singleton: opens_self,
|
|
7357
8878
|
singleton_self: true, singleton_cref: true)
|
|
7358
8879
|
end
|
|
7359
8880
|
|
|
8881
|
+
# Issue #3010 — `class << Const` opens the class object's own singleton class, so an `include` / `prepend` in its
|
|
8882
|
+
# body is a singleton mixin on `Const`, as one in `class << self` inside `Const`'s body is. The body walks with
|
|
8883
|
+
# the class `Const` resolves to from the lexical scope ({#constant_singleton_owners}, the class its `def`s record
|
|
8884
|
+
# under) as owner. Its edges are positioned only where the order is a fact: the `class <<` is a direct statement
|
|
8885
|
+
# of a file that declares that class, and no more-nested name the constant could denote is left. Anywhere else (a
|
|
8886
|
+
# method body, a conditional, another file) they are unpositioned, and a prepend also records, unpositioned, on
|
|
8887
|
+
# every other name the constant can denote ({#record_constant_singleton_candidates}), as #2998's
|
|
8888
|
+
# `Const.singleton_class.prepend P` does.
|
|
8889
|
+
def walk_extends_constant_singleton(node, qualified_prefix, accumulator)
|
|
8890
|
+
owner, others = constant_singleton_owners(node, qualified_prefix)
|
|
8891
|
+
if others.empty? && accumulator.direct?(node) && eval_file_declared_names(node).include?(owner)
|
|
8892
|
+
accumulator.direct_body(node.body)
|
|
8893
|
+
end
|
|
8894
|
+
walk_class_extends(node.body, qualified_prefix, owner, accumulator,
|
|
8895
|
+
in_singleton: true, singleton_self: true, singleton_cref: true)
|
|
8896
|
+
record_constant_singleton_candidates(node.body, others, accumulator)
|
|
8897
|
+
end
|
|
8898
|
+
|
|
8899
|
+
# Each candidate class past the owner takes the body's prepends unpositioned; an argument that names no module
|
|
8900
|
+
# (`prepend(*MODS)`) leaves its singleton set unknown, as {#taint_unrecorded_mixin} reads it on the owner.
|
|
8901
|
+
def record_constant_singleton_candidates(body, candidates, accumulator)
|
|
8902
|
+
return if candidates.empty?
|
|
8903
|
+
|
|
8904
|
+
each_singleton_body_prepend(body) do |call|
|
|
8905
|
+
candidates.each do |candidate|
|
|
8906
|
+
next unless record_extend_targets(call, candidate, accumulator).empty?
|
|
8907
|
+
|
|
8908
|
+
accumulator.taint(candidate, :extend, unnamed: true)
|
|
8909
|
+
end
|
|
8910
|
+
end
|
|
8911
|
+
end
|
|
8912
|
+
|
|
8913
|
+
# A `class << Const` whose operand is a constant read (`class << Foo = expr` opens an object's singleton).
|
|
8914
|
+
def constant_singleton_class?(node)
|
|
8915
|
+
node.expression.is_a?(Prism::ConstantReadNode) || node.expression.is_a?(Prism::ConstantPathNode)
|
|
8916
|
+
end
|
|
8917
|
+
|
|
8918
|
+
# `[owner, others]` for a `class << Const`: the class the constant resolves to from the lexical scope
|
|
8919
|
+
# ({#singleton_class_prefix}, where the body's `def`s record), and the other names it can denote from there
|
|
8920
|
+
# ({#constant_receiver_candidates}). `others` is empty when the constant can denote one name only, when it names
|
|
8921
|
+
# the enclosing class itself (`class << Foo` inside `class Foo`, which {#singleton_class_prefix} reads as
|
|
8922
|
+
# `class << self`), or when the owner is the innermost candidate and the file declares it ahead of the `class <<`
|
|
8923
|
+
# ({#declared_before?}): Ruby resolves the constant when the statement runs, so a nested class the file declares
|
|
8924
|
+
# later does not exist yet there.
|
|
8925
|
+
def constant_singleton_owners(node, qualified_prefix)
|
|
8926
|
+
owner = singleton_class_prefix(node, nil, qualified_prefix).join("::")
|
|
8927
|
+
candidates = constant_receiver_candidates(node.expression, qualified_prefix)
|
|
8928
|
+
# One candidate needs no re-parse: whichever statement declares it, the constant can name nothing else.
|
|
8929
|
+
return [owner, EMPTY_PREFIX] if candidates.size == 1 || qualified_prefix.join("::") == owner
|
|
8930
|
+
return [owner, EMPTY_PREFIX] if candidates.first == owner && declared_before?(node, owner)
|
|
8931
|
+
|
|
8932
|
+
[owner, candidates - [owner]]
|
|
8933
|
+
end
|
|
8934
|
+
|
|
8935
|
+
# Issue #3010 — whether `node`'s file declares `name` (a `class` / `module` declaration or a bare constant
|
|
8936
|
+
# write) at a statement that starts ahead of `node`. Asked only where a `class << Const` has a nested candidate
|
|
8937
|
+
# the file declares, so it re-parses the file rather than keep a table every file would pay for.
|
|
8938
|
+
def declared_before?(node, name)
|
|
8939
|
+
source = node.location.send(:source).source
|
|
8940
|
+
declaration_before?(Prism.parse(source).value, EMPTY_PREFIX, name, node.location.start_offset)
|
|
8941
|
+
end
|
|
8942
|
+
|
|
8943
|
+
def declaration_before?(node, prefix, name, limit)
|
|
8944
|
+
return false unless node.is_a?(Prism::Node) && node.location.start_offset < limit
|
|
8945
|
+
|
|
8946
|
+
case node
|
|
8947
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
8948
|
+
child = Source::ConstantPath.declaration_prefix(prefix, node.constant_path)
|
|
8949
|
+
return false if child.nil?
|
|
8950
|
+
return true if child.join("::") == name
|
|
8951
|
+
|
|
8952
|
+
return declaration_before?(node.body, child, name, limit)
|
|
8953
|
+
when Prism::ConstantWriteNode
|
|
8954
|
+
return true if (prefix + [node.name.to_s]).join("::") == name
|
|
8955
|
+
when Prism::DefNode, Prism::SingletonClassNode
|
|
8956
|
+
# A `def` body declares no constant, and a `class <<` body's are the singleton class's.
|
|
8957
|
+
return false
|
|
8958
|
+
end
|
|
8959
|
+
node.rigor_each_child { |child| return true if declaration_before?(child, prefix, name, limit) }
|
|
8960
|
+
false
|
|
8961
|
+
end
|
|
8962
|
+
|
|
8963
|
+
# The receiverless or `self.` `prepend` calls (direct or `send` spelling) of a `class <<` body, outside any
|
|
8964
|
+
# nested `def` or declaration body.
|
|
8965
|
+
def each_singleton_body_prepend(node, &)
|
|
8966
|
+
return unless node.is_a?(Prism::Node)
|
|
8967
|
+
return if CODE_BODY_NODES.any? { |klass| node.is_a?(klass) }
|
|
8968
|
+
|
|
8969
|
+
if node.is_a?(Prism::CallNode) && lexical_self_receiver?(node.receiver) &&
|
|
8970
|
+
mixin_call_view(node)&.first == :prepend
|
|
8971
|
+
yield node
|
|
8972
|
+
end
|
|
8973
|
+
node.rigor_each_child { |child| each_singleton_body_prepend(child, &) }
|
|
8974
|
+
end
|
|
8975
|
+
|
|
7360
8976
|
# The declaration arm of {#walk_class_extends}: under an unnameable cref a bare/`self::`
|
|
7361
8977
|
# header opens `#<singleton>::Name` — ownerless; nameable headers re-anchor at a
|
|
7362
8978
|
# real cref.
|
|
@@ -7370,6 +8986,7 @@ module Rigor
|
|
|
7370
8986
|
child_cref = unnameable_decl?(node, self_decl, singleton_cref)
|
|
7371
8987
|
accumulator.direct_body(node.body) if accumulator.direct?(node)
|
|
7372
8988
|
accumulator.body_statements(node.body)
|
|
8989
|
+
accumulator.class_body(node.body)
|
|
7373
8990
|
walk_class_extends(node.body, child_cref ? [] : child_prefix,
|
|
7374
8991
|
child_cref ? nil : child_prefix.join("::"), accumulator,
|
|
7375
8992
|
singleton_cref: child_cref)
|
|
@@ -7646,16 +9263,24 @@ module Rigor
|
|
|
7646
9263
|
return if singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator,
|
|
7647
9264
|
in_singleton: in_singleton)
|
|
7648
9265
|
|
|
7649
|
-
kind,
|
|
7650
|
-
|
|
9266
|
+
kind, arguments, via_send = mixin_call_view(node)
|
|
9267
|
+
if kind.nil?
|
|
9268
|
+
# Inside `class << self` a computed `include` / `prepend` reaches the singleton side; an `extend` reaches the
|
|
9269
|
+
# singleton's own singleton, which nothing names.
|
|
9270
|
+
kinds = in_singleton ? INSTANCE_MIXIN_NAMES : EXTEND_MIXIN_NAMES
|
|
9271
|
+
return taint_computed_mixin_send(node, current_class, accumulator, kinds, :extend, in_singleton: in_singleton)
|
|
9272
|
+
end
|
|
7651
9273
|
|
|
7652
9274
|
effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: in_singleton)
|
|
7653
9275
|
.select { |_owner, side| side == :extend || in_singleton }
|
|
7654
9276
|
return if effects.empty?
|
|
7655
|
-
|
|
7656
|
-
|
|
9277
|
+
# A `send` spelling is recorded only on the lexical `self` (#2995); `Const.send(:extend, M)` stays a taint.
|
|
9278
|
+
return if (!via_send || lexical_self_receiver?(node.receiver)) &&
|
|
9279
|
+
singleton_mixin_recorded?(node, kind, qualified_prefix, current_class, accumulator, in_singleton)
|
|
7657
9280
|
|
|
7658
|
-
|
|
9281
|
+
# Issue #2991 — as in {#record_mixin_call}: an unrecorded instance-side call (a hook's `base.include M` written
|
|
9282
|
+
# in `class << self`) lists its names, or the unnamed marker.
|
|
9283
|
+
taint_unrecorded_mixin(node, kind, arguments, effects, accumulator)
|
|
7659
9284
|
end
|
|
7660
9285
|
|
|
7661
9286
|
# Issue #1752 — a call that rewrites a singleton class reached through `singleton_class` (`singleton_class.
|
|
@@ -7679,23 +9304,53 @@ module Rigor
|
|
|
7679
9304
|
# Inside `class << self` a bare `singleton_class` is the singleton's own singleton, one level up from the
|
|
7680
9305
|
# owner's extend side, so that call stays a taint.
|
|
7681
9306
|
up_level = in_singleton && lexical_self_receiver?(receiver.receiver)
|
|
7682
|
-
|
|
9307
|
+
if !up_level && singleton_class_include_recorded?(node, owner, accumulator)
|
|
9308
|
+
record_prepend_candidates(node, receiver.receiver, qualified_prefix, owner, accumulator)
|
|
9309
|
+
return true
|
|
9310
|
+
end
|
|
7683
9311
|
|
|
7684
|
-
|
|
9312
|
+
# Issue #2994 — an `include` / `prepend` whose argument names no module (`singleton_class.include(*MODS)`)
|
|
9313
|
+
# leaves the owner's singleton set unknown, as {#taint_unrecorded_mixin} reads an `extend(*EMODS)`.
|
|
9314
|
+
accumulator.taint(owner, :extend, unnamed: unnamed_singleton_include?(node))
|
|
7685
9315
|
true
|
|
7686
9316
|
end
|
|
7687
9317
|
|
|
9318
|
+
def unnamed_singleton_include?(node)
|
|
9319
|
+
INSTANCE_MIXIN_NAMES.include?(node.name) &&
|
|
9320
|
+
(node.arguments&.arguments || []).any? { |arg| opaque_mixin_argument?(arg) }
|
|
9321
|
+
end
|
|
9322
|
+
|
|
7688
9323
|
# Issue #2094 — `Float.singleton_class.include(Ext)` (or `prepend`, which the extend table cannot order, so the
|
|
7689
9324
|
# edge is unpositioned as every receiver-form edge is) puts `Ext` on the singleton side as `Float.extend(Ext)`
|
|
7690
9325
|
# does, the fold #915 gives `class << self; include M`. A `send` spelling or an argument that is not a constant
|
|
7691
9326
|
# stays a taint.
|
|
9327
|
+
#
|
|
9328
|
+
# Issue #2998 — a prepend, spelt directly or as `send(:prepend, P)`, is recorded, and the methods walk files it as
|
|
9329
|
+
# a singleton prepend ({#record_own_singleton_prepend}). One on the enclosing class's own `singleton_class` (a
|
|
9330
|
+
# bare or `self.` receiver) runs where it is written, as `prepend P` in `class << self` does, so a direct
|
|
9331
|
+
# statement of the body is positioned; a constant receiver's stays unpositioned.
|
|
7692
9332
|
def singleton_class_include_recorded?(node, owner, accumulator)
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
arguments = node.arguments&.arguments || []
|
|
9333
|
+
kind, sent, = mixin_call_view(node)
|
|
9334
|
+
prepend = kind == :prepend
|
|
9335
|
+
arguments = prepend ? sent : node.arguments&.arguments || []
|
|
9336
|
+
return false unless prepend || node.name == :include
|
|
7696
9337
|
return false if arguments.empty? || arguments.any? { |arg| Source::ConstantPath.qualified_name(arg).nil? }
|
|
7697
9338
|
|
|
7698
|
-
|
|
9339
|
+
positioned = prepend && lexical_self_receiver?(node.receiver.receiver)
|
|
9340
|
+
!record_extend_targets(node, owner, accumulator, positioned: positioned).empty?
|
|
9341
|
+
end
|
|
9342
|
+
|
|
9343
|
+
# Issue #2998 — a constant receiver's `Const.singleton_class.prepend P` also records the edge, unpositioned, under
|
|
9344
|
+
# every other name the constant can denote from the call site, the names the methods walk files its
|
|
9345
|
+
# singleton-prepend entry under ({#singleton_prepend_owners}): which class Ruby reaches is not a fact here, so
|
|
9346
|
+
# each candidate's typing read declines where the prepend could answer.
|
|
9347
|
+
def record_prepend_candidates(node, object, qualified_prefix, owner, accumulator)
|
|
9348
|
+
return unless mixin_call_view(node)&.first == :prepend
|
|
9349
|
+
return unless object.is_a?(Prism::ConstantReadNode) || object.is_a?(Prism::ConstantPathNode)
|
|
9350
|
+
|
|
9351
|
+
(constant_receiver_candidates(object, qualified_prefix) - [owner]).each do |candidate|
|
|
9352
|
+
record_extend_targets(node, candidate, accumulator)
|
|
9353
|
+
end
|
|
7699
9354
|
end
|
|
7700
9355
|
|
|
7701
9356
|
# A bare or `self.` receiver: the lexical `self`.
|
|
@@ -7759,10 +9414,12 @@ module Rigor
|
|
|
7759
9414
|
accumulator.note(node, current_class, :extend, [current_class], complete: true)
|
|
7760
9415
|
end
|
|
7761
9416
|
|
|
7762
|
-
# Records the singleton-side edges of a receiverless `extend` (or, inside `class << self`,
|
|
7763
|
-
# `prepend`). False when it recorded nothing.
|
|
9417
|
+
# Records the singleton-side edges of a receiverless or `self.` `extend` (or, inside `class << self`,
|
|
9418
|
+
# `include` / `prepend`), and of their `send` spellings (#2995). False when it recorded nothing.
|
|
7764
9419
|
def singleton_mixin_recorded?(node, kind, qualified_prefix, current_class, accumulator, in_singleton)
|
|
7765
|
-
|
|
9420
|
+
unless lexical_self_receiver?(node.receiver)
|
|
9421
|
+
return receiver_extend_recorded?(node, kind, qualified_prefix, accumulator)
|
|
9422
|
+
end
|
|
7766
9423
|
return false unless current_class
|
|
7767
9424
|
return false unless in_singleton ? MIXIN_CALL_NAMES.include?(kind) : kind == :extend
|
|
7768
9425
|
|
|
@@ -7788,9 +9445,12 @@ module Rigor
|
|
|
7788
9445
|
# (`extend A; extend B` → singleton ancestors `[B, A]`) while the arguments of a single
|
|
7789
9446
|
# `extend A, B` keep call order (`[A, B]`). Prepending each call's argument list preserves both,
|
|
7790
9447
|
# and consumers iterating the list forward read the same order the singleton ancestry searches.
|
|
7791
|
-
|
|
9448
|
+
#
|
|
9449
|
+
# The arguments are {#mixin_call_view}'s, so a `send(:extend, M)` (#2995) reads `M` and not its name literal.
|
|
9450
|
+
# `positioned` says a receiver-form call still runs where it is written ({#singleton_class_include_recorded?}).
|
|
9451
|
+
def record_extend_targets(node, current_class, accumulator, positioned: false)
|
|
7792
9452
|
targets = []
|
|
7793
|
-
arguments = node
|
|
9453
|
+
arguments = mixin_call_view(node)[1]
|
|
7794
9454
|
arguments.each do |arg|
|
|
7795
9455
|
target = arg.is_a?(Prism::SelfNode) ? current_class : Source::ConstantPath.qualified_name(arg)
|
|
7796
9456
|
targets << target if target
|
|
@@ -7804,7 +9464,8 @@ module Rigor
|
|
|
7804
9464
|
# unpositioned and the chain settles to the readers' own answer.
|
|
7805
9465
|
accumulator.taint(current_class, :extend) if targets.any? { |target| list.include?(target) }
|
|
7806
9466
|
list.unshift(*targets)
|
|
7807
|
-
accumulator.note(node, current_class, :extend, targets, complete: targets.size == arguments.size
|
|
9467
|
+
accumulator.note(node, current_class, :extend, targets, complete: targets.size == arguments.size,
|
|
9468
|
+
positioned: positioned)
|
|
7808
9469
|
targets
|
|
7809
9470
|
end
|
|
7810
9471
|
|
|
@@ -7990,6 +9651,10 @@ module Rigor
|
|
|
7990
9651
|
# written directly in the body settles the default again. A named target does not read the running default,
|
|
7991
9652
|
# so a toggle does not contest it. A certain write clears the slot's contest.
|
|
7992
9653
|
class VisibilityTable < Hash
|
|
9654
|
+
include HookBlockSpans
|
|
9655
|
+
|
|
9656
|
+
attr_accessor :hook_block_spans
|
|
9657
|
+
|
|
7993
9658
|
# The bare toggles the walk has met so far; a body's statement loop compares it around each child.
|
|
7994
9659
|
attr_accessor :toggles
|
|
7995
9660
|
# Whether the running default of the body being walked rests on an uncertain toggle.
|
|
@@ -8112,6 +9777,22 @@ module Rigor
|
|
|
8112
9777
|
VISIBILITY_TOGGLES.include?(name) ? [name, arguments] : nil
|
|
8113
9778
|
end
|
|
8114
9779
|
|
|
9780
|
+
# Issue #1592 — a concern's `class_methods do … end` block is a `module_eval` of `Owner::ClassMethods`, whose
|
|
9781
|
+
# visibility starts public: its `def`s' visibilities are recorded on that module too, as its `def`s are.
|
|
9782
|
+
# Issue #2997 — in every body of the module, as {#record_concern_class_methods} reads its `def`s.
|
|
9783
|
+
def walk_class_methods_visibilities(node, child_prefix, accumulator)
|
|
9784
|
+
return unless node.is_a?(Prism::ModuleNode) && node.body.is_a?(Prism::StatementsNode)
|
|
9785
|
+
|
|
9786
|
+
node.body.body.each do |statement|
|
|
9787
|
+
next unless statement.is_a?(Prism::CallNode) && block_statement?(statement) &&
|
|
9788
|
+
statement.name == :class_methods && statement.block.body
|
|
9789
|
+
|
|
9790
|
+
accumulator.body do
|
|
9791
|
+
walk_method_visibilities(statement.block.body, [*child_prefix, "ClassMethods"], false, :public, accumulator)
|
|
9792
|
+
end
|
|
9793
|
+
end
|
|
9794
|
+
end
|
|
9795
|
+
|
|
8115
9796
|
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
8116
9797
|
def walk_method_visibilities(node, qualified_prefix, in_singleton_class, current_visibility, accumulator, # rubocop:disable Metrics/ParameterLists
|
|
8117
9798
|
def_owner_prefix = nil, singleton_cref: false,
|
|
@@ -8130,11 +9811,13 @@ module Rigor
|
|
|
8130
9811
|
# ownerless; nameable headers re-anchor at a real cref.
|
|
8131
9812
|
child_cref = unnameable_decl?(node, self_decl, singleton_cref)
|
|
8132
9813
|
body_prefix = child_cref ? [] : child_prefix
|
|
9814
|
+
accumulator.mark_hook_blocks(concern_hook_blocks(node, child_prefix), child_prefix) unless child_cref
|
|
8133
9815
|
if node.body
|
|
8134
9816
|
accumulator.body do
|
|
8135
9817
|
walk_method_visibilities(node.body, body_prefix, false, :public,
|
|
8136
9818
|
accumulator, nil, singleton_cref: child_cref)
|
|
8137
9819
|
end
|
|
9820
|
+
walk_class_methods_visibilities(node, child_prefix, accumulator) unless child_cref
|
|
8138
9821
|
end
|
|
8139
9822
|
return current_visibility
|
|
8140
9823
|
end
|
|
@@ -8163,6 +9846,8 @@ module Rigor
|
|
|
8163
9846
|
return current_visibility
|
|
8164
9847
|
end
|
|
8165
9848
|
when Prism::DefNode
|
|
9849
|
+
return current_visibility if accumulator.hook_block_self_def?(node, owner_prefix)
|
|
9850
|
+
|
|
8166
9851
|
contest_foreign_singleton_visibility(node, qualified_prefix, owner_prefix, in_singleton_class, accumulator)
|
|
8167
9852
|
record_def_visibility(node, owner_prefix, visibility_side(in_singleton_class, defs_singleton),
|
|
8168
9853
|
current_visibility, accumulator)
|
|
@@ -8273,6 +9958,7 @@ module Rigor
|
|
|
8273
9958
|
singleton_cref: singleton_cref,
|
|
8274
9959
|
defs_singleton: defs_singleton)
|
|
8275
9960
|
return unless node.body
|
|
9961
|
+
return if hook_block_singleton?(node, in_singleton_class, def_owner_prefix || qualified_prefix, accumulator)
|
|
8276
9962
|
|
|
8277
9963
|
accumulator.body do
|
|
8278
9964
|
walk_method_visibilities(node.body, qualified_prefix, true, :public, accumulator,
|
|
@@ -8775,6 +10461,7 @@ module Rigor
|
|
|
8775
10461
|
constant_receiver_candidates(receiver, qualified_prefix).each do |name|
|
|
8776
10462
|
record_surface_mark(tables, name, Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
|
|
8777
10463
|
end
|
|
10464
|
+
record_constant_singleton_definer(node, receiver, qualified_prefix, tables)
|
|
8778
10465
|
elsif singleton_class_of_receiver?(receiver) && singleton_class_named_call?(node)
|
|
8779
10466
|
record_singleton_class_names(node, receiver.receiver, qualified_prefix, tables)
|
|
8780
10467
|
end
|
|
@@ -8809,8 +10496,64 @@ module Rigor
|
|
|
8809
10496
|
return if def_receiver_targets_lexical_self?(receiver, owner_prefix)
|
|
8810
10497
|
|
|
8811
10498
|
constant_receiver_candidates(receiver, qualified_prefix).each do |name|
|
|
8812
|
-
|
|
8813
|
-
|
|
10499
|
+
record_object_singleton(tables, name, node.name)
|
|
10500
|
+
end
|
|
10501
|
+
end
|
|
10502
|
+
|
|
10503
|
+
# Issue #3016 — `Const.define_singleton_method(:x) { … }` with a literal name defines `x` on `Const`'s object, and
|
|
10504
|
+
# is filed as {#record_foreign_singleton_def} files `def Const.x`: an opaque singleton envelope on every name the
|
|
10505
|
+
# constant can denote. A computed name may define any name, so it marks each candidate with the singleton side's
|
|
10506
|
+
# open-names mark instead, as a computed `define_method` on a constant marks the instance side.
|
|
10507
|
+
def record_constant_singleton_definer(node, receiver, qualified_prefix, tables)
|
|
10508
|
+
names = define_singleton_method_names(node)
|
|
10509
|
+
return if names.nil?
|
|
10510
|
+
|
|
10511
|
+
candidates = constant_receiver_candidates(receiver, qualified_prefix)
|
|
10512
|
+
if names == :all
|
|
10513
|
+
candidates.each do |owner|
|
|
10514
|
+
record_surface_mark(tables, owner, Scope::DiscoveryIndex::ENVELOPE_OPEN_SINGLETON_NAMES_MARK)
|
|
10515
|
+
end
|
|
10516
|
+
return
|
|
10517
|
+
end
|
|
10518
|
+
|
|
10519
|
+
candidates.product(names) do |owner, name|
|
|
10520
|
+
record_envelope(tables.envelopes, owner, [:singleton, name], Source::ParameterEnvelope::OPAQUE)
|
|
10521
|
+
record_envelope(tables.envelopes, owner, [Scope::DiscoveryIndex::ENVELOPE_OBJECT_SINGLETON_KIND, name],
|
|
10522
|
+
Source::ParameterEnvelope::OPAQUE)
|
|
10523
|
+
end
|
|
10524
|
+
end
|
|
10525
|
+
|
|
10526
|
+
# A `def Const.x` written outside `Const`'s body, or one in a `Const.singleton_class.class_eval` block: the opaque
|
|
10527
|
+
# `[:singleton, x]` envelope, contested unless certain, and the
|
|
10528
|
+
# {Scope::DiscoveryIndex::ENVELOPE_OBJECT_SINGLETON_KIND} entry the class-object path reads (#3018).
|
|
10529
|
+
def record_object_singleton(tables, owner, method_name)
|
|
10530
|
+
record_envelope(tables.envelopes, owner, [:singleton, method_name], Source::ParameterEnvelope::OPAQUE)
|
|
10531
|
+
contest_envelope(tables, owner, [:singleton, method_name]) unless tables.certain
|
|
10532
|
+
record_envelope(tables.envelopes, owner, [Scope::DiscoveryIndex::ENVELOPE_OBJECT_SINGLETON_KIND, method_name],
|
|
10533
|
+
Source::ParameterEnvelope::OPAQUE)
|
|
10534
|
+
end
|
|
10535
|
+
|
|
10536
|
+
# Issue #3018 — `Const.singleton_class.class_eval { def x … }` (or `module_eval` / `class_exec` / `module_exec`)
|
|
10537
|
+
# defines `x` on `Const`'s object, as `def Const.x` does, and is filed as {#record_foreign_singleton_def} files
|
|
10538
|
+
# that def ({#record_object_singleton}) on every name the constant can denote. The eval-body walk files these
|
|
10539
|
+
# defs under no owner, since the receiver is a call. Every receiverless `def` in the block counts, one under a
|
|
10540
|
+
# conditional included, as {#each_refinement_def} yields them: not one under a `class` / `module` / `class <<`
|
|
10541
|
+
# body, which opens another definee, nor one inside another `def`, which installs only when that runs.
|
|
10542
|
+
# `instance_eval` on the singleton class is not filed: its defs land on the singleton class's own singleton (Ruby
|
|
10543
|
+
# 4.0.5 raises NoMethodError on `Const.x` there).
|
|
10544
|
+
def record_constant_singleton_eval_defs(node, qualified_prefix, tables)
|
|
10545
|
+
return unless CLASS_EVAL_BLOCK_CALLS.include?(node.name)
|
|
10546
|
+
|
|
10547
|
+
receiver = node.receiver
|
|
10548
|
+
return unless singleton_class_of_receiver?(receiver)
|
|
10549
|
+
|
|
10550
|
+
body = node.block.body
|
|
10551
|
+
return if body.nil?
|
|
10552
|
+
|
|
10553
|
+
# A bare, `self` or local receiver of `singleton_class` renders no constant, so it yields no candidate.
|
|
10554
|
+
candidates = constant_receiver_candidates(receiver.receiver, qualified_prefix)
|
|
10555
|
+
each_refinement_def(body) do |def_node|
|
|
10556
|
+
candidates.each { |owner| record_object_singleton(tables, owner, def_node.name) }
|
|
8814
10557
|
end
|
|
8815
10558
|
end
|
|
8816
10559
|
|
|
@@ -8859,22 +10602,33 @@ module Rigor
|
|
|
8859
10602
|
SURFACE_MIXIN_CALLS.include?(node.name) || SURFACE_MIXIN_HELPER.match?(node.name.to_s)
|
|
8860
10603
|
end
|
|
8861
10604
|
|
|
10605
|
+
# Issue #2995 — a `send` / `public_send` / `__send__` of `:include`, `:prepend` or `:extend` is that mixin call,
|
|
10606
|
+
# which the mixin walk records as the bare call, so it is judged by its own arguments as the bare call is.
|
|
8862
10607
|
def dynamic_surface_call?(node)
|
|
8863
|
-
return true if SURFACE_EVAL_CALLS.include?(node.name)
|
|
10608
|
+
return true if SURFACE_EVAL_CALLS.include?(node.name)
|
|
10609
|
+
|
|
10610
|
+
if SURFACE_SEND_CALLS.include?(node.name)
|
|
10611
|
+
kind, arguments, = mixin_call_view(node)
|
|
10612
|
+
return kind.nil? || dynamic_mixin_arguments?(arguments)
|
|
10613
|
+
end
|
|
8864
10614
|
|
|
8865
10615
|
arguments = node.arguments&.arguments || []
|
|
8866
10616
|
if SURFACE_NAMING_CALLS.include?(node.name)
|
|
8867
10617
|
arguments.empty? || literal_method_name(arguments.first).nil?
|
|
8868
10618
|
elsif surface_mixin_call?(node)
|
|
8869
|
-
arguments
|
|
8870
|
-
!(argument.is_a?(Prism::ConstantReadNode) || argument.is_a?(Prism::ConstantPathNode) ||
|
|
8871
|
-
argument.is_a?(Prism::SelfNode))
|
|
8872
|
-
end
|
|
10619
|
+
dynamic_mixin_arguments?(arguments)
|
|
8873
10620
|
else
|
|
8874
10621
|
false
|
|
8875
10622
|
end
|
|
8876
10623
|
end
|
|
8877
10624
|
|
|
10625
|
+
def dynamic_mixin_arguments?(arguments)
|
|
10626
|
+
arguments.any? do |argument|
|
|
10627
|
+
!(argument.is_a?(Prism::ConstantReadNode) || argument.is_a?(Prism::ConstantPathNode) ||
|
|
10628
|
+
argument.is_a?(Prism::SelfNode))
|
|
10629
|
+
end
|
|
10630
|
+
end
|
|
10631
|
+
|
|
8878
10632
|
# Every qualified name a constant receiver written inside `qualified_prefix` can denote, innermost
|
|
8879
10633
|
# first: the walk runs before any scope exists, so it cannot resolve the constant, and over-marking a
|
|
8880
10634
|
# same-named class elsewhere only withholds a check.
|
|
@@ -9155,15 +10909,76 @@ module Rigor
|
|
|
9155
10909
|
end
|
|
9156
10910
|
|
|
9157
10911
|
def record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
9158
|
-
return if qualified_prefix.empty?
|
|
9159
10912
|
return if call_node.arguments.nil? || call_node.arguments.arguments.empty?
|
|
9160
10913
|
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
10914
|
+
method_name = literal_method_name(call_node.arguments.arguments.first)
|
|
10915
|
+
receiver = call_node.receiver
|
|
10916
|
+
if method_name && (receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode))
|
|
10917
|
+
return record_constant_define_method(call_node, receiver, method_name, qualified_prefix, accumulator)
|
|
10918
|
+
end
|
|
10919
|
+
return if qualified_prefix.empty?
|
|
10920
|
+
if method_name.nil?
|
|
10921
|
+
return record_open_define_method(call_node, qualified_prefix, in_singleton_class,
|
|
10922
|
+
accumulator)
|
|
10923
|
+
end
|
|
9164
10924
|
|
|
9165
|
-
|
|
9166
|
-
|
|
10925
|
+
# A receiver other than a constant may still be `self` (`[self, k].each { |c| c.define_method(:x) {} }`), so
|
|
10926
|
+
# the name stays on the class the walk places the call in.
|
|
10927
|
+
record_method(accumulator, qualified_prefix.join("::"), method_name,
|
|
10928
|
+
in_singleton_class ? :singleton : :instance)
|
|
10929
|
+
end
|
|
10930
|
+
|
|
10931
|
+
# Issue #2975 — `Other.define_method(:x)` defines `Other#x`, whichever class body or side it is written in: the
|
|
10932
|
+
# constant resolves as a `class << Other` operand does ({#singleton_class_owner}), and the enclosing class
|
|
10933
|
+
# records nothing.
|
|
10934
|
+
def record_constant_define_method(call_node, receiver, method_name, qualified_prefix, accumulator)
|
|
10935
|
+
owner = singleton_class_owner(call_node, receiver, qualified_prefix, nil)
|
|
10936
|
+
record_method(accumulator, owner, method_name, :instance) if owner
|
|
10937
|
+
end
|
|
10938
|
+
|
|
10939
|
+
# Issue #2975 — the literal twins of {#record_self_definer_call}'s computed definers on `self`:
|
|
10940
|
+
# `send(:define_method, :x)` records `x` where a direct `define_method(:x)` there would, and
|
|
10941
|
+
# `define_singleton_method(:x)` records the class object's `x`. In a `class << self` body the latter defines on
|
|
10942
|
+
# the singleton's own singleton, which nothing names, so it records nothing.
|
|
10943
|
+
def record_literal_self_definer(node, class_name, in_singleton_class, accumulator)
|
|
10944
|
+
arguments = node.arguments&.arguments
|
|
10945
|
+
return if arguments.nil? || arguments.empty?
|
|
10946
|
+
|
|
10947
|
+
if node.name == :define_singleton_method
|
|
10948
|
+
name = literal_method_name(arguments.first)
|
|
10949
|
+
record_method(accumulator, class_name, name, :singleton) if name && !in_singleton_class
|
|
10950
|
+
elsif literal_method_name(arguments.first) == :define_method && arguments.length > 1
|
|
10951
|
+
name = literal_method_name(arguments[1])
|
|
10952
|
+
record_method(accumulator, class_name, name, in_singleton_class ? :singleton : :instance) if name
|
|
10953
|
+
end
|
|
10954
|
+
end
|
|
10955
|
+
|
|
10956
|
+
# Issue #2793 — a `define_method` on `self` whose name is not a literal defines a method this walk cannot name,
|
|
10957
|
+
# on the side and owner a literal one would record under (the walk's placement: a `Class.new { }` or
|
|
10958
|
+
# `Struct.new { }` body is its own class's, an eval body its receiver's, a `class << self` body the singleton
|
|
10959
|
+
# side), so that side reads as open to `call.undefined-method`.
|
|
10960
|
+
#
|
|
10961
|
+
# Issue #2901 — on a constant receiver (`Other.define_method(m)`) it defines instance methods of that constant's
|
|
10962
|
+
# class, whichever side the call sits on: each class the constant can denote from here opens its instance side
|
|
10963
|
+
# ({#constant_receiver_candidates}), and the enclosing class does not. Any other receiver opens nothing.
|
|
10964
|
+
def record_open_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
10965
|
+
receiver = call_node.receiver
|
|
10966
|
+
if receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
10967
|
+
constant_receiver_candidates(receiver, qualified_prefix).each do |name|
|
|
10968
|
+
record_surface_mark(accumulator, name, Scope::DiscoveryIndex::ENVELOPE_OPEN_INSTANCE_NAMES_MARK)
|
|
10969
|
+
end
|
|
10970
|
+
elsif receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
10971
|
+
record_surface_mark(accumulator, qualified_prefix.join("::"), open_names_mark(in_singleton_class))
|
|
10972
|
+
end
|
|
10973
|
+
end
|
|
10974
|
+
|
|
10975
|
+
# The side's open-names mark a computed definer on `self` records: the singleton side in a `class << self` body.
|
|
10976
|
+
def open_names_mark(in_singleton_class)
|
|
10977
|
+
if in_singleton_class
|
|
10978
|
+
Scope::DiscoveryIndex::ENVELOPE_OPEN_SINGLETON_NAMES_MARK
|
|
10979
|
+
else
|
|
10980
|
+
Scope::DiscoveryIndex::ENVELOPE_OPEN_INSTANCE_NAMES_MARK
|
|
10981
|
+
end
|
|
9167
10982
|
end
|
|
9168
10983
|
|
|
9169
10984
|
# The `attr_*` accessor macros that introduce methods Rigor must treat as source-declared. Without this, a class
|
|
@@ -9187,6 +11002,19 @@ module Rigor
|
|
|
9187
11002
|
record_method(accumulator, class_name, base, kind) if reader
|
|
9188
11003
|
record_method(accumulator, class_name, :"#{base}=", kind) if writer
|
|
9189
11004
|
end
|
|
11005
|
+
return unless attr_macro_computed?(call_node)
|
|
11006
|
+
|
|
11007
|
+
# Issue #2901 — `attr_accessor(*NAMES)` defines members this walk cannot name, so its side opens.
|
|
11008
|
+
record_surface_mark(accumulator, class_name, open_names_mark(in_singleton_class))
|
|
11009
|
+
end
|
|
11010
|
+
|
|
11011
|
+
# Whether an `attr_*` call ({#attr_macro}) passes a name argument that is not a Symbol or String literal.
|
|
11012
|
+
def attr_macro_computed?(call_node)
|
|
11013
|
+
arguments = call_node.arguments&.arguments
|
|
11014
|
+
return false if arguments.nil?
|
|
11015
|
+
|
|
11016
|
+
arguments = arguments.drop(1) if SURFACE_SEND_CALLS.include?(call_node.name)
|
|
11017
|
+
arguments.any? { |arg| literal_method_name(arg).nil? }
|
|
9190
11018
|
end
|
|
9191
11019
|
|
|
9192
11020
|
# Yields each name an `attr_*` call declares ({#attr_macro}): a Symbol or String literal argument. A
|
|
@@ -9231,13 +11059,14 @@ module Rigor
|
|
|
9231
11059
|
# because they are not monkey-patches), and no plugin surface reaches it — the ADR-16 synthetic-method
|
|
9232
11060
|
# tier feeds the DISPATCHER, which `Analysis::CheckRules` does not consult. A name table is the smallest
|
|
9233
11061
|
# thing that works; a plugin-supplied one can replace it whenever that surface exists.
|
|
9234
|
-
MODULE_ATTR_MACROS =
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
|
|
11062
|
+
MODULE_ATTR_MACROS = Ractor.make_shareable(
|
|
11063
|
+
{
|
|
11064
|
+
mattr_reader: [true, false, false], mattr_writer: [false, true, false],
|
|
11065
|
+
mattr_accessor: [true, true, false], cattr_reader: [true, false, false],
|
|
11066
|
+
cattr_writer: [false, true, false], cattr_accessor: [true, true, false],
|
|
11067
|
+
class_attribute: [true, true, true]
|
|
11068
|
+
}
|
|
11069
|
+
)
|
|
9241
11070
|
|
|
9242
11071
|
# Both kinds, unconditionally. `instance_accessor: false` (and its `instance_reader:` /
|
|
9243
11072
|
# `instance_writer:` / `instance_predicate:` siblings) narrows the real surface, and honouring them here
|
|
@@ -9260,6 +11089,70 @@ module Rigor
|
|
|
9260
11089
|
end
|
|
9261
11090
|
end
|
|
9262
11091
|
|
|
11092
|
+
# Issue #2793 — the `extend` that decides the side of an {Source::ForwardableDelegators::AMBIGUOUS} macro
|
|
11093
|
+
# (`def_delegators`, `def_delegator`, `delegate`): `Forwardable` makes them instance methods,
|
|
11094
|
+
# `SingleForwardable` singleton methods.
|
|
11095
|
+
FORWARDABLE_MODULES = { Forwardable: :instance, SingleForwardable: :singleton }.freeze
|
|
11096
|
+
|
|
11097
|
+
# Issue #2793 — a Forwardable delegator macro defines its literal names as methods, which join the existence
|
|
11098
|
+
# table so `call.undefined-method` stops firing on them (their return types stay unknown: the envelope is
|
|
11099
|
+
# opaque and no `def` node backs them). The side is the macro's own, or, for an ambiguous macro, the side
|
|
11100
|
+
# of the `extend` this walk met earlier in the body: singleton under a lone `extend SingleForwardable`,
|
|
11101
|
+
# instance otherwise (a class extended in another file or by a superclass is far likelier `Forwardable`).
|
|
11102
|
+
# Inside `class << self` the macro defines on the singleton class. A name the syntax hides
|
|
11103
|
+
# (`def_delegators :@x, *NAMES`) marks the class {Scope::DiscoveryIndex::ENVELOPE_OPEN_NAMES_MARK}.
|
|
11104
|
+
def record_forwardable_methods(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
11105
|
+
return if qualified_prefix.empty?
|
|
11106
|
+
|
|
11107
|
+
class_name = qualified_prefix.join("::")
|
|
11108
|
+
note_forwardable_extend(call_node, class_name, accumulator) unless in_singleton_class
|
|
11109
|
+
delegation = Source::ForwardableDelegators.of(call_node)
|
|
11110
|
+
return if delegation.nil?
|
|
11111
|
+
|
|
11112
|
+
kind = forwardable_side(delegation, class_name, in_singleton_class, accumulator)
|
|
11113
|
+
delegation.names.each { |name| record_method(accumulator, class_name, name, kind) }
|
|
11114
|
+
return unless delegation.open?
|
|
11115
|
+
|
|
11116
|
+
record_surface_mark(accumulator, class_name, Scope::DiscoveryIndex::ENVELOPE_OPEN_NAMES_MARK)
|
|
11117
|
+
end
|
|
11118
|
+
|
|
11119
|
+
def note_forwardable_extend(call_node, class_name, accumulator)
|
|
11120
|
+
return unless call_node.name == :extend && call_node.receiver.nil?
|
|
11121
|
+
|
|
11122
|
+
(call_node.arguments&.arguments || []).each do |argument|
|
|
11123
|
+
side = FORWARDABLE_MODULES[forwardable_module_name(argument)]
|
|
11124
|
+
next if side.nil?
|
|
11125
|
+
|
|
11126
|
+
((accumulator.forwardable_extends ||= {})[class_name] ||= Set.new) << side
|
|
11127
|
+
end
|
|
11128
|
+
end
|
|
11129
|
+
|
|
11130
|
+
def forwardable_module_name(node)
|
|
11131
|
+
case node
|
|
11132
|
+
when Prism::ConstantReadNode then node.name
|
|
11133
|
+
when Prism::ConstantPathNode then node.parent.nil? ? node.name : nil
|
|
11134
|
+
end
|
|
11135
|
+
end
|
|
11136
|
+
|
|
11137
|
+
def forwardable_side(delegation, class_name, in_singleton_class, accumulator)
|
|
11138
|
+
return :singleton if in_singleton_class
|
|
11139
|
+
return delegation.side unless delegation.side == Source::ForwardableDelegators::AMBIGUOUS
|
|
11140
|
+
|
|
11141
|
+
extended = accumulator.forwardable_extends&.[](class_name)
|
|
11142
|
+
extended == Set[:singleton] ? :singleton : :instance
|
|
11143
|
+
end
|
|
11144
|
+
|
|
11145
|
+
# The instance names a Forwardable macro defines, for {#call_superseded_names}: a delegator written after a
|
|
11146
|
+
# `def` of the name replaces it. Asked only outside `class << self`, and a macro whose side settles to the
|
|
11147
|
+
# singleton (`extend SingleForwardable; def_delegators :@x, :size`) leaves an instance `def size` standing.
|
|
11148
|
+
def forwardable_superseded_names(call_node, qualified_prefix, accumulator)
|
|
11149
|
+
delegation = Source::ForwardableDelegators.of(call_node)
|
|
11150
|
+
return [] if delegation.nil? || qualified_prefix.empty?
|
|
11151
|
+
|
|
11152
|
+
side = forwardable_side(delegation, qualified_prefix.join("::"), false, accumulator)
|
|
11153
|
+
side == :instance ? delegation.names : []
|
|
11154
|
+
end
|
|
11155
|
+
|
|
9263
11156
|
def record_both_kinds(accumulator, class_name, method_name)
|
|
9264
11157
|
record_method(accumulator, class_name, method_name, :instance)
|
|
9265
11158
|
record_method(accumulator, class_name, method_name, :singleton)
|
|
@@ -9290,8 +11183,12 @@ module Rigor
|
|
|
9290
11183
|
physical = buffer ? buffer.resolve(path) : path
|
|
9291
11184
|
source = File.read(physical)
|
|
9292
11185
|
root = Prism.parse(source, filepath: path).value
|
|
9293
|
-
|
|
9294
|
-
|
|
11186
|
+
file_classes = {}
|
|
11187
|
+
collect_class_decls(root, [], file_classes)
|
|
11188
|
+
accumulator.merge!(file_classes) # all or nothing: an overflow above contributes none of this file (#2455)
|
|
11189
|
+
rescue StandardError, SystemStackError => e
|
|
11190
|
+
raise if uncontainable_overflow?(e)
|
|
11191
|
+
|
|
9295
11192
|
# Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
|
|
9296
11193
|
next
|
|
9297
11194
|
end
|
|
@@ -9318,10 +11215,12 @@ module Rigor
|
|
|
9318
11215
|
paths.each do |path|
|
|
9319
11216
|
physical = buffer ? buffer.resolve(path) : path
|
|
9320
11217
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
9321
|
-
|
|
11218
|
+
fold_file_index(acc, build_file_index(path, root)) # all or nothing per file (#2455)
|
|
9322
11219
|
rescue DeclarationWalk::ContractError
|
|
9323
11220
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
9324
|
-
rescue StandardError
|
|
11221
|
+
rescue StandardError, SystemStackError => e
|
|
11222
|
+
raise if uncontainable_overflow?(e)
|
|
11223
|
+
|
|
9325
11224
|
# Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
|
|
9326
11225
|
next
|
|
9327
11226
|
end
|
|
@@ -9358,7 +11257,9 @@ module Rigor
|
|
|
9358
11257
|
file_summary(file_index).each { |key, table| per_file[key][path] = table unless table.nil? }
|
|
9359
11258
|
rescue DeclarationWalk::ContractError
|
|
9360
11259
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
9361
|
-
rescue StandardError
|
|
11260
|
+
rescue StandardError, SystemStackError => e
|
|
11261
|
+
raise if uncontainable_overflow?(e)
|
|
11262
|
+
|
|
9362
11263
|
next
|
|
9363
11264
|
end
|
|
9364
11265
|
{ def_index: finalize_def_index(acc), code_fingerprints: code_fingerprints,
|
|
@@ -9642,21 +11543,35 @@ module Rigor
|
|
|
9642
11543
|
paths.each do |path|
|
|
9643
11544
|
physical = buffer ? buffer.resolve(path) : path
|
|
9644
11545
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
11546
|
+
# All or nothing per file, as the incremental loop below: both tables are built in isolation and folded only
|
|
11547
|
+
# once the whole file walked, so a file that overflows the stack contributes none of either (#2455).
|
|
11548
|
+
file_classes = {}
|
|
11549
|
+
collect_class_decls(root, [], file_classes)
|
|
11550
|
+
file_index = if uncertain.include?(path)
|
|
11551
|
+
without_written_slots(build_file_index(path, root, uncertain: true), acc)
|
|
11552
|
+
else
|
|
11553
|
+
build_file_index(path, root)
|
|
11554
|
+
end
|
|
11555
|
+
fold_file_index(acc, file_index)
|
|
11556
|
+
classes.merge!(file_classes)
|
|
9651
11557
|
rescue DeclarationWalk::ContractError
|
|
9652
11558
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
9653
|
-
rescue StandardError
|
|
11559
|
+
rescue StandardError, SystemStackError => e
|
|
11560
|
+
raise if uncontainable_overflow?(e)
|
|
11561
|
+
|
|
9654
11562
|
# Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
|
|
9655
11563
|
next
|
|
9656
11564
|
end
|
|
9657
11565
|
finalize_project_index(classes, acc)
|
|
9658
11566
|
end
|
|
9659
11567
|
|
|
11568
|
+
# #2455 — a `SystemStackError` raised from C (`Prism.parse` of a source nested near its own cap) has no
|
|
11569
|
+
# backtrace and leaves the process unable to survive a second one, so it is not contained: it keeps the
|
|
11570
|
+
# run-level abort.
|
|
11571
|
+
def uncontainable_overflow?(error)
|
|
11572
|
+
error.is_a?(SystemStackError) && error.backtrace.nil?
|
|
11573
|
+
end
|
|
11574
|
+
|
|
9660
11575
|
# The shared tail of the two whole-project discovery walks: settles the def-index and re-anchors the
|
|
9661
11576
|
# `classes` table it accumulated beside it.
|
|
9662
11577
|
def finalize_project_index(classes, acc)
|
|
@@ -9697,7 +11612,7 @@ module Rigor
|
|
|
9697
11612
|
# @return `{ classes:, def_index:, bundles: }`.
|
|
9698
11613
|
# @param uncertain — issue #2021: as {#discovered_project_index_for_paths}. Such a file is always re-walked and
|
|
9699
11614
|
# keeps no bundle, since a bundle does not record that its contributions were all possible.
|
|
9700
|
-
def discovered_project_index_incremental(paths, seed_bundles:, buffer: nil, uncertain: EMPTY_PATH_SET) # rubocop:disable Metrics/AbcSize
|
|
11615
|
+
def discovered_project_index_incremental(paths, seed_bundles:, buffer: nil, uncertain: EMPTY_PATH_SET) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
9701
11616
|
classes = {}
|
|
9702
11617
|
acc = new_def_index_accumulator
|
|
9703
11618
|
bundles = {}
|
|
@@ -9726,7 +11641,9 @@ module Rigor
|
|
|
9726
11641
|
end
|
|
9727
11642
|
rescue DeclarationWalk::ContractError
|
|
9728
11643
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
9729
|
-
rescue StandardError
|
|
11644
|
+
rescue StandardError, SystemStackError => e
|
|
11645
|
+
raise if uncontainable_overflow?(e)
|
|
11646
|
+
|
|
9730
11647
|
# Skip files that fail to parse / read; the per-file analyzer surfaces the parse error separately.
|
|
9731
11648
|
next
|
|
9732
11649
|
end
|
|
@@ -10423,10 +12340,11 @@ module Rigor
|
|
|
10423
12340
|
|
|
10424
12341
|
# #1154 — tags each `[:own_write, class, ivar]` entry with the file that records it, so a reader of the merged
|
|
10425
12342
|
# census can tell a second file writing the same ivar of the same class (`UnseenIvarWriters`).
|
|
12343
|
+
# Issue #2079 — and each `[:own_write_type, class, ivar, value]` entry, which {IvarContributions} reads.
|
|
10426
12344
|
def attribute_own_writes(census, path)
|
|
10427
|
-
return census unless census.any? { |entry| entry.first
|
|
12345
|
+
return census unless census.any? { |entry| PATH_TAGGED_KINDS.include?(entry.first) }
|
|
10428
12346
|
|
|
10429
|
-
census.to_set { |entry| entry.first
|
|
12347
|
+
census.to_set { |entry| PATH_TAGGED_KINDS.include?(entry.first) ? [*entry, path].freeze : entry }
|
|
10430
12348
|
end
|
|
10431
12349
|
|
|
10432
12350
|
# Folds one file's Data + Struct member-layout tables into the cross-file accumulator (kept out of
|
|
@@ -10562,7 +12480,8 @@ module Rigor
|
|
|
10562
12480
|
singleton_cref)
|
|
10563
12481
|
else
|
|
10564
12482
|
census_constant_write(node, qualified_prefix, tables, self_owner, singleton_cref: singleton_cref)
|
|
10565
|
-
tables.write_census.visit(node, top_level: qualified_prefix.empty?, owner: ivar_owner
|
|
12483
|
+
tables.write_census.visit(node, top_level: qualified_prefix.empty?, owner: ivar_owner,
|
|
12484
|
+
receiver_owner: census_receiver_owner(node, qualified_prefix, self_owner))
|
|
10566
12485
|
end
|
|
10567
12486
|
|
|
10568
12487
|
rebound = rebound_block_self(node, qualified_prefix, nil, meta_owner, self_owner)
|
|
@@ -10604,6 +12523,16 @@ module Rigor
|
|
|
10604
12523
|
qualified_prefix
|
|
10605
12524
|
end
|
|
10606
12525
|
|
|
12526
|
+
# Issue #2488 — the class an explicit-receiver writer call (`E.attr_writer :w`, `E.define_method(:m) { … }`)
|
|
12527
|
+
# names, resolved as a `class_eval` receiver is in this census (keyed as written), or nil.
|
|
12528
|
+
def census_receiver_owner(node, qualified_prefix, self_owner)
|
|
12529
|
+
receiver = ReceiverWriterCall.receiver(node)
|
|
12530
|
+
return nil if receiver.nil?
|
|
12531
|
+
|
|
12532
|
+
name = eval_receiver_self(receiver, qualified_prefix, nil, rebound_self_base(self_owner))
|
|
12533
|
+
name if name.is_a?(String)
|
|
12534
|
+
end
|
|
12535
|
+
|
|
10607
12536
|
# Under an unnameable cref a bare/`self::` header pushes `#<singleton>::Name` — still
|
|
10608
12537
|
# unnameable — so the body keeps the OPAQUE self and the cref flag; nameable
|
|
10609
12538
|
# headers re-anchor at a real cref.
|
|
@@ -10859,7 +12788,7 @@ module Rigor
|
|
|
10859
12788
|
# A {DYNAMIC_TARGET_PREFIX} key is not a name and never publishes; it is expanded by
|
|
10860
12789
|
# {#apply_dynamic_target_writes} into a retraction over the names it could have written.
|
|
10861
12790
|
#
|
|
10862
|
-
# @return
|
|
12791
|
+
# @return the value table and the table of source paths per name.
|
|
10863
12792
|
def finalize_constant_writes(writes)
|
|
10864
12793
|
values = {}
|
|
10865
12794
|
sources = {}
|
|
@@ -11607,20 +13536,25 @@ module Rigor
|
|
|
11607
13536
|
|
|
11608
13537
|
# Recognises `Data.define(*Symbol)` and `Data.define(*Symbol) do ... end` at constant-write rvalue position. The
|
|
11609
13538
|
# receiver MUST be the bare `Data` constant (or `::Data`); other receivers (a local variable, a method call
|
|
11610
|
-
# return) are rejected because their identity is not statically known.
|
|
13539
|
+
# return) are rejected because their identity is not statically known. Issue #2968 — a splatted member list
|
|
13540
|
+
# (`Data.define(*FIELDS)`) still creates the class; {#meta_members_open?} marks its members unknown.
|
|
11611
13541
|
def data_define_call?(node)
|
|
11612
13542
|
return false unless node.is_a?(Prism::CallNode)
|
|
11613
13543
|
return false unless node.name == :define
|
|
11614
13544
|
return false unless meta_constant_receiver?(node.receiver, :Data)
|
|
11615
13545
|
|
|
11616
13546
|
args = node.arguments&.arguments || []
|
|
11617
|
-
args.all?(
|
|
13547
|
+
args.all? { |arg| meta_member_argument?(arg) }
|
|
11618
13548
|
end
|
|
11619
13549
|
|
|
11620
13550
|
# Recognises `Struct.new(*Symbol)` and `Struct.new(*Symbol, keyword_init: <expr>)` at constant-write rvalue
|
|
11621
13551
|
# position. A trailing `KeywordHashNode` (the `keyword_init: ...` form) is accepted but does not contribute to
|
|
11622
13552
|
# member discovery; every other argument MUST be a `Prism::SymbolNode`. At least one Symbol member is required —
|
|
11623
13553
|
# `Struct.new()` is a degenerate form callers don't typically use.
|
|
13554
|
+
#
|
|
13555
|
+
# Issue #2968 — a leading String (`Struct.new("Named", :a)`, the class `Struct::Named`) and a splatted member list
|
|
13556
|
+
# (`Struct.new(*FIELDS)`) create the class just the same. {#meta_member_positionals} drops the String;
|
|
13557
|
+
# {#meta_members_open?} marks a splatted list's members unknown.
|
|
11624
13558
|
def struct_new_call?(node)
|
|
11625
13559
|
return false unless meta_call_with_name?(node, :Struct, :new)
|
|
11626
13560
|
|
|
@@ -11628,7 +13562,12 @@ module Rigor
|
|
|
11628
13562
|
positional = struct_new_positionals(args)
|
|
11629
13563
|
return false if positional.nil? || positional.empty?
|
|
11630
13564
|
|
|
11631
|
-
positional.
|
|
13565
|
+
positional = positional.drop(1) if positional.first.is_a?(Prism::StringNode)
|
|
13566
|
+
positional.all? { |arg| meta_member_argument?(arg) }
|
|
13567
|
+
end
|
|
13568
|
+
|
|
13569
|
+
def meta_member_argument?(arg)
|
|
13570
|
+
arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::SplatNode)
|
|
11632
13571
|
end
|
|
11633
13572
|
|
|
11634
13573
|
# Recognises `Module.new` and `Module.new(&block)` / `Module.new do ... end` at constant-write rvalue position.
|
|
@@ -11734,8 +13673,18 @@ module Rigor
|
|
|
11734
13673
|
}.freeze
|
|
11735
13674
|
private_constant :UNENTERED_FLOW_ENTRY
|
|
11736
13675
|
|
|
11737
|
-
|
|
11738
|
-
|
|
13676
|
+
# ADR-129 WD1 — a Frame: {#with_unentered_evaluator} installs the walk's evaluation for the walk and restores the
|
|
13677
|
+
# outer one, and whether one is installed changes answers (an unentered construct is evaluated only inside a
|
|
13678
|
+
# walk).
|
|
13679
|
+
UNENTERED_EVALUATOR = TypingState.frame(
|
|
13680
|
+
:scope_index_unentered_evaluator,
|
|
13681
|
+
reason: "installed around the index walk that evaluates its unentered constructs; the lambda writes the " \
|
|
13682
|
+
"walk's own table (the walk's state, not typing state) and reads its converged-loop recording and " \
|
|
13683
|
+
"clause widening, and is frozen while installed",
|
|
13684
|
+
# G1′: the evaluation types under the Frame, but only the index walk, outside `type_of`, reads it.
|
|
13685
|
+
typing_never_reads: true
|
|
13686
|
+
)
|
|
13687
|
+
private_constant :UNENTERED_EVALUATOR
|
|
11739
13688
|
|
|
11740
13689
|
# Issues #2196 and #2056 — an `&&` / `||` / `and` / `or`, an `if` / `unless` / ternary or a `while` / `until`
|
|
11741
13690
|
# in a value position (an array element, a hash value, a call argument, an interpolation, a multi-value
|
|
@@ -11746,6 +13695,8 @@ module Rigor
|
|
|
11746
13695
|
# the scope this walk reached it with, under the index's own recorder, so each part is recorded with the scope
|
|
11747
13696
|
# the statement form records it with. What it does not visit (a dead arm) is walked as before.
|
|
11748
13697
|
def evaluate_unentered_flow(node, table, current_scope)
|
|
13698
|
+
return evaluate_unentered_block(node, table, current_scope) if node.is_a?(Prism::CallNode)
|
|
13699
|
+
|
|
11749
13700
|
entry = UNENTERED_FLOW_ENTRY[node.class]
|
|
11750
13701
|
return if entry.nil?
|
|
11751
13702
|
|
|
@@ -11754,10 +13705,27 @@ module Rigor
|
|
|
11754
13705
|
part = node.public_send(entry) || (node.conditions.first&.conditions&.first if node.is_a?(Prism::CaseNode))
|
|
11755
13706
|
return if part.nil? || table.key?(part)
|
|
11756
13707
|
|
|
11757
|
-
evaluate =
|
|
13708
|
+
evaluate = TypingState.current[UNENTERED_EVALUATOR]
|
|
11758
13709
|
evaluate&.call(node, current_scope, code_body_nodes(node, current_scope.source_path))
|
|
11759
13710
|
end
|
|
11760
13711
|
|
|
13712
|
+
# Issue #1246 — the block of a call in a value position (`show([1].map { x = "s"; x.upcase })`, a receiver
|
|
13713
|
+
# chain, an array element, a `->`'s body) is never entered: {StatementEvaluator} types the call through
|
|
13714
|
+
# `ExpressionTyper`, and this walk could only floor a local the block rebinds ({#closure_scope}). Such a block
|
|
13715
|
+
# is entered here instead, alone, from the scope this walk reached its call with, so its body is recorded with
|
|
13716
|
+
# the scopes the statement form records: a capture read before the block rebinds it reads its binding, and the
|
|
13717
|
+
# block's parameters are bound. The call's receiver and arguments stay with the walk. A class-creating or
|
|
13718
|
+
# `refine` block keeps the walk ({#unentered_block_entry}), and a code body inside the block is left out of the
|
|
13719
|
+
# recording ({#code_body_nodes}). A `super` block is a `SuperNode`'s, which the evaluator never enters either.
|
|
13720
|
+
def evaluate_unentered_block(node, table, current_scope)
|
|
13721
|
+
block = node.block
|
|
13722
|
+
return unless block.is_a?(Prism::BlockNode)
|
|
13723
|
+
return if table.key?(block) || class_body_block_call?(node, current_scope.source_path)
|
|
13724
|
+
|
|
13725
|
+
evaluate = TypingState.current[UNENTERED_EVALUATOR]
|
|
13726
|
+
evaluate&.call(node, current_scope, code_body_nodes(block, current_scope.source_path), block_only: true)
|
|
13727
|
+
end
|
|
13728
|
+
|
|
11761
13729
|
# The evaluation of an unentered construct starts with no class frame and no lexical nesting, which only a code
|
|
11762
13730
|
# body observes: a `def`, a `class` / `module` / `class << …` body, and the block of a class-creating call
|
|
11763
13731
|
# (`Class.new { … }`, `Struct.new`, `Data.define`, `Module.new`) or of a `refine`, whose `def`s and constants
|
|
@@ -11791,12 +13759,8 @@ module Rigor
|
|
|
11791
13759
|
# Index walks nest (a rule may re-index a file while another file's walk is suspended), so the outer one is
|
|
11792
13760
|
# restored on exit. A `propagate` called directly while an outer index walk is running would record into the
|
|
11793
13761
|
# OUTER index's table through it; nothing outside {#index} calls `propagate`.
|
|
11794
|
-
def with_unentered_evaluator(evaluate)
|
|
11795
|
-
|
|
11796
|
-
Thread.current[UNENTERED_EVALUATOR_KEY] = evaluate
|
|
11797
|
-
yield
|
|
11798
|
-
ensure
|
|
11799
|
-
Thread.current[UNENTERED_EVALUATOR_KEY] = outer
|
|
13762
|
+
def with_unentered_evaluator(evaluate, &)
|
|
13763
|
+
TypingState.current.with(UNENTERED_EVALUATOR, evaluate.freeze, &)
|
|
11800
13764
|
end
|
|
11801
13765
|
|
|
11802
13766
|
SELF_OPENING_BODIES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
@@ -11804,6 +13768,12 @@ module Rigor
|
|
|
11804
13768
|
|
|
11805
13769
|
# Issue #1717 — a `def` / `class` / `module` body opens its own `self`, so an enclosing unentered block's
|
|
11806
13770
|
# unknown-`self` mark ({Scope#block_self_unknown?}) stops at it, as the evaluator's fresh body scope does.
|
|
13771
|
+
# Issue #1246 — it opens its own local table too, so a name in it (a `def`'s parameter, a body local) is a new
|
|
13772
|
+
# variable, never the enclosing statement's local of that name: `show(Class.new { def call(o) = o + 1 })` after
|
|
13773
|
+
# `o = { x: 1 }` read the parameter as the Hash. Such a name reads `Dynamic[top]` ({#shadow_local}), as in an
|
|
13774
|
+
# unentered closure ({#closure_scope}); a body the evaluator entered is recorded and keeps its own scopes. Only
|
|
13775
|
+
# the parameters and the body take that table: a `def`'s receiver, a `class`'s name and superclass and a
|
|
13776
|
+
# `class << …`'s expression run in the enclosing scope (`class K < base.nope; base = 2; end`).
|
|
11807
13777
|
# A `case` narrows its clauses ({#propagate_subjectless_case}, {#propagate_subject_case}). Every other node
|
|
11808
13778
|
# hands its children the scope as is.
|
|
11809
13779
|
def propagate_children(node, table, current_scope)
|
|
@@ -11812,10 +13782,19 @@ module Rigor
|
|
|
11812
13782
|
|
|
11813
13783
|
return propagate_subject_case(node, table, current_scope)
|
|
11814
13784
|
end
|
|
13785
|
+
return propagate_code_body(node, table, current_scope) if SELF_OPENING_BODIES.include?(node.class)
|
|
11815
13786
|
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
13787
|
+
node.rigor_each_child { |child| propagate(child, table, current_scope) }
|
|
13788
|
+
end
|
|
13789
|
+
|
|
13790
|
+
def propagate_code_body(node, table, current_scope)
|
|
13791
|
+
outer_scope = current_scope.with_block_self_unknown(false)
|
|
13792
|
+
inner = [node.body]
|
|
13793
|
+
inner << node.parameters if node.is_a?(Prism::DefNode)
|
|
13794
|
+
inner_scope = node.locals.reduce(outer_scope) { |acc, name| shadow_local(acc, name) }
|
|
13795
|
+
node.rigor_each_child do |child|
|
|
13796
|
+
propagate(child, table, inner.any? { |part| part.equal?(child) } ? inner_scope : outer_scope)
|
|
13797
|
+
end
|
|
11819
13798
|
end
|
|
11820
13799
|
|
|
11821
13800
|
# Issue #1361 — the block of `Thread.new`, `Fiber.new`, `define_method` and the other calls
|
|
@@ -11833,6 +13812,10 @@ module Rigor
|
|
|
11833
13812
|
# argument one scope and threads none, so arguments that write keep the call's: an earlier one may rebind the
|
|
11834
13813
|
# receiver a later one reads (`return y&.concat((y = nil).to_s, y.upcase)`, where the evaluator threads no
|
|
11835
13814
|
# operand).
|
|
13815
|
+
#
|
|
13816
|
+
# Issue #1246 — the block of a value-position call is evaluated before this walk reaches it
|
|
13817
|
+
# ({#evaluate_unentered_block}), so it arrives recorded and keeps the evaluator's entry. The block entries
|
|
13818
|
+
# above therefore reach only a block that evaluation declines: a class-creating or `refine` block.
|
|
11836
13819
|
def propagate_call(node, table, current_scope, recorded: false)
|
|
11837
13820
|
block = node.block
|
|
11838
13821
|
operands =
|
|
@@ -11868,6 +13851,7 @@ module Rigor
|
|
|
11868
13851
|
current_scope = Narrowing.safe_navigation_block_scope(node, current_scope)
|
|
11869
13852
|
# Issue #1429 — nor a guard's narrowing of a global or constant the body may run after code rebinds.
|
|
11870
13853
|
current_scope = GuardRebinding.block_entry(current_scope, block, node)
|
|
13854
|
+
current_scope = current_scope.entering_block_without_gradual_marks(block)
|
|
11871
13855
|
entry =
|
|
11872
13856
|
if FreshFrameBlocks.fresh_entry?(node, current_scope)
|
|
11873
13857
|
FreshFrameBlocks.entry(current_scope, node)
|
|
@@ -11997,14 +13981,17 @@ module Rigor
|
|
|
11997
13981
|
# `it` as the outer block's). An enclosing binding of such a name is replaced with `Dynamic[top]`. Not the
|
|
11998
13982
|
# signature's parameter type: this walk evaluates nothing, and a body write to the name is never threaded,
|
|
11999
13983
|
# so any narrower claim could be stale. A name the enclosing scope does not bind is left unbound, which
|
|
12000
|
-
# reads the same `Dynamic[top]`.
|
|
12001
|
-
#
|
|
12002
|
-
#
|
|
13984
|
+
# reads the same `Dynamic[top]`. Issue #1246 — for the same reason a captured name the body (or a closure
|
|
13985
|
+
# nested in it) writes reads `Dynamic[top]` too ({#widen_condition_writes}): `x = 1; show([1].map { x = "s";
|
|
13986
|
+
# x.upcase })` read `x` as `1` after the write. A captured name the body only reads keeps the enclosing
|
|
13987
|
+
# binding. Issue #1360 — a `->` body and its parameter defaults run whenever the lambda is called, so they read
|
|
13988
|
+
# `$!`, `$@` and `$?` unbound ({FreshFrameBlocks.closure_entry}).
|
|
12003
13989
|
def closure_scope(closure, scope)
|
|
12004
13990
|
scope = GuardRebinding.block_entry(scope, closure, nil)
|
|
12005
13991
|
scope = FreshFrameBlocks.closure_entry(scope, closure, nil)
|
|
12006
13992
|
scope = shadow_local(scope, :it) if closure.parameters.is_a?(Prism::ItParametersNode)
|
|
12007
|
-
closure.locals.reduce(scope) { |acc, name| shadow_local(acc, name) }
|
|
13993
|
+
scope = closure.locals.reduce(scope) { |acc, name| shadow_local(acc, name) }
|
|
13994
|
+
widen_condition_writes(closure, scope)
|
|
12008
13995
|
end
|
|
12009
13996
|
|
|
12010
13997
|
def shadow_local(scope, name)
|