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,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../../source/node_walker"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
module ScopeIndexer
|
|
10
|
+
# Issue #2291 — the class-ivar census types an `@x = LIT` write from the type the constant's own write binds, the
|
|
11
|
+
# same way a method body reads `LIT`. The census runs before the file's in-source constant table is seeded, so
|
|
12
|
+
# without it the write typed under no constant table and a reader over the ivar (`def q = @q`) read `Dynamic[top]`
|
|
13
|
+
# where `def g = LIT` read the constant's type.
|
|
14
|
+
#
|
|
15
|
+
# One instance serves one census walk. The file's constant table is built on the first constant-valued write, so
|
|
16
|
+
# a file with none pays nothing; it is built as {ScopeIndexer#index} builds it later (typed under the census's
|
|
17
|
+
# scope, merged over the scope's project seed, a mutated literal widened), and resolved through the ordinary
|
|
18
|
+
# constant lookup. {#type_of} answers nil, leaving the write's ordinary typing, when:
|
|
19
|
+
#
|
|
20
|
+
# - the value is not a constant read (`LIT`, `A::LIT`, `::LIT`); a path over anything else (`self.class::LIT`)
|
|
21
|
+
# resolves to nothing, as it does in a method body;
|
|
22
|
+
# - the file may rebind a constant of that last segment: a second write in any form, an operator, `||=` or `&&=`
|
|
23
|
+
# write, a multiple-assignment target, or a `const_set` / `remove_const` naming it — or naming anything but a
|
|
24
|
+
# literal, which may be it.
|
|
25
|
+
#
|
|
26
|
+
# A constant another file assigns resolves only through the scope's own project seed, as it did before.
|
|
27
|
+
class ConstantIvarValue
|
|
28
|
+
WRITE_NODES = [Prism::ConstantWriteNode, Prism::ConstantPathWriteNode].freeze
|
|
29
|
+
REBIND_NODES = [Prism::ConstantOperatorWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode,
|
|
30
|
+
Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathOrWriteNode,
|
|
31
|
+
Prism::ConstantPathAndWriteNode, Prism::ConstantTargetNode,
|
|
32
|
+
Prism::ConstantPathTargetNode].freeze
|
|
33
|
+
REBIND_CALLS = %i[const_set remove_const].freeze
|
|
34
|
+
ANY_NAME = :any
|
|
35
|
+
private_constant :WRITE_NODES, :REBIND_NODES, :REBIND_CALLS, :ANY_NAME
|
|
36
|
+
|
|
37
|
+
def initialize(root, scope)
|
|
38
|
+
@root = root
|
|
39
|
+
@scope = scope
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The type `value` reads under the file's constant table, or nil when the write keeps its ordinary typing.
|
|
43
|
+
def type_of(value, scope)
|
|
44
|
+
return nil unless value.is_a?(Prism::ConstantReadNode) || value.is_a?(Prism::ConstantPathNode)
|
|
45
|
+
return nil if rebound?(value.name)
|
|
46
|
+
|
|
47
|
+
scope.with_discovery(scope.discovery.with(in_source_constants: constants)).type_of(value)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def constants
|
|
53
|
+
@constants ||= begin
|
|
54
|
+
literal_mutations = ScopeIndexer.collect_literal_receiver_mutations(@root)
|
|
55
|
+
per_file = ScopeIndexer.widen_mutated_constants(
|
|
56
|
+
ScopeIndexer.build_in_source_constants(@root, @scope), literal_mutations[:constants]
|
|
57
|
+
)
|
|
58
|
+
ScopeIndexer.merge_seeded_constants(@scope.in_source_constants, per_file)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def rebound?(name)
|
|
63
|
+
@rebound ||= rebound_names
|
|
64
|
+
@rebound.include?(ANY_NAME) || @rebound.include?(name)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def rebound_names
|
|
68
|
+
written = Set.new
|
|
69
|
+
rebound = Set.new
|
|
70
|
+
Source::NodeWalker.each(@root) do |node|
|
|
71
|
+
case node
|
|
72
|
+
when *WRITE_NODES
|
|
73
|
+
name = write_name(node)
|
|
74
|
+
rebound << name unless written.add?(name)
|
|
75
|
+
when *REBIND_NODES then rebound << write_name(node)
|
|
76
|
+
when Prism::CallNode then rebound << call_name(node) if REBIND_CALLS.include?(node.name)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
rebound
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def write_name(node) = node.respond_to?(:target) ? node.target.name : node.name
|
|
83
|
+
|
|
84
|
+
def call_name(node)
|
|
85
|
+
argument = node.arguments&.arguments&.first
|
|
86
|
+
return argument.unescaped.to_sym if argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
|
|
87
|
+
|
|
88
|
+
ANY_NAME
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -6,6 +6,7 @@ require_relative "../../reflection"
|
|
|
6
6
|
require_relative "../method_parameter_binder"
|
|
7
7
|
require_relative "../global_write_census"
|
|
8
8
|
require_relative "unseen_ivar_writers"
|
|
9
|
+
require_relative "derived_param_value"
|
|
9
10
|
|
|
10
11
|
module Rigor
|
|
11
12
|
module Inference
|
|
@@ -21,7 +22,13 @@ module Rigor
|
|
|
21
22
|
# every gate, and {EMPTY} otherwise; an absent write keeps the census's ordinary rvalue typing, so a declined
|
|
22
23
|
# case reads exactly what it read before:
|
|
23
24
|
#
|
|
24
|
-
# - the write's value
|
|
25
|
+
# - the write's value reads at least one parameter, and every local it reads is a parameter with a declared type.
|
|
26
|
+
# A bare read (`@x = x`) records that type; an expression over parameters (`@x = x.dup.freeze`,
|
|
27
|
+
# `@x = Array(x)`, `@x = f ? x : y`, issue #2321) records the value typed with each parameter it reads bound to
|
|
28
|
+
# its declared type, unless {DerivedParamValue} declines it (a parameter that holds `untyped` at any depth or
|
|
29
|
+
# has a default outside its declared type, or a call argument whose type may match a narrower overload). A
|
|
30
|
+
# value that holds a block, a lambda or a nested `def` is not credited: their own parameters are locals the
|
|
31
|
+
# scan does not bind;
|
|
25
32
|
# - the write is not inside a block or a lambda, so a block parameter of the same name and a block that runs
|
|
26
33
|
# under another `self` (`instance_eval`, `Class.new`, `define_method`) never borrow the method's parameter;
|
|
27
34
|
# - the method body never writes the parameter, in any form and at any depth (`x = Integer(x)`, `x ||= …`,
|
|
@@ -33,8 +40,8 @@ module Rigor
|
|
|
33
40
|
# - the class's primary RBS declaration is the project's (`sig/` or rbs-inline), not core, stdlib or a gem's:
|
|
34
41
|
# a project reopening of a library class (`Thread::SizedQueue#initialize`) would otherwise borrow the
|
|
35
42
|
# library's parameter type for its own `def`;
|
|
36
|
-
# - the body neither calls `binding` nor passes code
|
|
37
|
-
# `
|
|
43
|
+
# - the body neither calls `binding` nor passes code as an argument to an `eval`-family call
|
|
44
|
+
# (`OPAQUE_EVAL_CALLS`), any of which can rewrite the parameter unseen;
|
|
38
45
|
# - nothing outside the class's own census may write the ivar ({UnseenIvarWriters}): no project ancestor, and
|
|
39
46
|
# no other object's code (`o.instance_variable_set(:@x, v)`);
|
|
40
47
|
# - the declared type is not `untyped`, which is what the write already records.
|
|
@@ -57,8 +64,8 @@ module Rigor
|
|
|
57
64
|
module_function
|
|
58
65
|
|
|
59
66
|
# Runs the block with `scope` as the one whose project tables the unseen-writer and single-file gates read, in
|
|
60
|
-
# place of the census's own scope. `rigor sig-gen` indexes each file
|
|
61
|
-
#
|
|
67
|
+
# place of the census's own scope. `rigor sig-gen` indexes each file over the project tables less the member
|
|
68
|
+
# layouts; this scope carries all of them, so another file's writer, ancestor or reopening is seen.
|
|
62
69
|
def with_writer_scope(scope)
|
|
63
70
|
return yield if scope.nil?
|
|
64
71
|
|
|
@@ -82,13 +89,15 @@ module Rigor
|
|
|
82
89
|
writes = []
|
|
83
90
|
return EMPTY if scan(def_node.body, written, writes, in_block: false) == :opaque
|
|
84
91
|
|
|
85
|
-
|
|
86
|
-
return EMPTY if
|
|
92
|
+
reads = unwritten_reads(writes, written)
|
|
93
|
+
return EMPTY if reads.empty?
|
|
87
94
|
|
|
88
95
|
types = declared_types(def_node, class_name, scope)
|
|
89
96
|
writers = writer_scope(scope)
|
|
90
|
-
credited =
|
|
91
|
-
|
|
97
|
+
credited = reads.each_with_object({}.compare_by_identity) do |(write, names), acc|
|
|
98
|
+
next unless names.all? { |name| types.key?(name) }
|
|
99
|
+
|
|
100
|
+
type = value_type(write.value, names, types, def_node, scope)
|
|
92
101
|
acc[write] = type if type && !UnseenIvarWriters.possible?(writers, class_name, write.name)
|
|
93
102
|
end
|
|
94
103
|
credited.empty? ? EMPTY : credited
|
|
@@ -96,7 +105,45 @@ module Rigor
|
|
|
96
105
|
EMPTY
|
|
97
106
|
end
|
|
98
107
|
|
|
99
|
-
#
|
|
108
|
+
# `{write => names of the locals its value reads}` for each write whose value reads at least one local and no
|
|
109
|
+
# local the body writes.
|
|
110
|
+
def unwritten_reads(writes, written)
|
|
111
|
+
writes.each_with_object({}.compare_by_identity) do |write, acc|
|
|
112
|
+
names = parameter_reads(write.value)
|
|
113
|
+
next if names.nil? || names.empty? || names.any? { |name| written.include?(name) }
|
|
114
|
+
|
|
115
|
+
acc[write] = names
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The names of the locals `value` reads, or nil when it holds a block, a lambda or a scope barrier.
|
|
120
|
+
def parameter_reads(value, names = Set.new)
|
|
121
|
+
return names unless value.is_a?(Prism::Node)
|
|
122
|
+
return nil if BLOCK_NODES.any? { |klass| value.is_a?(klass) }
|
|
123
|
+
return nil if SCOPE_BARRIERS.any? { |klass| value.is_a?(klass) }
|
|
124
|
+
|
|
125
|
+
names << value.name if value.is_a?(Prism::LocalVariableReadNode)
|
|
126
|
+
value.rigor_each_child do |child|
|
|
127
|
+
return nil if parameter_reads(child, names).nil?
|
|
128
|
+
end
|
|
129
|
+
names
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# A bare read takes the declared type itself; an expression over the parameters is typed with each bound to
|
|
133
|
+
# its declared type, unless {DerivedParamValue} declines it.
|
|
134
|
+
def value_type(value, names, types, def_node, scope)
|
|
135
|
+
return types[value.name] if value.is_a?(Prism::LocalVariableReadNode)
|
|
136
|
+
return nil if DerivedParamValue.declined?(names, types, def_node, scope)
|
|
137
|
+
|
|
138
|
+
bound = names.reduce(scope) { |acc, name| acc.with_local(name, types[name]) }
|
|
139
|
+
return nil if DerivedParamValue.overload_sensitive_argument?(value, bound)
|
|
140
|
+
|
|
141
|
+
bound.type_of(value)
|
|
142
|
+
rescue StandardError
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Collects the `@x = …` writes outside every block, and the name of every local the body writes in
|
|
100
147
|
# any form at any depth. A read at depth zero outside a block names the method's own local, which is the
|
|
101
148
|
# parameter when the body never writes the name.
|
|
102
149
|
#
|
|
@@ -113,7 +160,7 @@ module Rigor
|
|
|
113
160
|
when Prism::CallNode
|
|
114
161
|
return :opaque if opaque_call?(node)
|
|
115
162
|
end
|
|
116
|
-
writes << node if !in_block &&
|
|
163
|
+
writes << node if !in_block && node.is_a?(Prism::InstanceVariableWriteNode)
|
|
117
164
|
inner = in_block || BLOCK_NODES.any? { |klass| node.is_a?(klass) }
|
|
118
165
|
node.rigor_each_child do |child|
|
|
119
166
|
return :opaque if scan(child, written, writes, in_block: inner) == :opaque
|
|
@@ -129,10 +176,6 @@ module Rigor
|
|
|
129
176
|
OPAQUE_EVAL_CALLS.include?(node.name) && !(node.arguments&.arguments || []).empty?
|
|
130
177
|
end
|
|
131
178
|
|
|
132
|
-
def local_read_write?(node)
|
|
133
|
-
node.is_a?(Prism::InstanceVariableWriteNode) && node.value.is_a?(Prism::LocalVariableReadNode)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
179
|
# The declared, non-`untyped` type of each parameter, or `{}` when a gate declines the method.
|
|
137
180
|
def declared_types(def_node, class_name, scope)
|
|
138
181
|
environment = scope&.environment
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../acceptance"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
module ScopeIndexer
|
|
10
|
+
# Issue #2321 — the declines {DeclaredParamWrites} applies to a write whose value is an expression over declared
|
|
11
|
+
# parameters, beyond the ones a bare read (`@x = x`) shares. Each says the expression's type, evaluated with the
|
|
12
|
+
# parameters bound to their declared types, may not be the type the ivar holds at runtime:
|
|
13
|
+
#
|
|
14
|
+
# - a parameter whose declared type holds `untyped` at any depth (`Array[untyped]`, #2325): the answer is built
|
|
15
|
+
# on the part the author left untyped;
|
|
16
|
+
# - an optional parameter whose default value's type is not within its declared type
|
|
17
|
+
# (`def initialize(x = nil)` under `(?Integer x)`): the value may run on the default, which the binding does not
|
|
18
|
+
# carry;
|
|
19
|
+
# - an argument of any call in the value (`a + b`, `a + (b > 0 ? b : 0.5)`, `a[i]`, `Array(x)`, `super(…)`,
|
|
20
|
+
# `yield(…)`), typed under the parameter bindings, whose type is anything but a literal or an instance of a
|
|
21
|
+
# class no RBS class names as its superclass: the engine picks the first overload such an argument matches
|
|
22
|
+
# (`Integer#+` given `Integer | Float` answers `Integer`), not the join of every overload it may reach. A
|
|
23
|
+
# receiver dispatches per member and is not declined. A splat or keyword argument types as neither, so it
|
|
24
|
+
# declines too.
|
|
25
|
+
module DerivedParamValue
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# Whether a write over the parameters `names`, declared `types`, is not credited for a parameter it reads.
|
|
29
|
+
def declined?(names, types, def_node, scope)
|
|
30
|
+
names.any? do |name|
|
|
31
|
+
carries_dynamic?(types[name]) || default_outside?(def_node, name, types[name], scope)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Whether a call in `value` takes an argument whose type, under the parameter bindings of `bound`, may match an
|
|
36
|
+
# overload narrower than itself.
|
|
37
|
+
def overload_sensitive_argument?(value, bound)
|
|
38
|
+
return false unless value.is_a?(Prism::Node)
|
|
39
|
+
|
|
40
|
+
if value.is_a?(Prism::CallNode) || value.is_a?(Prism::SuperNode) || value.is_a?(Prism::YieldNode)
|
|
41
|
+
arguments = value.arguments&.arguments || []
|
|
42
|
+
return true if arguments.any? { |argument| overload_sensitive?(bound.type_of(argument), bound) }
|
|
43
|
+
end
|
|
44
|
+
value.rigor_each_child { |child| return true if overload_sensitive_argument?(child, bound) }
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def carries_dynamic?(type)
|
|
49
|
+
case type
|
|
50
|
+
when Type::Dynamic then true
|
|
51
|
+
when Type::Union, Type::Intersection then type.members.any? { |member| carries_dynamic?(member) }
|
|
52
|
+
when Type::Nominal then type.type_args.any? { |arg| carries_dynamic?(arg) }
|
|
53
|
+
when Type::Tuple then type.elements.any? { |element| carries_dynamic?(element) }
|
|
54
|
+
when Type::HashShape then type.pairs.each_value.any? { |pair_value| carries_dynamic?(pair_value) }
|
|
55
|
+
# No RBS parameter spelling yields a refinement over `untyped`; the arm keeps the walk total.
|
|
56
|
+
when Type::Difference, Type::Refined then carries_dynamic?(type.base)
|
|
57
|
+
else false
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Whether an argument of this declared type may match an overload narrower than itself.
|
|
62
|
+
def overload_sensitive?(type, scope)
|
|
63
|
+
case type
|
|
64
|
+
when Type::Constant then false
|
|
65
|
+
when Type::Nominal then !leaf_class?(type.class_name, scope&.environment)
|
|
66
|
+
else true
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def leaf_class?(class_name, environment)
|
|
71
|
+
return false if environment.nil? || environment.rbs_module?(class_name)
|
|
72
|
+
# A guard: a validated RBS signature names no unknown class, so this answers only for a broken environment.
|
|
73
|
+
return false unless environment.class_known?(class_name)
|
|
74
|
+
|
|
75
|
+
superclasses = environment.rbs_loader&.superclass_names
|
|
76
|
+
return false if superclasses.nil? || superclasses.empty?
|
|
77
|
+
|
|
78
|
+
!superclasses.include?(class_name.delete_prefix("::"))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Whether `name` is an optional parameter of `def_node` whose default value's type is not within `declared`.
|
|
82
|
+
def default_outside?(def_node, name, declared, scope)
|
|
83
|
+
default = default_value(def_node.parameters, name)
|
|
84
|
+
return false if default.nil?
|
|
85
|
+
|
|
86
|
+
default_type = scope.type_of(default)
|
|
87
|
+
return true if carries_dynamic?(default_type)
|
|
88
|
+
|
|
89
|
+
!Acceptance.accepts(declared, default_type).yes? && !empty_collection_within?(declared, default_type)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# An empty `[]` or `{}` default is within any `Array[T]` or `Hash[K, V]` the declaration names, which the
|
|
93
|
+
# acceptance check answers only `maybe` (the empty literal has no element type to compare).
|
|
94
|
+
def empty_collection_within?(declared, default_type)
|
|
95
|
+
collection =
|
|
96
|
+
if default_type.is_a?(Type::Tuple) && default_type.elements.empty? then "Array"
|
|
97
|
+
elsif default_type.is_a?(Type::HashShape) && default_type.pairs.empty? then "Hash"
|
|
98
|
+
end
|
|
99
|
+
return false if collection.nil?
|
|
100
|
+
|
|
101
|
+
members = declared.is_a?(Type::Union) ? declared.members : [declared]
|
|
102
|
+
members.any? { |member| member.is_a?(Type::Nominal) && member.class_name == collection }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def default_value(parameters, name)
|
|
106
|
+
candidates = parameters.optionals + parameters.keywords
|
|
107
|
+
candidates.find { |param| param.respond_to?(:value) && param.name == name }&.value
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../../type"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module ScopeIndexer
|
|
9
|
+
# Issue #2499 — the accumulator idiom: a heap slot (an instance, class or global variable, or a constant)
|
|
10
|
+
# seeded with an empty literal and filled later, often from a method, a subclass, a mixin or another file
|
|
11
|
+
# the census cannot see. The seed is never proof that the slot is still empty, so the census records it
|
|
12
|
+
# without its emptiness: `""` and `+""` as `String`, `[]` as `Array[untyped]`, `{}` as
|
|
13
|
+
# `Hash[untyped, untyped]`. `String.new`, `Array.new` and `Hash.new` already type as their nominal.
|
|
14
|
+
#
|
|
15
|
+
# Only the empty spellings count. A non-empty literal (`@order = "asc"`, `MODES = %w[a b]`,
|
|
16
|
+
# `@opts = { a: 1 }`) keeps its exact type: such slots are modes and config tables, and an RBS parameter
|
|
17
|
+
# typed `"asc" | "desc"` must still accept them. A local keeps its flow precision; this applies only where
|
|
18
|
+
# the census seeds a heap slot.
|
|
19
|
+
module EmptySeed
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# `type` with its emptiness dropped when `node` is an empty seed literal, otherwise `type` itself.
|
|
23
|
+
def erase(node, type)
|
|
24
|
+
return type unless empty_seed?(node)
|
|
25
|
+
|
|
26
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
27
|
+
Type::Combinator.union(*members.map { |member| unpinned(member) })
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# `{ name => value node }` for each leading target of a multiple assignment whose right-hand side is a literal
|
|
31
|
+
# list (`@a, @b = {}, []`), so each slot is erased from its own element. Empty for any other right-hand side.
|
|
32
|
+
def multi_write_values(node)
|
|
33
|
+
value = node.value
|
|
34
|
+
return EMPTY_VALUES unless value.is_a?(Prism::ArrayNode)
|
|
35
|
+
|
|
36
|
+
elements = value.elements
|
|
37
|
+
node.lefts.each_with_index.with_object({}) do |(target, index), values|
|
|
38
|
+
break values if index >= elements.size || elements[index].is_a?(Prism::SplatNode)
|
|
39
|
+
next unless target.respond_to?(:name)
|
|
40
|
+
|
|
41
|
+
values[target.name] = elements[index]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
EMPTY_VALUES = {}.freeze
|
|
46
|
+
private_constant :EMPTY_VALUES
|
|
47
|
+
|
|
48
|
+
# A chained write (`@a = @b = ""`) stores the inner write's value in the outer slot too, so it is followed.
|
|
49
|
+
def empty_seed?(node)
|
|
50
|
+
case node
|
|
51
|
+
when Prism::StringNode then node.unescaped.empty?
|
|
52
|
+
when Prism::ArrayNode, Prism::HashNode then node.elements.empty?
|
|
53
|
+
when Prism::CallNode then copied_empty_seed?(node)
|
|
54
|
+
when *CHAINED_WRITES then empty_seed?(node.value)
|
|
55
|
+
else false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
CHAINED_WRITES = [
|
|
60
|
+
Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::ClassVariableWriteNode,
|
|
61
|
+
Prism::GlobalVariableWriteNode, Prism::ConstantWriteNode, Prism::ConstantPathWriteNode
|
|
62
|
+
].freeze
|
|
63
|
+
private_constant :CHAINED_WRITES
|
|
64
|
+
|
|
65
|
+
# `+""`, the unfrozen empty String the accumulator idiom spells under `# frozen_string_literal: true`, and a
|
|
66
|
+
# `.dup` of an empty seed (`"".dup`, `[].dup`, `{}.dup`).
|
|
67
|
+
def copied_empty_seed?(node)
|
|
68
|
+
return false unless node.arguments.nil? && node.block.nil?
|
|
69
|
+
|
|
70
|
+
case node.name
|
|
71
|
+
when :+@ then node.receiver.is_a?(Prism::StringNode) && node.receiver.unescaped.empty?
|
|
72
|
+
when :dup then empty_seed?(node.receiver)
|
|
73
|
+
else false
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def unpinned(member)
|
|
78
|
+
case member
|
|
79
|
+
when Type::Constant
|
|
80
|
+
member.value.is_a?(String) && member.value.empty? ? Type::Combinator.nominal_of("String") : member
|
|
81
|
+
when Type::Tuple
|
|
82
|
+
if member.elements.empty?
|
|
83
|
+
Type::Combinator.nominal_of("Array",
|
|
84
|
+
type_args: [Type::Combinator.untyped])
|
|
85
|
+
else
|
|
86
|
+
member
|
|
87
|
+
end
|
|
88
|
+
when Type::HashShape
|
|
89
|
+
return member unless member.pairs.empty?
|
|
90
|
+
|
|
91
|
+
Type::Combinator.nominal_of("Hash", type_args: [Type::Combinator.untyped, Type::Combinator.untyped])
|
|
92
|
+
else member
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
private_class_method :copied_empty_seed?, :unpinned
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../analysis/dependency_recorder"
|
|
4
|
+
require_relative "../../run_scoped_slots"
|
|
5
|
+
require_relative "../../type"
|
|
6
|
+
require_relative "../refined_dispatch/own_method"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
module ScopeIndexer
|
|
11
|
+
# Issues #2079 and #2170 — the instance-side ivar writes the class-ivar census of one file does not walk, joined
|
|
12
|
+
# into the ivars that census already holds for a class `K`:
|
|
13
|
+
#
|
|
14
|
+
# - the writes another file's reopening of `K` makes;
|
|
15
|
+
# - the writes of every project module and superclass on `K`'s instance resolution chain (`include`,
|
|
16
|
+
# `prepend`, transitively, and `<`), from this file's own census of that ancestor where it declares one
|
|
17
|
+
# (`writes`, taken before the read-before-write `nil` is added, so an ancestor's `initialize` exempts nothing
|
|
18
|
+
# and its read-before-write adds nothing, and only for an ivar an instance body of the ancestor writes in this
|
|
19
|
+
# file) and from every other file's entries for it.
|
|
20
|
+
#
|
|
21
|
+
# Another file's write comes from its `[:own_write_type, owner, ivar, value, path]` census entry
|
|
22
|
+
# ({IvarWriterCensus}): a literal joins as its `Constant`, anything else as `Dynamic[top]`. An ivar the census
|
|
23
|
+
# has no entry for gains none, so a read of an ivar only another file writes still reads `Dynamic[top]`. The
|
|
24
|
+
# join only widens, and every joined type is in the seed, so a `nil` it brings is declaration-sourced (ADR-58
|
|
25
|
+
# WD1). A file the scope names no path for (a census built outside the project pre-pass, `rigor sig-gen`'s
|
|
26
|
+
# unseeded scopes) joins nothing.
|
|
27
|
+
#
|
|
28
|
+
# ADR-46: each ivar read files `own_write_type:<owner>#<ivar>` for `K` and each chain owner, which the
|
|
29
|
+
# incremental session fires when a file's entries for the pair appear, vanish or change value; and the chain
|
|
30
|
+
# files its class edges ({Scope::ResolutionChain#record}), so an `include` added or removed re-checks the reader.
|
|
31
|
+
module IvarContributions
|
|
32
|
+
TABLE_KEY = RunScopedSlots.register(:__rigor_ivar_contributions_table__)
|
|
33
|
+
EMPTY = {}.freeze
|
|
34
|
+
private_constant :TABLE_KEY, :EMPTY
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
def join!(accumulator, writes, scope)
|
|
39
|
+
path = scope.source_path
|
|
40
|
+
return if path.nil?
|
|
41
|
+
|
|
42
|
+
path = canonical(path)
|
|
43
|
+
|
|
44
|
+
table = table(scope.discovered_global_write_census)
|
|
45
|
+
accumulator.each do |class_name, entries|
|
|
46
|
+
next unless class_name.is_a?(String) && !entries.empty?
|
|
47
|
+
|
|
48
|
+
owners = chain_owners(scope, class_name)
|
|
49
|
+
entries.each_key do |ivar|
|
|
50
|
+
joined = contributed(owners, class_name, ivar, table, writes, path)
|
|
51
|
+
entries[ivar] = Type::Combinator.union(entries[ivar], joined) if joined
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# `K` and the project classes and modules on its instance chain, in chain order, with their class edges filed.
|
|
57
|
+
# A module's own census takes no chain: its ancestors' writes land on whatever includes it.
|
|
58
|
+
def chain_owners(scope, class_name)
|
|
59
|
+
return [class_name] if RefinedDispatch::OwnMethod.module_name?(scope, class_name)
|
|
60
|
+
|
|
61
|
+
chain = Scope::ResolutionChain.for(scope, class_name, :instance, :methods)
|
|
62
|
+
# A guard: the class body's own `include` / `<` resolution already files these edges; kept so the read
|
|
63
|
+
# states its own dependency.
|
|
64
|
+
chain.record(scope) if Analysis::DependencyRecorder.active?
|
|
65
|
+
ancestors = chain.entries.filter_map do |entry|
|
|
66
|
+
# An external entry has no name, so `filter_map` drops it. The side check is a guard: an instance chain
|
|
67
|
+
# holds only instance-side entries.
|
|
68
|
+
entry.name if entry.side == :instance && entry.name != class_name
|
|
69
|
+
end
|
|
70
|
+
[class_name, *ancestors.uniq]
|
|
71
|
+
rescue StandardError
|
|
72
|
+
[class_name]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The union of what the owners contribute to `ivar`, or nil for nothing.
|
|
76
|
+
def contributed(owners, class_name, ivar, table, writes, path)
|
|
77
|
+
types = []
|
|
78
|
+
owners.each do |owner|
|
|
79
|
+
if Analysis::DependencyRecorder.active?
|
|
80
|
+
Analysis::DependencyRecorder.read_name(:own_write_type, "#{owner}##{ivar}")
|
|
81
|
+
end
|
|
82
|
+
by_path = table.dig(owner, ivar) || EMPTY
|
|
83
|
+
# This file's census of an ancestor counts only an ivar an instance body of it writes here: the census
|
|
84
|
+
# also holds a `def self.` write of a module (#1958), which runs on the module object.
|
|
85
|
+
own = writes.dig(owner, ivar) if owner != class_name && by_path.key?(path)
|
|
86
|
+
types << own if own
|
|
87
|
+
by_path.each do |value_path, values|
|
|
88
|
+
next if value_path == path
|
|
89
|
+
|
|
90
|
+
values.each { |value| types << value_type(value) }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
types.empty? ? nil : Type::Combinator.union(*types)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A String literal joins as `String`, not its `Constant`: a write elsewhere cannot see an in-place mutation of
|
|
97
|
+
# the String (`@s << "abc"`) in this file, in another or in an ancestor, so the value would be pinned too
|
|
98
|
+
# narrowly. Every other literal kind is immutable and joins as its `Constant`.
|
|
99
|
+
def value_type(value)
|
|
100
|
+
return Type::Combinator.untyped unless value.is_a?(Array)
|
|
101
|
+
|
|
102
|
+
literal = value[1]
|
|
103
|
+
literal.is_a?(String) ? Type::Combinator.nominal_of("String") : Type::Combinator.constant_of(literal)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# `path` resolved through symlinks, so a file the pre-pass tagged under one spelling (`/tmp/…`) is the one
|
|
107
|
+
# analysed under another (`/private/tmp/…`); the expanded spelling when it cannot be resolved.
|
|
108
|
+
def canonical(path)
|
|
109
|
+
File.realpath(path)
|
|
110
|
+
rescue SystemCallError
|
|
111
|
+
File.expand_path(path)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# `{ owner => { ivar => { path => [value, ...] } } }` over `census`'s path-tagged `:own_write_type` entries,
|
|
115
|
+
# built once per census and kept in one run-scoped slot keyed by its identity.
|
|
116
|
+
def table(census)
|
|
117
|
+
slot = Thread.current[TABLE_KEY]
|
|
118
|
+
return slot[1] if slot && slot[0].equal?(census)
|
|
119
|
+
|
|
120
|
+
table = {}
|
|
121
|
+
canonical_paths = {}
|
|
122
|
+
census.each do |entry|
|
|
123
|
+
next unless entry.size == 5 && entry[0] == :own_write_type
|
|
124
|
+
|
|
125
|
+
by_path = ((table[entry[1]] ||= {})[entry[2]] ||= {})
|
|
126
|
+
(by_path[canonical_paths[entry[4]] ||= canonical(entry[4])] ||= []) << entry[3]
|
|
127
|
+
end
|
|
128
|
+
Thread.current[TABLE_KEY] = [census, table]
|
|
129
|
+
table
|
|
130
|
+
end
|
|
131
|
+
private_class_method :chain_owners, :contributed, :value_type, :canonical, :table
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|