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,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/node_children"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# Issue #2039 — the instance variables an implicit-self or `self.` call's callee visibly assigns a value that may
|
|
10
|
+
# be `nil`, read without a proof. {SelfCallIvarWrites.of} declines for any reason it cannot rule out another
|
|
11
|
+
# write (a `yield` or a call on a parameter in the callee, `include Comparable`, `method_missing`, a reopening);
|
|
12
|
+
# the statement reset (`InterveningIvarReset.widen` under `mark: :unproven`) still leaves a name this answers
|
|
13
|
+
# unmarked, since a callee that writes `@tag = nil` where the program shows it makes the read nil-reportable
|
|
14
|
+
# whatever else it runs.
|
|
15
|
+
#
|
|
16
|
+
# A write counts only when its value can be `nil` by its syntax alone ({.nil_value?}): a `nil` literal, a
|
|
17
|
+
# safe-navigation call, a conditional with a missing arm or an arm that is one of these, an `a || b` whose `b` is
|
|
18
|
+
# one and whose `a` is no truthy literal, or an `a && b` whose `a` is one, or whose `b` is one and whose `a` is
|
|
19
|
+
# not `false`. A method call's result, a parameter or any other computed value does not (`@tag = @tag.strip`
|
|
20
|
+
# rewrites a value the guard proved present), nor does a call past a safe-navigation call (`@peer&.tag.upcase`
|
|
21
|
+
# raises on a `nil` receiver rather than answering `nil`). A multiple assignment counts a target whose own value
|
|
22
|
+
# in a literal list is one of these, or is missing; every target when the value is one of these; every target
|
|
23
|
+
# after the first when the value is a literal Ruby does not splat (`@a, @b = 1`); and a nested target
|
|
24
|
+
# (`(@a, @b), @c = ...`) on the same rule against its own value. A splat, a rest target, or any other value
|
|
25
|
+
# (`@a, @b = pair`, which Ruby splats when it is an Array) counts nothing.
|
|
26
|
+
#
|
|
27
|
+
# The callee is the first `def` of the call's name found on `self`'s class or, failing that, a project entry of
|
|
28
|
+
# its instance chain (`Scope#user_def_for`). An implicit-self or `self.` call in a read body is read the same
|
|
29
|
+
# way (`def reset = clear_tag`), up to {DEF_BUDGET} bodies. A `define_method`, `attr_writer`,
|
|
30
|
+
# `instance_variable_set`, `send` or `method_missing` writer shows no write node and answers nothing, and so
|
|
31
|
+
# does a nil-writer deeper than the budget.
|
|
32
|
+
module DirectIvarWrites
|
|
33
|
+
# Bodies that do not run where they are written, or run on another `self`; a lambda literal's body runs only
|
|
34
|
+
# when it is called.
|
|
35
|
+
NON_RUNNING_NODES = [
|
|
36
|
+
Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::LambdaNode
|
|
37
|
+
].freeze
|
|
38
|
+
# Literals that are never `nil` or `false` and that a multiple assignment does not splat (no Array literal).
|
|
39
|
+
TRUTHY_LITERALS = [
|
|
40
|
+
Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode, Prism::StringNode,
|
|
41
|
+
Prism::InterpolatedStringNode, Prism::XStringNode, Prism::InterpolatedXStringNode, Prism::SymbolNode,
|
|
42
|
+
Prism::InterpolatedSymbolNode, Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode,
|
|
43
|
+
Prism::TrueNode, Prism::HashNode, Prism::RangeNode, Prism::SourceFileNode, Prism::SourceLineNode,
|
|
44
|
+
Prism::SourceEncodingNode
|
|
45
|
+
].freeze
|
|
46
|
+
# Calls by these names leave the method (or may, through a `catch` or a `rescue` above it).
|
|
47
|
+
LEAVING_CALLS = %i[raise fail throw exit exit! abort].freeze
|
|
48
|
+
# The most `def` bodies one read follows.
|
|
49
|
+
DEF_BUDGET = 8
|
|
50
|
+
private_constant :NON_RUNNING_NODES, :TRUTHY_LITERALS, :LEAVING_CALLS, :DEF_BUDGET
|
|
51
|
+
|
|
52
|
+
module_function
|
|
53
|
+
|
|
54
|
+
# The names, as a `Set`, of the instance variables the callee of `call_node` assigns a possibly-nil value in
|
|
55
|
+
# its own body; empty when the callee resolves to no `def` the tables hold.
|
|
56
|
+
def of(call_node, scope)
|
|
57
|
+
names = Set.new
|
|
58
|
+
self_type = scope.self_type
|
|
59
|
+
return names unless self_type.is_a?(Type::Nominal)
|
|
60
|
+
|
|
61
|
+
read_callee(call_node, self_type.class_name.to_s, scope, names, {}.compare_by_identity)
|
|
62
|
+
names
|
|
63
|
+
rescue StandardError
|
|
64
|
+
Set.new
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Whether `node`, a value, can be `nil` by its syntax alone: a `nil` literal, a safe-navigation call, an
|
|
68
|
+
# `if` / `unless` / ternary / `case` with a missing arm or an arm whose value is one of these (a `case ... in`
|
|
69
|
+
# with no `else` raises instead), or an `||` / `&&` that can answer one of these.
|
|
70
|
+
def nil_value?(node)
|
|
71
|
+
case node
|
|
72
|
+
when nil, Prism::NilNode then true
|
|
73
|
+
when Prism::CallNode then node.safe_navigation?
|
|
74
|
+
when Prism::ParenthesesNode then nil_value?(last_value(node.body))
|
|
75
|
+
when Prism::IfNode then nil_value?(last_value(node.statements)) || else_nil?(node.subsequent)
|
|
76
|
+
when Prism::UnlessNode then nil_value?(last_value(node.statements)) || else_nil?(node.else_clause)
|
|
77
|
+
when Prism::CaseNode, Prism::CaseMatchNode then case_nil?(node)
|
|
78
|
+
when Prism::OrNode, Prism::AndNode then logical_nil?(node)
|
|
79
|
+
else false
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# A `case ... in` with no `else` raises NoMatchingPatternError rather than answering `nil`.
|
|
84
|
+
def case_nil?(node)
|
|
85
|
+
return true if node.conditions.any? { |arm| nil_value?(last_value(arm.statements)) }
|
|
86
|
+
return false if node.is_a?(Prism::CaseMatchNode) && node.else_clause.nil?
|
|
87
|
+
|
|
88
|
+
else_nil?(node.else_clause)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# `a || b` answers `b` unless `a` is truthy; `a && b` answers `a` unless `a` is truthy, and `b` otherwise.
|
|
92
|
+
def logical_nil?(node)
|
|
93
|
+
if node.is_a?(Prism::OrNode)
|
|
94
|
+
nil_value?(node.right) && !truthy_literal?(node.left)
|
|
95
|
+
else
|
|
96
|
+
nil_value?(node.left) || (nil_value?(node.right) && !node.left.is_a?(Prism::FalseNode))
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def read_callee(call_node, class_name, scope, names, visited)
|
|
101
|
+
def_node = callee_def(call_node, class_name, scope)
|
|
102
|
+
return if def_node.nil? || visited.key?(def_node) || visited.size >= DEF_BUDGET
|
|
103
|
+
|
|
104
|
+
visited[def_node] = true
|
|
105
|
+
read_body(def_node.body, names) { |call, into| read_callee(call, class_name, scope, into, visited) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Issue #2482 — the straight-line last-write rule. Over the top-level statements of a `def` body, in source
|
|
109
|
+
# order, a `nil` write to `@x` is dropped when a later top-level statement writes `@x` unconditionally (a plain
|
|
110
|
+
# `@x = v`, `@x ||= v` or a multiple-assignment target) with a value that is not `nil` by its syntax alone
|
|
111
|
+
# ({.non_nil_value?}). A value this module merely does not count as `nil` (`@user = User.find_by(id)`) drops
|
|
112
|
+
# nothing: a call's result may be `nil` at run time.
|
|
113
|
+
# Nothing is dropped across a statement that may leave the method first ({.may_leave?}), and a statement that
|
|
114
|
+
# may leave keeps its own `nil` writes. A write nested in a conditional, a loop, a `rescue`, a block or an
|
|
115
|
+
# `&&` / `||` is not unconditional and drops nothing. A body with a `rescue` or `ensure` is read whole.
|
|
116
|
+
def read_body(body, names, &on_self_call)
|
|
117
|
+
statements = straight_line(body)
|
|
118
|
+
return collect(body, names) { |call| on_self_call.call(call, names) } unless statements
|
|
119
|
+
|
|
120
|
+
live = Set.new
|
|
121
|
+
statements.each do |statement|
|
|
122
|
+
own = Set.new
|
|
123
|
+
collect(statement, own) { |call| on_self_call.call(call, own) }
|
|
124
|
+
if may_leave?(statement)
|
|
125
|
+
names.merge(live).merge(own)
|
|
126
|
+
live.clear
|
|
127
|
+
else
|
|
128
|
+
live.subtract(overwritten(statement)).merge(own)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
names.merge(live)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# The top-level statements of a `def` body, seen through a lone parenthesised group (`def r = (a; b)`); nil
|
|
135
|
+
# for a body with a `rescue` / `ensure` clause, which is no straight line.
|
|
136
|
+
def straight_line(body)
|
|
137
|
+
return nil unless body.is_a?(Prism::StatementsNode)
|
|
138
|
+
|
|
139
|
+
only = body.body.first if body.body.size == 1
|
|
140
|
+
return straight_line(only.body) if only.is_a?(Prism::ParenthesesNode) && only.body.is_a?(Prism::StatementsNode)
|
|
141
|
+
|
|
142
|
+
body.body
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The instance variables the top-level `statement` writes unconditionally with a value that is not `nil` by its
|
|
146
|
+
# syntax alone.
|
|
147
|
+
def overwritten(statement)
|
|
148
|
+
case statement
|
|
149
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode
|
|
150
|
+
non_nil_value?(statement.value) ? [statement.name] : []
|
|
151
|
+
when Prism::MultiWriteNode then non_nil_targets(statement, statement.value)
|
|
152
|
+
else []
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The ivar targets of `targets` that `value` binds to a value not `nil` by its syntax alone: each against its own
|
|
157
|
+
# element of an Array literal, or the first against a literal Ruby does not splat. A rest target answers none,
|
|
158
|
+
# since the census declines it ({.destructure_nil_writes}); so does any other value (`@a, @tag = pair`, where
|
|
159
|
+
# `pair` may answer `[1]`).
|
|
160
|
+
def non_nil_targets(targets, value)
|
|
161
|
+
return [] unless targets.rest.nil?
|
|
162
|
+
|
|
163
|
+
elements =
|
|
164
|
+
if value.is_a?(Prism::ArrayNode)
|
|
165
|
+
return [] if value.elements.any?(Prism::SplatNode)
|
|
166
|
+
|
|
167
|
+
value.elements
|
|
168
|
+
elsif truthy_literal?(value) then [value]
|
|
169
|
+
else
|
|
170
|
+
[]
|
|
171
|
+
end
|
|
172
|
+
targets.lefts.zip(elements).flat_map { |target, element| non_nil_target(target, element) }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def non_nil_target(target, value)
|
|
176
|
+
case target
|
|
177
|
+
when Prism::InstanceVariableTargetNode then non_nil_value?(value) ? [target.name] : []
|
|
178
|
+
when Prism::MultiTargetNode then non_nil_targets(target, value)
|
|
179
|
+
else []
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Whether `node` is never `nil` by its syntax alone: a truthy literal, an Array literal or a lambda literal.
|
|
184
|
+
def non_nil_value?(node) = truthy_literal?(node) || node.is_a?(Prism::ArrayNode) || node.is_a?(Prism::LambdaNode)
|
|
185
|
+
|
|
186
|
+
# Whether control may leave the method from inside `node` before its next statement runs: a `return`,
|
|
187
|
+
# `break`, `next`, `redo`, `retry`, `yield` or `super` (which may pass the block on), a call with a block or a
|
|
188
|
+
# `&` argument (whose block may `break` or `throw`), or a `raise` / `throw` / `exit` call. Read through every
|
|
189
|
+
# child, a lambda's body included, so it answers yes rather than miss one.
|
|
190
|
+
def may_leave?(node)
|
|
191
|
+
return false unless node.is_a?(Prism::Node)
|
|
192
|
+
return true if leaving_node?(node)
|
|
193
|
+
|
|
194
|
+
found = false
|
|
195
|
+
node.rigor_each_child { |child| found ||= may_leave?(child) }
|
|
196
|
+
found
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def leaving_node?(node)
|
|
200
|
+
case node
|
|
201
|
+
when Prism::ReturnNode, Prism::BreakNode, Prism::NextNode, Prism::RedoNode, Prism::RetryNode, Prism::YieldNode,
|
|
202
|
+
Prism::SuperNode, Prism::ForwardingSuperNode
|
|
203
|
+
true
|
|
204
|
+
when Prism::CallNode then !node.block.nil? || LEAVING_CALLS.include?(node.name)
|
|
205
|
+
else false
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# The chain fallback passes over an external entry rather than stopping at it: a `def` found past one only
|
|
210
|
+
# keeps a reset unmarked, which reports no more than the reset did before #2039.
|
|
211
|
+
def callee_def(call_node, class_name, scope)
|
|
212
|
+
return nil unless call_node.is_a?(Prism::CallNode)
|
|
213
|
+
|
|
214
|
+
name = call_node.name
|
|
215
|
+
own = def_node(scope.user_def_for(class_name, name))
|
|
216
|
+
return own if own
|
|
217
|
+
|
|
218
|
+
Scope::ResolutionChain.for(scope, class_name, :instance, :methods).search(scope) do |entry|
|
|
219
|
+
entry.external? ? nil : def_node(scope.user_def_for(entry.name, name))
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def def_node(node) = node.is_a?(Prism::DefNode) && node.receiver.nil? ? node : nil
|
|
224
|
+
|
|
225
|
+
def collect(node, names, &on_self_call)
|
|
226
|
+
return unless node.is_a?(Prism::Node)
|
|
227
|
+
return if NON_RUNNING_NODES.any? { |klass| node.is_a?(klass) }
|
|
228
|
+
|
|
229
|
+
nil_writes(node, names)
|
|
230
|
+
on_self_call.call(node) if self_call?(node)
|
|
231
|
+
node.rigor_each_child { |child| collect(child, names, &on_self_call) }
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def self_call?(node)
|
|
235
|
+
node.is_a?(Prism::CallNode) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def nil_writes(node, names)
|
|
239
|
+
case node
|
|
240
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode
|
|
241
|
+
names << node.name if nil_value?(node.value)
|
|
242
|
+
when Prism::MultiWriteNode then multi_nil_writes(node, names)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def multi_nil_writes(node, names) = destructure_nil_writes(node, node.value, names)
|
|
247
|
+
|
|
248
|
+
# The targets of `targets` (a `MultiWriteNode`, or a nested `MultiTargetNode`) that `value` (nil for a missing
|
|
249
|
+
# element) may bind to `nil`: each against its own element of a literal list (a missing element is `nil`),
|
|
250
|
+
# every one when `value` itself may be `nil`, and every one after the first when `value` is a literal Ruby does
|
|
251
|
+
# not splat. A splat, a rest target or a computed value answers nothing, since it may be an Array.
|
|
252
|
+
def destructure_nil_writes(targets, value, names)
|
|
253
|
+
return unless targets.rest.nil?
|
|
254
|
+
|
|
255
|
+
if value.is_a?(Prism::ArrayNode)
|
|
256
|
+
return if value.elements.any?(Prism::SplatNode)
|
|
257
|
+
|
|
258
|
+
targets.lefts.each_with_index { |target, index| target_nil_write(target, value.elements[index], names) }
|
|
259
|
+
elsif nil_value?(value)
|
|
260
|
+
targets.lefts.each { |target| target_nil_write(target, nil, names) }
|
|
261
|
+
elsif truthy_literal?(value)
|
|
262
|
+
first, *others = targets.lefts
|
|
263
|
+
target_nil_write(first, value, names)
|
|
264
|
+
others.each { |target| target_nil_write(target, nil, names) }
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def target_nil_write(target, value, names)
|
|
269
|
+
case target
|
|
270
|
+
when Prism::InstanceVariableTargetNode then names << target.name if nil_value?(value)
|
|
271
|
+
when Prism::MultiTargetNode then destructure_nil_writes(target, value, names)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Whether `node` is a literal that is never `nil` or `false` and is no Array literal: `1 || nil` is never `nil`,
|
|
276
|
+
# and `@a, @b = 1` does not splat its value.
|
|
277
|
+
def truthy_literal?(node) = TRUTHY_LITERALS.any? { |klass| node.is_a?(klass) }
|
|
278
|
+
|
|
279
|
+
def last_value(statements)
|
|
280
|
+
statements.is_a?(Prism::StatementsNode) ? statements.body.last : statements
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Whether an `else` clause (or `elsif`) leaves the conditional's value possibly `nil`: missing, or its value
|
|
284
|
+
# is.
|
|
285
|
+
def else_nil?(clause)
|
|
286
|
+
case clause
|
|
287
|
+
when nil then true
|
|
288
|
+
when Prism::ElseNode then nil_value?(last_value(clause.statements))
|
|
289
|
+
else nil_value?(clause)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
private_class_method :read_callee, :read_body, :straight_line, :overwritten, :non_nil_targets,
|
|
293
|
+
:non_nil_target, :non_nil_value?,
|
|
294
|
+
:may_leave?, :leaving_node?, :callee_def, :def_node, :collect, :self_call?, :nil_writes,
|
|
295
|
+
:case_nil?, :logical_nil?, :multi_nil_writes,
|
|
296
|
+
:destructure_nil_writes,
|
|
297
|
+
:target_nil_write, :truthy_literal?, :last_value, :else_nil?
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
@@ -16,6 +16,11 @@ module Rigor
|
|
|
16
16
|
# with an argument return a sub-ARRAY, not an element, so they are excluded by arity below.
|
|
17
17
|
ELEMENT_READERS = %i[[] first last dig].to_set.freeze
|
|
18
18
|
|
|
19
|
+
# A literal Symbol or String key an `[]` / `dig` step selects, which names one value of a `HashShape`
|
|
20
|
+
# (issue #2363). Against a `Tuple` it selects no statically known position, so it widens every one, as a
|
|
21
|
+
# non-literal index does.
|
|
22
|
+
KeyStep = Data.define(:key)
|
|
23
|
+
|
|
19
24
|
module_function
|
|
20
25
|
|
|
21
26
|
# Widens an element read through an index / `first` / `last` / `dig` chain rooted at a LOCAL,
|
|
@@ -61,7 +66,8 @@ module Rigor
|
|
|
61
66
|
|
|
62
67
|
# `[<local read>, [step, …]]` for a chain of element reads rooted at a local variable, or
|
|
63
68
|
# `nil` when the expression is anything else. A step is an Integer position (negative counts
|
|
64
|
-
# from the end, as Ruby's own indexing does)
|
|
69
|
+
# from the end, as Ruby's own indexing does), a {KeyStep} for a literal Symbol or String key, or `:all`
|
|
70
|
+
# when the index is anything else.
|
|
65
71
|
#
|
|
66
72
|
# Every mutation site's receiver is asked, and nearly every one is a plain variable read, so a
|
|
67
73
|
# receiver that is no call answers before the step list is allocated.
|
|
@@ -104,7 +110,12 @@ module Rigor
|
|
|
104
110
|
# `:all` stands for "some position, not statically known" — every position is widened, which
|
|
105
111
|
# is the only answer that cannot under-approximate.
|
|
106
112
|
def index_step(node)
|
|
107
|
-
|
|
113
|
+
case node
|
|
114
|
+
when Prism::IntegerNode then node.value
|
|
115
|
+
when Prism::SymbolNode then KeyStep.new(node.unescaped.to_sym)
|
|
116
|
+
when Prism::StringNode then KeyStep.new(node.unescaped)
|
|
117
|
+
else :all
|
|
118
|
+
end
|
|
108
119
|
end
|
|
109
120
|
|
|
110
121
|
# The type at `steps` inside `type`, or `nil` when the path does not resolve to a widenable
|
|
@@ -117,6 +128,10 @@ module Rigor
|
|
|
117
128
|
tuple_positions(type, steps.first).lazy
|
|
118
129
|
.filter_map { |i| element_at_path(type.elements[i], steps.drop(1)) }
|
|
119
130
|
.first
|
|
131
|
+
when Type::HashShape
|
|
132
|
+
hash_shape_keys(type, steps.first).lazy
|
|
133
|
+
.filter_map { |key| element_at_path(type.pairs[key], steps.drop(1)) }
|
|
134
|
+
.first
|
|
120
135
|
when Type::Union
|
|
121
136
|
type.members.lazy.filter_map { |member| element_at_path(member, steps) }.first
|
|
122
137
|
end
|
|
@@ -136,6 +151,7 @@ module Rigor
|
|
|
136
151
|
|
|
137
152
|
case type
|
|
138
153
|
when Type::Tuple then widen_tuple_path(type, steps, method_name, arg_types, &element_widening)
|
|
154
|
+
when Type::HashShape then widen_hash_shape_path(type, steps, method_name, arg_types, &element_widening)
|
|
139
155
|
when Type::Union then widen_union_path(type, steps, method_name, arg_types, &element_widening)
|
|
140
156
|
end
|
|
141
157
|
end
|
|
@@ -154,6 +170,25 @@ module Rigor
|
|
|
154
170
|
widened_any ? Type::Tuple.new(elements) : nil
|
|
155
171
|
end
|
|
156
172
|
|
|
173
|
+
# A `HashShape` value reached by key: `h = { k: Set["a"] }; h[:k].add(1)` mutates the Set the shape's `:k`
|
|
174
|
+
# value carries, which widens in place while the keys and their policy survive, as a `Tuple`'s arity does.
|
|
175
|
+
def widen_hash_shape_path(shape, steps, method_name, arg_types, &)
|
|
176
|
+
pairs = shape.pairs.dup
|
|
177
|
+
rest = steps.drop(1)
|
|
178
|
+
widened_any = false
|
|
179
|
+
hash_shape_keys(shape, steps.first).each do |key|
|
|
180
|
+
inner = widen_through_path(pairs[key], rest, method_name, arg_types, &)
|
|
181
|
+
next if inner.nil?
|
|
182
|
+
|
|
183
|
+
pairs[key] = inner
|
|
184
|
+
widened_any = true
|
|
185
|
+
end
|
|
186
|
+
return nil unless widened_any
|
|
187
|
+
|
|
188
|
+
Type::HashShape.new(pairs, required_keys: shape.required_keys, optional_keys: shape.optional_keys,
|
|
189
|
+
read_only_keys: shape.read_only_keys, extra_keys: shape.extra_keys)
|
|
190
|
+
end
|
|
191
|
+
|
|
157
192
|
def widen_union_path(union, steps, method_name, arg_types, &)
|
|
158
193
|
widened_any = false
|
|
159
194
|
members = union.members.map do |member|
|
|
@@ -170,11 +205,22 @@ module Rigor
|
|
|
170
205
|
# position of THIS tuple, so no widening is justified.
|
|
171
206
|
def tuple_positions(tuple, step)
|
|
172
207
|
size = tuple.elements.size
|
|
173
|
-
return (0...size).to_a if step == :all
|
|
208
|
+
return (0...size).to_a if step == :all || step.is_a?(KeyStep)
|
|
174
209
|
|
|
175
210
|
position = step.negative? ? step + size : step
|
|
176
211
|
position.between?(0, size - 1) ? [position] : []
|
|
177
212
|
end
|
|
213
|
+
|
|
214
|
+
# The keys of `shape` one step selects: its literal key when the shape has it, every key for a step that is
|
|
215
|
+
# not statically known. An Integer step is a position (`first` / `last`, or an `[]` that would read a
|
|
216
|
+
# `Tuple`), which names no key here, so it selects nothing and the shape keeps today's reading.
|
|
217
|
+
def hash_shape_keys(shape, step)
|
|
218
|
+
case step
|
|
219
|
+
when :all then shape.pairs.keys
|
|
220
|
+
when KeyStep then shape.pairs.key?(step.key) ? [step.key] : []
|
|
221
|
+
else []
|
|
222
|
+
end
|
|
223
|
+
end
|
|
178
224
|
end
|
|
179
225
|
end
|
|
180
226
|
end
|