rigortype 0.4.6 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
- data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
- data/docs/handbook/01-getting-started.md +5 -2
- data/docs/handbook/11-sig-gen.md +50 -3
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/manual/02-cli-reference.md +152 -26
- data/docs/manual/03-configuration.md +36 -3
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/09-editor-integration.md +13 -2
- data/docs/manual/10-mcp-server.md +38 -2
- data/docs/manual/12-caching.md +4 -3
- data/docs/manual/plugins/rigor-actionpack.md +19 -11
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
- data/lib/rigor/analysis/baseline.rb +24 -27
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +124 -44
- data/lib/rigor/analysis/crash_signature.rb +2 -2
- data/lib/rigor/analysis/dependency_recorder.rb +4 -4
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/edit_replay.rb +93 -6
- data/lib/rigor/analysis/fact_store.rb +3 -3
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
- data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
- data/lib/rigor/analysis/incremental_session.rb +27 -16
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
- data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
- 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 +77 -71
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
- data/lib/rigor/analysis/template_unit_positions.rb +2 -3
- data/lib/rigor/analysis/template_units.rb +9 -1
- data/lib/rigor/analysis/worker_session.rb +24 -31
- data/lib/rigor/bleeding_edge.rb +3 -6
- data/lib/rigor/builtins/imported_refinements.rb +6 -8
- data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
- data/lib/rigor/builtins/regex_refinement.rb +4 -6
- data/lib/rigor/cache/descriptor.rb +6 -5
- data/lib/rigor/cache/incremental_snapshot.rb +31 -12
- data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
- data/lib/rigor/cache/rbs_constant_table.rb +1 -1
- data/lib/rigor/cache/rbs_descriptor.rb +54 -3
- data/lib/rigor/cache/rbs_environment.rb +4 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
- data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
- data/lib/rigor/cache/store.rb +11 -10
- data/lib/rigor/ci_detector.rb +1 -1
- data/lib/rigor/cli/annotate_command.rb +24 -19
- data/lib/rigor/cli/baseline_command.rb +145 -55
- data/lib/rigor/cli/catalogue.rb +256 -0
- data/lib/rigor/cli/check_command.rb +108 -102
- 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 +18 -17
- data/lib/rigor/cli/explain_command.rb +18 -8
- data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
- data/lib/rigor/cli/json_document.rb +41 -0
- data/lib/rigor/cli/lens_command.rb +12 -8
- 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 +84 -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 +1 -1
- data/lib/rigor/cli/trace_command.rb +14 -9
- data/lib/rigor/cli/triage_command.rb +10 -11
- data/lib/rigor/cli/triage_renderer.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +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 +209 -54
- data/lib/rigor/config_audit.rb +1 -1
- data/lib/rigor/configuration/severity_profile.rb +5 -5
- data/lib/rigor/configuration.rb +72 -30
- data/lib/rigor/effects/definition_context.rb +0 -2
- data/lib/rigor/effects/envelope.rb +1 -1
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/mutation_classifier.rb +4 -7
- data/lib/rigor/effects/propagator.rb +1 -1
- data/lib/rigor/effects/registry.rb +2 -2
- data/lib/rigor/effects/snapshot.rb +1 -1
- data/lib/rigor/effects/taint_cause.rb +0 -2
- data/lib/rigor/effects/unit_scan.rb +9 -13
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
- data/lib/rigor/environment/class_registry.rb +3 -3
- data/lib/rigor/environment/core_root.rb +77 -0
- data/lib/rigor/environment/default_libraries.rb +1 -1
- data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
- data/lib/rigor/environment/gated_signature_guard.rb +1 -1
- data/lib/rigor/environment/gated_signature_plan.rb +5 -2
- data/lib/rigor/environment/lockfile_resolver.rb +28 -12
- data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
- data/lib/rigor/environment/member_consistency.rb +56 -7
- data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
- data/lib/rigor/environment/project_path_gems.rb +124 -0
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
- data/lib/rigor/environment/rbs_loader.rb +480 -67
- 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 +41 -10
- data/lib/rigor/flow_contribution/merger.rb +4 -4
- data/lib/rigor/inference/acceptance.rb +93 -32
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/block_run_count.rb +178 -0
- data/lib/rigor/inference/budget_trace.rb +8 -2
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
- data/lib/rigor/inference/callee_write_census.rb +815 -0
- data/lib/rigor/inference/construction_escape.rb +217 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
- data/lib/rigor/inference/deep_erasure.rb +39 -7
- data/lib/rigor/inference/def_handle.rb +1 -1
- data/lib/rigor/inference/def_node_resolver.rb +1 -1
- data/lib/rigor/inference/def_return_typer.rb +46 -4
- data/lib/rigor/inference/definer_resolution.rb +4 -2
- data/lib/rigor/inference/definers_index.rb +106 -0
- data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
- data/lib/rigor/inference/element_read_widening.rb +49 -3
- data/lib/rigor/inference/expression_typer.rb +116 -49
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- data/lib/rigor/inference/global_write_census.rb +76 -2
- data/lib/rigor/inference/global_writer_reach.rb +260 -0
- data/lib/rigor/inference/hkt_body.rb +1 -2
- data/lib/rigor/inference/hkt_body_parser.rb +3 -5
- data/lib/rigor/inference/hkt_reducer.rb +2 -2
- data/lib/rigor/inference/hkt_registry.rb +3 -5
- data/lib/rigor/inference/indexed_narrowing.rb +4 -7
- data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
- data/lib/rigor/inference/ivar_writer_census.rb +51 -0
- data/lib/rigor/inference/key_presence_guard.rb +2 -1
- data/lib/rigor/inference/loop_value_record.rb +8 -4
- data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
- data/lib/rigor/inference/match_rebinding.rb +11 -1
- data/lib/rigor/inference/memo_audit.rb +55 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +64 -14
- data/lib/rigor/inference/module_function_state.rb +11 -1
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_rejoin.rb +240 -4
- data/lib/rigor/inference/mutation_widening.rb +157 -27
- data/lib/rigor/inference/narrowing.rb +47 -33
- data/lib/rigor/inference/optimistic_origin.rb +1 -1
- data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
- data/lib/rigor/inference/pre_eval_constants.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +6 -3
- data/lib/rigor/inference/project_yielder.rb +11 -17
- data/lib/rigor/inference/receiver_alias.rb +20 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
- data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
- data/lib/rigor/inference/remover_holder_scan.rb +233 -0
- data/lib/rigor/inference/remover_narrowing.rb +224 -0
- data/lib/rigor/inference/rewrite_mutation.rb +53 -2
- data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
- data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
- data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
- data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
- data/lib/rigor/inference/scope_indexer.rb +137 -46
- data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
- data/lib/rigor/inference/set_mutation.rb +189 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
- data/lib/rigor/inference/statement_evaluator.rb +2328 -138
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
- data/lib/rigor/inference/toplevel_def_census.rb +59 -0
- data/lib/rigor/inference/unknown_store_widening.rb +103 -4
- data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
- data/lib/rigor/inference/void_tail_summary.rb +2 -1
- data/lib/rigor/language_server/buffer_table.rb +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/plugins.rb +1 -1
- data/lib/rigor/mcp/server.rb +73 -2
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- data/lib/rigor/plugin/bundled_catalog.rb +1 -12
- data/lib/rigor/plugin/io_boundary.rb +1 -1
- data/lib/rigor/plugin/loader.rb +25 -8
- data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
- data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
- data/lib/rigor/plugin/manifest.rb +9 -13
- data/lib/rigor/plugin/registry.rb +3 -3
- data/lib/rigor/plugin/services.rb +2 -3
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
- data/lib/rigor/plugin/trust_policy.rb +2 -2
- data/lib/rigor/plugin/type_node_resolver.rb +2 -4
- data/lib/rigor/plugin.rb +6 -8
- data/lib/rigor/plugin_gap_advisory.rb +5 -1
- data/lib/rigor/project_environment.rb +16 -2
- data/lib/rigor/protection/analysis_guard.rb +1 -1
- data/lib/rigor/protection/discovery_seed.rb +44 -5
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
- data/lib/rigor/rbs_extended/reporter.rb +27 -3
- data/lib/rigor/rbs_extended.rb +2 -3
- data/lib/rigor/reflection/constant_ancestors.rb +5 -3
- data/lib/rigor/rooted_glob.rb +15 -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 +19 -6
- data/lib/rigor/scope/resolution_chain.rb +3 -1
- data/lib/rigor/scope.rb +365 -31
- data/lib/rigor/sig_gen/classification.rb +4 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
- data/lib/rigor/sig_gen/generator.rb +371 -58
- data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
- data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
- data/lib/rigor/sig_gen/layout_index.rb +3 -3
- data/lib/rigor/sig_gen/method_candidate.rb +23 -4
- data/lib/rigor/sig_gen/observation_collector.rb +3 -5
- 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/renderer.rb +15 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -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 +217 -37
- data/lib/rigor/type/acceptance_router.rb +2 -2
- data/lib/rigor/type/accepts_result.rb +1 -1
- data/lib/rigor/type/app.rb +4 -6
- data/lib/rigor/type/combinator.rb +9 -13
- data/lib/rigor/type/constant.rb +4 -5
- data/lib/rigor/type/hash_shape.rb +21 -40
- data/lib/rigor/type/union.rb +1 -1
- data/lib/rigor/type_node/identifier.rb +1 -2
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node/string_literal.rb +1 -2
- data/lib/rigor/type_node/symbol_literal.rb +1 -2
- data/lib/rigor/type_node/union.rb +3 -6
- data/lib/rigor/value_semantics.rb +1 -10
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +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 +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
- data/plugins/rigor-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 +4 -5
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- 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 +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
- data/sig/rigor/analysis/baseline.rbs +0 -3
- data/sig/rigor/analysis/fact_store.rbs +1 -0
- data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
- data/sig/rigor/cli/diff_command.rbs +1 -1
- data/sig/rigor/cli/explain_command.rbs +1 -1
- data/sig/rigor/effects/registry.rbs +2 -2
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +1 -0
- 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/io_boundary.rbs +2 -1
- data/sig/rigor/plugin/manifest.rbs +9 -9
- data/sig/rigor/plugin/protocol_contract.rbs +3 -7
- data/sig/rigor/scope.rbs +23 -3
- data/skills/rigor-plugin-author/SKILL.md +4 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
- metadata +36 -3
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/node_children"
|
|
6
|
+
require_relative "../source/node_walker"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
# Issue #2595 — the writes a def makes to its parameters that the callee floor's direct and block censuses
|
|
11
|
+
# (`StatementEvaluator#own_content_parameters`) do not see, read from the def's AST alone:
|
|
12
|
+
#
|
|
13
|
+
# - a shape mutator the content scans do not count (`h.merge!(x)`, `h.update(x)`, `a.pop`, `h.compact!`), from
|
|
14
|
+
# `MutationWidening::SHAPE_MUTATORS`, which the caller passes in as `mutators:`;
|
|
15
|
+
# - a mutation whose receiver may be the parameter or a part of it: the parameter read through any expression
|
|
16
|
+
# (`(h).merge!`, `h.itself.merge!`, `[h].first.merge!`, `(c ? {} : h).merge!`), or an alias. An alias is a
|
|
17
|
+
# local, ivar, class variable or global assigned from it (`other = h`, `x = y = h`, `a, b = h, 1`, `@x = h`,
|
|
18
|
+
# `@@x = h`, `$g = h`, `instance_variable_set(:@x, h)`), a block parameter of a call whose receiver or argument
|
|
19
|
+
# holds it (`h.tap { |x| }`, `[h].each { |x| }`, `each_with_object(h)`), or a call result on it
|
|
20
|
+
# (`v = h[:list]`) unless the call returns a new object (`fresh:`, `h.dup`, `h.merge(x)`). A literal, and a
|
|
21
|
+
# copier's result (`copiers:`, `h.values`, `h.slice(:a)`), only holds what it is built from (`t = [h]`): a
|
|
22
|
+
# write to it does not count, an element taken out does;
|
|
23
|
+
# - an opaque dispatch on such a value (`send`, `instance_eval`, `method`, ...) and a mutator symbol block
|
|
24
|
+
# (`[h].each(&:clear)`).
|
|
25
|
+
#
|
|
26
|
+
# Issue #2336 — and a hand-off the callee floor cannot resolve, which reads the handed value as written: an
|
|
27
|
+
# argument of `yield`, of `call` on any explicit receiver (a Method, a Proc, a lambda: `method(:poke).call(h)`,
|
|
28
|
+
# `->(x) { }.call(h)`, `blk.(h)`), of `instance_exec` / `class_exec` / `module_exec`, or of a `send` /
|
|
29
|
+
# `public_send` / `__send__` whose method name is no literal; an argument of any call inside a block that runs
|
|
30
|
+
# later as a method body (`define_method(:go) { poke(h) }`, {Tables#foreign_calls}), which the hand-off walk
|
|
31
|
+
# leaves out; and, when `returns:` is true, a value the def returns (its body's last expression, a `return`
|
|
32
|
+
# outside a lambda) that is the parameter itself, whose caller may write it (`r = ident(h); r[:k] = 1`).
|
|
33
|
+
#
|
|
34
|
+
# `seeds` names the parameters and how each holds the caller's argument: a positional or keyword parameter is it
|
|
35
|
+
# (`:same`); a rest or keyword-rest parameter holds the caller's arguments as elements (`:held`), so a write to the
|
|
36
|
+
# rest array itself does not count and a write to an element taken out of it does.
|
|
37
|
+
#
|
|
38
|
+
# The walk is flow-insensitive, so an alias made anywhere counts everywhere; a local is keyed by its name and the
|
|
39
|
+
# scope level it lives at, so two sibling blocks' locals of one name share a key. Both only ever count more. A
|
|
40
|
+
# nested def is not entered; a block and a lambda are. A write the content scans already count on the parameter
|
|
41
|
+
# read directly (a name in `handled:`, an index write on `h`) is left to them: their block channel keeps the element
|
|
42
|
+
# type. `rebinds` (`{ name => end offset }`, `StatementEvaluator#parameter_rebinds`) says where the def rebinds a
|
|
43
|
+
# parameter to a new object (`h = h.dup`): a node that starts after it no longer reaches the caller's argument,
|
|
44
|
+
# read as `StatementEvaluator#after_rebind?` reads it.
|
|
45
|
+
#
|
|
46
|
+
# {Census#aliases} answers the parameters a read may be (not a part of), so a hand-off of an alias (`poke(other)`)
|
|
47
|
+
# is followed as the parameter's own hand-off.
|
|
48
|
+
module CalleeWriteCensus
|
|
49
|
+
# `written` is the parameter names written; `alias_table` maps a key (`[local, level]`, `[ivar, :ivar]` or
|
|
50
|
+
# `[cvar_or_global, :global]`) to `{ status => Set[parameter] }`: the value under the key may be the parameter
|
|
51
|
+
# (status `:same`), a part of it (`:part`: `v = h[:list]`, `h.each { |k, v| }`), or hold it as an element
|
|
52
|
+
# (`:held`: `arr = [h]`, which a write to `arr` does not reach).
|
|
53
|
+
Census = Data.define(:written, :alias_table, :names)
|
|
54
|
+
|
|
55
|
+
# The name tables a scan reads, which the caller owns: `mutators` (the shape and content mutators), `handled`
|
|
56
|
+
# (the names the content scans count on the parameter read directly), `fresh` (calls whose result is a new
|
|
57
|
+
# object), `copiers` (calls whose result is a new container of the receiver's elements) and `foreign_calls`
|
|
58
|
+
# (calls whose block is a method body).
|
|
59
|
+
Tables = Data.define(:mutators, :handled, :fresh, :copiers, :foreign_calls)
|
|
60
|
+
|
|
61
|
+
NONE = [].freeze
|
|
62
|
+
EMPTY = Census.new(written: NONE, alias_table: {}.freeze, names: Set.new.freeze)
|
|
63
|
+
STATUSES = %i[same part held].freeze
|
|
64
|
+
STATUS_INDEX = { same: 0, part: 1, held: 2 }.freeze
|
|
65
|
+
# A step is how a value relates to the one it is built from, as the status it gives each of {STATUSES}.
|
|
66
|
+
ID = %i[same part held].freeze
|
|
67
|
+
WRAP = %i[held held held].freeze
|
|
68
|
+
UNWRAP = %i[part part same].freeze
|
|
69
|
+
# The mode of a mutator symbol block's record: every status counts, since the elements take the mutator.
|
|
70
|
+
ANY = :any
|
|
71
|
+
# Calls whose result is the receiver itself (or, for `to_a`, `to_h` and `to_s`, may be: each returns its receiver
|
|
72
|
+
# on one core class and a copy on another, read as the receiver), calls whose block parameter is the receiver,
|
|
73
|
+
# and calls whose result is the block's value.
|
|
74
|
+
SELF_RETURNERS = %i[tap itself freeze to_a to_h to_s].to_set.freeze
|
|
75
|
+
SELF_YIELDERS = %i[tap then yield_self].to_set.freeze
|
|
76
|
+
BLOCK_RETURNERS = %i[then yield_self].to_set.freeze
|
|
77
|
+
# Memo iterators whose memo parameter comes first (`inject(m) { |memo, e| }`); `each_with_object` puts it last.
|
|
78
|
+
MEMO_FIRST = %i[inject reduce].to_set.freeze
|
|
79
|
+
# Calls whose result is their memo argument's (`h.each_with_object({}) { ... }` is that `{}`).
|
|
80
|
+
MEMO_RETURNERS = %i[each_with_object inject reduce].to_set.freeze
|
|
81
|
+
# Iterators whose block parameter is a new array of the receiver's elements, and which return the receiver when
|
|
82
|
+
# given a block and a new enumerator otherwise.
|
|
83
|
+
SLICERS = %i[each_slice each_cons].to_set.freeze
|
|
84
|
+
# Calls that copy (a new container of the receiver's elements) only when given an argument (`first(2)`).
|
|
85
|
+
ARGUMENT_COPIERS = %i[first last min max].to_set.freeze
|
|
86
|
+
# Every step {ID}, {WRAP} and {UNWRAP} compose to, as `{ outer => { inner => outer after inner } }`, closed once
|
|
87
|
+
# at load: the steps are a handful of frozen tables, so the walk composes them without allocating.
|
|
88
|
+
COMPOSED = begin
|
|
89
|
+
steps = [ID, WRAP, UNWRAP]
|
|
90
|
+
table = {}
|
|
91
|
+
loop do
|
|
92
|
+
size = steps.size
|
|
93
|
+
steps.product(steps).each do |outer, inner|
|
|
94
|
+
composed = inner.map { |status| outer[STATUS_INDEX.fetch(status)] }
|
|
95
|
+
steps << composed.freeze unless steps.include?(composed)
|
|
96
|
+
(table[outer] ||= {})[inner] = steps[steps.index(composed)]
|
|
97
|
+
end
|
|
98
|
+
break if steps.size == size
|
|
99
|
+
end
|
|
100
|
+
table.each_value(&:freeze).freeze
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Calls on an alias that run code the walk cannot see against it.
|
|
104
|
+
OPAQUE = %i[send public_send __send__ instance_eval instance_exec instance_variable_set method
|
|
105
|
+
public_method].to_set.freeze
|
|
106
|
+
# The names {Census#written} reports an anonymous rest (`*`), keyword rest (`**`) or `...` under when one is
|
|
107
|
+
# handed to an opaque sink; no parameter can be named so.
|
|
108
|
+
ANONYMOUS_REST = :*
|
|
109
|
+
ANONYMOUS_KEYWORD_REST = :**
|
|
110
|
+
ANONYMOUS_FORWARDED = [ANONYMOUS_REST, ANONYMOUS_KEYWORD_REST].freeze
|
|
111
|
+
# Calls whose arguments the walk cannot follow, on an explicit receiver for `call` and on any for the rest.
|
|
112
|
+
OPAQUE_ARGUMENTS = %i[call instance_exec class_exec module_exec].to_set.freeze
|
|
113
|
+
# Dispatches whose target is their first argument: opaque to the walk when it is no literal name.
|
|
114
|
+
SENDS = %i[send public_send __send__].to_set.freeze
|
|
115
|
+
IVAR_ANY_KEY = %i[* ivar].freeze
|
|
116
|
+
PARAMETER_NODES = [Prism::RequiredParameterNode, Prism::OptionalParameterNode, Prism::RestParameterNode,
|
|
117
|
+
Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode,
|
|
118
|
+
Prism::KeywordRestParameterNode].freeze
|
|
119
|
+
private_constant :STATUSES, :STATUS_INDEX, :ID, :WRAP, :UNWRAP, :ANY, :SELF_RETURNERS, :SELF_YIELDERS,
|
|
120
|
+
:BLOCK_RETURNERS, :MEMO_FIRST, :MEMO_RETURNERS, :SLICERS, :ARGUMENT_COPIERS, :COMPOSED, :OPAQUE,
|
|
121
|
+
:OPAQUE_ARGUMENTS, :ANONYMOUS_FORWARDED, :SENDS, :IVAR_ANY_KEY, :PARAMETER_NODES
|
|
122
|
+
|
|
123
|
+
module_function
|
|
124
|
+
|
|
125
|
+
# The {Census} of `body` for the parameters `seeds` (`{ name => :same | :held }`), reading `tables` ({Tables}).
|
|
126
|
+
# `returns` says whether a returned parameter counts.
|
|
127
|
+
#
|
|
128
|
+
# `anonymous` says the def has an anonymous rest, keyword rest or `...`, which the walk reads even with no
|
|
129
|
+
# seed: handed to an opaque sink, it reports {ANONYMOUS_REST} / {ANONYMOUS_KEYWORD_REST} as written.
|
|
130
|
+
def scan(body, seeds, tables:, rebinds:, returns: false, anonymous: false)
|
|
131
|
+
return EMPTY if body.nil? || (seeds.empty? && !anonymous)
|
|
132
|
+
|
|
133
|
+
walk = Walk.new(tables, returns: returns)
|
|
134
|
+
walk.visit(body, 0)
|
|
135
|
+
walk.note_tail(body, 0) if returns
|
|
136
|
+
walk.census(seeds, rebinds)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# True when a `send` / `public_send` / `__send__` call names its target with a literal (`send(:poke, h)`,
|
|
140
|
+
# `send("poke", h)`), which the callee floor resolves as a call of that name.
|
|
141
|
+
def literal_send?(node)
|
|
142
|
+
target = node.arguments&.arguments&.first
|
|
143
|
+
target.is_a?(Prism::SymbolNode) || target.is_a?(Prism::StringNode)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The parameter names an argument `node` at walk depth `depth` may BE (not a part of, nor a container holding):
|
|
147
|
+
# through an alias in `census` (`other = h; poke(other)`), or through an expression over a parameter or alias
|
|
148
|
+
# (`poke(args.first)`, `poke(h.itself)`). A read of a parameter itself answers nothing here; the caller reads it.
|
|
149
|
+
def handed_aliases(census, seeds, node, depth, fresh:, copiers:)
|
|
150
|
+
if (name = read_name(node))
|
|
151
|
+
return NONE unless census.names.include?(name)
|
|
152
|
+
|
|
153
|
+
return lookup(census.alias_table, read_key(node, depth), :same)
|
|
154
|
+
end
|
|
155
|
+
return NONE unless node.is_a?(Prism::CallNode) || node.is_a?(Prism::ParenthesesNode)
|
|
156
|
+
|
|
157
|
+
tables = Tables.new(mutators: NONE, handled: NONE, fresh: fresh, copiers: copiers, foreign_calls: NONE)
|
|
158
|
+
found = Walk.new(tables).value_sources(node, depth)
|
|
159
|
+
found.nil? ? NONE : same_parameters(census, seeds, found)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The parameters the flat `found` sources give a value as the value itself.
|
|
163
|
+
def same_parameters(census, seeds, found)
|
|
164
|
+
result = nil
|
|
165
|
+
each_record(found, 2) do |key, step|
|
|
166
|
+
STATUSES.each do |status|
|
|
167
|
+
next unless apply(step, status) == :same
|
|
168
|
+
|
|
169
|
+
parameters = lookup(census.alias_table, key, status)
|
|
170
|
+
(result ||= []).concat(parameters) unless parameters.empty?
|
|
171
|
+
end
|
|
172
|
+
seed = seed(key, seeds)
|
|
173
|
+
(result ||= []) << key.first if seed && apply(step, seed) == :same
|
|
174
|
+
end
|
|
175
|
+
result.nil? ? NONE : result.uniq
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# The name part of {#read_key}, allocating nothing; nil for anything but a read.
|
|
179
|
+
def read_name(node)
|
|
180
|
+
case node
|
|
181
|
+
when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode,
|
|
182
|
+
Prism::GlobalVariableReadNode
|
|
183
|
+
node.name
|
|
184
|
+
when Prism::ItLocalVariableReadNode then :it
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# The key a read node stands for at walk depth `depth`, or nil.
|
|
189
|
+
def read_key(node, depth)
|
|
190
|
+
case node
|
|
191
|
+
when Prism::LocalVariableReadNode then [node.name, depth - node.depth]
|
|
192
|
+
when Prism::ItLocalVariableReadNode then [:it, depth]
|
|
193
|
+
when Prism::InstanceVariableReadNode then [node.name, :ivar]
|
|
194
|
+
when Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode then [node.name, :global]
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# The parameters `key` holds under `status`; an ivar also holds what a non-literal `instance_variable_set` stored.
|
|
199
|
+
def lookup(table, key, status)
|
|
200
|
+
found = table[key]&.[](status)
|
|
201
|
+
if key.last == :ivar
|
|
202
|
+
any = table[IVAR_ANY_KEY]&.[](status)
|
|
203
|
+
found = found.nil? ? any : found | any if any
|
|
204
|
+
end
|
|
205
|
+
found.nil? ? NONE : found.to_a
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# The status a value reached through `step` has when its source has `status`: a literal wraps it (`[h]` holds
|
|
209
|
+
# `h`), a call or an element of it unwraps it (`[h].first` is `h`, `h[:list]` a part of `h`), a self-returner
|
|
210
|
+
# (`h.tap`) and anything else pass it on.
|
|
211
|
+
def apply(step, status) = step[STATUS_INDEX.fetch(status)]
|
|
212
|
+
|
|
213
|
+
# How the parameter `key` reads holds the caller's argument (`seeds`), or nil when `key` is no parameter read at
|
|
214
|
+
# method scope.
|
|
215
|
+
def seed(key, seeds) = key.last.equal?(0) ? seeds[key.first] : nil
|
|
216
|
+
|
|
217
|
+
# Yields each `width`-long record (at most four) of the flat `list` (nil: none) as separate arguments, allocating
|
|
218
|
+
# nothing; a block taking fewer arguments ignores the rest.
|
|
219
|
+
def each_record(list, width)
|
|
220
|
+
return if list.nil?
|
|
221
|
+
|
|
222
|
+
i = 0
|
|
223
|
+
while i < list.size
|
|
224
|
+
yield list[i], list[i + 1], list[i + 2], list[i + 3]
|
|
225
|
+
i += width
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# True when `node` starts after the def rebinds the parameter `name` ({#scan}'s `rebinds`).
|
|
230
|
+
def rebound?(rebinds, name, node)
|
|
231
|
+
offset = rebinds[name]
|
|
232
|
+
!offset.nil? && node.location.start_offset >= offset
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# The step `outer` after `inner` ({COMPOSED}).
|
|
236
|
+
def compose(outer, inner) = COMPOSED.fetch(outer).fetch(inner)
|
|
237
|
+
|
|
238
|
+
# True when a mutation under `mode` (a step, or {ANY}) reaches a value of `status`: the value itself or a part
|
|
239
|
+
# of it, not a container holding it, unless every status counts.
|
|
240
|
+
def reaches?(mode, status) = mode == ANY || apply(mode, status) != :held
|
|
241
|
+
|
|
242
|
+
# The value forms {Walk} reads a value's sources through: what a value may be built from, and how.
|
|
243
|
+
module ValueSources
|
|
244
|
+
private
|
|
245
|
+
|
|
246
|
+
# `acc` (nil until the first) with each `key, step` a value may be built from at `depth` appended, flat;
|
|
247
|
+
# `step` is how the value relates to the key ({CalleeWriteCensus.apply}).
|
|
248
|
+
def sources(node, depth, acc, step)
|
|
249
|
+
case node
|
|
250
|
+
when Prism::LocalVariableReadNode, Prism::ItLocalVariableReadNode, Prism::InstanceVariableReadNode,
|
|
251
|
+
Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode
|
|
252
|
+
(acc ||= []).push(CalleeWriteCensus.read_key(node, depth), step)
|
|
253
|
+
when Prism::ParenthesesNode, Prism::StatementsNode, Prism::OrNode, Prism::AndNode, Prism::ElseNode,
|
|
254
|
+
Prism::AssocNode, Prism::SplatNode, Prism::AssocSplatNode
|
|
255
|
+
node.rigor_each_child { |child| acc = sources(child, depth, acc, step) }
|
|
256
|
+
when Prism::ArrayNode, Prism::HashNode, Prism::KeywordHashNode
|
|
257
|
+
inner = CalleeWriteCensus.compose(step, WRAP)
|
|
258
|
+
node.rigor_each_child { |child| acc = sources(child, depth, acc, inner) }
|
|
259
|
+
when Prism::CallNode then acc = call_sources(node, depth, acc, step)
|
|
260
|
+
else acc = branch_sources(node, depth, acc, step)
|
|
261
|
+
end
|
|
262
|
+
acc
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# The value forms that pick one of several branches, or pass on a write's value (`x = y = h`).
|
|
266
|
+
def branch_sources(node, depth, acc, step)
|
|
267
|
+
case node
|
|
268
|
+
when Prism::IfNode then sources(node.subsequent, depth, sources(node.statements, depth, acc, step), step)
|
|
269
|
+
when Prism::UnlessNode
|
|
270
|
+
sources(node.else_clause, depth, sources(node.statements, depth, acc, step), step)
|
|
271
|
+
when Prism::CaseNode, Prism::CaseMatchNode
|
|
272
|
+
node.conditions.each { |condition| acc = sources(condition.statements, depth, acc, step) }
|
|
273
|
+
sources(node.else_clause, depth, acc, step)
|
|
274
|
+
when Prism::BeginNode then begin_sources(node, depth, acc, step)
|
|
275
|
+
when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::ClassVariableWriteNode,
|
|
276
|
+
Prism::GlobalVariableWriteNode, Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode
|
|
277
|
+
sources(node.value, depth, acc, step)
|
|
278
|
+
else acc
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# A `begin` block's value: its statements, each `rescue` clause's, or its `else` clause's.
|
|
283
|
+
def begin_sources(node, depth, acc, step)
|
|
284
|
+
acc = sources(node.statements, depth, acc, step)
|
|
285
|
+
rescue_clause = node.rescue_clause
|
|
286
|
+
while rescue_clause
|
|
287
|
+
acc = sources(rescue_clause.statements, depth, acc, step)
|
|
288
|
+
rescue_clause = rescue_clause.subsequent
|
|
289
|
+
end
|
|
290
|
+
sources(node.else_clause, depth, acc, step)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def call_sources(node, depth, acc, step)
|
|
294
|
+
name = node.name
|
|
295
|
+
return acc if @fresh.include?(name)
|
|
296
|
+
# A copier builds a new container of the receiver's elements (`h.values`, `h.slice(:a)`, `to_h { }`): a write
|
|
297
|
+
# to it misses the receiver, an element taken out of it may be a part.
|
|
298
|
+
return memo_sources(node, depth, acc, step) if MEMO_RETURNERS.include?(name) && node.arguments
|
|
299
|
+
return sources(node.receiver, depth, acc, step) if SLICERS.include?(name) && node.block
|
|
300
|
+
|
|
301
|
+
return sources(node.receiver, depth, acc, CalleeWriteCensus.compose(step, WRAP)) if copier?(node)
|
|
302
|
+
return ivar_get_sources(node, acc, step) if name == :instance_variable_get && self_receiver?(node)
|
|
303
|
+
|
|
304
|
+
if BLOCK_RETURNERS.include?(name) && node.block.is_a?(Prism::BlockNode)
|
|
305
|
+
acc = sources(node.block.body, depth + 1, acc, step)
|
|
306
|
+
end
|
|
307
|
+
inner = SELF_RETURNERS.include?(name) ? step : CalleeWriteCensus.compose(step, UNWRAP)
|
|
308
|
+
sources(node.receiver, depth, acc, inner)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def copier?(node)
|
|
312
|
+
name = node.name
|
|
313
|
+
@copiers.include?(name) || SLICERS.include?(name) || (name == :to_h && node.block) ||
|
|
314
|
+
(ARGUMENT_COPIERS.include?(name) && node.arguments)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# A memo iterator's result: `each_with_object`'s is its memo argument; `inject(m)`'s is the block's last value,
|
|
318
|
+
# read as the argument or an element of the receiver.
|
|
319
|
+
def memo_sources(node, depth, acc, step)
|
|
320
|
+
node.arguments.arguments.each { |argument| acc = sources(argument, depth, acc, step) }
|
|
321
|
+
return acc if node.name == :each_with_object
|
|
322
|
+
|
|
323
|
+
sources(node.receiver, depth, acc, CalleeWriteCensus.compose(step, UNWRAP))
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def ivar_get_sources(node, acc, step)
|
|
327
|
+
target = node.arguments&.arguments&.first
|
|
328
|
+
(acc || []).push(target.is_a?(Prism::SymbolNode) ? [target.unescaped.to_sym, :ivar] : IVAR_ANY_KEY, step)
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
private_constant :ValueSources
|
|
332
|
+
|
|
333
|
+
# How {Walk} binds a block's parameters to the values of the call that yields to it.
|
|
334
|
+
module BlockBindings
|
|
335
|
+
private
|
|
336
|
+
|
|
337
|
+
# A block parameter takes an element of the receiver (`[h].each { |x| }`, `h.each { |k, v| }`), the receiver
|
|
338
|
+
# itself under a self-yielder (`h.tap { |x| }`), or an argument itself (`each_with_object(h) { |_, m| }`). A
|
|
339
|
+
# slicer's block parameter (`each_slice`, `each_cons`) is a new array of the receiver's elements.
|
|
340
|
+
def note_block(node, depth)
|
|
341
|
+
params = block_parameters(node.block, depth + 1)
|
|
342
|
+
return if params.empty?
|
|
343
|
+
|
|
344
|
+
arguments = node.arguments&.arguments
|
|
345
|
+
memo = memo_index(node.name, node.block, params) if arguments && !arguments.empty?
|
|
346
|
+
found = sources(node.receiver, depth, nil, receiver_block_step(node.name, node.block))
|
|
347
|
+
return note_memo_block(node, depth, params, memo, found) if memo
|
|
348
|
+
|
|
349
|
+
arguments&.each { |argument| found = sources(argument, depth, found, ID) }
|
|
350
|
+
edge(found, node) { params } if found
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# A memo iterator with a memo argument (`each_with_object(m)`, `inject(m)`, `reduce(m)`) binds its memo
|
|
354
|
+
# parameter (at `memo`) from the argument alone, and the element parameters from the receiver (`found`).
|
|
355
|
+
def note_memo_block(node, depth, params, memo, found)
|
|
356
|
+
elements = params.dup.tap { |list| list.delete_at(memo) }
|
|
357
|
+
edge(found, node) { elements } if found && !elements.empty?
|
|
358
|
+
held = nil
|
|
359
|
+
node.arguments.arguments.each { |argument| held = sources(argument, depth, held, ID) }
|
|
360
|
+
edge(held, node) { params[memo] } if held
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# A slicer's block parameter holds a slice only when the block takes it whole (`|s|`, `it`, `_1` alone); a
|
|
364
|
+
# block that destructures it (`|a, b|`, `_2`) takes the receiver's elements.
|
|
365
|
+
def receiver_block_step(name, block)
|
|
366
|
+
if SELF_YIELDERS.include?(name) then ID
|
|
367
|
+
elsif SLICERS.include?(name) && whole_parameter?(block) then WRAP
|
|
368
|
+
else UNWRAP
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# The index, among the `keys` a memo iterator `name`'s block binds, of its memo parameter, or nil. The memo is
|
|
373
|
+
# bound only when the block takes at least two top-level positional parameters (requireds and optionals) and
|
|
374
|
+
# the memo's is a plain required one (the last of `each_with_object`'s, the first of `inject`'s); `|x|`, `it`
|
|
375
|
+
# or `_1` alone is the element, and a destructured memo binds nothing.
|
|
376
|
+
def memo_index(name, block, keys)
|
|
377
|
+
positional = top_level_parameters(block)
|
|
378
|
+
return if positional.nil? || positional.size < 2
|
|
379
|
+
|
|
380
|
+
memo = name == :each_with_object ? positional.last : (positional.first if MEMO_FIRST.include?(name))
|
|
381
|
+
memo_name = parameter_name(memo)
|
|
382
|
+
memo_name && keys.index { |key| key.first == memo_name }
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# The name a plain parameter binds: a required parameter's, or a numbered placeholder's (`:_2`); nil otherwise.
|
|
386
|
+
def parameter_name(param)
|
|
387
|
+
case param
|
|
388
|
+
when Symbol then param
|
|
389
|
+
when Prism::RequiredParameterNode then param.name
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# The block's top-level positional parameters, requireds then optionals (`:_1`, `:_2` for numbered ones), or nil
|
|
394
|
+
# for `it` / none.
|
|
395
|
+
def top_level_parameters(block)
|
|
396
|
+
params = block.parameters
|
|
397
|
+
case params
|
|
398
|
+
when Prism::NumberedParametersNode then (1..params.maximum).map { |i| :"_#{i}" }
|
|
399
|
+
when Prism::BlockParametersNode
|
|
400
|
+
inner = params.parameters
|
|
401
|
+
inner.nil? ? nil : [*inner.requireds, *inner.optionals]
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# True when the block takes its one yielded value whole: `it`, `_1` alone, or one plain required parameter.
|
|
406
|
+
def whole_parameter?(block)
|
|
407
|
+
params = block.parameters
|
|
408
|
+
case params
|
|
409
|
+
when Prism::ItParametersNode then true
|
|
410
|
+
when Prism::NumberedParametersNode then params.maximum == 1
|
|
411
|
+
when Prism::BlockParametersNode
|
|
412
|
+
inner = params.parameters
|
|
413
|
+
!inner.nil? && inner.requireds.size == 1 && inner.requireds.first.is_a?(Prism::RequiredParameterNode) &&
|
|
414
|
+
inner.optionals.empty? && inner.rest.nil? && inner.posts.empty? && inner.keywords.empty? &&
|
|
415
|
+
inner.keyword_rest.nil?
|
|
416
|
+
else false
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def block_parameters(block, depth)
|
|
421
|
+
params = block.parameters
|
|
422
|
+
case params
|
|
423
|
+
when Prism::NumberedParametersNode then (1..params.maximum).map { |i| [:"_#{i}", depth] }
|
|
424
|
+
when Prism::ItParametersNode then [[:it, depth]]
|
|
425
|
+
when Prism::BlockParametersNode
|
|
426
|
+
found = []
|
|
427
|
+
Source::NodeWalker.each(params.parameters) do |param|
|
|
428
|
+
found << [param.name, depth] if PARAMETER_NODES.any? { |klass| param.is_a?(klass) } && param.name
|
|
429
|
+
end
|
|
430
|
+
found
|
|
431
|
+
else NONE
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
private_constant :BlockBindings
|
|
436
|
+
|
|
437
|
+
# Issue #2336 — how {Walk} reads the values a def returns ({CalleeWriteCensus.scan}'s `returns:`).
|
|
438
|
+
module Returns
|
|
439
|
+
# Records what the def's value may be: the last expression of each branch of `node` (a body, a branch, a
|
|
440
|
+
# `begin` clause), read as {ValueSources#sources} reads a value.
|
|
441
|
+
def note_tail(node, depth)
|
|
442
|
+
case node
|
|
443
|
+
when nil, Prism::ReturnNode then nil
|
|
444
|
+
when Prism::StatementsNode then note_tail(node.body.last, depth)
|
|
445
|
+
when Prism::ParenthesesNode then note_tail(node.body, depth)
|
|
446
|
+
when Prism::ElseNode then note_tail(node.statements, depth)
|
|
447
|
+
when Prism::IfNode, Prism::UnlessNode
|
|
448
|
+
note_tail(node.statements, depth)
|
|
449
|
+
note_tail(node.is_a?(Prism::IfNode) ? node.subsequent : node.else_clause, depth)
|
|
450
|
+
when Prism::CaseNode, Prism::CaseMatchNode
|
|
451
|
+
node.conditions.each { |condition| note_tail(condition.statements, depth) }
|
|
452
|
+
note_tail(node.else_clause, depth)
|
|
453
|
+
when Prism::BeginNode then note_begin_tail(node, depth)
|
|
454
|
+
else note_returned(node, depth)
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# A `return` outside a lambda returns from the def, when the scan counts returns ({#note_tail} ran first).
|
|
459
|
+
def note_return(node, depth)
|
|
460
|
+
values = node.arguments&.arguments
|
|
461
|
+
note_returned(values.first, depth) if values&.size == 1 && @lambdas.zero? && @returns
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def note_begin_tail(node, depth)
|
|
465
|
+
note_tail(node.statements, depth)
|
|
466
|
+
rescue_clause = node.rescue_clause
|
|
467
|
+
while rescue_clause
|
|
468
|
+
note_tail(rescue_clause.statements, depth)
|
|
469
|
+
rescue_clause = rescue_clause.subsequent
|
|
470
|
+
end
|
|
471
|
+
note_tail(node.else_clause, depth)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# Records the variable a value the def may return reads, `key, node` flat: a read itself, through a write's
|
|
475
|
+
# value (`@x = h`), either side of `||` / `&&` and a parenthesised body. A call's result is not read as the
|
|
476
|
+
# parameter, even one that returns its receiver (`h.tap { }`), so the census stays exact on the spellings it
|
|
477
|
+
# reads; an element taken out (`[h].first`) or a conversion (`h.to_s`) is left out too.
|
|
478
|
+
def note_returned(value, depth)
|
|
479
|
+
case value
|
|
480
|
+
when Prism::LocalVariableReadNode, Prism::ItLocalVariableReadNode, Prism::InstanceVariableReadNode,
|
|
481
|
+
Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode
|
|
482
|
+
(@returned ||= []).push(CalleeWriteCensus.read_key(value, depth), value)
|
|
483
|
+
when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::ClassVariableWriteNode,
|
|
484
|
+
Prism::GlobalVariableWriteNode, Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode
|
|
485
|
+
note_returned(value.value, depth)
|
|
486
|
+
when Prism::OrNode, Prism::AndNode
|
|
487
|
+
note_returned(value.left, depth)
|
|
488
|
+
note_returned(value.right, depth)
|
|
489
|
+
when Prism::ParenthesesNode then note_tail(value.body, depth)
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# `written` with each parameter a returned value is itself ({#note_returned}), unless the def rebound it first.
|
|
494
|
+
def returned_parameters(written, table, seeds, rebinds)
|
|
495
|
+
CalleeWriteCensus.each_record(@returned, 2) do |key, node|
|
|
496
|
+
parameters = table.dig(key, :same)
|
|
497
|
+
written = (written || Set.new).merge(parameters) if parameters
|
|
498
|
+
next unless CalleeWriteCensus.seed(key, seeds) == :same
|
|
499
|
+
next if CalleeWriteCensus.rebound?(rebinds, key.first, node)
|
|
500
|
+
|
|
501
|
+
(written ||= Set.new) << key.first
|
|
502
|
+
end
|
|
503
|
+
written
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
private_constant :Returns
|
|
507
|
+
|
|
508
|
+
# Issue #2336 — how {Walk} reads a hand-off to code it cannot see: an opaque sink's arguments, and a call inside
|
|
509
|
+
# a block that is a method body.
|
|
510
|
+
module OpaqueSinks
|
|
511
|
+
private
|
|
512
|
+
|
|
513
|
+
def visit_counted(child, depth, counter)
|
|
514
|
+
instance_variable_set(counter, instance_variable_get(counter) + 1)
|
|
515
|
+
visit(child, depth)
|
|
516
|
+
ensure
|
|
517
|
+
instance_variable_set(counter, instance_variable_get(counter) - 1)
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# True for a call whose block runs later as a method body (`foreign_calls`: `define_method`, ...).
|
|
521
|
+
def foreign_block?(node)
|
|
522
|
+
node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode) && @foreign_calls.include?(node.name)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# True for a call whose arguments the walk reads as written ({CalleeWriteCensus}, issue #2336).
|
|
526
|
+
def opaque_argument_call?(node)
|
|
527
|
+
name = node.name
|
|
528
|
+
return true if @foreign.positive?
|
|
529
|
+
return !CalleeWriteCensus.literal_send?(node) if SENDS.include?(name)
|
|
530
|
+
return false unless OPAQUE_ARGUMENTS.include?(name)
|
|
531
|
+
|
|
532
|
+
# A receiverless `call` is a self-call to a def of that name, which the hand-off walk resolves; the `*_exec`
|
|
533
|
+
# calls run their block whatever the receiver.
|
|
534
|
+
name != :call || !self_receiver?(node)
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# Records each argument of `node` (a call or a `yield`) as written, through any container: the code that
|
|
538
|
+
# receives it is not seen.
|
|
539
|
+
def opaque_arguments(node, depth)
|
|
540
|
+
node.arguments&.arguments&.each do |argument|
|
|
541
|
+
case argument
|
|
542
|
+
when Prism::KeywordHashNode
|
|
543
|
+
argument.elements.each do |element|
|
|
544
|
+
value = element.value
|
|
545
|
+
value.nil? ? anonymous_argument(ANONYMOUS_KEYWORD_REST) : opaque_argument(value, depth, node)
|
|
546
|
+
end
|
|
547
|
+
when Prism::SplatNode
|
|
548
|
+
expression = argument.expression
|
|
549
|
+
expression.nil? ? anonymous_argument(ANONYMOUS_REST) : opaque_argument(expression, depth, node)
|
|
550
|
+
when Prism::ForwardingArgumentsNode then ANONYMOUS_FORWARDED.each { |rest| anonymous_argument(rest) }
|
|
551
|
+
else opaque_argument(argument, depth, node)
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# An anonymous rest (`*`), keyword rest (`**`) or `...` handed on: every argument it carries is written.
|
|
557
|
+
def anonymous_argument(rest) = (@anonymous ||= Set.new) << rest
|
|
558
|
+
|
|
559
|
+
def opaque_argument(value, depth, node)
|
|
560
|
+
CalleeWriteCensus.each_record(sources(value, depth, nil, ID), 2) do |key, _step|
|
|
561
|
+
(@mutations ||= []).push(key, node, true, ANY)
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
private_constant :OpaqueSinks
|
|
566
|
+
|
|
567
|
+
# One pass over a body, collecting alias edges and mutation sites; {#census} closes them.
|
|
568
|
+
class Walk
|
|
569
|
+
include ValueSources
|
|
570
|
+
include BlockBindings
|
|
571
|
+
include Returns
|
|
572
|
+
include OpaqueSinks
|
|
573
|
+
|
|
574
|
+
def initialize(tables, returns: false)
|
|
575
|
+
@mutators = tables.mutators
|
|
576
|
+
@handled = tables.handled
|
|
577
|
+
@fresh = tables.fresh
|
|
578
|
+
@copiers = tables.copiers
|
|
579
|
+
@foreign_calls = tables.foreign_calls
|
|
580
|
+
@returns = returns
|
|
581
|
+
@edges = nil
|
|
582
|
+
@mutations = nil
|
|
583
|
+
@returned = nil
|
|
584
|
+
@anonymous = nil
|
|
585
|
+
@foreign = 0
|
|
586
|
+
@lambdas = 0
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# {ValueSources#sources} of `node` at `depth`, as `node` itself: the flat `key, step` list, or nil.
|
|
590
|
+
def value_sources(node, depth) = sources(node, depth, nil, ID)
|
|
591
|
+
|
|
592
|
+
# A nested def is a scope of its own; a block and a lambda are entered one level deeper. The block of a
|
|
593
|
+
# foreign-self call ({#foreign_block?}) and a lambda are counted as they are entered.
|
|
594
|
+
def visit(node, depth)
|
|
595
|
+
return if node.nil? || node.is_a?(Prism::DefNode)
|
|
596
|
+
|
|
597
|
+
note(node, depth)
|
|
598
|
+
foreign = foreign_block?(node)
|
|
599
|
+
node.rigor_each_child do |child|
|
|
600
|
+
if child.is_a?(Prism::LambdaNode) then visit_counted(child, depth + 1, :@lambdas)
|
|
601
|
+
elsif foreign && child.is_a?(Prism::BlockNode) then visit_counted(child, depth + 1, :@foreign)
|
|
602
|
+
else visit(child, child.is_a?(Prism::BlockNode) ? depth + 1 : depth)
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# Every parameter in `seeds` some mutation reaches.
|
|
608
|
+
def census(seeds, rebinds)
|
|
609
|
+
return EMPTY if @edges.nil? && @mutations.nil? && @returned.nil? && @anonymous.nil?
|
|
610
|
+
|
|
611
|
+
table = close(seeds, rebinds)
|
|
612
|
+
written = written_parameters(table, seeds, rebinds)
|
|
613
|
+
written = returned_parameters(written, table, seeds, rebinds) if @returned
|
|
614
|
+
written = (written || Set.new).merge(@anonymous) if @anonymous
|
|
615
|
+
return EMPTY if written.nil? && table.empty?
|
|
616
|
+
|
|
617
|
+
Census.new(written: written.nil? ? NONE : written.to_a.freeze, alias_table: table.freeze,
|
|
618
|
+
names: alias_names(table))
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
private
|
|
622
|
+
|
|
623
|
+
# The names {Census#aliases} can answer for: every key's name.
|
|
624
|
+
def alias_names(table)
|
|
625
|
+
names = Set.new
|
|
626
|
+
table.each_key { |key| names << key.first }
|
|
627
|
+
names.freeze
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
# The parameters the recorded mutations reach, or nil for none. A record is `key, node, counts_self, mode`.
|
|
631
|
+
def written_parameters(table, seeds, rebinds)
|
|
632
|
+
written = nil
|
|
633
|
+
CalleeWriteCensus.each_record(@mutations, 4) do |key, node, counts_self, mode|
|
|
634
|
+
written = merge_written(written, table[key], mode)
|
|
635
|
+
written = merge_written(written, table[IVAR_ANY_KEY], mode) if key.last == :ivar
|
|
636
|
+
status = CalleeWriteCensus.seed(key, seeds)
|
|
637
|
+
next if status.nil? || !counts_self || !CalleeWriteCensus.reaches?(mode, status)
|
|
638
|
+
|
|
639
|
+
(written ||= Set.new) << key.first unless CalleeWriteCensus.rebound?(rebinds, key.first, node)
|
|
640
|
+
end
|
|
641
|
+
written
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def merge_written(written, by_status, mode)
|
|
645
|
+
by_status&.each do |status, parameters|
|
|
646
|
+
written = (written || Set.new).merge(parameters) if CalleeWriteCensus.reaches?(mode, status)
|
|
647
|
+
end
|
|
648
|
+
written
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# `found` holds the sources flat, `key, step, key, step, ...` ({#sources}); a node that reads none records
|
|
652
|
+
# nothing, so the walk allocates only where a value may carry a parameter.
|
|
653
|
+
def note(node, depth)
|
|
654
|
+
case node
|
|
655
|
+
when Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode
|
|
656
|
+
found = sources(node.value, depth, nil, ID)
|
|
657
|
+
edge(found, node) { [node.name, depth - node.depth] } if found
|
|
658
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode,
|
|
659
|
+
Prism::InstanceVariableAndWriteNode
|
|
660
|
+
found = sources(node.value, depth, nil, ID)
|
|
661
|
+
edge(found, node) { [node.name, :ivar] } if found
|
|
662
|
+
when Prism::ClassVariableWriteNode, Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
|
|
663
|
+
Prism::GlobalVariableWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode
|
|
664
|
+
found = sources(node.value, depth, nil, ID)
|
|
665
|
+
edge(found, node) { [node.name, :global] } if found
|
|
666
|
+
else note_other(node, depth)
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def note_other(node, depth)
|
|
671
|
+
case node
|
|
672
|
+
when Prism::MultiWriteNode
|
|
673
|
+
found = sources(node.value, depth, nil, UNWRAP)
|
|
674
|
+
edge(found, node) { targets(node, depth) } if found
|
|
675
|
+
when Prism::ForNode
|
|
676
|
+
found = sources(node.collection, depth, nil, UNWRAP)
|
|
677
|
+
edge(found, node) { targets(node.index, depth) } if found
|
|
678
|
+
when Prism::CallNode then note_call(node, depth)
|
|
679
|
+
when Prism::IndexOrWriteNode, Prism::IndexAndWriteNode, Prism::IndexOperatorWriteNode, Prism::IndexTargetNode
|
|
680
|
+
mutation(node.receiver, depth, node, nil)
|
|
681
|
+
when Prism::YieldNode then opaque_arguments(node, depth)
|
|
682
|
+
when Prism::ReturnNode then note_return(node, depth)
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def note_call(node, depth)
|
|
687
|
+
name = node.name
|
|
688
|
+
mutation(node.receiver, depth, node, name) if @mutators.include?(name) || OPAQUE.include?(name)
|
|
689
|
+
opaque_arguments(node, depth) if opaque_argument_call?(node)
|
|
690
|
+
note_ivar_set(node, depth) if name == :instance_variable_set && self_receiver?(node)
|
|
691
|
+
case node.block
|
|
692
|
+
when Prism::BlockNode then note_block(node, depth)
|
|
693
|
+
when Prism::BlockArgumentNode then note_symbol_block(node, depth)
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def note_ivar_set(node, depth)
|
|
698
|
+
arguments = node.arguments&.arguments
|
|
699
|
+
return unless arguments && arguments.size >= 2
|
|
700
|
+
|
|
701
|
+
found = sources(arguments[1], depth, nil, ID)
|
|
702
|
+
return if found.nil?
|
|
703
|
+
|
|
704
|
+
target = arguments.first
|
|
705
|
+
name = target.is_a?(Prism::SymbolNode) || target.is_a?(Prism::StringNode) ? target.unescaped.to_sym : nil
|
|
706
|
+
edge(found, node) { name ? [name, :ivar] : IVAR_ANY_KEY }
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
# `[h].each(&:clear)`: the receiver's elements, and so whatever it holds, take the mutator.
|
|
710
|
+
def note_symbol_block(node, depth)
|
|
711
|
+
symbol = node.block.expression
|
|
712
|
+
return unless symbol.is_a?(Prism::SymbolNode) && @mutators.include?(symbol.unescaped.to_sym)
|
|
713
|
+
|
|
714
|
+
CalleeWriteCensus.each_record(sources(node.receiver, depth, nil, ID), 2) do |key, _step|
|
|
715
|
+
(@mutations ||= []).push(key, node, true, ANY)
|
|
716
|
+
end
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def self_receiver?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
720
|
+
|
|
721
|
+
# Records a mutation of whatever `receiver`, any expression, may be built from ({#sources}). The parameter read
|
|
722
|
+
# directly under a name the content scans count (`h << x`, `h[k] = v`; `name` nil for an index write) is theirs.
|
|
723
|
+
def mutation(receiver, depth, node, name)
|
|
724
|
+
found = sources(receiver, depth, nil, ID)
|
|
725
|
+
return if found.nil?
|
|
726
|
+
|
|
727
|
+
counts_self = !receiver.is_a?(Prism::LocalVariableReadNode) || !(name.nil? || @handled.include?(name))
|
|
728
|
+
CalleeWriteCensus.each_record(found, 2) do |key, step|
|
|
729
|
+
(@mutations ||= []).push(key, node, counts_self, step)
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
# Records that the targets the block answers (one key, or a list of keys) take the values `found` names. The
|
|
734
|
+
# edges are kept flat, `targets, found, node, ...`.
|
|
735
|
+
def edge(found, node)
|
|
736
|
+
targets = yield
|
|
737
|
+
(@edges ||= []).push(targets, found, node) unless targets.empty?
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
def targets(node, depth)
|
|
741
|
+
found = []
|
|
742
|
+
Source::NodeWalker.each(node) do |target|
|
|
743
|
+
case target
|
|
744
|
+
when Prism::LocalVariableTargetNode then found << [target.name, depth - target.depth]
|
|
745
|
+
when Prism::InstanceVariableTargetNode then found << [target.name, :ivar]
|
|
746
|
+
when Prism::ClassVariableTargetNode, Prism::GlobalVariableTargetNode then found << [target.name, :global]
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
found
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
# `{ key => { status => Set[parameter] } }` closed over the edges. A read of a parameter itself has its seed's
|
|
753
|
+
# status unless the edge runs after the parameter is rebound. Each source merges straight into its targets'
|
|
754
|
+
# sets, so a pass that adds nothing allocates nothing.
|
|
755
|
+
def close(seeds, rebinds)
|
|
756
|
+
table = {}
|
|
757
|
+
return table if @edges.nil?
|
|
758
|
+
|
|
759
|
+
changed = true
|
|
760
|
+
while changed
|
|
761
|
+
changed = false
|
|
762
|
+
CalleeWriteCensus.each_record(@edges, 3) do |targets, found, node|
|
|
763
|
+
changed = true if close_edge(table, targets, found, node, seeds, rebinds)
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
table
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
# True when an edge's sources added a parameter under one of its targets.
|
|
770
|
+
def close_edge(table, targets, found, node, seeds, rebinds)
|
|
771
|
+
grew = false
|
|
772
|
+
CalleeWriteCensus.each_record(found, 2) do |key, step|
|
|
773
|
+
grew = true if flow(table, targets, step, table[key])
|
|
774
|
+
grew = true if key.last == :ivar && flow(table, targets, step, table[IVAR_ANY_KEY])
|
|
775
|
+
seed = CalleeWriteCensus.seed(key, seeds)
|
|
776
|
+
next if seed.nil? || CalleeWriteCensus.rebound?(rebinds, key.first, node)
|
|
777
|
+
|
|
778
|
+
status = CalleeWriteCensus.apply(step, seed)
|
|
779
|
+
each_target(targets) { |target| grew = true if stored?(table, target, status, key.first) }
|
|
780
|
+
end
|
|
781
|
+
grew
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# Adds what `by_status` (a source's entry, nil: none) holds to each target through `step`. The statuses are read
|
|
785
|
+
# by name rather than by iterating the entry, which may be a target's own and grow meanwhile.
|
|
786
|
+
def flow(table, targets, step, by_status)
|
|
787
|
+
return false if by_status.nil?
|
|
788
|
+
|
|
789
|
+
grew = false
|
|
790
|
+
STATUSES.each do |status|
|
|
791
|
+
parameters = by_status[status]
|
|
792
|
+
next if parameters.nil?
|
|
793
|
+
|
|
794
|
+
reached = CalleeWriteCensus.apply(step, status)
|
|
795
|
+
each_target(targets) { |target| grew = true if stored?(table, target, reached, parameters) }
|
|
796
|
+
end
|
|
797
|
+
grew
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
# Yields each key of `targets`, one key (`[name, level]`, whose first is a Symbol) or a list of keys.
|
|
801
|
+
def each_target(targets, &)
|
|
802
|
+
targets.first.is_a?(Symbol) ? yield(targets) : targets.each(&)
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
# True when `parameters` (a set, or one name) added something under `target` and `status`.
|
|
806
|
+
def stored?(table, target, status, parameters)
|
|
807
|
+
set = ((table[target] ||= {})[status] ||= Set.new)
|
|
808
|
+
size = set.size
|
|
809
|
+
parameters.is_a?(Symbol) ? set << parameters : set.merge(parameters)
|
|
810
|
+
set.size != size
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
end
|
|
814
|
+
end
|
|
815
|
+
end
|