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
|
@@ -272,6 +272,8 @@ module Rigor
|
|
|
272
272
|
|
|
273
273
|
def ivar_name?(name) = variable_kind(name) == :ivar
|
|
274
274
|
|
|
275
|
+
def local_name?(name) = variable_kind(name) == :local
|
|
276
|
+
|
|
275
277
|
# Ruby spells a global with a leading `$`, a class variable with `@@`, an instance variable with a single
|
|
276
278
|
# `@`, and a local with none of them.
|
|
277
279
|
def variable_kind(name)
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "anonymous_meta_class"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# Issue #2490 — a block, inside an instance body of a class `K`, whose `self` is provably not an instance of `K`, so
|
|
10
|
+
# an `@x` write directly in it writes some other object's ivar and the class-ivar censuses must not credit it to
|
|
11
|
+
# `K`. Read syntactically, before any type is known, by both the per-file census ({ScopeIndexer}) and the cross-file
|
|
12
|
+
# {IvarWriterCensus}. False positives come first, so a receiver that may hold a `K` instance keeps the credit:
|
|
13
|
+
#
|
|
14
|
+
# - `instance_eval` / `instance_exec` on a receiver that is provably no `K` instance ({.provably_other?}): a
|
|
15
|
+
# constant or `self.class` (a class object), `X.new` / `X.allocate` on a constant `X` not spelling `K`, a
|
|
16
|
+
# literal. Any other receiver (a parameter, a local, `dup`, `itself`) may be a `K` and still counts;
|
|
17
|
+
# - `class_eval` / `module_eval` / `class_exec` / `module_exec` on an explicit receiver: `self` is the receiver
|
|
18
|
+
# module, so `@x` directly in the block is that module object's own ivar;
|
|
19
|
+
# - `define_method` on a constant not spelling `K` (its block runs on that class's instances), and
|
|
20
|
+
# `define_singleton_method` on a class object or a literal;
|
|
21
|
+
# - a `Class.new` / `Module.new` / `Struct.new` / `Data.define` block: `self` is the new class.
|
|
22
|
+
#
|
|
23
|
+
# `class << obj` inside an instance body is one more barred body; the callers bar it themselves, since it is no
|
|
24
|
+
# call. Only a barred body's own statements are dropped: a `define_method` block nested in a `class_eval` or
|
|
25
|
+
# `class << obj` body whose module may be `K` or an instance's singleton class runs on a `K` instance and still
|
|
26
|
+
# counts, and so does a receiver-less `def` nested in either (#2941, {.reentry_blocks}).
|
|
27
|
+
# `def.ivar-write-mismatch`'s collector reads the same rule (#2943, {.census_children}).
|
|
28
|
+
module CensusForeignBlock
|
|
29
|
+
INSTANCE_EVALS = %i[instance_eval instance_exec].to_set.freeze
|
|
30
|
+
MODULE_EVALS = %i[class_eval module_eval class_exec module_exec].to_set.freeze
|
|
31
|
+
DEFINERS = %i[define_method define_singleton_method].to_set.freeze
|
|
32
|
+
SEND_CALLS = %i[send public_send __send__].to_set.freeze
|
|
33
|
+
# The constructors whose receiver names the class of the object they answer.
|
|
34
|
+
CONSTRUCTORS = %i[new allocate].to_set.freeze
|
|
35
|
+
# The literal receivers, none of them an instance of a project class.
|
|
36
|
+
LITERALS = [Prism::StringNode, Prism::InterpolatedStringNode, Prism::SymbolNode, Prism::IntegerNode,
|
|
37
|
+
Prism::FloatNode, Prism::ArrayNode, Prism::HashNode, Prism::RangeNode, Prism::RegularExpressionNode,
|
|
38
|
+
Prism::NilNode, Prism::TrueNode, Prism::FalseNode, Prism::LambdaNode].freeze
|
|
39
|
+
# The calls that answer a module of their receiver.
|
|
40
|
+
MODULE_OF_INSTANCE = %i[class singleton_class].to_set.freeze
|
|
41
|
+
# The constants naming a module every class has among its ancestors, whose methods an instance runs.
|
|
42
|
+
ANCESTOR_ROOTS = %w[Object BasicObject Kernel].to_set.freeze
|
|
43
|
+
# The calls whose answer is foreign when their receiver is: the object itself, a copy, a new instance, its class.
|
|
44
|
+
FOREIGN_PRESERVING = %i[new allocate dup clone itself class singleton_class].to_set.freeze
|
|
45
|
+
# Issue #2983 — the scalar literals, whose blockless calls answer no instance of a project class, unlike a
|
|
46
|
+
# container's (`[k].first`) or a lambda's.
|
|
47
|
+
SCALAR_LITERALS = (LITERALS - [Prism::ArrayNode, Prism::HashNode, Prism::LambdaNode]).freeze
|
|
48
|
+
# A String or Symbol literal that spells a constant path, which a call may resolve to the class
|
|
49
|
+
# (`"K".constantize`).
|
|
50
|
+
CONSTANT_SPELLING = /\A(?:::)?[A-Z]\w*(?:::[A-Z]\w*)*\z/
|
|
51
|
+
# Separates the qualifications of a {.candidate_key}; no constant name holds it.
|
|
52
|
+
CANDIDATE_SEPARATOR = "|"
|
|
53
|
+
# Marks a candidate key whose code runs on the constant's own value's module ({.root_constant}).
|
|
54
|
+
OBJECT_MARK = "@"
|
|
55
|
+
COPIES = %i[dup clone itself].to_set.freeze
|
|
56
|
+
BARRIERS = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
57
|
+
NONE = [].freeze
|
|
58
|
+
# Every name {.foreign?} can answer true for, so any other call returns at one Set probe.
|
|
59
|
+
CALLS = (INSTANCE_EVALS | MODULE_EVALS | DEFINERS | %i[new define]).freeze
|
|
60
|
+
|
|
61
|
+
module_function
|
|
62
|
+
|
|
63
|
+
# Whether `node`'s literal block runs with a `self` that is provably not an instance of `class_name`.
|
|
64
|
+
def foreign?(node, class_name)
|
|
65
|
+
unless node.is_a?(Prism::CallNode) && CALLS.include?(node.name) && node.block.is_a?(Prism::BlockNode)
|
|
66
|
+
return false
|
|
67
|
+
end
|
|
68
|
+
return true if AnonymousMetaClass.block_form_receiver(node)
|
|
69
|
+
|
|
70
|
+
name = node.name
|
|
71
|
+
receiver = node.receiver
|
|
72
|
+
return false if self_node?(receiver)
|
|
73
|
+
return true if MODULE_EVALS.include?(name)
|
|
74
|
+
return provably_other?(receiver, class_name) if INSTANCE_EVALS.include?(name)
|
|
75
|
+
return provably_other?(receiver, class_name) if name == :define_singleton_method
|
|
76
|
+
|
|
77
|
+
foreign_constant?(receiver, class_name)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A receiver that is provably no instance of `class_name`: a constant or `self.class` (a class object, `K`
|
|
81
|
+
# itself included), `X.new` / `X.allocate` on a constant `X` not spelling `class_name`, or a literal.
|
|
82
|
+
def provably_other?(receiver, class_name)
|
|
83
|
+
case receiver
|
|
84
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then true
|
|
85
|
+
when Prism::CallNode
|
|
86
|
+
return receiver.arguments.nil? if receiver.name == :class
|
|
87
|
+
|
|
88
|
+
CONSTRUCTORS.include?(receiver.name) && foreign_constant?(receiver.receiver, class_name)
|
|
89
|
+
else LITERALS.any? { |klass| receiver.is_a?(klass) }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A constant that does not spell `class_name`.
|
|
94
|
+
def foreign_constant?(node, class_name)
|
|
95
|
+
(node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)) && !own_class?(node, class_name)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The children of `node` a census walk for `class_name` still descends into when `node` runs code whose `self`
|
|
99
|
+
# is another object, or nil for an ordinary node: a {.foreign?} call keeps its receiver and arguments, and a
|
|
100
|
+
# `class << obj` body its expression; both keep the code nested in them that runs on a `class_name` instance
|
|
101
|
+
# again ({.reentry_blocks}) unless `reentry` is false, as it is in a singleton-side `def`, whose `self.class`
|
|
102
|
+
# and `singleton_class` are no module of an instance. A reentered `def` comes back as the {Prism::DefNode},
|
|
103
|
+
# which a walk that stops at a `def` skips and the class-ivar census walks into (#2941).
|
|
104
|
+
def census_children(node, class_name, reentry: true)
|
|
105
|
+
case node
|
|
106
|
+
when Prism::SingletonClassNode
|
|
107
|
+
return [node.expression] unless reentry
|
|
108
|
+
|
|
109
|
+
[node.expression, *reentry_blocks(node, class_name)]
|
|
110
|
+
when Prism::CallNode
|
|
111
|
+
return nil unless foreign?(node, class_name)
|
|
112
|
+
return [node.receiver, node.arguments].compact unless reentry
|
|
113
|
+
|
|
114
|
+
[node.receiver, node.arguments, *reentry_blocks(node, class_name)].compact
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The code inside barred code whose `self` may again be an instance of `class_name`, in a `class_eval`-family
|
|
119
|
+
# block whose module may be `class_name` or the singleton class of one of its instances ({.foreign_module?}) or in
|
|
120
|
+
# a `class << obj` body on an operand {.provably_other?} does not name:
|
|
121
|
+
#
|
|
122
|
+
# - a `define_method` block on the implicit or `self` receiver, called directly or through `send` /
|
|
123
|
+
# `public_send` / `__send__` with the literal name;
|
|
124
|
+
# - a receiver-less `def` (#2941), which defines an instance method of that module, or a singleton method of
|
|
125
|
+
# the `class << obj` operand, just as the `define_method` does.
|
|
126
|
+
#
|
|
127
|
+
# Ruby runs each on a `K` instance (a `K` method, or a singleton method of the instance), so its writes are
|
|
128
|
+
# `K`'s; an untyped receiver keeps the credit, as {.foreign?} keeps it for `instance_eval`.
|
|
129
|
+
def reentry_blocks(node, class_name)
|
|
130
|
+
body =
|
|
131
|
+
case node
|
|
132
|
+
when Prism::SingletonClassNode then node.body unless provably_other?(node.expression, class_name)
|
|
133
|
+
when Prism::CallNode
|
|
134
|
+
node.block.body if MODULE_EVALS.include?(node.name) && !foreign_module?(node.receiver, class_name)
|
|
135
|
+
end
|
|
136
|
+
return NONE if body.nil?
|
|
137
|
+
|
|
138
|
+
found = []
|
|
139
|
+
collect_define_method_blocks(body, found)
|
|
140
|
+
found
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# A `class_eval` receiver whose module is provably neither `class_name`, an ancestor of it nor an instance's
|
|
144
|
+
# singleton class, read from inside an instance body. A receiver chain is foreign only when it is:
|
|
145
|
+
#
|
|
146
|
+
# - rooted at a constant other than `class_name`'s own spelling and {ANCESTOR_ROOTS} (`Other.class_eval`,
|
|
147
|
+
# `Other.dup`), a `X.new` or `Class.new` on such a constant among them, or rooted at a literal, through
|
|
148
|
+
# {FOREIGN_PRESERVING} calls only;
|
|
149
|
+
# - `.class` / `.singleton_class` on a receiver {.provably_other?} names (`Other.new.class`, `"s".class`,
|
|
150
|
+
# `self.class.class`);
|
|
151
|
+
# - rooted at a scalar literal through blockless calls of any name (`"s".upcase.class`, #2983), unless the
|
|
152
|
+
# literal is a String or Symbol that spells a constant (`"K".constantize`).
|
|
153
|
+
#
|
|
154
|
+
# A `superclass` call may answer an ancestor (`Other.superclass` is `Object`), a `const_get` or registry call may
|
|
155
|
+
# answer the class (`Other.const_get(:K)`), and every other receiver
|
|
156
|
+
# (`self.class`, `singleton_class`, a parameter's `o.class`, `self.class.base_class`) may name `class_name`, so
|
|
157
|
+
# they keep the credit. Parentheses are looked through. The rule is syntactic: a constant naming another
|
|
158
|
+
# ancestor of the class (`Base.class_eval` with `K < Base`), an alias of it or one of its instances reads as
|
|
159
|
+
# foreign here, and the cross-file census files the barred block's code under that constant instead
|
|
160
|
+
# ({.root_constant}), whose entries the class's census joins (#2983).
|
|
161
|
+
def foreign_module?(receiver, class_name)
|
|
162
|
+
receiver = unwrap(receiver)
|
|
163
|
+
return true if scalar_literal_rooted?(receiver)
|
|
164
|
+
|
|
165
|
+
case receiver
|
|
166
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
167
|
+
foreign_constant?(receiver, class_name) && !ANCESTOR_ROOTS.include?(receiver.slice.delete_prefix("::"))
|
|
168
|
+
when Prism::CallNode then foreign_module_call?(receiver, class_name)
|
|
169
|
+
else LITERALS.any? { |klass| receiver.is_a?(klass) }
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# A call in the chain keeps it foreign only when it answers its receiver's own object, a copy, a new instance
|
|
174
|
+
# or its class ({FOREIGN_PRESERVING}); any other call (`const_get`, a registry's `fetch`, `superclass`) may
|
|
175
|
+
# answer the class, so the receiver may be it.
|
|
176
|
+
def foreign_module_call?(call, class_name)
|
|
177
|
+
if MODULE_OF_INSTANCE.include?(call.name) && call.arguments.nil? &&
|
|
178
|
+
provably_other?(unwrap(call.receiver), class_name)
|
|
179
|
+
return true
|
|
180
|
+
end
|
|
181
|
+
return false unless FOREIGN_PRESERVING.include?(call.name)
|
|
182
|
+
|
|
183
|
+
foreign_module?(call.receiver, class_name)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Issue #2983 — whether `receiver` is a chain of blockless calls rooted at a scalar literal that spells no
|
|
187
|
+
# constant: whatever the calls are, Ruby's methods on such a literal answer no project class or its instance.
|
|
188
|
+
def scalar_literal_rooted?(receiver)
|
|
189
|
+
node = receiver
|
|
190
|
+
while node.is_a?(Prism::CallNode)
|
|
191
|
+
return false if node.block
|
|
192
|
+
|
|
193
|
+
node = unwrap(node.receiver)
|
|
194
|
+
end
|
|
195
|
+
return false if node.equal?(receiver) || SCALAR_LITERALS.none? { |klass| node.is_a?(klass) }
|
|
196
|
+
|
|
197
|
+
!((node.is_a?(Prism::StringNode) || node.is_a?(Prism::SymbolNode)) && CONSTANT_SPELLING.match?(node.unescaped))
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Issue #2983 — the constant a `class_eval` receiver chain is rooted at through {FOREIGN_PRESERVING} calls
|
|
201
|
+
# (`Anc`, `INST.class`, `AliasK.new.class`, `Other.singleton_class`), as its candidate key under `nesting`
|
|
202
|
+
# ({.candidate_key}), or nil. The module such a chain names is the constant's, its class's or its singleton's,
|
|
203
|
+
# so the code a barred block reenters runs on the constant's instances or on the object it holds; the
|
|
204
|
+
# cross-file census files it under the key, and a class whose census joins the constant the key resolves to (an
|
|
205
|
+
# ancestor, an alias, an instance constant of it) sees the write.
|
|
206
|
+
#
|
|
207
|
+
# Review round 2 of #3001 — a `.class` or `.singleton_class` taken of the constant's own value (`INST.class`,
|
|
208
|
+
# `X.singleton_class`, through `dup` / `clone` / `itself`) names the module of that object, which is a class
|
|
209
|
+
# instance only when the constant binds one; the key then carries {OBJECT_MARK}, and the join credits it only
|
|
210
|
+
# when the constant resolves to an instance binding (`INST = K.new`). One taken of a class object (`X.class` of a
|
|
211
|
+
# `.class`, `X.new.class.singleton_class`) names `Class` or a metaclass, whose methods no instance runs: nil.
|
|
212
|
+
def root_constant(receiver, nesting)
|
|
213
|
+
node = unwrap(receiver)
|
|
214
|
+
object = false
|
|
215
|
+
while node.is_a?(Prism::CallNode) && FOREIGN_PRESERVING.include?(node.name)
|
|
216
|
+
inner = unwrap(node.receiver)
|
|
217
|
+
if MODULE_OF_INSTANCE.include?(node.name)
|
|
218
|
+
value = through_copies(inner)
|
|
219
|
+
return if value.is_a?(Prism::CallNode) && MODULE_OF_INSTANCE.include?(value.name)
|
|
220
|
+
|
|
221
|
+
object = true if constant_node?(value)
|
|
222
|
+
end
|
|
223
|
+
node = inner
|
|
224
|
+
end
|
|
225
|
+
return unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
226
|
+
|
|
227
|
+
key = candidate_key(node.slice, nesting)
|
|
228
|
+
object ? "#{OBJECT_MARK}#{key}" : key
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# `node` with `dup`, `clone` and `itself` calls looked through: the object they answer has the same class.
|
|
232
|
+
def through_copies(node)
|
|
233
|
+
node = unwrap(node.receiver) while node.is_a?(Prism::CallNode) && COPIES.include?(node.name)
|
|
234
|
+
node
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def constant_node?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
238
|
+
|
|
239
|
+
# Review round 1 of #3001 — the qualifications a constant spelled `spelling` may have under the lexical
|
|
240
|
+
# `nesting` (the enclosing class and module names), nearest first as Ruby's lookup tries them, joined by
|
|
241
|
+
# {CANDIDATE_SEPARATOR}: `Anc` under `N::K` is `"N::K::Anc|N::Anc|Anc"`. A rooted `::Anc`, or one at the top
|
|
242
|
+
# level, has the one candidate `"Anc"`. `ScopeIndexer::IvarContributions` resolves the key to the first
|
|
243
|
+
# candidate the project declares.
|
|
244
|
+
def candidate_key(spelling, nesting)
|
|
245
|
+
return spelling.delete_prefix("::") if spelling.start_with?("::") || nesting.empty?
|
|
246
|
+
|
|
247
|
+
nesting.size.downto(0).map { |depth| [*nesting.first(depth), spelling].join("::") }.join(CANDIDATE_SEPARATOR)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Review round 3 of #3001 — whether `call` is `Class.new`, `Module.new`, `Struct.new` or `Data.define` on the
|
|
251
|
+
# bare constant, whose answer is a class.
|
|
252
|
+
def meta_new?(call)
|
|
253
|
+
!AnonymousMetaClass::META_NEW_SELECTORS[AnonymousMetaClass.literal_constant_name(call.receiver)].nil?
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Issue #2983 — for a constant write `C = K` or `C = K.new` / `K.allocate` under the lexical `nesting`, the
|
|
257
|
+
# census entry `[:binds_class, key, name, kind]`: `K`'s {.candidate_key}, `C`'s qualified name (a path write's
|
|
258
|
+
# as written) and `:instance` for a constructor or `:alias`, else nil. `C` is an alias or an instance of the
|
|
259
|
+
# class `K` resolves to.
|
|
260
|
+
def binding_entry(node, nesting)
|
|
261
|
+
value = unwrap(node.value)
|
|
262
|
+
instance = value.is_a?(Prism::CallNode) && CONSTRUCTORS.include?(value.name) && value.block.nil?
|
|
263
|
+
# Review round 3 of #3001: `Struct.new(:a)`, `Class.new(Base)` and `Module.new` answer a class, not an instance.
|
|
264
|
+
# A guard beside `IvarContributions.module?`, which drops the same credit once the constant is a declared class.
|
|
265
|
+
return if instance && meta_new?(value)
|
|
266
|
+
|
|
267
|
+
value = value.receiver if instance
|
|
268
|
+
return unless value.is_a?(Prism::ConstantReadNode) || value.is_a?(Prism::ConstantPathNode)
|
|
269
|
+
|
|
270
|
+
name = node.is_a?(Prism::ConstantWriteNode) ? [*nesting, node.name].join("::") : node.target.slice
|
|
271
|
+
[:binds_class, candidate_key(value.slice, nesting), name.delete_prefix("::"),
|
|
272
|
+
instance ? :instance : :alias].freeze
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Issue #2983 — the module a String `class_eval` / `module_eval` may define methods on, for
|
|
276
|
+
# `IvarWriterCensus#record_string_eval`: `owner`, the class owning a class body, for an implicit or `self`
|
|
277
|
+
# receiver there (one in an instance body raises); `instance`, the instance body's class, for a receiver
|
|
278
|
+
# {.foreign_module?} does not bar; otherwise the receiver chain's root constant ({.root_constant}); or nil.
|
|
279
|
+
def string_eval_target(node, instance, owner)
|
|
280
|
+
return (owner unless instance) if self_node?(node.receiver)
|
|
281
|
+
return instance if instance && !foreign_module?(node.receiver, instance)
|
|
282
|
+
|
|
283
|
+
nesting = instance || owner
|
|
284
|
+
root_constant(node.receiver, nesting ? nesting.split("::") : NONE)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# For a barred `class_eval`-family block whose receiver {.foreign_module?} takes as another module and whose
|
|
288
|
+
# chain is rooted at a constant ({.root_constant}), that constant's candidate key and the code in the block that
|
|
289
|
+
# runs on its module's instances, as {.reentry_blocks} collects it; else nil.
|
|
290
|
+
def rooted_reentry(node, class_name)
|
|
291
|
+
return unless MODULE_EVALS.include?(node.name) && foreign_module?(node.receiver, class_name)
|
|
292
|
+
|
|
293
|
+
root = root_constant(node.receiver, class_name.split("::"))
|
|
294
|
+
return if root.nil?
|
|
295
|
+
|
|
296
|
+
found = []
|
|
297
|
+
collect_define_method_blocks(node.block.body, found)
|
|
298
|
+
[root, found]
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# `node` with any parentheses around a single expression looked through.
|
|
302
|
+
def unwrap(node)
|
|
303
|
+
while node.is_a?(Prism::ParenthesesNode)
|
|
304
|
+
body = node.body
|
|
305
|
+
body = body.body.first if body.is_a?(Prism::StatementsNode) && body.body.size == 1
|
|
306
|
+
break if body.nil? || body.is_a?(Prism::StatementsNode)
|
|
307
|
+
|
|
308
|
+
node = body
|
|
309
|
+
end
|
|
310
|
+
node
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def collect_define_method_blocks(node, found)
|
|
314
|
+
return unless node.is_a?(Prism::Node)
|
|
315
|
+
return found << node if node.is_a?(Prism::DefNode) && node.receiver.nil?
|
|
316
|
+
return if BARRIERS.any? { |klass| node.is_a?(klass) }
|
|
317
|
+
|
|
318
|
+
if node.is_a?(Prism::CallNode)
|
|
319
|
+
return found << node.block if define_method_call?(node)
|
|
320
|
+
return if node.block.is_a?(Prism::BlockNode) && CALLS.include?(node.name)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
node.rigor_each_child { |child| collect_define_method_blocks(child, found) }
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# A `define_method` with a literal block on the implicit or `self` receiver, called directly or through a `send`
|
|
327
|
+
# whose first argument is the literal name.
|
|
328
|
+
def define_method_call?(node)
|
|
329
|
+
return false unless node.is_a?(Prism::CallNode) && self_node?(node.receiver)
|
|
330
|
+
return false unless node.block.is_a?(Prism::BlockNode)
|
|
331
|
+
return true if node.name == :define_method
|
|
332
|
+
return false unless SEND_CALLS.include?(node.name)
|
|
333
|
+
|
|
334
|
+
first = node.arguments&.arguments&.first
|
|
335
|
+
(first.is_a?(Prism::SymbolNode) || first.is_a?(Prism::StringNode)) && first.unescaped == "define_method"
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# `self.class`, or a constant spelling `class_name`.
|
|
339
|
+
def own_class?(node, class_name)
|
|
340
|
+
case node
|
|
341
|
+
when Prism::CallNode
|
|
342
|
+
node.name == :class && self_node?(node.receiver) && node.arguments.nil?
|
|
343
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
344
|
+
spelled = node.slice.delete_prefix("::")
|
|
345
|
+
spelled == class_name || spelled == class_name.split("::").last
|
|
346
|
+
else
|
|
347
|
+
false
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def self_node?(node) = node.nil? || node.is_a?(Prism::SelfNode)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
@@ -57,7 +57,7 @@ module Rigor
|
|
|
57
57
|
widened = names.reduce(scope) do |acc, name|
|
|
58
58
|
ivar = name.start_with?("@")
|
|
59
59
|
current = ivar ? scope.ivar(name) : scope.local(name)
|
|
60
|
-
next acc if current.nil? || current
|
|
60
|
+
next acc if current.nil? || Type::Combinator.untyped?(current)
|
|
61
61
|
|
|
62
62
|
ivar ? acc.with_ivar(name, untyped).without_ivar_guard(name) : acc.with_local(name, untyped)
|
|
63
63
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../type"
|
|
4
4
|
require_relative "../reflection"
|
|
5
5
|
require_relative "external_ancestor_resolution"
|
|
6
|
+
require_relative "../memo_audit"
|
|
6
7
|
require_relative "project_method_ownership"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
@@ -185,9 +186,15 @@ module Rigor
|
|
|
185
186
|
return false unless scope.known_user_class?(class_name)
|
|
186
187
|
|
|
187
188
|
by_method = (ProjectMethodOwnership.memo(scope)[:ancestry][class_name] ||= {})
|
|
188
|
-
|
|
189
|
+
if by_method.key?(method_sym)
|
|
190
|
+
return MemoAudit.check("ancestry non-escaping", by_method[method_sym], class_name, method_sym) do
|
|
191
|
+
compute_ancestry_non_escaping?(class_name, method_sym, scope)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
189
194
|
|
|
190
|
-
by_method[method_sym] =
|
|
195
|
+
by_method[method_sym] = MemoAudit.key_audit("ancestry non-escaping") do
|
|
196
|
+
compute_ancestry_non_escaping?(class_name, method_sym, scope)
|
|
197
|
+
end
|
|
191
198
|
end
|
|
192
199
|
|
|
193
200
|
def compute_ancestry_non_escaping?(class_name, method_sym, scope)
|
|
@@ -236,7 +243,7 @@ module Rigor
|
|
|
236
243
|
OBJECT_NON_ESCAPING = %i[tap then yield_self].freeze
|
|
237
244
|
|
|
238
245
|
ENUMERABLE_NON_ESCAPING = %i[
|
|
239
|
-
each map collect flat_map collect_concat
|
|
246
|
+
each reverse_each map collect flat_map collect_concat
|
|
240
247
|
select filter reject filter_map
|
|
241
248
|
find detect find_index find_all
|
|
242
249
|
any? all? none? one? count tally sum
|
|
@@ -248,7 +255,7 @@ module Rigor
|
|
|
248
255
|
zip
|
|
249
256
|
].freeze
|
|
250
257
|
|
|
251
|
-
ARRAY_EXTRA = %i[each_index].freeze
|
|
258
|
+
ARRAY_EXTRA = %i[each_index rfind index rindex].freeze
|
|
252
259
|
HASH_EXTRA = %i[
|
|
253
260
|
each_pair each_key each_value
|
|
254
261
|
transform_keys transform_values
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../source/node_walker"
|
|
5
|
+
require_relative "../source/node_children"
|
|
6
|
+
require_relative "../source/literals"
|
|
7
|
+
require_relative "../typing_state"
|
|
8
|
+
require_relative "../memo_audit"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
module Inference
|
|
12
|
+
# Issue #2555 L3 — which locals of a body may have ESCAPED, so a write to them never carries the `:synthesised`
|
|
13
|
+
# construction fact ({Scope#construction_fact?}).
|
|
14
|
+
#
|
|
15
|
+
# The fact lets the mutation seams re-grow a carrier without the `Dynamic[top]` floor, and the floor existed
|
|
16
|
+
# because a store the seam does not see leaves the carrier stale. A store reaches a local's object unseen only
|
|
17
|
+
# through another holder of it, and almost any position can make one: an alias, a container, an argument (a
|
|
18
|
+
# constructor or an attribute writer keeps it, `Array(a)` and `ident(a)` return it), a call that returns the
|
|
19
|
+
# receiver for some class (`Set#merge`, `a.each`), or a method the project defines. So every reference escapes
|
|
20
|
+
# unless it is in one of these positions:
|
|
21
|
+
#
|
|
22
|
+
# - the receiver of a call on the per-class {READERS} list, which neither returns, keeps nor mutates its receiver;
|
|
23
|
+
# - the receiver of an {ITERATORS} call or of a mutator the seams model ({.mutators_for}), with the call's value
|
|
24
|
+
# discarded;
|
|
25
|
+
# - the body's value position (its last statement, or `return a`), after which nothing in the body runs;
|
|
26
|
+
# - a bare read whose value is discarded.
|
|
27
|
+
#
|
|
28
|
+
# A listed name the project defines anywhere (a reopen, a prepended or included module, a refinement, any
|
|
29
|
+
# class at all) escapes all the same ({.project_defines?}). Every argument position escapes, and so does
|
|
30
|
+
# any reference inside a block or lambda, a write whose value is used (`a = b = …`), and every local of a
|
|
31
|
+
# body that reaches the frame reflectively: a call of `binding`, `eval`, `local_variable_get`,
|
|
32
|
+
# `local_variable_set`, `instance_variable_set` or a string-form `instance_eval` / `class_eval` /
|
|
33
|
+
# `module_eval`, a `send` / `__send__` / `public_send` dispatch whose first argument is one of those names
|
|
34
|
+
# as a literal Symbol or String, or a `TOPLEVEL_BINDING` reference.
|
|
35
|
+
# The scan is flow insensitive and runs once per body, so an escape anywhere drops the fact for the whole body:
|
|
36
|
+
# the conservative direction, since without the fact the seams answer as before. A nested `def` or class body is
|
|
37
|
+
# scanned too, though its locals are its own; a same-named local there can only over-report an escape.
|
|
38
|
+
module ConstructionEscape
|
|
39
|
+
CLASSES = %w[Array Hash Set].freeze
|
|
40
|
+
|
|
41
|
+
# The calls that answer a value other than the receiver, keep no reference to it, and change nothing in it.
|
|
42
|
+
READERS = Ractor.make_shareable(
|
|
43
|
+
{
|
|
44
|
+
"Array" => %i[
|
|
45
|
+
size length count empty? any? none? all? include? first last [] dig fetch index find_index sum min max
|
|
46
|
+
join dup map select filter reject sort sort_by uniq compact flatten reverse take drop to_h to_set ==
|
|
47
|
+
].to_set,
|
|
48
|
+
"Hash" => %i[
|
|
49
|
+
size length count empty? any? include? key? has_key? member? value? has_value? [] dig fetch keys values
|
|
50
|
+
dup map select filter reject to_a ==
|
|
51
|
+
].to_set,
|
|
52
|
+
"Set" => %i[size length count empty? any? include? member? first dup map select filter reject to_a ==].to_set
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# The calls that answer the receiver itself and yield only its members: safe with their value discarded.
|
|
57
|
+
ITERATORS = Ractor.make_shareable(
|
|
58
|
+
{
|
|
59
|
+
"Array" => %i[each each_with_index each_index reverse_each].to_set,
|
|
60
|
+
"Hash" => %i[each each_pair each_key each_value each_with_index].to_set,
|
|
61
|
+
"Set" => %i[each].to_set
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Calls that read or rebind the frame's locals by name: one anywhere in a body escapes every local in it.
|
|
66
|
+
FRAME_CALLS = %i[binding eval local_variable_get local_variable_set instance_variable_set].to_set.freeze
|
|
67
|
+
STRING_EVAL_CALLS = %i[instance_eval class_eval module_eval].to_set.freeze
|
|
68
|
+
# The spellings that reach a {FRAME_CALLS} or {STRING_EVAL_CALLS} name through a literal Symbol or String
|
|
69
|
+
# argument (`send(:eval, …)`, `send("eval", …)`).
|
|
70
|
+
SEND_CALLS = %i[send __send__ public_send].to_set.freeze
|
|
71
|
+
|
|
72
|
+
CLOSURES = [Prism::BlockNode, Prism::LambdaNode].freeze
|
|
73
|
+
LOCAL_REFERENCES = [
|
|
74
|
+
Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode,
|
|
75
|
+
Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode, Prism::LocalVariableTargetNode
|
|
76
|
+
].freeze
|
|
77
|
+
LOCAL_WRITES = [
|
|
78
|
+
Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
|
|
79
|
+
Prism::LocalVariableAndWriteNode
|
|
80
|
+
].freeze
|
|
81
|
+
ALL = :all
|
|
82
|
+
EMPTY_CALLS = Set.new.freeze
|
|
83
|
+
# ADR-129 WD1 — a run-scoped Memo, emptied when a run ends so a long-lived process never serves a verdict from
|
|
84
|
+
# a body an earlier run parsed: keyed by body node, the only input of the scan.
|
|
85
|
+
CACHE = TypingState.memo(
|
|
86
|
+
:construction_escape,
|
|
87
|
+
run_scoped: true, audit: "construction escape",
|
|
88
|
+
reason: "keyed by the body node; the escape scan is a pure function of the body"
|
|
89
|
+
)
|
|
90
|
+
private_constant :CLOSURES, :LOCAL_REFERENCES, :LOCAL_WRITES, :ALL, :EMPTY_CALLS, :CACHE,
|
|
91
|
+
:STRING_EVAL_CALLS, :SEND_CALLS
|
|
92
|
+
|
|
93
|
+
# One body's answer: per name, ALL or the classes it escapes as, and the calls the scan admitted on it.
|
|
94
|
+
Result = Data.define(:escapes, :calls)
|
|
95
|
+
private_constant :Result
|
|
96
|
+
|
|
97
|
+
module_function
|
|
98
|
+
|
|
99
|
+
# True when the local `name`, bound to a carrier of `class_name`, may escape somewhere in `body`, or when
|
|
100
|
+
# `body` is unknown. `scope` answers whether the project defines one of the admitted calls on the class
|
|
101
|
+
# ({.project_defines?}).
|
|
102
|
+
def escaped?(body, name, class_name, scope)
|
|
103
|
+
return true if body.nil? || class_name.nil?
|
|
104
|
+
|
|
105
|
+
result = escape_scan(body)
|
|
106
|
+
return true if result.equal?(ALL)
|
|
107
|
+
|
|
108
|
+
reason = result.escapes[name]
|
|
109
|
+
return true if !reason.nil? && (reason.equal?(ALL) || reason.include?(class_name))
|
|
110
|
+
|
|
111
|
+
result.calls.fetch(name, EMPTY_CALLS).any? { |call| project_defines?(scope, class_name, call) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Whether the project defines a method named `call` anywhere ({Scope#project_method_name?}): on any class or
|
|
115
|
+
# module, instance or singleton side, in a `refine` block, in any file. An admitted call of such a name escapes
|
|
116
|
+
# the local whatever the receiver's class, because a prepended or included module, a refinement or a reopen can
|
|
117
|
+
# put that name on Array, Hash or Set, and telling which owner reaches the receiver is not worth the precision
|
|
118
|
+
# it buys: without the fact the seams answer as before. When no def is found the def-node lookup on the class
|
|
119
|
+
# itself still runs, so an incremental run records the missing `Array#size` and a later reopen re-analyses
|
|
120
|
+
# this file.
|
|
121
|
+
def project_defines?(scope, class_name, call)
|
|
122
|
+
scope.project_method_name?(call) || !scope.user_def_for(class_name, call).nil?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The receiver mutators the seams model for `class_name` ({MutationWidening}, {HashLookupMutation},
|
|
126
|
+
# {SetMutation}).
|
|
127
|
+
def mutators_for(class_name)
|
|
128
|
+
case class_name
|
|
129
|
+
when "Array" then MutationWidening::ARRAY_MUTATORS
|
|
130
|
+
when "Hash" then MutationWidening::HASH_MUTATORS | HashLookupMutation::MUTATORS
|
|
131
|
+
else SetMutation::MUTATORS
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# {.scan} of `body`, memoised for the run.
|
|
136
|
+
def escape_scan(body)
|
|
137
|
+
return scan(body) if MemoAudit.enabled? && MemoAudit.suspended?
|
|
138
|
+
|
|
139
|
+
table = TypingState.current.weak_key_map(CACHE)
|
|
140
|
+
stored = table[body]
|
|
141
|
+
return table[body] = MemoAudit.key_audit("construction escape") { scan(body) } if stored.nil?
|
|
142
|
+
|
|
143
|
+
MemoAudit.check("construction escape", stored, body) { scan(body) }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def scan(body)
|
|
147
|
+
escapes = {}
|
|
148
|
+
calls = {}
|
|
149
|
+
Source::NodeWalker.each_with_ancestors(body) do |node, ancestors|
|
|
150
|
+
return ALL if frame_access?(node)
|
|
151
|
+
next unless LOCAL_REFERENCES.any? { |k| node.is_a?(k) }
|
|
152
|
+
|
|
153
|
+
record(escapes, calls, node, ancestors)
|
|
154
|
+
end
|
|
155
|
+
Result.new(escapes: escapes.freeze, calls: calls.freeze)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def record(escapes, calls, node, ancestors)
|
|
159
|
+
name = node.name
|
|
160
|
+
call = receiver_call(node, ancestors)
|
|
161
|
+
unsafe =
|
|
162
|
+
if call
|
|
163
|
+
unsafe_classes(call, ancestors).tap { |u| (calls[name] ||= Set.new) << call.name if u.size < CLASSES.size }
|
|
164
|
+
else
|
|
165
|
+
escape_reference?(node, ancestors) ? CLASSES : []
|
|
166
|
+
end
|
|
167
|
+
return if unsafe.empty? || escapes[name].equal?(ALL)
|
|
168
|
+
|
|
169
|
+
escapes[name] = unsafe.size == CLASSES.size ? ALL : (escapes[name] || Set.new).merge(unsafe)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# A node that reaches the frame's locals by name: a {FRAME_CALLS} or string-eval call, a `send`-family
|
|
173
|
+
# dispatch on a literal frame-call name (`send(:eval, …)`), or a `TOPLEVEL_BINDING` reference under any
|
|
174
|
+
# qualification (a project's own `X::TOPLEVEL_BINDING` escapes too, costing only precision), whose
|
|
175
|
+
# `local_variable_get` / `local_variable_set` reads and rebinds the top frame.
|
|
176
|
+
def frame_access?(node)
|
|
177
|
+
case node
|
|
178
|
+
when Prism::CallNode then frame_call_name?(node)
|
|
179
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then node.name == :TOPLEVEL_BINDING
|
|
180
|
+
else false
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def frame_call_name?(node)
|
|
185
|
+
name = node.name
|
|
186
|
+
return true if FRAME_CALLS.include?(name)
|
|
187
|
+
return true if STRING_EVAL_CALLS.include?(name) && !node.arguments.nil?
|
|
188
|
+
return false unless SEND_CALLS.include?(name)
|
|
189
|
+
|
|
190
|
+
arguments = node.arguments&.arguments
|
|
191
|
+
return false if arguments.nil? || arguments.empty?
|
|
192
|
+
|
|
193
|
+
dispatched = Source::Literals.symbol_or_string(arguments.first)
|
|
194
|
+
FRAME_CALLS.include?(dispatched) ||
|
|
195
|
+
(STRING_EVAL_CALLS.include?(dispatched) && arguments.size > 1)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# The call a read outside any closure is the receiver of, or nil.
|
|
199
|
+
def receiver_call(node, ancestors)
|
|
200
|
+
return nil unless node.is_a?(Prism::LocalVariableReadNode) && !in_closure?(ancestors)
|
|
201
|
+
|
|
202
|
+
parent = ancestors.last
|
|
203
|
+
parent if parent.is_a?(Prism::CallNode) && parent.receiver.equal?(node)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# The classes for which `call` on the local may hand it on.
|
|
207
|
+
def unsafe_classes(call, ancestors)
|
|
208
|
+
discarded = !value_used?(ancestors, ancestors.size - 2, call)
|
|
209
|
+
CLASSES.reject do |class_name|
|
|
210
|
+
READERS.fetch(class_name).include?(call.name) ||
|
|
211
|
+
(discarded && (ITERATORS.fetch(class_name).include?(call.name) ||
|
|
212
|
+
mutators_for(class_name).include?(call.name)))
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def in_closure?(ancestors)
|
|
217
|
+
ancestors.any? { |a| CLOSURES.any? { |k| a.is_a?(k) } }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Whether a reference that is no call's receiver may hand the object on.
|
|
221
|
+
def escape_reference?(node, ancestors)
|
|
222
|
+
return true if in_closure?(ancestors)
|
|
223
|
+
return value_used?(ancestors, ancestors.size - 1, node) if LOCAL_WRITES.any? { |k| node.is_a?(k) }
|
|
224
|
+
return false unless node.is_a?(Prism::LocalVariableReadNode)
|
|
225
|
+
|
|
226
|
+
parent = ancestors.last
|
|
227
|
+
return false if parent.is_a?(Prism::ArgumentsNode) && ancestors[-2].is_a?(Prism::ReturnNode)
|
|
228
|
+
|
|
229
|
+
!(parent.is_a?(Prism::StatementsNode) && !value_used?(ancestors, ancestors.size - 1, node))
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Whether the value of `node`, whose parent is `ancestors[index]`, reaches anything. The body's own value (its
|
|
233
|
+
# last statement) is the return position, after which nothing in the body runs, so it reads as unused.
|
|
234
|
+
def value_used?(ancestors, index, node)
|
|
235
|
+
while index >= 0
|
|
236
|
+
parent = ancestors[index]
|
|
237
|
+
case parent
|
|
238
|
+
when Prism::StatementsNode
|
|
239
|
+
return false unless parent.body.last.equal?(node)
|
|
240
|
+
when Prism::IfNode, Prism::UnlessNode, Prism::CaseNode, Prism::CaseMatchNode
|
|
241
|
+
return true if parent.predicate.equal?(node)
|
|
242
|
+
when Prism::WhenNode, Prism::InNode
|
|
243
|
+
return true unless parent.statements.equal?(node)
|
|
244
|
+
when Prism::WhileNode, Prism::UntilNode, Prism::EnsureNode
|
|
245
|
+
return !parent.statements.equal?(node)
|
|
246
|
+
when Prism::ElseNode, Prism::BeginNode, Prism::ParenthesesNode, Prism::ProgramNode, Prism::RescueNode
|
|
247
|
+
nil
|
|
248
|
+
else
|
|
249
|
+
return true
|
|
250
|
+
end
|
|
251
|
+
node = parent
|
|
252
|
+
index -= 1
|
|
253
|
+
end
|
|
254
|
+
false
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|