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,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "block_call_timing"
|
|
6
|
+
require_relative "closure_escape_analyzer"
|
|
7
|
+
require_relative "project_method_ownership"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Inference
|
|
11
|
+
# Issue #2303 — whether a project method's body may run the block it is called with more than once ON ONE PATH,
|
|
12
|
+
# for the expression-position block-value pass (`ExpressionTyper#block_may_repeat?` through
|
|
13
|
+
# {ProjectYielder.block_value_may_repeat?}). That pass lays the #587 (b) captured binding under a repeating block,
|
|
14
|
+
# which adds values a single run never reads, so a callee read as repeating when it is not reports diagnostics on
|
|
15
|
+
# correct code (`s = "a"; r = either(f) { t = s; s = nil; t }; r.upcase`). The rule here therefore errs toward
|
|
16
|
+
# "runs once", unlike {ProjectYielder.may_run_block_repeatedly?}, whose only asker widens a return type.
|
|
17
|
+
#
|
|
18
|
+
# The walk counts block runs per path, saturating at {MANY}:
|
|
19
|
+
#
|
|
20
|
+
# - a `yield`, or `call` / `yield` / `===` / `[]` on the `&blk` parameter, is one run;
|
|
21
|
+
# - a sequence sums, and a branch (`if` / `unless` / ternary / `case` / `case ... in`) takes the largest arm,
|
|
22
|
+
# so `f ? yield(1) : yield(2)` is one run;
|
|
23
|
+
# - a `return`, `break`, `next` or receiver-less `raise` / `fail` ends its path, so the `yield` after
|
|
24
|
+
# `return yield(1) if k` is a different path from the one before it;
|
|
25
|
+
# - the body of `while` / `until` / `for`, of `Kernel#loop`, or the block of a catalogued core iterator
|
|
26
|
+
# ({ClosureEscapeAnalyzer.iterator_name?}) is {MANY} when a run lies on a path that reaches the next
|
|
27
|
+
# iteration (falling through the body, or a `next`); a run only on a path that leaves it (`return`, `break`,
|
|
28
|
+
# `raise`) adds what that path holds, so `xs.each { |x| return yield(x) if x > 1 }` is one run;
|
|
29
|
+
# - a hand-off of the block (`g(&blk)`, `g(&)`, `...`) is {MANY};
|
|
30
|
+
# - a block under any other call runs its body once (`then { yield }`, `synchronize { yield }`,
|
|
31
|
+
# `File.open(f) { yield f }`), a lambda body is not run at its definition, and a nested `def` is not this
|
|
32
|
+
# method's body.
|
|
33
|
+
#
|
|
34
|
+
# Reading `&blk` other than calling it (`return unless blk`) is no run.
|
|
35
|
+
#
|
|
36
|
+
# A result is `[fall, total, nxt, brk]`: the most runs on a path that falls through the node, on any path
|
|
37
|
+
# through it, on a path that ends at a `next`, and on one that ends at a `break` (nil where no path does).
|
|
38
|
+
module BlockRunCount
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
MANY = 2
|
|
42
|
+
ZERO = [0, 0, nil, nil].freeze
|
|
43
|
+
ALWAYS = [MANY, MANY, nil, nil].freeze
|
|
44
|
+
BLOCK_RUN_CALLS = Set[:call, :yield, :===, :[]].freeze
|
|
45
|
+
LOOP_NODES = [Prism::WhileNode, Prism::UntilNode, Prism::ForNode].freeze
|
|
46
|
+
RAISING_CALLS = Set[:raise, :fail].freeze
|
|
47
|
+
private_constant :ZERO, :ALWAYS, :BLOCK_RUN_CALLS, :LOOP_NODES, :RAISING_CALLS
|
|
48
|
+
|
|
49
|
+
# Memoised per `def` node for the run ({ProjectMethodOwnership.memo}): every call site, and every pass over
|
|
50
|
+
# one, asks the same callee.
|
|
51
|
+
def may_repeat?(def_node, scope)
|
|
52
|
+
memo = ProjectMethodOwnership.memo(scope)[:block_runs]
|
|
53
|
+
return memo[def_node] if memo.key?(def_node)
|
|
54
|
+
|
|
55
|
+
memo[def_node] = runs(def_node.body, def_node.parameters&.block&.name)[1] >= MANY
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def runs(node, block_name)
|
|
59
|
+
case node
|
|
60
|
+
when nil, Prism::DefNode, Prism::LambdaNode then ZERO
|
|
61
|
+
when Prism::YieldNode then run_after(sequence(node.compact_child_nodes, block_name))
|
|
62
|
+
when Prism::ForwardingArgumentsNode then ALWAYS
|
|
63
|
+
when Prism::BlockArgumentNode then block_argument_runs(node, block_name)
|
|
64
|
+
when Prism::IfNode then branch(runs(node.predicate, block_name), [node.statements, node.subsequent], block_name)
|
|
65
|
+
when Prism::UnlessNode
|
|
66
|
+
branch(runs(node.predicate, block_name), [node.statements, node.else_clause], block_name)
|
|
67
|
+
when Prism::CaseNode, Prism::CaseMatchNode then case_runs(node, block_name)
|
|
68
|
+
when Prism::CallNode then call_runs(node, block_name)
|
|
69
|
+
else other_runs(node, block_name)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def other_runs(node, block_name)
|
|
74
|
+
result = sequence(node.compact_child_nodes, block_name)
|
|
75
|
+
case node
|
|
76
|
+
when *LOOP_NODES then iterated(result)
|
|
77
|
+
when Prism::ReturnNode then [nil, result[1], result[2], result[3]]
|
|
78
|
+
when Prism::NextNode then [nil, result[1], larger(result[2], result[0]), result[3]]
|
|
79
|
+
when Prism::BreakNode then [nil, result[1], result[2], larger(result[3], result[0])]
|
|
80
|
+
else result
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# A body run any number of times: {MANY} when a run reaches the next iteration, else the runs of the paths
|
|
85
|
+
# that leave it. It may run zero times or be left by a `break`, and the walk continues after it either way.
|
|
86
|
+
def iterated((fall, total, nxt, brk))
|
|
87
|
+
again = [fall, nxt].compact.max || 0
|
|
88
|
+
return ALWAYS if again.positive?
|
|
89
|
+
|
|
90
|
+
[[0, brk].compact.max, total, nil, nil]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A body run once: a `next` or `break` in it ends the run, and the walk continues after the call.
|
|
94
|
+
def run_once((fall, total, nxt, brk))
|
|
95
|
+
[[fall, nxt, brk].compact.max, total, nil, nil]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def block_argument_runs(node, block_name)
|
|
99
|
+
expression = node.expression
|
|
100
|
+
forwards = expression.nil? || (expression.is_a?(Prism::LocalVariableReadNode) && expression.name == block_name)
|
|
101
|
+
forwards ? ALWAYS : runs(expression, block_name)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def sequence(nodes, block_name)
|
|
105
|
+
nodes.reduce(ZERO) do |acc, child|
|
|
106
|
+
break acc if acc[0].nil?
|
|
107
|
+
|
|
108
|
+
chain(acc, runs(child, block_name))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# `first` then `second`; nothing follows a path that does not fall through `first`.
|
|
113
|
+
def chain(first, second)
|
|
114
|
+
fall, total, nxt, brk = first
|
|
115
|
+
return first if fall.nil?
|
|
116
|
+
|
|
117
|
+
[second[0] && sum(fall, second[0]), [total, sum(fall, second[1])].max,
|
|
118
|
+
larger(nxt, second[2] && sum(fall, second[2])), larger(brk, second[3] && sum(fall, second[3]))]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def run_after(result)
|
|
122
|
+
chain(result, [1, 1, nil, nil])
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# `head` runs first, then exactly one of `arms` (a nil arm is the path that takes none).
|
|
126
|
+
def branch(head, arms, block_name)
|
|
127
|
+
return head if head[0].nil?
|
|
128
|
+
|
|
129
|
+
arms.map { |arm| chain(head, runs(arm, block_name)) }.reduce do |a, b|
|
|
130
|
+
[larger(a[0], b[0]), [a[1], b[1]].max, larger(a[2], b[2]), larger(a[3], b[3])]
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def case_runs(node, block_name)
|
|
135
|
+
clauses = node.conditions
|
|
136
|
+
tests = clauses.flat_map { |clause| clause.is_a?(Prism::WhenNode) ? clause.conditions : [clause.pattern] }
|
|
137
|
+
arms = clauses.map(&:statements) + [node.else_clause]
|
|
138
|
+
branch(sequence([node.predicate, *tests], block_name), arms, block_name)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def call_runs(node, block_name)
|
|
142
|
+
return run_after(runs(node.arguments, block_name)) if block_run_call?(node, block_name)
|
|
143
|
+
|
|
144
|
+
block = node.block
|
|
145
|
+
literal = block.is_a?(Prism::BlockNode)
|
|
146
|
+
head = sequence([node.receiver, node.arguments, (block unless literal)], block_name)
|
|
147
|
+
return [nil, head[1], head[2], head[3]] if raising_call?(node)
|
|
148
|
+
return head unless literal
|
|
149
|
+
|
|
150
|
+
body = runs(block.body, block_name)
|
|
151
|
+
chain(head, repeating_call?(node) ? iterated(body) : run_once(body))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def sum(left, right)
|
|
155
|
+
(left + right).clamp(0, MANY)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def larger(left, right)
|
|
159
|
+
[left, right].compact.max
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def repeating_call?(node)
|
|
163
|
+
ClosureEscapeAnalyzer.iterator_name?(node.name) || BlockCallTiming.kernel_loop?(node)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def raising_call?(node)
|
|
167
|
+
node.receiver.nil? && RAISING_CALLS.include?(node.name)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# `blk.call` and its spellings.
|
|
171
|
+
def block_run_call?(node, block_name)
|
|
172
|
+
receiver = node.receiver
|
|
173
|
+
!block_name.nil? && receiver.is_a?(Prism::LocalVariableReadNode) && receiver.name == block_name &&
|
|
174
|
+
BLOCK_RUN_CALLS.include?(node.name)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -16,7 +16,7 @@ module Rigor
|
|
|
16
16
|
# non-stores) vs memo-key normalization (collapse arg-granularity thrash — a signature whose distinct-key
|
|
17
17
|
# count far exceeds its body-eval savings).
|
|
18
18
|
#
|
|
19
|
-
#
|
|
19
|
+
# The cutoff categories, one per guard site:
|
|
20
20
|
#
|
|
21
21
|
# - {RECURSION_GUARD} — `ExpressionTyper#infer_user_method_return` detected a `(receiver, method)` cycle
|
|
22
22
|
# and returned `Dynamic[top]` (the de-facto recursion-depth budget, effective depth 1).
|
|
@@ -27,6 +27,8 @@ module Rigor
|
|
|
27
27
|
# fuel and fell back to the plain `(receiver, method)` guard (in-cycle call → `Dynamic[top]`).
|
|
28
28
|
# - {RECURSION_FIXPOINT_CAP} — the fixpoint return-summary iteration (ADR-55 slice 2) hit its 3-evaluation
|
|
29
29
|
# cap without converging and collapsed the summary to `untyped` (today's behaviour).
|
|
30
|
+
# - {BLOCK_WRITEBACK_CAP} — the block / loop write-back fixpoint ({BodyFixpoint.converge}) collapsed a
|
|
31
|
+
# local that had not converged to `Dynamic[top]`.
|
|
30
32
|
#
|
|
31
33
|
# Enabled only when `RIGOR_BUDGET_TRACE` is set (to any non-empty value) in the environment, or via
|
|
32
34
|
# {enable!} in tests. When disabled, {hit} is a single boolean check and returns immediately, so normal
|
|
@@ -52,6 +54,10 @@ module Rigor
|
|
|
52
54
|
# `Dynamic[top]` (the escaping-block floor). Shared by slice B's loop-body fixpoint and by slice C's
|
|
53
55
|
# content-evidence fixpoint, where the collapsed name is one element / key / value slot of a collection join.
|
|
54
56
|
BLOCK_WRITEBACK_CAP = :block_writeback_cap
|
|
57
|
+
# `CalleeMutationChain` discovered more than {CalleeMutationChain::MAX_NODES} nodes from one parameter and took
|
|
58
|
+
# the rest as mutating (the callee floor's FP-safe cut). A label: it may vary with the order, the floor does not
|
|
59
|
+
# (ADR-127).
|
|
60
|
+
CALLEE_CHAIN_CAP = :callee_chain_cap
|
|
55
61
|
|
|
56
62
|
# ADR-57 return-memo profile counters (not cutoffs — see the module doc). All bumped by
|
|
57
63
|
# `ExpressionTyper#infer_user_method_return` / `#compute_user_method_return`.
|
|
@@ -82,7 +88,7 @@ module Rigor
|
|
|
82
88
|
|
|
83
89
|
CATEGORIES = [
|
|
84
90
|
RECURSION_GUARD, ANCESTOR_WALK_LIMIT, HKT_FUEL_EXHAUSTED, RECURSION_UNROLL_FUEL,
|
|
85
|
-
RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP,
|
|
91
|
+
RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP, CALLEE_CHAIN_CAP,
|
|
86
92
|
MEMO_ENTRIES, MEMO_HITS, MEMO_MISSES, MEMO_BODY_EVALS,
|
|
87
93
|
MEMO_REFUSE_ON_STACK, MEMO_REFUSE_UNROLL, MEMO_REFUSE_TRANSIENT
|
|
88
94
|
].freeze
|
|
@@ -9,8 +9,8 @@ module Rigor
|
|
|
9
9
|
#
|
|
10
10
|
# Exception is the base of every Ruby error class (RuntimeError, StandardError, KeyError, …). The
|
|
11
11
|
# Init_Exception block in `references/ruby/error.c` registers the entire hierarchy in one pass, so
|
|
12
|
-
# the YAML carries 27 classes — but only the base `Exception` row is wired into `CATALOG_BY_CLASS
|
|
13
|
-
#
|
|
12
|
+
# the YAML carries 27 classes — but only the base `Exception` row is wired into `CATALOG_BY_CLASS`.
|
|
13
|
+
# A `RuntimeError` receiver hits the Exception arm via `is_a?(Exception)` and the
|
|
14
14
|
# catalog answers with the base-class entries; subclass-specific methods (`KeyError#receiver`,
|
|
15
15
|
# `NameError#name`, …) intentionally miss the lookup until a later slice routes per-subclass
|
|
16
16
|
# class_names.
|
|
@@ -16,11 +16,8 @@ module Rigor
|
|
|
16
16
|
#
|
|
17
17
|
# No mutation blocklist is needed. The numeric classes expose no foldable bang or indirect-mutator
|
|
18
18
|
# method that the static C classifier mis-attributes (every `:leaf` numeric method is a pure value
|
|
19
|
-
# computation), so the generic `MethodCatalog` loader
|
|
20
|
-
#
|
|
21
|
-
# `method_entry` / `load_catalog` copy of `MethodCatalog`; folding it onto the shared loader also
|
|
22
|
-
# picks up alias resolution (e.g. `Integer#magnitude` → `abs`, `Integer#inspect` → `to_s`), which the
|
|
23
|
-
# old bespoke loader silently dropped.
|
|
19
|
+
# computation), so the generic `MethodCatalog` loader covers it directly. Using the shared loader
|
|
20
|
+
# rather than a bespoke copy also gives it alias resolution (e.g. `Integer#magnitude` → `abs`).
|
|
24
21
|
NUMERIC_CATALOG = MethodCatalog.for_topic("numeric")
|
|
25
22
|
end
|
|
26
23
|
end
|
|
@@ -20,8 +20,8 @@ module Rigor
|
|
|
20
20
|
mutating_selectors: {
|
|
21
21
|
"Random" => Set[
|
|
22
22
|
# `rand_random` -> `random_real` / `random_ulong_limited` advance the MT state on the receiver
|
|
23
|
-
# (instance #rand) and on
|
|
24
|
-
# indirect mutator.
|
|
23
|
+
# (instance #rand) and on the process-wide default generator (singleton .rand). The classifier
|
|
24
|
+
# misses the indirect mutator.
|
|
25
25
|
:rand,
|
|
26
26
|
# `random_bytes` / `random_s_bytes` consume MT output the same way #rand does — every call
|
|
27
27
|
# mutates the underlying generator.
|
|
@@ -8,107 +8,118 @@ module Rigor
|
|
|
8
8
|
#
|
|
9
9
|
# This module is the closure over a parameter-flow graph, independent of how the graph is read. `own` answers a
|
|
10
10
|
# def's own census, `{ name => slot }` (a slot is a positional index or a keyword symbol); `edges` answers the
|
|
11
|
-
# {Edge}s along which a def hands one of its parameters to a resolved callee. A node is one parameter of one def
|
|
12
|
-
# and the walk follows only the edges that hand on the parameter the node is, so a def's
|
|
13
|
-
#
|
|
14
|
-
# least fixpoint, so a recursive or mutually recursive chain terminates and a cycle with no mutation on it mutates
|
|
15
|
-
# nothing.
|
|
11
|
+
# {Edge}s along which a def hands one of its parameters to a resolved callee. A node is one parameter of one def
|
|
12
|
+
# under one owner context, and the walk follows only the edges that hand on the parameter the node is, so a def's
|
|
13
|
+
# hand-offs of its OTHER parameters cost nothing.
|
|
16
14
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# the walk
|
|
15
|
+
# ADR-127 — the answer is a property of that graph, not of a walk. A parameter `r` is mutated iff an own-mutated
|
|
16
|
+
# node is reachable from `r` through nodes that are not themselves own-mutated, or more than {MAX_NODES} distinct
|
|
17
|
+
# nodes are reachable from `r` (a cut: the walk did not see them, and a literal shape kept on evidence it never saw
|
|
18
|
+
# reports a false positive on correct code). Nothing a walk concludes is kept, so what one walk did cannot change
|
|
19
|
+
# another's answer; only graph facts are persisted (the successor `store`). Which label an answer carries (an
|
|
20
|
+
# exact one, or a cut and its {BudgetTrace::CALLEE_CHAIN_CAP} hit) may vary with the order, the floor does not.
|
|
20
21
|
module CalleeMutationChain
|
|
21
22
|
# `name` and `slot` are the forwarding def's parameter, `callee` the def it is handed to, and `callee_names`
|
|
22
|
-
# the callee's parameters it may bind there.
|
|
23
|
-
|
|
23
|
+
# the callee's parameters it may bind there. Issue #2515 — `owner` is the evaluator the callee's own census and
|
|
24
|
+
# hand-offs resolve in when the hand-off crosses classes (`Help.c(p, o)`, `Helper.new.c(p, o)`), nil for a
|
|
25
|
+
# self-call, which stays in the evaluator the hop started in. `approx` is true when the hand-off resolved to more
|
|
26
|
+
# than one candidate, or by name; the floor it implies is an over-approximation, not proof the callee writes.
|
|
27
|
+
Edge = Data.define(:name, :slot, :callee, :callee_names, :owner, :approx) do
|
|
28
|
+
def initialize(name:, slot:, callee:, callee_names:, owner: nil, approx: false) = super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# ADR-127 — the most distinct nodes one parameter may reach before the walk is cut and reads as mutated.
|
|
32
|
+
MAX_NODES = 64
|
|
24
33
|
|
|
25
|
-
|
|
34
|
+
NO_TRIPLES = [].freeze
|
|
35
|
+
private_constant :NO_TRIPLES
|
|
26
36
|
|
|
27
37
|
module_function
|
|
28
38
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
# `own` and `edges` take the def and the owner evaluator its hop resolves in (nil: the closure's own). `store`
|
|
40
|
+
# (nil: none) is the run's successor store, `{ def => { owner_key => { name => triples } } }`, and `ctx` the key
|
|
41
|
+
# the nil owner is stored under (the side and class the closure runs in).
|
|
42
|
+
def close(root, own:, edges:, store: nil, ctx: nil)
|
|
43
|
+
census = own.call(root, nil)
|
|
44
|
+
handed = edges.call(root, nil)
|
|
32
45
|
return census if handed.empty?
|
|
33
46
|
|
|
34
|
-
|
|
35
|
-
handed.each
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# The parameter-flow graph one closure reads: per def, per parameter name, whether the node is mutated and the
|
|
43
|
-
# nodes it hands itself on to.
|
|
44
|
-
class Flow
|
|
45
|
-
Node = Struct.new(:mutated, :successors)
|
|
47
|
+
result = nil
|
|
48
|
+
handed.each do |edge|
|
|
49
|
+
name = edge.name
|
|
50
|
+
next if census.key?(name) || result&.key?(name)
|
|
51
|
+
next unless reaches_mutation?(root, name, own, edges, store, ctx)
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
@own = own
|
|
49
|
-
@edges = edges
|
|
50
|
-
@nodes = {}.compare_by_identity
|
|
51
|
-
@count = 0
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def mutated?(def_node, name)
|
|
55
|
-
node = @nodes.dig(def_node, name)
|
|
56
|
-
!node.nil? && node.mutated
|
|
53
|
+
(result ||= census.dup)[name] = edge.slot
|
|
57
54
|
end
|
|
55
|
+
result || census
|
|
56
|
+
end
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
# Breadth first from `root`'s `name`, over a walk-local `seen`. A node counts when it is first discovered and its
|
|
59
|
+
# own mutation is checked then, so more than {MAX_NODES} discovered nodes is decided without computing the
|
|
60
|
+
# frontier's edges. The first own-mutated node proves the parameter mutated; it is reachable through nodes that
|
|
61
|
+
# are not own-mutated by construction, since those are the only ones expanded.
|
|
62
|
+
def reaches_mutation?(root, name, own, edges, store, ctx)
|
|
63
|
+
seen = {}.compare_by_identity
|
|
64
|
+
mark(seen, root, name, nil)
|
|
65
|
+
queue = [root, name, nil]
|
|
66
|
+
discovered = 1
|
|
67
|
+
i = 0
|
|
68
|
+
while i < queue.size
|
|
69
|
+
triples = successors(queue[i], queue[i + 1], queue[i + 2], edges, store, ctx)
|
|
70
|
+
i += 3
|
|
71
|
+
j = 0
|
|
72
|
+
while j < triples.size
|
|
73
|
+
callee = triples[j]
|
|
74
|
+
callee_name = triples[j + 1]
|
|
75
|
+
owner = triples[j + 2]
|
|
76
|
+
j += 3
|
|
77
|
+
next unless mark(seen, callee, callee_name, owner)
|
|
78
|
+
|
|
79
|
+
discovered += 1
|
|
80
|
+
if discovered > MAX_NODES
|
|
81
|
+
BudgetTrace.hit(BudgetTrace::CALLEE_CHAIN_CAP)
|
|
82
|
+
return true
|
|
83
|
+
end
|
|
84
|
+
return true if own.call(callee, owner).key?(callee_name)
|
|
66
85
|
|
|
67
|
-
queue.
|
|
86
|
+
queue.push(callee, callee_name, owner)
|
|
68
87
|
end
|
|
69
88
|
end
|
|
89
|
+
false
|
|
90
|
+
end
|
|
70
91
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
each_node do |node|
|
|
76
|
-
next if node.mutated
|
|
77
|
-
next unless node.successors.any? { |callee, name| mutated?(callee, name) }
|
|
78
|
-
|
|
79
|
-
node.mutated = true
|
|
80
|
-
changed = true
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
92
|
+
# True when the node was not yet in the walk-local `seen`, which now holds it.
|
|
93
|
+
def mark(seen, def_node, name, owner)
|
|
94
|
+
by_name = ((seen[def_node] ||= {})[owner] ||= {})
|
|
95
|
+
return false if by_name.key?(name)
|
|
84
96
|
|
|
85
|
-
|
|
97
|
+
by_name[name] = true
|
|
98
|
+
end
|
|
86
99
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
# The flat `[callee, name, owner, ...]` triples the node `(def_node, name, owner)` hands itself to: a graph fact,
|
|
101
|
+
# persisted in `store` when there is one. A node its def mutates is never asked.
|
|
102
|
+
def successors(def_node, name, owner, edges, store, ctx)
|
|
103
|
+
return compute_successors(def_node, name, owner, edges) if store.nil?
|
|
91
104
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
(@nodes[def_node] ||= {})[name] = Node.new(mutated, successors)
|
|
98
|
-
successors
|
|
105
|
+
by_name = ((store[def_node] ||= {})[owner.nil? ? ctx : owner] ||= {})
|
|
106
|
+
if by_name.key?(name)
|
|
107
|
+
return MemoAudit.check("successor", by_name[name], name, owner) do
|
|
108
|
+
compute_successors(def_node, name, owner, edges)
|
|
109
|
+
end
|
|
99
110
|
end
|
|
100
111
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
next unless edge.name == name
|
|
112
|
+
by_name[name] = compute_successors(def_node, name, owner, edges)
|
|
113
|
+
end
|
|
104
114
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
def compute_successors(def_node, name, owner, edges)
|
|
116
|
+
triples = nil
|
|
117
|
+
edges.call(def_node, owner).each do |edge|
|
|
118
|
+
next unless edge.name == name
|
|
108
119
|
|
|
109
|
-
|
|
110
|
-
@nodes.each_value { |by_name| by_name.each_value(&) }
|
|
120
|
+
edge.callee_names.each { |callee_name| (triples ||= []).push(edge.callee, callee_name, edge.owner || owner) }
|
|
111
121
|
end
|
|
122
|
+
triples.nil? ? NO_TRIPLES : triples.freeze
|
|
112
123
|
end
|
|
113
124
|
end
|
|
114
125
|
end
|