rigortype 0.3.8 → 0.4.0
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/capability_roles/capability_roles.rbs +36 -0
- data/data/core_overlay/enumerable.rbs +51 -0
- data/data/core_overlay/enumerator.rbs +84 -0
- data/data/core_overlay/hash_rbs3.rbs +41 -0
- data/data/core_overlay/process.rbs +40 -0
- data/data/core_overlay/string_io.rbs +33 -0
- data/data/effects/core.yml +3 -3
- data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
- data/docs/handbook/02-everyday-types.md +17 -14
- data/docs/handbook/03-narrowing.md +132 -14
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/05-methods-and-blocks.md +1 -1
- data/docs/handbook/06-classes.md +2 -2
- data/docs/handbook/07-rbs-and-extended.md +19 -1
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +525 -13
- data/docs/handbook/README.md +1 -1
- data/docs/handbook/appendix-elixir.md +2 -2
- data/docs/handbook/appendix-go.md +2 -2
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +2 -2
- data/docs/handbook/appendix-phpstan.md +1 -1
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +4 -4
- data/docs/handbook/appendix-typescript.md +1 -1
- data/docs/llms.txt +2 -0
- data/docs/manual/02-cli-reference.md +95 -6
- data/docs/manual/03-configuration.md +18 -0
- data/docs/manual/04-diagnostics.md +15 -0
- data/docs/manual/07-plugins.md +15 -5
- data/docs/manual/08-skills.md +23 -2
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/11-ci.md +9 -0
- data/docs/manual/15-type-protection-coverage.md +8 -0
- data/docs/manual/16-rbs-extended-annotations.md +46 -8
- data/docs/manual/18-removing-dead-code.md +10 -8
- data/docs/manual/19-effect-labels.md +10 -0
- data/docs/manual/plugins/README.md +7 -0
- data/docs/manual/plugins/rigor-actioncable.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +7 -0
- data/docs/manual/plugins/rigor-actionpack.md +261 -0
- data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
- data/docs/manual/plugins/rigor-activejob.md +7 -0
- data/docs/manual/plugins/rigor-activerecord.md +138 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
- data/docs/manual/plugins/rigor-grape.md +106 -0
- data/docs/manual/plugins/rigor-graphql.md +23 -2
- data/docs/manual/plugins/rigor-pundit.md +8 -1
- data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
- data/docs/manual/plugins/rigor-rails-routes.md +7 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
- data/docs/manual/plugins/rigor-sidekiq.md +8 -1
- data/docs/manual/plugins/rigor-sorbet.md +21 -5
- data/exe/rigor +19 -4
- data/lib/rigor/analysis/baseline.rb +3 -3
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
- data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
- data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
- data/lib/rigor/analysis/check_rules.rb +718 -79
- data/lib/rigor/analysis/crash_signature.rb +8 -24
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
- data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
- data/lib/rigor/analysis/erb_template_detector.rb +1 -2
- data/lib/rigor/analysis/fact_store.rb +15 -1
- data/lib/rigor/analysis/incremental.rb +42 -0
- data/lib/rigor/analysis/incremental_session.rb +117 -26
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/graph.rb +3 -5
- data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
- data/lib/rigor/analysis/reachability/project_files.rb +2 -2
- data/lib/rigor/analysis/reachability/scan.rb +5 -5
- data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
- data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
- data/lib/rigor/analysis/result.rb +1 -3
- data/lib/rigor/analysis/rule_catalog.rb +131 -1
- data/lib/rigor/analysis/run_cache_key.rb +21 -12
- data/lib/rigor/analysis/run_cache_probe.rb +3 -5
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
- data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
- data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +407 -51
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +91 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +399 -0
- data/lib/rigor/analysis/worker_session.rb +62 -20
- data/lib/rigor/bleeding_edge.rb +10 -24
- data/lib/rigor/builtins/hkt_builtins.rb +3 -2
- data/lib/rigor/builtins/imported_refinements.rb +159 -21
- data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
- data/lib/rigor/builtins/regex_refinement.rb +21 -13
- data/lib/rigor/builtins/static_return_refinements.rb +7 -6
- data/lib/rigor/cache/annotation_location.rb +2 -4
- data/lib/rigor/cache/descriptor.rb +63 -20
- data/lib/rigor/cache/engine_source.rb +33 -5
- data/lib/rigor/cache/file_digest.rb +9 -1
- data/lib/rigor/cache/incremental_snapshot.rb +72 -6
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
- data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
- data/lib/rigor/cache/rbs_constant_table.rb +0 -3
- data/lib/rigor/cache/rbs_descriptor.rb +112 -14
- data/lib/rigor/cache/rbs_environment.rb +8 -4
- data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
- data/lib/rigor/cache/store.rb +76 -32
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/annotate_command.rb +5 -6
- data/lib/rigor/cli/check_command.rb +57 -10
- data/lib/rigor/cli/check_invocation.rb +6 -11
- data/lib/rigor/cli/check_runner_factory.rb +1 -5
- data/lib/rigor/cli/coverage_command.rb +1 -1
- data/lib/rigor/cli/coverage_mutation.rb +12 -3
- data/lib/rigor/cli/coverage_scan.rb +1 -5
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/doc_links.rb +4 -4
- data/lib/rigor/cli/docs_command.rb +6 -6
- data/lib/rigor/cli/doctor_command.rb +85 -38
- data/lib/rigor/cli/effects_command.rb +2 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +34 -1
- data/lib/rigor/cli/fused_protection_report.rb +7 -1
- data/lib/rigor/cli/lsp_command.rb +1 -1
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
- data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
- data/lib/rigor/cli/mutation_protection_report.rb +7 -1
- data/lib/rigor/cli/plugin_command.rb +4 -4
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/prism_colorizer.rb +1 -2
- data/lib/rigor/cli/protection_fork_scan.rb +6 -6
- data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +212 -31
- data/lib/rigor/cli/skill_command.rb +2 -2
- data/lib/rigor/cli/skill_deep_probe.rb +4 -4
- data/lib/rigor/cli/skill_describe.rb +35 -35
- data/lib/rigor/cli/trace_command.rb +4 -4
- data/lib/rigor/cli/trace_renderer.rb +5 -6
- data/lib/rigor/cli/triage_command.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +29 -11
- data/lib/rigor/cli/type_of_renderer.rb +20 -9
- data/lib/rigor/cli/type_of_template_probe.rb +189 -0
- data/lib/rigor/cli/type_scan_command.rb +4 -4
- data/lib/rigor/cli/unused_command.rb +11 -3
- data/lib/rigor/cli/upgrade_command.rb +1 -1
- data/lib/rigor/cli.rb +89 -9
- data/lib/rigor/config_audit.rb +30 -5
- data/lib/rigor/configuration/severity_profile.rb +25 -9
- data/lib/rigor/configuration.rb +109 -7
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +12 -5
- data/lib/rigor/effects/callee_rule.rb +368 -0
- data/lib/rigor/effects/catalog.rb +7 -4
- data/lib/rigor/effects/collector.rb +11 -5
- data/lib/rigor/effects/config_envelopes.rb +15 -10
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/definition_lines.rb +25 -6
- data/lib/rigor/effects/effect_table.rb +11 -4
- data/lib/rigor/effects/entry_points.rb +0 -2
- data/lib/rigor/effects/envelope_check.rb +9 -9
- data/lib/rigor/effects/envelope_index.rb +20 -8
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +5 -7
- data/lib/rigor/effects/identity.rb +20 -6
- data/lib/rigor/effects/inline_anchor.rb +7 -7
- data/lib/rigor/effects/label_intent.rb +3 -4
- data/lib/rigor/effects/liskov_check.rb +8 -8
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +22 -1
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +47 -37
- data/lib/rigor/effects/propagator.rb +297 -19
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +122 -73
- data/lib/rigor/effects/signature_sources.rb +4 -6
- data/lib/rigor/effects/snapshot.rb +12 -11
- data/lib/rigor/effects/snapshot_diff.rb +1 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +393 -38
- data/lib/rigor/effects/unknown_label_check.rb +3 -8
- data/lib/rigor/effects/unknown_label_report.rb +3 -4
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
- data/lib/rigor/environment/class_registry.rb +3 -1
- data/lib/rigor/environment/failure_slot.rb +2 -2
- data/lib/rigor/environment/installed_gem_set.rb +85 -0
- data/lib/rigor/environment/lockfile_resolver.rb +68 -4
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
- data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
- data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
- data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
- data/lib/rigor/environment/rbs_loader.rb +674 -79
- data/lib/rigor/environment.rb +174 -24
- data/lib/rigor/flow_contribution/merger.rb +0 -2
- data/lib/rigor/flow_contribution.rb +11 -13
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +292 -17
- data/lib/rigor/inference/block_auto_splat.rb +216 -0
- data/lib/rigor/inference/block_call_timing.rb +338 -0
- data/lib/rigor/inference/block_parameter_binder.rb +75 -30
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +7 -6
- data/lib/rigor/inference/budget_trace.rb +3 -6
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +389 -18
- data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/coverage_scanner.rb +4 -6
- data/lib/rigor/inference/def_return_typer.rb +11 -7
- data/lib/rigor/inference/define_method_block_self.rb +64 -0
- data/lib/rigor/inference/dynamic_origin.rb +1 -1
- data/lib/rigor/inference/element_read_widening.rb +180 -0
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +2124 -468
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
- data/lib/rigor/inference/fork_map.rb +5 -7
- data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
- data/lib/rigor/inference/global_write_census.rb +239 -0
- data/lib/rigor/inference/guard_rebinding.rb +447 -0
- data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
- data/lib/rigor/inference/hkt_reducer.rb +2 -3
- data/lib/rigor/inference/hkt_registry.rb +4 -7
- data/lib/rigor/inference/index_write_widening.rb +16 -7
- data/lib/rigor/inference/indexed_narrowing.rb +61 -9
- data/lib/rigor/inference/jump_targets.rb +82 -0
- data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
- data/lib/rigor/inference/last_line.rb +340 -0
- data/lib/rigor/inference/last_status.rb +144 -0
- data/lib/rigor/inference/macro_block_self_type.rb +168 -21
- data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
- data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
- data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
- data/lib/rigor/inference/match_rebinding.rb +392 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
- data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +225 -25
- data/lib/rigor/inference/method_parameter_binder.rb +12 -9
- data/lib/rigor/inference/multi_target_binder.rb +341 -55
- data/lib/rigor/inference/mutation_rejoin.rb +171 -0
- data/lib/rigor/inference/mutation_widening.rb +156 -85
- data/lib/rigor/inference/narrowing.rb +835 -162
- data/lib/rigor/inference/operand_effects.rb +167 -0
- data/lib/rigor/inference/operand_walk.rb +88 -0
- data/lib/rigor/inference/optimistic_origin.rb +153 -16
- data/lib/rigor/inference/origin_lookup.rb +2 -3
- data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
- data/lib/rigor/inference/pre_eval_constants.rb +6 -5
- data/lib/rigor/inference/precision_scanner.rb +3 -4
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +9 -4
- data/lib/rigor/inference/project_patched_scanner.rb +10 -6
- data/lib/rigor/inference/protection_scanner.rb +1 -2
- data/lib/rigor/inference/range_constant.rb +57 -0
- data/lib/rigor/inference/rbs_type_translator.rb +49 -11
- data/lib/rigor/inference/receiver_alias.rb +93 -4
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +76 -0
- data/lib/rigor/inference/repeated_or_writes.rb +463 -0
- data/lib/rigor/inference/return_barrier.rb +54 -0
- data/lib/rigor/inference/rewrite_mutation.rb +120 -0
- data/lib/rigor/inference/scope_indexer.rb +5228 -627
- data/lib/rigor/inference/statement_evaluator.rb +3452 -595
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +97 -0
- data/lib/rigor/inference/struct_fold_safety.rb +6 -7
- data/lib/rigor/inference/synthetic_method_index.rb +1 -2
- data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/inference/version_guard.rb +28 -21
- data/lib/rigor/inference/void_origin.rb +3 -3
- data/lib/rigor/inference/void_tail_summary.rb +2 -4
- data/lib/rigor/language_server/buffer_table.rb +4 -4
- data/lib/rigor/language_server/completion_provider.rb +1 -1
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -1
- data/lib/rigor/language_server/hover_provider.rb +1 -1
- data/lib/rigor/language_server/hover_renderer.rb +4 -4
- data/lib/rigor/language_server/incremental_sync.rb +6 -6
- data/lib/rigor/language_server/project_context.rb +7 -8
- data/lib/rigor/language_server/publish_batcher.rb +2 -2
- data/lib/rigor/language_server/selection_range_provider.rb +2 -2
- data/lib/rigor/language_server/server.rb +9 -9
- data/lib/rigor/language_server/signature_help_provider.rb +1 -1
- data/lib/rigor/language_server/uri.rb +1 -1
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +258 -26
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +167 -0
- data/lib/rigor/plugin/effect_attribution.rb +65 -11
- data/lib/rigor/plugin/effect_entry_points.rb +3 -3
- data/lib/rigor/plugin/fact_store.rb +6 -6
- data/lib/rigor/plugin/io_boundary.rb +93 -8
- data/lib/rigor/plugin/isolation.rb +29 -6
- data/lib/rigor/plugin/loader.rb +33 -13
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
- data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
- data/lib/rigor/plugin/manifest.rb +94 -11
- data/lib/rigor/plugin/registry.rb +76 -12
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin/trust_policy.rb +2 -4
- data/lib/rigor/plugin/type_node_resolver.rb +3 -3
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/plugin_gap_advisory.rb +96 -0
- data/lib/rigor/project_environment.rb +138 -0
- data/lib/rigor/protection/analysis_guard.rb +93 -14
- data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
- data/lib/rigor/protection/dependency_closure.rb +5 -8
- data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
- data/lib/rigor/protection/discovery_seed.rb +14 -12
- data/lib/rigor/protection/kill_signature.rb +1 -4
- data/lib/rigor/protection/measurement_integrity.rb +1 -3
- data/lib/rigor/protection/mutation_cache.rb +7 -11
- data/lib/rigor/protection/mutation_scanner.rb +12 -15
- data/lib/rigor/protection/mutator.rb +2 -1
- data/lib/rigor/protection/test_suite_oracle.rb +5 -5
- data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/reporter.rb +36 -10
- data/lib/rigor/rbs_extended.rb +88 -21
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +139 -0
- data/lib/rigor/reflection.rb +93 -110
- data/lib/rigor/runtime/jit.rb +6 -8
- data/lib/rigor/scope/discovery_index.rb +165 -2
- data/lib/rigor/scope.rb +1041 -32
- data/lib/rigor/sig_gen/alias_index.rb +289 -0
- data/lib/rigor/sig_gen/classification.rb +22 -5
- data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
- data/lib/rigor/sig_gen/generator.rb +673 -53
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/layout_index.rb +3 -4
- data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +18 -16
- data/lib/rigor/sig_gen/path_mapper.rb +4 -6
- data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
- data/lib/rigor/sig_gen/renderer.rb +128 -14
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
- data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +189 -41
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +153 -6
- data/lib/rigor/source/literals.rb +0 -23
- data/lib/rigor/source/node_children.rb +0 -2
- data/lib/rigor/source/node_locator.rb +5 -11
- data/lib/rigor/source/node_walker.rb +0 -6
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +1 -3
- data/lib/rigor/triage.rb +3 -5
- data/lib/rigor/type/accepts_result.rb +24 -6
- data/lib/rigor/type/combinator.rb +144 -5
- data/lib/rigor/type/data_class.rb +2 -2
- data/lib/rigor/type/data_instance.rb +4 -4
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/float_range.rb +128 -0
- data/lib/rigor/type/hash_shape.rb +6 -6
- data/lib/rigor/type/integer_range.rb +13 -8
- data/lib/rigor/type/nominal.rb +8 -2
- data/lib/rigor/type/refined.rb +4 -3
- data/lib/rigor/type/struct_class.rb +3 -3
- data/lib/rigor/type/struct_instance.rb +4 -4
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +1 -1
- data/lib/rigor/type_node/range_literal.rb +25 -0
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
- data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
- data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
- data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
- data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
- data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
- data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
- data/plugins/rigor-grape/sig/grape.rbs +263 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
- data/sig/rigor/analysis/baseline.rbs +69 -7
- data/sig/rigor/analysis/fact_store.rbs +1 -1
- data/sig/rigor/analysis/project_scan.rbs +74 -0
- data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
- data/sig/rigor/cache.rbs +2 -3
- data/sig/rigor/effects/config_envelopes.rbs +100 -0
- data/sig/rigor/effects/effect_table.rbs +60 -0
- data/sig/rigor/effects/envelope.rbs +97 -0
- data/sig/rigor/effects/envelope_index.rbs +33 -0
- data/sig/rigor/effects/file_collection.rbs +81 -0
- data/sig/rigor/effects/label.rbs +26 -0
- data/sig/rigor/effects/label_set.rbs +46 -0
- data/sig/rigor/effects/method_key.rbs +24 -0
- data/sig/rigor/effects/origin.rbs +50 -0
- data/sig/rigor/effects/plugin_facts.rbs +141 -0
- data/sig/rigor/effects/registry.rbs +68 -0
- data/sig/rigor/effects/summary.rbs +50 -0
- data/sig/rigor/effects/taint_cause.rbs +12 -0
- data/sig/rigor/environment.rbs +17 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +9 -14
- data/sig/rigor/plugin/additional_initializer.rbs +36 -0
- data/sig/rigor/plugin/base.rbs +30 -7
- data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
- data/sig/rigor/plugin/effect_attribution.rbs +66 -0
- data/sig/rigor/plugin/effect_edge.rbs +33 -0
- data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +51 -11
- data/sig/rigor/plugin/protocol_contract.rbs +68 -0
- data/sig/rigor/plugin/registry.rbs +62 -1
- data/sig/rigor/plugin.rbs +1 -1
- data/sig/rigor/rbs_extended.rbs +1 -1
- data/sig/rigor/reflection.rbs +9 -6
- data/sig/rigor/scope.rbs +148 -12
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +33 -1
- data/sig/rigor.rbs +39 -20
- data/skills/rigor-ask/SKILL.md +16 -11
- data/skills/rigor-baseline-reduce/SKILL.md +4 -6
- data/skills/rigor-ci-setup/SKILL.md +17 -21
- data/skills/rigor-doctor/SKILL.md +24 -22
- data/skills/rigor-doctor/references/01-checks.md +97 -33
- data/skills/rigor-editor-setup/SKILL.md +6 -4
- data/skills/rigor-mcp-setup/SKILL.md +5 -4
- data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
- data/skills/rigor-next-steps/SKILL.md +4 -2
- data/skills/rigor-plugin-author/SKILL.md +19 -23
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
- data/skills/rigor-plugin-review/SKILL.md +6 -4
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
- data/skills/rigor-plugin-tune/SKILL.md +4 -2
- data/skills/rigor-project-init/SKILL.md +15 -10
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +43 -14
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
- data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
- data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
- data/skills/rigor-protection-uplift/SKILL.md +4 -6
- data/skills/rigor-rbs-setup/SKILL.md +4 -2
- data/skills/rigor-type-oracle/SKILL.md +226 -0
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
- data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +133 -3
- data/lib/rigor/cli/probe_environment.rb +0 -85
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
|
@@ -9,6 +9,12 @@ require_relative "../environment"
|
|
|
9
9
|
require_relative "../rbs_extended"
|
|
10
10
|
require_relative "../analysis/fact_store"
|
|
11
11
|
require_relative "../builtins/regex_refinement"
|
|
12
|
+
require_relative "guard_rebinding"
|
|
13
|
+
require_relative "last_line"
|
|
14
|
+
require_relative "operand_effects"
|
|
15
|
+
require_relative "optimistic_origin"
|
|
16
|
+
require_relative "project_method_ownership"
|
|
17
|
+
require_relative "receiver_alias"
|
|
12
18
|
|
|
13
19
|
module Rigor
|
|
14
20
|
module Inference
|
|
@@ -41,14 +47,36 @@ module Rigor
|
|
|
41
47
|
module Narrowing
|
|
42
48
|
TRUSTED_EQUALITY_LITERAL_CLASSES = [String, Symbol, Integer, TrueClass, FalseClass, NilClass].freeze
|
|
43
49
|
SINGLETON_LITERAL_CLASSES = [TrueClass, FalseClass, NilClass].freeze
|
|
44
|
-
|
|
50
|
+
# `scope` is the analysed file's seeded scope where the caller has one, and nil otherwise (the
|
|
51
|
+
# Fact-shaped and RSpec-matcher entries hold an environment but no scope). Only the #898 singleton
|
|
52
|
+
# path reads it, and only to withhold a `Bot`, so a nil scope is the pre-#898 answer exactly.
|
|
53
|
+
ClassNarrowingContext = Data.define(:exact, :polarity, :environment, :scope)
|
|
45
54
|
# A recognised `=~` regex pattern operand: the pattern `source` string and whether it was
|
|
46
55
|
# compiled in extended (`//x`) mode. Extraction from a literal `RegularExpressionNode` and from
|
|
47
56
|
# a value-pinned `Constant[Regexp]` share this carrier so the participation walk and the
|
|
48
57
|
# extended-mode bail read one shape.
|
|
49
58
|
RegexMatchPattern = Data.define(:source, :extended)
|
|
59
|
+
# Issue #1429 — a predicate receiver whose binding an edge can narrow ({.receiver_slot}): a local, an instance
|
|
60
|
+
# variable or a global (`name` its Symbol), or a constant reference (`name` its {.constant_key}). `current` is
|
|
61
|
+
# the type the receiver reads before the predicate, a global's read type when the scope does not bind it.
|
|
62
|
+
ReceiverSlot = Data.define(:kind, :name, :current)
|
|
63
|
+
# The globals a guard narrows without a record ({Scope#with_guarded_global}): `$_` and `$~` live in the frame's
|
|
64
|
+
# special-variable slot, which a called method does not reach and their own machinery forgets
|
|
65
|
+
# (`Scope#forget_last_line`, `Scope#forget_match_globals`), and `$!` / `$@` read the rescue clause that is
|
|
66
|
+
# running, which a call that returns leaves as it was.
|
|
67
|
+
UNRECORDED_GUARD_GLOBALS = %i[$_ $~ $! $@].freeze
|
|
68
|
+
# Issue #1429 — the value-pinned classes: a `Nominal` of one of them is the single value itself, which no
|
|
69
|
+
# program replaces with another class's instance, so it counts with the literal carriers
|
|
70
|
+
# ({.disjoint_nominal_guard?}, `respond_to?`'s edge).
|
|
71
|
+
VALUE_PINNED_CLASSES = %w[NilClass TrueClass FalseClass].freeze
|
|
50
72
|
private_constant :TRUSTED_EQUALITY_LITERAL_CLASSES, :SINGLETON_LITERAL_CLASSES, :ClassNarrowingContext,
|
|
51
|
-
:RegexMatchPattern
|
|
73
|
+
:RegexMatchPattern, :ReceiverSlot, :UNRECORDED_GUARD_GLOBALS, :VALUE_PINNED_CLASSES
|
|
74
|
+
|
|
75
|
+
# Issue #1017 — the tallest stack of conditionals a condition may carry and still contribute facts. A
|
|
76
|
+
# conditional guard analyses its predicate and both arms, so each level can triple the analysis; past this
|
|
77
|
+
# height the guard declines to "no narrowing". Height counts conditionals reached through the shapes the
|
|
78
|
+
# analyser walks (parentheses, a body's last statement, `&&` / `||`, `!`, a predicate, an arm).
|
|
79
|
+
CONDITIONAL_GUARD_DEPTH = 2
|
|
52
80
|
|
|
53
81
|
module_function
|
|
54
82
|
|
|
@@ -117,10 +145,9 @@ module Rigor
|
|
|
117
145
|
# falsey fragments above: `:truthy` when no inhabitant is falsey (the falsey fragment is
|
|
118
146
|
# `Bot`), `:falsey` when no inhabitant is truthy, nil when both fragments are inhabited —
|
|
119
147
|
# or when the type itself is nil / `Bot` (dead code is not a certainty claim). This is the
|
|
120
|
-
# single owner of the judgment
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
# flowing through it can never disagree.
|
|
148
|
+
# single owner of the judgment branch elision reads (`StatementEvaluator#live_branch_for_if`,
|
|
149
|
+
# which a value-position conditional reaches too since issue #1003), so the type a dead
|
|
150
|
+
# branch is elided from and the scope that stops flowing through it can never disagree.
|
|
124
151
|
def predicate_certainty(type)
|
|
125
152
|
return nil if type.nil? || type.is_a?(Type::Bot)
|
|
126
153
|
|
|
@@ -139,9 +166,11 @@ module Rigor
|
|
|
139
166
|
# counterpart of the scope narrowing {.case_when_scopes} performs for the same condition
|
|
140
167
|
# shape, kept here so the branch a `case` expression's type drops and the clause whose
|
|
141
168
|
# body scope goes dead derive from one judgment.
|
|
142
|
-
def class_pattern_certainty(subject_type, class_name, environment:)
|
|
143
|
-
truthy_bot = narrow_class(subject_type, class_name, environment: environment
|
|
144
|
-
|
|
169
|
+
def class_pattern_certainty(subject_type, class_name, environment:, scope: nil)
|
|
170
|
+
truthy_bot = narrow_class(subject_type, class_name, environment: environment,
|
|
171
|
+
scope: scope).is_a?(Type::Bot)
|
|
172
|
+
falsey_bot = narrow_not_class(subject_type, class_name, environment: environment,
|
|
173
|
+
scope: scope).is_a?(Type::Bot)
|
|
145
174
|
|
|
146
175
|
return :no if truthy_bot && !falsey_bot
|
|
147
176
|
return :yes if !truthy_bot && falsey_bot
|
|
@@ -219,6 +248,54 @@ module Rigor
|
|
|
219
248
|
narrow_integer_comparison_dispatch(type, comparator, bound)
|
|
220
249
|
end
|
|
221
250
|
|
|
251
|
+
# Float-comparison fragment of `type` against a numeric literal `bound` (ADR-109 WD5). This
|
|
252
|
+
# is a TRUTHY-EDGE rule only: `x > c` is false for NaN, so the set that satisfies it is the
|
|
253
|
+
# range `Float[c..]`, but the set that fails it is that range's complement PLUS NaN, which no
|
|
254
|
+
# range spells. Callers apply it to the truthy edge and keep the entry type on the falsy edge.
|
|
255
|
+
# `x > c` narrows to the closed envelope `Float[c..]` rather than the exact `(c, ∞]` (one
|
|
256
|
+
# double wider; the exact form is deferred until a rule demands the removed point), `x < c`
|
|
257
|
+
# to the exact `Float[...c]`. Hooks in:
|
|
258
|
+
# - `Constant<Float>` is preserved when it satisfies the comparison, otherwise collapsed to
|
|
259
|
+
# `Bot` (a NaN constant satisfies nothing);
|
|
260
|
+
# - `FloatRange` becomes its intersection with the half-line, `Bot` when empty;
|
|
261
|
+
# - `Nominal[Float]` becomes the half-line itself;
|
|
262
|
+
# - `Union` narrows each member independently; Integer-rooted and other carriers flow
|
|
263
|
+
# through unchanged (the Integer rule above owns them).
|
|
264
|
+
def narrow_float_comparison(type, comparator, bound)
|
|
265
|
+
return type unless (bound.is_a?(Integer) || bound.is_a?(Float)) && %i[< <= > >=].include?(comparator)
|
|
266
|
+
|
|
267
|
+
bound = bound.to_f
|
|
268
|
+
return type if bound.nan? || bound.infinite?
|
|
269
|
+
|
|
270
|
+
narrow_float_comparison_dispatch(type, comparator, bound)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# The truthy edge of a numeric comparison: the Integer rule for Integer-rooted members, then
|
|
274
|
+
# the Float rule for Float-rooted ones. Never apply this to a falsy edge (see
|
|
275
|
+
# {.narrow_float_comparison}).
|
|
276
|
+
def narrow_numeric_comparison_truthy(type, comparator, bound)
|
|
277
|
+
narrow_float_comparison(narrow_integer_comparison(type, comparator, bound), comparator, bound)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Intersects the Float-rooted members of `type` with `range` (a `FloatRange`), leaving every
|
|
281
|
+
# other member unchanged. `x.nan?` narrows its FALSY edge with `non_nan_float`; `x.finite?`
|
|
282
|
+
# narrows its TRUTHY edge with `finite_float`; the other edge of each keeps the entry type
|
|
283
|
+
# (NaN is truthy for `nan?` and nothing else is known there; `!finite?` admits NaN and ±∞).
|
|
284
|
+
def narrow_float_to_range(type, range)
|
|
285
|
+
case type
|
|
286
|
+
when Type::Constant
|
|
287
|
+
type.value.is_a?(Float) && !range.covers?(type.value) ? Type::Combinator.bot : type
|
|
288
|
+
when Type::FloatRange
|
|
289
|
+
intersect_float_bounds(type, range.min, range.max, range.exclude_end?)
|
|
290
|
+
when Type::Nominal
|
|
291
|
+
float_nominal?(type) ? range : type
|
|
292
|
+
when Type::Union
|
|
293
|
+
Type::Combinator.union(*type.members.map { |m| narrow_float_to_range(m, range) })
|
|
294
|
+
else
|
|
295
|
+
type
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
222
299
|
# Equality fragment of `type` against an Integer `value`. `Constant<Integer>` is preserved
|
|
223
300
|
# when it equals `value`, otherwise collapses to `Bot`. `IntegerRange` covers? `value`
|
|
224
301
|
# narrows to `Constant[value]`; an out-of-range comparison collapses to `Bot`.
|
|
@@ -261,17 +338,35 @@ module Rigor
|
|
|
261
338
|
# preserved; disjoint hierarchies collapse to `Bot`. Classes the environment cannot
|
|
262
339
|
# resolve fall back to the conservative answer (the type unchanged) so the analyzer never
|
|
263
340
|
# asserts narrowing it cannot prove.
|
|
264
|
-
def narrow_class(type, class_name, exact: false, environment: Environment.default)
|
|
265
|
-
context = ClassNarrowingContext.new(exact: exact, polarity: :positive, environment: environment
|
|
341
|
+
def narrow_class(type, class_name, exact: false, environment: Environment.default, scope: nil)
|
|
342
|
+
context = ClassNarrowingContext.new(exact: exact, polarity: :positive, environment: environment,
|
|
343
|
+
scope: scope)
|
|
266
344
|
narrow_class_dispatch(type, class_name, context)
|
|
267
345
|
end
|
|
268
346
|
|
|
347
|
+
# Issue #1429 — true when `type` holds a member whose `Bot` under `is_a?(class_name)` rests only on Rigor's
|
|
348
|
+
# reading of its type: a `Nominal`, other than the value-pinned `nil` / `true` / `false` classes, whose class
|
|
349
|
+
# the environment orders `:disjoint` from `class_name`. A `Nominal` is a type Rigor inferred or read from a
|
|
350
|
+
# signature, and a program can hold another object there: `$stdout` is typed `IO`, and a test runs it with a
|
|
351
|
+
# `StringIO`, which is not an `IO` subclass. The narrowing still reads such a guard's edge as `Bot`, so the
|
|
352
|
+
# arm is not checked; what this answers is whether the `Bot` may stand as a verdict that the code is dead
|
|
353
|
+
# (`flow.unreachable-clause`). The carriers that record what the file literally shows (`Constant`, `Tuple`,
|
|
354
|
+
# `HashShape`, `Singleton`) and the value-pinned classes do not count, so their verdicts stand.
|
|
355
|
+
def disjoint_nominal_guard?(type, class_name, environment: Environment.default)
|
|
356
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
357
|
+
members.any? do |member|
|
|
358
|
+
member.is_a?(Type::Nominal) && !VALUE_PINNED_CLASSES.include?(member.class_name) &&
|
|
359
|
+
member.class_name != class_name && environment.class_ordering(member.class_name, class_name) == :disjoint
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
269
363
|
# Mirror of {.narrow_class} for the falsey edge of `is_a?`/`kind_of?`/`instance_of?`.
|
|
270
364
|
# Inhabitants that DO satisfy the predicate are removed; inhabitants that do not are
|
|
271
365
|
# preserved. Conservative on Top/Dynamic/Bot (preserved unchanged) because the analyzer
|
|
272
366
|
# cannot prove the negative without a richer carrier.
|
|
273
|
-
def narrow_not_class(type, class_name, exact: false, environment: Environment.default)
|
|
274
|
-
context = ClassNarrowingContext.new(exact: exact, polarity: :negative, environment: environment
|
|
367
|
+
def narrow_not_class(type, class_name, exact: false, environment: Environment.default, scope: nil)
|
|
368
|
+
context = ClassNarrowingContext.new(exact: exact, polarity: :negative, environment: environment,
|
|
369
|
+
scope: scope)
|
|
275
370
|
narrow_class_dispatch(type, class_name, context)
|
|
276
371
|
end
|
|
277
372
|
|
|
@@ -285,11 +380,11 @@ module Rigor
|
|
|
285
380
|
# Walk the current type's union members, keep each part disjoint from `base`, and add
|
|
286
381
|
# the removed-value Constant once when any current member covers it. `assert s is
|
|
287
382
|
# ~non-empty-string` over `s: String | nil` narrows to `Constant[""] | NilClass`.
|
|
288
|
-
# - `IntegerRange[a, b]` (v0.0.5+ slice). Complement is the two open halves
|
|
289
|
-
# a-1
|
|
383
|
+
# - `IntegerRange[a, b]` (v0.0.5+ slice). Complement is the two open halves
|
|
384
|
+
# `Integer[..a-1]` and `Integer[b+1..]`, each intersected with the integer-domain parts of
|
|
290
385
|
# `current_type`. Non-integer parts (nil, String, …) of a Union receiver survive
|
|
291
|
-
# unchanged. `assert n is ~
|
|
292
|
-
#
|
|
386
|
+
# unchanged. `assert n is ~Integer[5..10]` over `n: Integer | nil` narrows to `Integer[..4] |
|
|
387
|
+
# Integer[11..] | NilClass`.
|
|
293
388
|
# - `Type::Intersection[M1, M2, …]` (v0.0.5+ slice). De Morgan: `D \ (M1 ∩ M2) = (D \ M1)
|
|
294
389
|
# ∪ (D \ M2)`. Each member's complement is computed independently within
|
|
295
390
|
# `current_type` and the results are unioned. Members the algebra cannot complement
|
|
@@ -335,12 +430,18 @@ module Rigor
|
|
|
335
430
|
fact.type
|
|
336
431
|
end
|
|
337
432
|
|
|
433
|
+
# Issue #1429 — the key a constant reference's guard narrowing is recorded under ({Scope#constant_narrowing}):
|
|
434
|
+
# its spelling, with a leading `::` kept, since `::Foo` and `Foo` may name two constants. nil for a path on a
|
|
435
|
+
# dynamic base (`expr::Foo`) and for any other node.
|
|
436
|
+
def constant_key(node)
|
|
437
|
+
name = Source::ConstantPath.qualified_name_or_nil(node)
|
|
438
|
+
return nil if name.nil?
|
|
439
|
+
|
|
440
|
+
Source::ConstantPath.rooted?(node) ? "::#{name}" : name
|
|
441
|
+
end
|
|
442
|
+
|
|
338
443
|
# Public predicate analyser. Returns `[truthy_scope, falsey_scope]`, always; when no
|
|
339
444
|
# narrowing rule matches the predicate node both entries are the receiver scope unchanged.
|
|
340
|
-
#
|
|
341
|
-
# @param node [Prism::Node, nil]
|
|
342
|
-
# @param scope [Rigor::Scope]
|
|
343
|
-
# @return [Array(Rigor::Scope, Rigor::Scope)]
|
|
344
445
|
def predicate_scopes(node, scope)
|
|
345
446
|
return [scope, scope] if node.nil?
|
|
346
447
|
|
|
@@ -348,6 +449,37 @@ module Rigor
|
|
|
348
449
|
result || [scope, scope]
|
|
349
450
|
end
|
|
350
451
|
|
|
452
|
+
# Issue #1468 — the scope the arguments and block of a safe-navigation call `recv&.m(…)` run under. Ruby
|
|
453
|
+
# evaluates them only once `recv` is known non-nil, so `klass&.new(klass.flag?)` reads `klass` without its
|
|
454
|
+
# `nil`, as `if klass then klass.new(klass.flag?) end` does. Only `nil` is removed: `false&.m(x)` still calls
|
|
455
|
+
# `m`. The call's own value keeps its skipped-call `nil` (`ExpressionTyper#safe_navigation_call_type`).
|
|
456
|
+
#
|
|
457
|
+
# The receiver is one a guard narrows ({.receiver_slot}: a local, instance variable, global or constant), or a
|
|
458
|
+
# safe-navigation call on such a receiver whose own arguments and block hold no write or jump
|
|
459
|
+
# ({OperandEffects}): `a&.b(a.c)&.d(a.e)` runs `d` only when `a&.b(…)` answered non-nil, which it cannot on a
|
|
460
|
+
# nil `a`, and nothing between the two reads rebinds `a`. `scope` itself when nothing narrows, and for a call
|
|
461
|
+
# that is not `&.`.
|
|
462
|
+
def safe_navigation_scope(call_node, scope)
|
|
463
|
+
return scope unless call_node.safe_navigation?
|
|
464
|
+
|
|
465
|
+
non_nil_receiver_scope(call_node.receiver, scope) || scope
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# The scope the block of a safe-navigation call is entered from: {.safe_navigation_scope}, unless the narrowing
|
|
469
|
+
# may not hold for the whole body. An argument that writes may rebind the receiver before the block runs
|
|
470
|
+
# (`f&.two(f = nil) { f.run }`), and a block that writes the receiver hands the next iteration what it wrote
|
|
471
|
+
# (`a&.each { a.size; a = nil }`); either keeps `scope`.
|
|
472
|
+
def safe_navigation_block_scope(call_node, scope)
|
|
473
|
+
return scope unless call_node.safe_navigation?
|
|
474
|
+
return scope if OperandEffects.any?(call_node.arguments)
|
|
475
|
+
|
|
476
|
+
root = call_node.receiver
|
|
477
|
+
root = root.receiver while root.is_a?(Prism::CallNode) && root.safe_navigation?
|
|
478
|
+
return scope if root.respond_to?(:name) && OperandEffects.written_variables(call_node.block).include?(root.name)
|
|
479
|
+
|
|
480
|
+
safe_navigation_scope(call_node, scope)
|
|
481
|
+
end
|
|
482
|
+
|
|
351
483
|
# Slice 7 phase 5 — `case`/`when` narrowing.
|
|
352
484
|
#
|
|
353
485
|
# Given the subject of a `case` (the expression after the `case` keyword) and an array of
|
|
@@ -365,12 +497,6 @@ module Rigor
|
|
|
365
497
|
# scope. The catalogue mirrors {.case_equality_target_class}: static class/module
|
|
366
498
|
# constants narrow as `is_a?`; integer/float-endpoint ranges narrow to `Numeric`;
|
|
367
499
|
# string-endpoint ranges and regexp literals narrow to `String`.
|
|
368
|
-
#
|
|
369
|
-
# @param subject [Prism::Node, nil] the `case` subject.
|
|
370
|
-
# @param conditions [Array<Prism::Node>] the `when`
|
|
371
|
-
# clause's `conditions` array.
|
|
372
|
-
# @param scope [Rigor::Scope]
|
|
373
|
-
# @return [Array(Rigor::Scope, Rigor::Scope)]
|
|
374
500
|
def case_when_scopes(subject, conditions, scope)
|
|
375
501
|
# C1 — `case x when /re/` runs `/re/ === x`, which sets the regex match-data globals
|
|
376
502
|
# exactly as a successful `=~` does. Narrow `$~`/`$&`/`$1..$N` on the clause body (the
|
|
@@ -378,17 +504,17 @@ module Rigor
|
|
|
378
504
|
# a different regex. Applied even when the subject is not a narrowable local read.
|
|
379
505
|
body_scope = apply_when_regex_globals(conditions, scope)
|
|
380
506
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
current = scope.local(local_name)
|
|
385
|
-
return [body_scope, scope] if current.nil?
|
|
507
|
+
# Issue #1429 — a global or constant subject (`case $stdout when StringIO`) narrows as a local does.
|
|
508
|
+
slot = receiver_slot(subject, scope, kinds: CASE_SUBJECT_KINDS)
|
|
509
|
+
return [body_scope, scope] if slot.nil?
|
|
386
510
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
[truthy, falsey]
|
|
511
|
+
truthy_type, falsey_type = case_when_types(scope, slot.current, conditions)
|
|
512
|
+
[narrow_slot(body_scope, slot, truthy_type), narrow_slot(scope, slot, falsey_type)]
|
|
390
513
|
end
|
|
391
514
|
|
|
515
|
+
CASE_SUBJECT_KINDS = %i[local global constant].freeze
|
|
516
|
+
private_constant :CASE_SUBJECT_KINDS
|
|
517
|
+
|
|
392
518
|
# When the clause has exactly one `RegularExpressionNode` literal condition, narrow the
|
|
393
519
|
# match-data globals on the body edge (same rule as `analyse_regex_match_predicate`'s
|
|
394
520
|
# truthy edge). With multiple regex conditions (`when /a/, /b/`) the body is reachable
|
|
@@ -411,8 +537,29 @@ module Rigor
|
|
|
411
537
|
|
|
412
538
|
# Internal analyser. Returns `[truthy_scope, falsey_scope]` when the predicate shape is
|
|
413
539
|
# recognised, or `nil` to signal "no narrowing" so the public surface can fall back to the
|
|
414
|
-
# entry scope.
|
|
540
|
+
# entry scope. Issue #1359 — a condition whose value is a `gets`-family reader's, the bare call
|
|
541
|
+
# or a variable written with it (`while gets`, `if (line = gets)`), also binds `$_` on each edge
|
|
542
|
+
# to the line the reader returned ({LastLine.predicate_scopes}).
|
|
415
543
|
def analyse(node, scope)
|
|
544
|
+
edges = analyse_shape(node, scope)
|
|
545
|
+
reader = last_line_condition(node)
|
|
546
|
+
reader ? LastLine.predicate_scopes(reader, scope, edges) : edges
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# The reader call a condition's value is, or nil.
|
|
550
|
+
def last_line_condition(node)
|
|
551
|
+
call =
|
|
552
|
+
case node
|
|
553
|
+
when Prism::CallNode then node
|
|
554
|
+
when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::ClassVariableWriteNode,
|
|
555
|
+
Prism::GlobalVariableWriteNode
|
|
556
|
+
node.value
|
|
557
|
+
end
|
|
558
|
+
call if call.is_a?(Prism::CallNode) && LastLine::READERS.include?(call.name)
|
|
559
|
+
end
|
|
560
|
+
private_class_method :last_line_condition
|
|
561
|
+
|
|
562
|
+
def analyse_shape(node, scope)
|
|
416
563
|
case node
|
|
417
564
|
when Prism::ParenthesesNode
|
|
418
565
|
analyse_parentheses(node, scope)
|
|
@@ -424,20 +571,19 @@ module Rigor
|
|
|
424
571
|
analyse_local_write(node, scope)
|
|
425
572
|
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode
|
|
426
573
|
analyse_ivar(node, scope)
|
|
427
|
-
when Prism::ClassVariableWriteNode
|
|
428
|
-
|
|
429
|
-
when Prism::
|
|
430
|
-
|
|
574
|
+
when Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode
|
|
575
|
+
analyse_cvar_or_global_write(node, scope)
|
|
576
|
+
when Prism::GlobalVariableReadNode, Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
577
|
+
analyse_slot_read(node, scope)
|
|
431
578
|
when Prism::CallNode
|
|
432
|
-
analyse_call(node, scope)
|
|
433
|
-
when Prism::AndNode
|
|
434
|
-
|
|
435
|
-
when Prism::OrNode
|
|
436
|
-
analyse_or(node, scope)
|
|
579
|
+
analyse_call(write_receiver_as_read(node) || node, scope)
|
|
580
|
+
when Prism::AndNode, Prism::OrNode, Prism::IfNode, Prism::UnlessNode
|
|
581
|
+
analyse_composition(node, scope)
|
|
437
582
|
when Prism::MatchWriteNode
|
|
438
583
|
analyse_match_write(node, scope)
|
|
439
584
|
end
|
|
440
585
|
end
|
|
586
|
+
private_class_method :analyse_shape
|
|
441
587
|
|
|
442
588
|
# rubocop:disable-next Metrics/ClassLength
|
|
443
589
|
class << self
|
|
@@ -480,7 +626,7 @@ module Rigor
|
|
|
480
626
|
end
|
|
481
627
|
|
|
482
628
|
# Complement of an `IntegerRange[a, b]` within `current_type`. Splits the range
|
|
483
|
-
# complement into the two open halves `
|
|
629
|
+
# complement into the two open halves `Integer[..a-1]` and `Integer[b+1..]` (skipping a
|
|
484
630
|
# half when its bound is infinity), then intersects each half with the integer-domain
|
|
485
631
|
# parts of `current_type`. Non-integer parts of a Union receiver (nil, String, …)
|
|
486
632
|
# survive unchanged.
|
|
@@ -505,8 +651,8 @@ module Rigor
|
|
|
505
651
|
Type::Combinator.union(*survivors)
|
|
506
652
|
end
|
|
507
653
|
|
|
508
|
-
# Returns the two open halves of an IntegerRange's complement: the left half
|
|
509
|
-
# a-1
|
|
654
|
+
# Returns the two open halves of an IntegerRange's complement: the left half
|
|
655
|
+
# `Integer[..a-1]` (when `a` is finite) and the right half `Integer[b+1..]` (when `b` is finite).
|
|
510
656
|
# Universal ranges (both bounds infinite) yield an empty array — the complement is
|
|
511
657
|
# empty.
|
|
512
658
|
def integer_range_complement_halves(range)
|
|
@@ -794,6 +940,12 @@ module Rigor
|
|
|
794
940
|
]
|
|
795
941
|
end
|
|
796
942
|
|
|
943
|
+
def analyse_cvar_or_global_write(node, scope)
|
|
944
|
+
return analyse_cvar_write(node, scope) if node.is_a?(Prism::ClassVariableWriteNode)
|
|
945
|
+
|
|
946
|
+
analyse_global_write(node, scope)
|
|
947
|
+
end
|
|
948
|
+
|
|
797
949
|
def analyse_cvar_write(node, scope)
|
|
798
950
|
current = scope.cvar(node.name)
|
|
799
951
|
return nil if current.nil?
|
|
@@ -814,6 +966,76 @@ module Rigor
|
|
|
814
966
|
]
|
|
815
967
|
end
|
|
816
968
|
|
|
969
|
+
# Issue #1429 — `if $g`, `return unless $g`, `if CONFIG`: the truthiness guard on a global or constant
|
|
970
|
+
# read, the counterpart of {#analyse_local_read} ({#narrow_slot}).
|
|
971
|
+
def analyse_slot_read(node, scope)
|
|
972
|
+
slot = receiver_slot(node, scope, kinds: GLOBAL_AND_CONSTANT)
|
|
973
|
+
return nil if slot.nil?
|
|
974
|
+
|
|
975
|
+
[
|
|
976
|
+
narrow_slot(scope, slot, narrow_truthy(slot.current)),
|
|
977
|
+
narrow_slot(scope, slot, narrow_falsey(slot.current))
|
|
978
|
+
]
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
GLOBAL_AND_CONSTANT = %i[global constant].freeze
|
|
982
|
+
ALL_SLOT_KINDS = %i[local ivar global constant].freeze
|
|
983
|
+
private_constant :GLOBAL_AND_CONSTANT, :ALL_SLOT_KINDS
|
|
984
|
+
|
|
985
|
+
# The {ReceiverSlot} `node` reads, among `kinds`, or nil. A local or instance variable needs a binding. A
|
|
986
|
+
# global the scope does not bind is read as it reads unbound, and a constant reference as it resolves
|
|
987
|
+
# (through any narrowing already on it), so a guard narrows it from that type.
|
|
988
|
+
def receiver_slot(node, scope, kinds: ALL_SLOT_KINDS)
|
|
989
|
+
case node
|
|
990
|
+
when Prism::LocalVariableReadNode then variable_slot(:local, node.name, scope.local(node.name), kinds)
|
|
991
|
+
when Prism::InstanceVariableReadNode then variable_slot(:ivar, node.name, scope.ivar(node.name), kinds)
|
|
992
|
+
when Prism::GlobalVariableReadNode then global_slot(node, scope, kinds)
|
|
993
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then constant_slot(node, scope, kinds)
|
|
994
|
+
end
|
|
995
|
+
end
|
|
996
|
+
|
|
997
|
+
def variable_slot(kind, name, current, kinds)
|
|
998
|
+
return nil if current.nil? || !kinds.include?(kind)
|
|
999
|
+
|
|
1000
|
+
ReceiverSlot.new(kind: kind, name: name, current: current)
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
def global_slot(node, scope, kinds)
|
|
1004
|
+
return nil unless kinds.include?(:global)
|
|
1005
|
+
|
|
1006
|
+
ReceiverSlot.new(kind: :global, name: node.name, current: scope.global(node.name) || scope.type_of(node))
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
def constant_slot(node, scope, kinds)
|
|
1010
|
+
return nil unless kinds.include?(:constant)
|
|
1011
|
+
|
|
1012
|
+
key = constant_key(node)
|
|
1013
|
+
return nil if key.nil?
|
|
1014
|
+
|
|
1015
|
+
ReceiverSlot.new(kind: :constant, name: key, current: scope.type_of(node))
|
|
1016
|
+
end
|
|
1017
|
+
|
|
1018
|
+
# `scope` with `slot`'s receiver read as `type` on one edge. A local or instance variable rebinds as its
|
|
1019
|
+
# analysers always did. A global or constant is left alone when the edge learns nothing (`type` is what
|
|
1020
|
+
# it read), and otherwise narrows with its pre-guard type recorded, so code that may rebind it restores
|
|
1021
|
+
# the binding ({Scope#forget_guard_narrowings}); the frame-local specials skip the record.
|
|
1022
|
+
def narrow_slot(scope, slot, type)
|
|
1023
|
+
case slot.kind
|
|
1024
|
+
when :local then scope.with_local(slot.name, type)
|
|
1025
|
+
when :ivar then scope.with_ivar(slot.name, type)
|
|
1026
|
+
when :global then narrow_global_slot(scope, slot, type)
|
|
1027
|
+
else
|
|
1028
|
+
type == slot.current ? scope : scope.with_constant_narrowing(slot.name, type, slot.current)
|
|
1029
|
+
end
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
def narrow_global_slot(scope, slot, type)
|
|
1033
|
+
return scope if type == slot.current
|
|
1034
|
+
|
|
1035
|
+
record = !UNRECORDED_GUARD_GLOBALS.include?(slot.name)
|
|
1036
|
+
scope.with_guarded_global(slot.name, type, slot.current, record: record)
|
|
1037
|
+
end
|
|
1038
|
+
|
|
817
1039
|
# `if /(?<x>...)/ =~ str` — Prism wraps the `=~` call in a `MatchWriteNode` listing the
|
|
818
1040
|
# named-capture targets. The parent `eval_match_write` has already bound each target to
|
|
819
1041
|
# `String | nil`; in the truthy branch (the regex matched) every named capture is
|
|
@@ -869,6 +1091,25 @@ module Rigor
|
|
|
869
1091
|
# literals (Slice 6 phase 2 sub-phase 2).
|
|
870
1092
|
# Anything else returns nil so the surrounding analyser falls through to the
|
|
871
1093
|
# no-narrowing fallback.
|
|
1094
|
+
# Issue #1223 — `(x = e).nil?`, `(x ||= e).empty?`, `(@x = e).is_a?(C)`. A write's value is the binding
|
|
1095
|
+
# it leaves, and a predicate is narrowed in the scope after its receiver ran, so the call narrows the
|
|
1096
|
+
# variable exactly as the same call on a read of it would. Every analyser keys its receiver on a read
|
|
1097
|
+
# node, so the call is analysed with its receiver replaced by one ({ReceiverAlias.read_of}). Before #1223
|
|
1098
|
+
# the statement evaluator did not thread a receiver's write at all, so the local was unbound and there
|
|
1099
|
+
# was nothing to narrow; nil when the receiver is not such a write, bare or parenthesised on its own.
|
|
1100
|
+
def write_receiver_as_read(node)
|
|
1101
|
+
write = written_receiver(node.receiver)
|
|
1102
|
+
write && node.copy(receiver: ReceiverAlias.read_of(write))
|
|
1103
|
+
end
|
|
1104
|
+
|
|
1105
|
+
def written_receiver(receiver)
|
|
1106
|
+
while receiver.is_a?(Prism::ParenthesesNode) && receiver.body.is_a?(Prism::StatementsNode) &&
|
|
1107
|
+
receiver.body.body.size == 1
|
|
1108
|
+
receiver = receiver.body.body.first
|
|
1109
|
+
end
|
|
1110
|
+
receiver if ReceiverAlias.variable_write?(receiver)
|
|
1111
|
+
end
|
|
1112
|
+
|
|
872
1113
|
def analyse_call(node, scope)
|
|
873
1114
|
return nil if node.block
|
|
874
1115
|
|
|
@@ -994,7 +1235,7 @@ module Rigor
|
|
|
994
1235
|
|
|
995
1236
|
def simple_dispatch_name?(name)
|
|
996
1237
|
%i[nil? ! is_a? kind_of? instance_of? == != === =~ match? key? has_key? empty? any?
|
|
997
|
-
none? respond_to?].include?(name)
|
|
1238
|
+
none? respond_to? nan? finite?].include?(name)
|
|
998
1239
|
end
|
|
999
1240
|
|
|
1000
1241
|
def dispatch_call_simple(node, scope, name)
|
|
@@ -1009,6 +1250,7 @@ module Rigor
|
|
|
1009
1250
|
when :key?, :has_key? then analyse_key_presence_predicate(node, scope)
|
|
1010
1251
|
when :empty?, :any?, :none? then analyse_array_emptiness_predicate(node, scope, name)
|
|
1011
1252
|
when :respond_to? then analyse_respond_to_predicate(node, scope)
|
|
1253
|
+
when :nan?, :finite? then analyse_float_class_predicate(node, scope, name)
|
|
1012
1254
|
end
|
|
1013
1255
|
end
|
|
1014
1256
|
|
|
@@ -1018,11 +1260,19 @@ module Rigor
|
|
|
1018
1260
|
# symbol is one of `NilClass`'s own methods (`:to_s`, `:inspect`, `:nil?`, …) — `nil`
|
|
1019
1261
|
# DOES respond to those, so the truthy edge admits a nil receiver and we narrow nothing.
|
|
1020
1262
|
#
|
|
1263
|
+
# Issue #1429 — the truthy edge also admits `m` itself. `respond_to?` is a guard written in the code
|
|
1264
|
+
# (ADR-117 Decision point 3), so a member of the receiver's type whose class Rigor knows to lack `m`
|
|
1265
|
+
# is not what the edge holds: members that may respond keep their type, and when no member may, the
|
|
1266
|
+
# receiver reads `Dynamic[top]` ({#respond_to_admitted}). `$stdout.respond_to?(:string) ?
|
|
1267
|
+
# $stdout.string : ""` on an `IO`-typed `$stdout` therefore does not report `undefined method`.
|
|
1268
|
+
# `Dynamic[top]` rather than `Dynamic[IO]`: method availability on a `Dynamic[T]` is checked against
|
|
1269
|
+
# `T` (special-types.md), so only the untyped carrier is sure to keep the guarded call quiet.
|
|
1270
|
+
#
|
|
1021
1271
|
# Conservative floor: narrow only on a literal `Symbol`/`String` argument resolved
|
|
1022
1272
|
# against the RBS environment; a non-literal symbol, a missing argument, or a symbol
|
|
1023
1273
|
# that IS in `NilClass`'s method set declines. The falsey edge is always the no-op
|
|
1024
|
-
# ("does not respond" proves little about the receiver's type).
|
|
1025
|
-
#
|
|
1274
|
+
# ("does not respond" proves little about the receiver's type). A local, instance variable,
|
|
1275
|
+
# global or constant receiver narrows ({#receiver_slot}).
|
|
1026
1276
|
def analyse_respond_to_predicate(node, scope)
|
|
1027
1277
|
return nil if node.block
|
|
1028
1278
|
return nil if node.arguments.nil? || node.arguments.arguments.size != 1
|
|
@@ -1031,16 +1281,63 @@ module Rigor
|
|
|
1031
1281
|
return nil if sym.nil?
|
|
1032
1282
|
return nil if nilclass_method?(sym, scope)
|
|
1033
1283
|
|
|
1034
|
-
|
|
1035
|
-
return nil if
|
|
1284
|
+
slot = receiver_slot(node.receiver, scope)
|
|
1285
|
+
return nil if slot.nil?
|
|
1036
1286
|
|
|
1037
|
-
|
|
1038
|
-
return nil if current.
|
|
1287
|
+
admitted = respond_to_admitted(narrow_non_nil(slot.current), sym.to_sym, scope)
|
|
1288
|
+
return nil if admitted.equal?(slot.current) || admitted == slot.current
|
|
1039
1289
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1290
|
+
[narrow_slot(scope, slot, admitted), scope]
|
|
1291
|
+
end
|
|
1292
|
+
|
|
1293
|
+
# The members of `type` that may respond to `method_name`, or, when Rigor knows every member lacks it
|
|
1294
|
+
# ({#lacks_method?}), `Dynamic[top]`: the members Rigor read the edge from are wrong about the object there,
|
|
1295
|
+
# and no class is named to read it as instead. A member Rigor cannot judge (`Dynamic`, `Bot`, a class no
|
|
1296
|
+
# signature declares) may respond.
|
|
1297
|
+
#
|
|
1298
|
+
# When every member lacking `m` records what the file literally shows (a literal, a `Tuple`, a `HashShape`, a
|
|
1299
|
+
# class object, `nil` / `true` / `false`), the edge is `Bot`. For a literal value that is exact
|
|
1300
|
+
# (`1.respond_to?(:string)` is false in every run); a class object's arm goes unchecked when a gem without a
|
|
1301
|
+
# signature adds the method (`Time.respond_to?(:zone)` under ActiveSupport).
|
|
1302
|
+
def respond_to_admitted(type, method_name, scope)
|
|
1303
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
1304
|
+
kept = members.reject { |member| lacks_method?(member, method_name, scope) }
|
|
1305
|
+
return type if kept.size == members.size
|
|
1306
|
+
return Type::Combinator.union(*kept) unless kept.empty?
|
|
1307
|
+
return Type::Combinator.bot if members.all? { |member| literal_carrier?(member) }
|
|
1042
1308
|
|
|
1043
|
-
|
|
1309
|
+
Type::Combinator.untyped
|
|
1310
|
+
end
|
|
1311
|
+
|
|
1312
|
+
LITERAL_CARRIERS = [Type::Constant, Type::Tuple, Type::HashShape, Type::Singleton].freeze
|
|
1313
|
+
private_constant :LITERAL_CARRIERS
|
|
1314
|
+
|
|
1315
|
+
def literal_carrier?(member)
|
|
1316
|
+
LITERAL_CARRIERS.any? { |carrier| member.is_a?(carrier) } ||
|
|
1317
|
+
(member.is_a?(Type::Nominal) && VALUE_PINNED_CLASSES.include?(member.class_name))
|
|
1318
|
+
end
|
|
1319
|
+
|
|
1320
|
+
# True only when every class `member` dispatches through is declared in RBS, is not a mixin module or
|
|
1321
|
+
# `Object` / `BasicObject` (whose instances may be of any class), is not a plugin's open receiver, and
|
|
1322
|
+
# neither its signature nor the project defines `method_name` on it.
|
|
1323
|
+
def lacks_method?(member, method_name, scope)
|
|
1324
|
+
targets = ProjectMethodOwnership.targets(member)
|
|
1325
|
+
return false if targets.nil? || targets.empty?
|
|
1326
|
+
|
|
1327
|
+
targets.all? { |class_name, kind| class_lacks_method?(class_name, method_name, kind, scope) }
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
OPEN_SURFACE_CLASSES = %w[Object BasicObject].freeze
|
|
1331
|
+
private_constant :OPEN_SURFACE_CLASSES
|
|
1332
|
+
|
|
1333
|
+
def class_lacks_method?(class_name, method_name, kind, scope)
|
|
1334
|
+
return false if kind == :instance && OPEN_SURFACE_CLASSES.include?(class_name)
|
|
1335
|
+
return false unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
1336
|
+
return false if kind == :instance && scope.environment.rbs_module?(class_name)
|
|
1337
|
+
return false if scope.environment.plugin_registry&.open_receiver?(class_name)
|
|
1338
|
+
return false if ProjectMethodOwnership.defines?(class_name, method_name, kind, scope)
|
|
1339
|
+
|
|
1340
|
+
ExternalAncestorResolution.method_definition(class_name, method_name, kind, scope: scope).nil?
|
|
1044
1341
|
end
|
|
1045
1342
|
|
|
1046
1343
|
# True when `nil` responds to `sym` — i.e. `NilClass` (own, inherited
|
|
@@ -1359,13 +1656,17 @@ module Rigor
|
|
|
1359
1656
|
# `String`; every other numbered group present in the pattern stays `String | nil` on
|
|
1360
1657
|
# both edges (a truthy match leaves an optional group nil at runtime), so we do not
|
|
1361
1658
|
# narrow it on the truthy edge.
|
|
1659
|
+
#
|
|
1660
|
+
# Both edges start from `scope` with the match globals forgotten (#1385): the match rebinds every one of them,
|
|
1661
|
+
# so a `$N` an earlier match narrowed says nothing here. Without that, a `when /(x)/` arm, or the right operand
|
|
1662
|
+
# of `s =~ /(a)(b)(c)/ && t =~ /(x)/`, kept `$2` and `$3` narrowed to `String` where Ruby reads nil.
|
|
1362
1663
|
def regex_match_predicate_scopes(scope, unconditional)
|
|
1363
1664
|
string_t = Type::Combinator.nominal_of("String")
|
|
1364
1665
|
match_data_t = Type::Combinator.nominal_of("MatchData")
|
|
1365
1666
|
nil_t = Type::Combinator.constant_of(nil)
|
|
1366
1667
|
|
|
1367
|
-
truthy = scope
|
|
1368
|
-
falsey =
|
|
1668
|
+
truthy = scope.forget_match_globals
|
|
1669
|
+
falsey = truthy
|
|
1369
1670
|
truthy = truthy.with_global(:$~, match_data_t)
|
|
1370
1671
|
falsey = falsey.with_global(:$~, nil_t)
|
|
1371
1672
|
REGEX_MATCH_GLOBALS.each do |name|
|
|
@@ -1399,8 +1700,10 @@ module Rigor
|
|
|
1399
1700
|
# them to non-nil `String` would be unsound. The walker is intentionally light (char
|
|
1400
1701
|
# scan, not a regex-AST parse): backslash escapes are skipped; `(?:…)`, lookahead
|
|
1401
1702
|
# `(?=…)`/`(?!…)`, and lookbehind `(?<=…)`/`(?<!…)` do not capture; named groups
|
|
1402
|
-
# `(?<name>…)` do.
|
|
1403
|
-
#
|
|
1703
|
+
# `(?<name>…)` / `(?'name'…)` do. Once the pattern has a named group, Ruby numbers only
|
|
1704
|
+
# the named groups and a plain `(…)` does not capture (#1471): `/(?<x>a)(b)/` has one
|
|
1705
|
+
# group, so `$2` is nil. Conservatism is one-directional — when in doubt a group is
|
|
1706
|
+
# treated as conditional (dropped from the Set), never the reverse.
|
|
1404
1707
|
def unconditional_capture_groups(source)
|
|
1405
1708
|
# `unconditional` collects every capturing index; a group is later removed (with its
|
|
1406
1709
|
# whole subtree) when it is optionally quantified, nested under an optional ancestor,
|
|
@@ -1410,7 +1713,15 @@ module Rigor
|
|
|
1410
1713
|
# mutually exclusive, so its descendant captures may be absent on a successful match;
|
|
1411
1714
|
# the group itself still participates. Closing a frame rolls its subtree up to the
|
|
1412
1715
|
# parent so an optional / alternated ancestor disqualifies it.
|
|
1413
|
-
state = { unconditional: Set.new, stack: [[nil, [], false]], group_index: 0 }
|
|
1716
|
+
state = { unconditional: Set.new, stack: [[nil, [], false]], group_index: 0, named: named_groups?(source) }
|
|
1717
|
+
each_structural_char(source) { |pos, chr| scan_group_char(source, pos, chr, state) }
|
|
1718
|
+
# Drain the virtual root: a top-level `|` disqualifies all.
|
|
1719
|
+
finalize_frame(state, state[:stack].pop, optional: false)
|
|
1720
|
+
state[:unconditional]
|
|
1721
|
+
end
|
|
1722
|
+
|
|
1723
|
+
# Yields each position of `source` outside a backslash escape and a character class, with its char.
|
|
1724
|
+
def each_structural_char(source)
|
|
1414
1725
|
pos = 0
|
|
1415
1726
|
length = source.length
|
|
1416
1727
|
while pos < length
|
|
@@ -1423,12 +1734,13 @@ module Rigor
|
|
|
1423
1734
|
pos = skip_char_class(source, pos) + 1
|
|
1424
1735
|
next
|
|
1425
1736
|
end
|
|
1426
|
-
|
|
1737
|
+
yield pos, chr
|
|
1427
1738
|
pos += 1
|
|
1428
1739
|
end
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1740
|
+
end
|
|
1741
|
+
|
|
1742
|
+
def named_groups?(source)
|
|
1743
|
+
each_structural_char(source) { |pos, chr| break true if chr == "(" && named_group?(source, pos) } == true
|
|
1432
1744
|
end
|
|
1433
1745
|
|
|
1434
1746
|
# Updates the walk `state` at a group-relevant char during
|
|
@@ -1438,7 +1750,7 @@ module Rigor
|
|
|
1438
1750
|
case chr
|
|
1439
1751
|
when "("
|
|
1440
1752
|
idx = nil
|
|
1441
|
-
if capturing_group?(source, pos)
|
|
1753
|
+
if capturing_group?(source, pos, named: state[:named])
|
|
1442
1754
|
idx = (state[:group_index] += 1)
|
|
1443
1755
|
state[:unconditional] << idx
|
|
1444
1756
|
end
|
|
@@ -1477,11 +1789,20 @@ module Rigor
|
|
|
1477
1789
|
end
|
|
1478
1790
|
end
|
|
1479
1791
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1792
|
+
# A plain `(…)` captures unless the pattern has a named group (`named:`); of the `(?…)` forms only a named
|
|
1793
|
+
# group does.
|
|
1794
|
+
def capturing_group?(source, pos, named:)
|
|
1795
|
+
return !named unless source[pos + 1] == "?"
|
|
1796
|
+
|
|
1797
|
+
named_group?(source, pos)
|
|
1798
|
+
end
|
|
1799
|
+
|
|
1800
|
+
# `(?<name>…)` and `(?'name'…)` are named groups; `(?<=…)`/`(?<!…)` (lookbehind) and
|
|
1801
|
+
# `(?:…)`/`(?=…)`/`(?!…)` are not.
|
|
1802
|
+
def named_group?(source, pos)
|
|
1803
|
+
return false unless source[pos + 1] == "?"
|
|
1804
|
+
return true if source[pos + 2] == "'"
|
|
1482
1805
|
|
|
1483
|
-
# `(?<name>…)` captures; `(?<=…)`/`(?<!…)` (lookbehind) and
|
|
1484
|
-
# `(?:…)`/`(?=…)`/`(?!…)` do not.
|
|
1485
1806
|
source[pos + 2] == "<" && source[pos + 3] != "=" && source[pos + 3] != "!"
|
|
1486
1807
|
end
|
|
1487
1808
|
|
|
@@ -1551,19 +1872,41 @@ module Rigor
|
|
|
1551
1872
|
return nil unless node.arguments.arguments.size == 2
|
|
1552
1873
|
|
|
1553
1874
|
low, high = node.arguments.arguments
|
|
1554
|
-
return nil unless
|
|
1875
|
+
return nil unless numeric_literal_node?(low) && numeric_literal_node?(high)
|
|
1555
1876
|
|
|
1556
1877
|
local_name = node.receiver.name
|
|
1557
1878
|
current = scope.local(local_name)
|
|
1558
1879
|
return nil if current.nil?
|
|
1559
1880
|
|
|
1560
|
-
|
|
1561
|
-
|
|
1881
|
+
# Truthy edge only, for Integer and Float members alike (ADR-109 WD5): a Float literal
|
|
1882
|
+
# bound leaves the Integer members untouched, and vice versa.
|
|
1883
|
+
truthy = narrow_numeric_comparison_truthy(
|
|
1884
|
+
narrow_numeric_comparison_truthy(current, :>=, low.value),
|
|
1562
1885
|
:<=, high.value
|
|
1563
1886
|
)
|
|
1564
1887
|
[scope.with_local(local_name, truthy), scope]
|
|
1565
1888
|
end
|
|
1566
1889
|
|
|
1890
|
+
# `x.nan?` / `x.finite?` (ADR-109 WD5). `nan?` narrows the FALSY edge to the non-NaN Floats
|
|
1891
|
+
# and leaves the truthy edge alone (NaN is not a carrier); `finite?` narrows the TRUTHY
|
|
1892
|
+
# edge to the finite Floats and leaves the falsy edge alone (it admits NaN and ±∞). A
|
|
1893
|
+
# non-Float member is untouched on both edges.
|
|
1894
|
+
def analyse_float_class_predicate(node, scope, predicate)
|
|
1895
|
+
return nil unless argument_free?(node)
|
|
1896
|
+
return nil unless node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
1897
|
+
|
|
1898
|
+
local_name = node.receiver.name
|
|
1899
|
+
current = scope.local(local_name)
|
|
1900
|
+
return nil if current.nil?
|
|
1901
|
+
|
|
1902
|
+
case predicate
|
|
1903
|
+
when :nan?
|
|
1904
|
+
[scope, scope.with_local(local_name, narrow_float_to_range(current, Type::Combinator.non_nan_float))]
|
|
1905
|
+
when :finite?
|
|
1906
|
+
[scope.with_local(local_name, narrow_float_to_range(current, Type::Combinator.finite_float)), scope]
|
|
1907
|
+
end
|
|
1908
|
+
end
|
|
1909
|
+
|
|
1567
1910
|
# Helper for {.narrow_integer_not_equal}. Only adjusts when the value sits exactly on
|
|
1568
1911
|
# one endpoint, so the result stays contiguous; otherwise the input range is preserved.
|
|
1569
1912
|
def narrow_integer_range_not_equal(range, value)
|
|
@@ -1628,20 +1971,96 @@ module Rigor
|
|
|
1628
1971
|
current = scope.local(local_name)
|
|
1629
1972
|
return nil if current.nil?
|
|
1630
1973
|
|
|
1631
|
-
truthy
|
|
1974
|
+
# ADR-109 WD5 — the Float rule runs on the truthy edge only: the falsy edge of `x > c`
|
|
1975
|
+
# admits NaN, so its Float-rooted members keep the entry type (the Integer rule below
|
|
1976
|
+
# leaves them untouched by construction).
|
|
1977
|
+
truthy = narrow_numeric_comparison_truthy(current, normalised_op, bound)
|
|
1632
1978
|
falsey = narrow_integer_comparison(current, INVERT_COMPARISON_OP[normalised_op], bound)
|
|
1633
1979
|
[scope.with_local(local_name, truthy), scope.with_local(local_name, falsey)]
|
|
1634
1980
|
end
|
|
1635
1981
|
|
|
1982
|
+
# An Integer or Float literal. Prism folds a leading minus into the literal, so `-1.5` is one
|
|
1983
|
+
# `FloatNode`.
|
|
1984
|
+
def numeric_literal_node?(node)
|
|
1985
|
+
node.is_a?(Prism::IntegerNode) || node.is_a?(Prism::FloatNode)
|
|
1986
|
+
end
|
|
1987
|
+
|
|
1636
1988
|
def comparison_local_literal(left, right, comparator)
|
|
1637
|
-
if left.is_a?(Prism::LocalVariableReadNode) &&
|
|
1989
|
+
if left.is_a?(Prism::LocalVariableReadNode) && numeric_literal_node?(right)
|
|
1638
1990
|
return [left.name, comparator, right.value]
|
|
1639
1991
|
end
|
|
1640
|
-
return nil unless right.is_a?(Prism::LocalVariableReadNode) &&
|
|
1992
|
+
return nil unless right.is_a?(Prism::LocalVariableReadNode) && numeric_literal_node?(left)
|
|
1641
1993
|
|
|
1642
1994
|
[right.name, REVERSE_COMPARISON_OP[comparator], left.value]
|
|
1643
1995
|
end
|
|
1644
1996
|
|
|
1997
|
+
def narrow_float_comparison_dispatch(type, comparator, bound)
|
|
1998
|
+
case type
|
|
1999
|
+
when Type::Constant
|
|
2000
|
+
# A non-Float constant belongs to another rule and is never dropped here. The test is
|
|
2001
|
+
# the positive comparison, negated once: NaN fails every comparison, which is Ruby's
|
|
2002
|
+
# answer too, and rewriting `!(v < c)` as `v >= c` would silently let NaN through.
|
|
2003
|
+
return type unless type.value.is_a?(Float)
|
|
2004
|
+
|
|
2005
|
+
float_constant_satisfies?(type.value, comparator, bound) ? type : Type::Combinator.bot
|
|
2006
|
+
when Type::FloatRange
|
|
2007
|
+
lo, hi, hi_exclusive = float_comparison_half_line(comparator, bound)
|
|
2008
|
+
intersect_float_bounds(type, lo, hi, hi_exclusive)
|
|
2009
|
+
when Type::Nominal
|
|
2010
|
+
return type unless float_nominal?(type)
|
|
2011
|
+
|
|
2012
|
+
lo, hi, hi_exclusive = float_comparison_half_line(comparator, bound)
|
|
2013
|
+
intersect_float_bounds(Type::Combinator.non_nan_float, lo, hi, hi_exclusive)
|
|
2014
|
+
when Type::Union
|
|
2015
|
+
Type::Combinator.union(
|
|
2016
|
+
*type.members.map { |m| narrow_float_comparison_dispatch(m, comparator, bound) }
|
|
2017
|
+
)
|
|
2018
|
+
else
|
|
2019
|
+
type
|
|
2020
|
+
end
|
|
2021
|
+
end
|
|
2022
|
+
|
|
2023
|
+
def float_nominal?(nominal)
|
|
2024
|
+
nominal.class_name == "Float" && nominal.type_args.empty?
|
|
2025
|
+
end
|
|
2026
|
+
|
|
2027
|
+
def float_constant_satisfies?(value, comparator, bound)
|
|
2028
|
+
case comparator
|
|
2029
|
+
when :< then value < bound
|
|
2030
|
+
when :<= then value <= bound
|
|
2031
|
+
when :> then value > bound
|
|
2032
|
+
when :>= then value >= bound
|
|
2033
|
+
end
|
|
2034
|
+
end
|
|
2035
|
+
|
|
2036
|
+
# `[lo, hi]` with `hi` exclusive or not: `x < c` is `[-∞, c)`, `x <= c` is `[-∞, c]`, and both
|
|
2037
|
+
# `x > c` and `x >= c` are the closed envelope `[c, ∞]` (WD5).
|
|
2038
|
+
def float_comparison_half_line(comparator, bound)
|
|
2039
|
+
case comparator
|
|
2040
|
+
when :< then [-Float::INFINITY, bound, true]
|
|
2041
|
+
when :<= then [-Float::INFINITY, bound, false]
|
|
2042
|
+
else [bound, Float::INFINITY, false]
|
|
2043
|
+
end
|
|
2044
|
+
end
|
|
2045
|
+
|
|
2046
|
+
# Intersects `range` with `[lo, hi]` (`hi` exclusive when `hi_exclusive`), keeping whichever
|
|
2047
|
+
# upper bound is tighter on the canonical closed form together with its own exclusivity, so
|
|
2048
|
+
# `Float[0.0...1.0] ∩ [-∞, 2.0]` still displays `Float[0.0...1.0]`. Empty is `Bot`.
|
|
2049
|
+
def intersect_float_bounds(range, lower, upper, upper_exclusive)
|
|
2050
|
+
new_min = [range.min, lower].max
|
|
2051
|
+
half_canonical_max = upper_exclusive ? upper.prev_float : upper
|
|
2052
|
+
if half_canonical_max < range.canonical_max
|
|
2053
|
+
new_max = upper
|
|
2054
|
+
exclusive = upper_exclusive
|
|
2055
|
+
else
|
|
2056
|
+
new_max = range.max
|
|
2057
|
+
exclusive = range.exclude_end?
|
|
2058
|
+
end
|
|
2059
|
+
return Type::Combinator.bot if new_min > (exclusive ? new_max.prev_float : new_max)
|
|
2060
|
+
|
|
2061
|
+
Type::Combinator.float_range(new_min, new_max, exclude_end: exclusive)
|
|
2062
|
+
end
|
|
2063
|
+
|
|
1645
2064
|
def narrow_integer_equal_dispatch(type, value)
|
|
1646
2065
|
case type
|
|
1647
2066
|
when Type::Constant then narrow_integer_equal_constant(type, value)
|
|
@@ -1814,9 +2233,30 @@ module Rigor
|
|
|
1814
2233
|
analyse_class_predicate_on_local(node, scope, class_name, exact)
|
|
1815
2234
|
when Prism::CallNode
|
|
1816
2235
|
analyse_class_predicate_on_chain(node, scope, class_name, exact)
|
|
2236
|
+
else
|
|
2237
|
+
analyse_class_predicate_on_slot(node.receiver, scope, class_name, exact)
|
|
1817
2238
|
end
|
|
1818
2239
|
end
|
|
1819
2240
|
|
|
2241
|
+
# Issue #1429 — `$stdout.is_a?(StringIO)`, `STDOUT.kind_of?(StringIO)`: a global or constant receiver
|
|
2242
|
+
# narrows as a local does ({#class_predicate_scopes}), with the pre-guard type recorded ({#narrow_slot}).
|
|
2243
|
+
def analyse_class_predicate_on_slot(receiver, scope, class_name, exact)
|
|
2244
|
+
slot = receiver_slot(receiver, scope, kinds: GLOBAL_AND_CONSTANT)
|
|
2245
|
+
return nil if slot.nil?
|
|
2246
|
+
|
|
2247
|
+
class_slot_scopes(scope, slot, class_name, exact: exact)
|
|
2248
|
+
end
|
|
2249
|
+
|
|
2250
|
+
def class_slot_scopes(scope, slot, class_name, exact:)
|
|
2251
|
+
environment = scope.environment
|
|
2252
|
+
truthy = narrow_class(slot.current, class_name, exact: exact, environment: environment, scope: scope)
|
|
2253
|
+
[
|
|
2254
|
+
narrow_slot(scope, slot, truthy),
|
|
2255
|
+
narrow_slot(scope, slot, narrow_not_class(slot.current, class_name, exact: exact,
|
|
2256
|
+
environment: environment, scope: scope))
|
|
2257
|
+
]
|
|
2258
|
+
end
|
|
2259
|
+
|
|
1820
2260
|
# The class name a `is_a?` / `kind_of?` / `instance_of?` argument denotes: the top-level
|
|
1821
2261
|
# `bare_name` when the reference is written `::Foo` (nil when nothing declares it there),
|
|
1822
2262
|
# the lexical resolution otherwise.
|
|
@@ -1854,8 +2294,10 @@ module Rigor
|
|
|
1854
2294
|
current = scope.type_of(node.receiver)
|
|
1855
2295
|
return nil if current.nil?
|
|
1856
2296
|
|
|
1857
|
-
truthy_type = narrow_class(current, class_name, exact: exact, environment: scope.environment
|
|
1858
|
-
|
|
2297
|
+
truthy_type = narrow_class(current, class_name, exact: exact, environment: scope.environment,
|
|
2298
|
+
scope: scope)
|
|
2299
|
+
falsey_type = narrow_not_class(current, class_name, exact: exact, environment: scope.environment,
|
|
2300
|
+
scope: scope)
|
|
1859
2301
|
|
|
1860
2302
|
[
|
|
1861
2303
|
scope.with_method_chain_narrowing(*address, truthy_type),
|
|
@@ -1907,13 +2349,25 @@ module Rigor
|
|
|
1907
2349
|
# shapes at once. Because the repair lives in the shared derivation, `is_a?`, `case`/`when`, `===`
|
|
1908
2350
|
# and the constant typer were fixed together rather than one guard shape at a time. A scope no
|
|
1909
2351
|
# declaration walk built still falls back to the peel; see {Reflection.lexical_nesting_chain}.
|
|
2352
|
+
#
|
|
2353
|
+
# A path whose head and tail have DIFFERENT owners is the one shape no `<prefix>::<full path>`
|
|
2354
|
+
# candidate can name (#656), so the whole-path loop falls through to the segment-wise walk
|
|
2355
|
+
# before it gives up: `A::B` guarded inside `P::Guard` with `P::A < P::Base` is `P::Base::B`,
|
|
2356
|
+
# which the loop above only ever reached as the top-level `A::B` — a real but different class,
|
|
2357
|
+
# and every reader call on the narrowed receiver was then checked against it. The walk is
|
|
2358
|
+
# {Reflection.resolve_constant_path_name}, shared with the constant typer so the guard and the
|
|
2359
|
+
# value it guards cannot name two classes, and it takes this scope's acceptance test so a
|
|
2360
|
+
# candidate is still adopted only when the environment or discovery knows it as a class.
|
|
1910
2361
|
def resolve_class_name_lexically(bare_name, scope)
|
|
1911
2362
|
chain = lexical_nesting_for(scope)
|
|
1912
2363
|
chain.each do |prefix|
|
|
1913
2364
|
candidate = "#{prefix}::#{bare_name}"
|
|
1914
2365
|
return candidate if class_known_to_scope?(scope, candidate)
|
|
1915
2366
|
end
|
|
1916
|
-
bare_name
|
|
2367
|
+
walked = Reflection.resolve_constant_path_name(bare_name, scope) do |candidate|
|
|
2368
|
+
class_known_to_scope?(scope, candidate)
|
|
2369
|
+
end
|
|
2370
|
+
walked || bare_name
|
|
1917
2371
|
end
|
|
1918
2372
|
|
|
1919
2373
|
# Combines the environment's RBS-known set with the scope's in-source
|
|
@@ -1934,16 +2388,8 @@ module Rigor
|
|
|
1934
2388
|
end
|
|
1935
2389
|
|
|
1936
2390
|
def class_predicate_scopes(scope, name, current, class_name, exact:)
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
name,
|
|
1940
|
-
narrow_class(current, class_name, exact: exact, environment: scope.environment)
|
|
1941
|
-
),
|
|
1942
|
-
scope.with_local(
|
|
1943
|
-
name,
|
|
1944
|
-
narrow_not_class(current, class_name, exact: exact, environment: scope.environment)
|
|
1945
|
-
)
|
|
1946
|
-
]
|
|
2391
|
+
slot = ReceiverSlot.new(kind: :local, name: name, current: current)
|
|
2392
|
+
class_slot_scopes(scope, slot, class_name, exact: exact)
|
|
1947
2393
|
end
|
|
1948
2394
|
|
|
1949
2395
|
# Slice 7 phase 4 — `===`-narrowing. The case-equality predicate `<receiver> === local`
|
|
@@ -1977,9 +2423,23 @@ module Rigor
|
|
|
1977
2423
|
analyse_case_equality_receiver(node.receiver, scope, arg.name, current)
|
|
1978
2424
|
when Prism::CallNode
|
|
1979
2425
|
analyse_case_equality_on_chain(node.receiver, arg, scope)
|
|
2426
|
+
else
|
|
2427
|
+
analyse_case_equality_on_slot(node.receiver, arg, scope)
|
|
1980
2428
|
end
|
|
1981
2429
|
end
|
|
1982
2430
|
|
|
2431
|
+
# Issue #1429 — `StringIO === $stdout`, `StringIO === STDOUT`: the class-constant receiver form on a global or
|
|
2432
|
+
# constant argument. The Range / Regexp receivers stay local-only.
|
|
2433
|
+
def analyse_case_equality_on_slot(receiver, arg, scope)
|
|
2434
|
+
class_name = lexical_class_name(receiver, scope)
|
|
2435
|
+
return nil if class_name.nil?
|
|
2436
|
+
|
|
2437
|
+
slot = receiver_slot(arg, scope, kinds: GLOBAL_AND_CONSTANT)
|
|
2438
|
+
return nil if slot.nil?
|
|
2439
|
+
|
|
2440
|
+
class_slot_scopes(scope, slot, class_name, exact: false)
|
|
2441
|
+
end
|
|
2442
|
+
|
|
1983
2443
|
# `Class === <local/ivar>.<method>` — the case-equality counterpart of
|
|
1984
2444
|
# {.analyse_class_predicate_on_chain}. The `open3` idiom `if Hash === cmd.last` narrows
|
|
1985
2445
|
# `cmd.last` to the class inside the branch, recorded as a single-hop method-chain
|
|
@@ -1996,8 +2456,10 @@ module Rigor
|
|
|
1996
2456
|
current = scope.type_of(chain_arg)
|
|
1997
2457
|
return nil if current.nil?
|
|
1998
2458
|
|
|
1999
|
-
truthy_type = narrow_class(current, class_name, exact: false, environment: scope.environment
|
|
2000
|
-
|
|
2459
|
+
truthy_type = narrow_class(current, class_name, exact: false, environment: scope.environment,
|
|
2460
|
+
scope: scope)
|
|
2461
|
+
falsey_type = narrow_not_class(current, class_name, exact: false, environment: scope.environment,
|
|
2462
|
+
scope: scope)
|
|
2001
2463
|
[
|
|
2002
2464
|
scope.with_method_chain_narrowing(*address, truthy_type),
|
|
2003
2465
|
scope.with_method_chain_narrowing(*address, falsey_type)
|
|
@@ -2012,7 +2474,8 @@ module Rigor
|
|
|
2012
2474
|
target_class = case_equality_target_class(receiver)
|
|
2013
2475
|
return nil if target_class.nil?
|
|
2014
2476
|
|
|
2015
|
-
narrowed = narrow_class(current, target_class, exact: false, environment: scope.environment
|
|
2477
|
+
narrowed = narrow_class(current, target_class, exact: false, environment: scope.environment,
|
|
2478
|
+
scope: scope)
|
|
2016
2479
|
[
|
|
2017
2480
|
scope.with_local(local_name, narrowed),
|
|
2018
2481
|
scope.with_local(local_name, current)
|
|
@@ -2195,6 +2658,10 @@ module Rigor
|
|
|
2195
2658
|
# that position, or nil when the overload shape does not allow a precise match.
|
|
2196
2659
|
def lookup_positional_arg(call_node, method_def, target_name)
|
|
2197
2660
|
arguments = call_node.arguments&.arguments || []
|
|
2661
|
+
# A `(?)` overload (`RBS::Types::UntypedFunction`, #1430) names no parameter and accepts any argument list,
|
|
2662
|
+
# so a call may reach it with the target's position holding anything: no overload proves the target.
|
|
2663
|
+
return nil unless method_def.method_types.all? { |mt| mt.type.respond_to?(:required_positionals) }
|
|
2664
|
+
|
|
2198
2665
|
method_def.method_types.each do |mt|
|
|
2199
2666
|
params = mt.type.required_positionals + mt.type.optional_positionals
|
|
2200
2667
|
index = params.find_index { |param| param.name == target_name }
|
|
@@ -2210,7 +2677,9 @@ module Rigor
|
|
|
2210
2677
|
# cannot statically classify are treated as "no narrowing": the body falls back to the
|
|
2211
2678
|
# union of what we did learn (or the entry type when nothing learned), and the falsey
|
|
2212
2679
|
# edge is the entry type (because we cannot prove the unknown condition didn't match).
|
|
2213
|
-
|
|
2680
|
+
#
|
|
2681
|
+
# `[body type, next-clause type]` of a `when` clause's conditions over the subject type `current`.
|
|
2682
|
+
def case_when_types(scope, current, conditions)
|
|
2214
2683
|
truthy_members = []
|
|
2215
2684
|
falsey_type = current
|
|
2216
2685
|
fully_narrowable = true
|
|
@@ -2227,10 +2696,7 @@ module Rigor
|
|
|
2227
2696
|
end
|
|
2228
2697
|
|
|
2229
2698
|
truthy = truthy_members.empty? ? current : Type::Combinator.union(*truthy_members)
|
|
2230
|
-
[
|
|
2231
|
-
scope.with_local(local_name, truthy),
|
|
2232
|
-
scope.with_local(local_name, fully_narrowable ? falsey_type : current)
|
|
2233
|
-
]
|
|
2699
|
+
[truthy, fully_narrowable ? falsey_type : current]
|
|
2234
2700
|
end
|
|
2235
2701
|
|
|
2236
2702
|
# Per-condition rule. Returns `nil` when the condition shape is not recognised (caller
|
|
@@ -2283,8 +2749,9 @@ module Rigor
|
|
|
2283
2749
|
|
|
2284
2750
|
def class_when_result(scope, current, target, falsey_acc)
|
|
2285
2751
|
{
|
|
2286
|
-
truthy: narrow_class(current, target, exact: false, environment: scope.environment),
|
|
2287
|
-
falsey: narrow_not_class(falsey_acc, target, exact: false, environment: scope.environment
|
|
2752
|
+
truthy: narrow_class(current, target, exact: false, environment: scope.environment, scope: scope),
|
|
2753
|
+
falsey: narrow_not_class(falsey_acc, target, exact: false, environment: scope.environment,
|
|
2754
|
+
scope: scope),
|
|
2288
2755
|
fully_narrowable: true
|
|
2289
2756
|
}
|
|
2290
2757
|
end
|
|
@@ -2368,6 +2835,11 @@ module Rigor
|
|
|
2368
2835
|
# through {#rooted_class_predicate_name}, which layers #614's extra decline on top of the
|
|
2369
2836
|
# same walk. A rooted `::Foo` names the top level and never a lexically nearer shadow, so
|
|
2370
2837
|
# it keeps the un-walked spelling. nil for any non-constant shape, as before.
|
|
2838
|
+
#
|
|
2839
|
+
# PUBLIC, alone among the helpers in this block (#655): `case`/`when`'s VALUE side lives in
|
|
2840
|
+
# `ExpressionTyper` and matched on the as-written spelling, so the two halves of one `case`
|
|
2841
|
+
# disagreed — the flow side narrowed through this walk while the value side resolved the
|
|
2842
|
+
# pattern against whatever the environment knew under that literal name.
|
|
2371
2843
|
def lexical_class_name(node, scope)
|
|
2372
2844
|
bare_name = static_class_name(node)
|
|
2373
2845
|
return nil if bare_name.nil?
|
|
@@ -2375,6 +2847,7 @@ module Rigor
|
|
|
2375
2847
|
|
|
2376
2848
|
resolve_class_name_lexically(bare_name, scope)
|
|
2377
2849
|
end
|
|
2850
|
+
public :lexical_class_name
|
|
2378
2851
|
|
|
2379
2852
|
# ----- narrow_class / narrow_not_class helpers -----
|
|
2380
2853
|
|
|
@@ -2437,8 +2910,7 @@ module Rigor
|
|
|
2437
2910
|
end
|
|
2438
2911
|
|
|
2439
2912
|
# Issue #657 — `Bot` is the assertion "this can never match", and only a KNOWN-disjoint ordering
|
|
2440
|
-
# supports it
|
|
2441
|
-
# the negative edge (where the pre-state survives) and an over-claim on the positive one:
|
|
2913
|
+
# supports it (the reason {#declines_bot?} exists, and the repro it was written against):
|
|
2442
2914
|
#
|
|
2443
2915
|
# module Taggable; def tag = "t"; end
|
|
2444
2916
|
# class Integer; include Taggable; end
|
|
@@ -2460,8 +2932,7 @@ module Rigor
|
|
|
2460
2932
|
def narrow_constant_to_class(constant, class_name, context)
|
|
2461
2933
|
rigor_class = constant.value.class.name
|
|
2462
2934
|
return constant if subclass_of?(rigor_class, class_name, context)
|
|
2463
|
-
return Type::Combinator.untyped if
|
|
2464
|
-
class_ordering(rigor_class, class_name, context) == :unknown
|
|
2935
|
+
return Type::Combinator.untyped if declines_bot?(rigor_class, class_name, context)
|
|
2465
2936
|
|
|
2466
2937
|
Type::Combinator.bot
|
|
2467
2938
|
end
|
|
@@ -2477,9 +2948,12 @@ module Rigor
|
|
|
2477
2948
|
# `is_a?(Integer)`) narrow DOWN to `Nominal[class_name]`; otherwise (disjoint
|
|
2478
2949
|
# hierarchies under `is_a?`, mismatch under `instance_of?`) collapse to `Bot`.
|
|
2479
2950
|
# Conservative when the analyzer environment cannot resolve either class.
|
|
2951
|
+
#
|
|
2952
|
+
# A disjoint `Bot` narrows the edge but is not always a verdict that the code is dead: see
|
|
2953
|
+
# {.disjoint_nominal_guard?} (issue #1429).
|
|
2480
2954
|
def narrow_nominal_to_class(nominal, class_name, context)
|
|
2481
2955
|
return nominal if nominal.class_name == class_name
|
|
2482
|
-
return
|
|
2956
|
+
return exact_nominal_to_class(nominal, class_name, context) if context.exact
|
|
2483
2957
|
|
|
2484
2958
|
case class_ordering(nominal.class_name, class_name, context)
|
|
2485
2959
|
when :superclass then Type::Combinator.nominal_of(class_name)
|
|
@@ -2494,6 +2968,17 @@ module Rigor
|
|
|
2494
2968
|
end
|
|
2495
2969
|
end
|
|
2496
2970
|
|
|
2971
|
+
# `instance_of?(class_name)` compares the exact class, so its truthy edge holds `class_name` itself. Issue
|
|
2972
|
+
# #1429: when `class_name` is a class below the nominal's (`Numeric` under `instance_of?(Integer)`), that is
|
|
2973
|
+
# the edge's type; the edge was `Bot` before, for every class but the nominal's own. A module is never an
|
|
2974
|
+
# object's class, and any other ordering leaves no class the object can have, so those stay `Bot`.
|
|
2975
|
+
def exact_nominal_to_class(nominal, class_name, context)
|
|
2976
|
+
return Type::Combinator.bot if context.environment.rbs_module?(class_name)
|
|
2977
|
+
return Type::Combinator.bot unless class_ordering(nominal.class_name, class_name, context) == :superclass
|
|
2978
|
+
|
|
2979
|
+
Type::Combinator.nominal_of(class_name)
|
|
2980
|
+
end
|
|
2981
|
+
|
|
2497
2982
|
def narrow_nominal_not_class(nominal, class_name, context)
|
|
2498
2983
|
return Type::Combinator.bot if nominal.class_name == class_name
|
|
2499
2984
|
return nominal if context.exact
|
|
@@ -2505,8 +2990,14 @@ module Rigor
|
|
|
2505
2990
|
end
|
|
2506
2991
|
end
|
|
2507
2992
|
|
|
2993
|
+
# A shape projects to `Array` / `Hash`, and reopening either of those to include a project module
|
|
2994
|
+
# leaves the ordering `:unknown` for the same reason #657's `Integer` does — so `[1, 2]` under `when
|
|
2995
|
+
# Taggable` reached the same unsupported `Bot`. The carrier differs; the missing evidence does not.
|
|
2508
2996
|
def narrow_shape_to_class(shape, projected_class, class_name, context)
|
|
2509
|
-
subclass_of?(projected_class, class_name, context)
|
|
2997
|
+
return shape if subclass_of?(projected_class, class_name, context)
|
|
2998
|
+
return Type::Combinator.untyped if declines_bot?(projected_class, class_name, context)
|
|
2999
|
+
|
|
3000
|
+
Type::Combinator.bot
|
|
2510
3001
|
end
|
|
2511
3002
|
|
|
2512
3003
|
def narrow_shape_not_class(shape, projected_class, class_name, context)
|
|
@@ -2517,8 +3008,69 @@ module Rigor
|
|
|
2517
3008
|
# subclass of `Module`). Asking `Foo.is_a?(Class)` returns true; `Foo.is_a?(Foo)`
|
|
2518
3009
|
# returns false unless `Foo` is `Class` itself. We approximate this by treating
|
|
2519
3010
|
# singletons uniformly as `Class` instances.
|
|
3011
|
+
#
|
|
3012
|
+
# The approximation answers through `Class`, so a target `Class` cannot be ordered against — a
|
|
3013
|
+
# project module, typically — lands on the same `:unknown` #657 collapsed to `Bot`, and `case Widget
|
|
3014
|
+
# when Meta` was reported unreachable where `Widget` had `extend Meta`.
|
|
3015
|
+
#
|
|
3016
|
+
# Issue #898 — that decline is silent about a target the environment DOES order against `Class`, and
|
|
3017
|
+
# `extend Comparable` is one: both names are in RBS, neither includes the other, so the ordering says
|
|
3018
|
+
# `:disjoint` and the collapse fired on an arm MRI takes (`Widget.is_a?(Comparable)` is true — the
|
|
3019
|
+
# `extend` put `Comparable` in `Widget`'s SINGLETON ancestry, which asking about `Class` cannot see).
|
|
3020
|
+
# {#singleton_extend_declines_bot?} supplies the missing half from the project's own `extend` record.
|
|
2520
3021
|
def narrow_singleton_to_class(singleton, class_name, context)
|
|
2521
|
-
subclass_of?("Class", class_name, context)
|
|
3022
|
+
return singleton if subclass_of?("Class", class_name, context)
|
|
3023
|
+
return Type::Combinator.untyped if declines_bot?("Class", class_name, context)
|
|
3024
|
+
return Type::Combinator.untyped if singleton_extend_declines_bot?(singleton, class_name, context)
|
|
3025
|
+
|
|
3026
|
+
Type::Combinator.bot
|
|
3027
|
+
end
|
|
3028
|
+
|
|
3029
|
+
# Whether the class object's own `extend` record forbids the `Bot` the `Class` approximation would
|
|
3030
|
+
# otherwise authorise (#898). An extended `M` puts every ancestor of `M` in the singleton's
|
|
3031
|
+
# ancestry, so the question per extended module is `M <= target`: `:equal` (`extend Comparable`
|
|
3032
|
+
# under `when Comparable`) and `:subclass` support the arm, and `:unknown` (`extend Meta` under
|
|
3033
|
+
# `when Comparable`, `Meta` being project source the environment never joined to RBS) cannot rule
|
|
3034
|
+
# it out. `:superclass` and `:disjoint` support NOTHING and must keep the collapse — `Integer`
|
|
3035
|
+
# under `when Integer` orders `:superclass` against an extended `Comparable`, and reading that as
|
|
3036
|
+
# support would retract `case Widget when Integer` from every class that extends a core module.
|
|
3037
|
+
#
|
|
3038
|
+
# The answer is `Dynamic[top]`, never the preserved `Singleton[C]`, and the negative edge
|
|
3039
|
+
# ({#narrow_singleton_not_class}) is untouched — so this record is read ONLY to withhold a claim,
|
|
3040
|
+
# never to make one. That asymmetry is deliberate, and it is what keeps the fix honest against an
|
|
3041
|
+
# extend record that is structurally incomplete: `Widget.extend(m)` at a call site and an `extend`
|
|
3042
|
+
# written in a file outside the analysed set are both invisible to the walk behind
|
|
3043
|
+
# {Scope#singleton_extends_of}, and the declaration-side record joined to it below is no more
|
|
3044
|
+
# complete — RBS says nothing about a class it does not declare. A shape that turned this table into a positive
|
|
3045
|
+
# `:subclass` verdict would have to be right about ABSENCE too, and it cannot be. Preserving
|
|
3046
|
+
# `Singleton[C]` would also hand the arm a receiver whose singleton method table has no `clamp` on
|
|
3047
|
+
# it, trading the unreachable-clause false positive for an undefined-method one — the same trap
|
|
3048
|
+
# #657 recorded on the `Constant` carrier.
|
|
3049
|
+
#
|
|
3050
|
+
# Issue #915 — two sources, read as one. The source record above sees only what Ruby source spells;
|
|
3051
|
+
# a project that declares `extend M` in its own `sig/` puts the same module in the same singleton
|
|
3052
|
+
# ancestry, and the environment's declaration-side answer
|
|
3053
|
+
# ({Environment#singleton_extended_modules}) is consulted alongside it. Neither is asked to be
|
|
3054
|
+
# complete; each can only add a module that IS there, which is all the one-directional read needs.
|
|
3055
|
+
def singleton_extend_declines_bot?(singleton, class_name, context)
|
|
3056
|
+
return false if context.exact
|
|
3057
|
+
|
|
3058
|
+
extended = singleton_ancestor_modules(singleton, context)
|
|
3059
|
+
return false if extended.empty?
|
|
3060
|
+
|
|
3061
|
+
extended.any? do |module_name|
|
|
3062
|
+
%i[equal subclass unknown].include?(class_ordering(module_name, class_name, context))
|
|
3063
|
+
end
|
|
3064
|
+
end
|
|
3065
|
+
|
|
3066
|
+
def singleton_ancestor_modules(singleton, context)
|
|
3067
|
+
scope = context.scope
|
|
3068
|
+
from_source = scope.nil? ? [] : scope.singleton_extends_of(singleton.class_name)
|
|
3069
|
+
from_rbs = context.environment&.singleton_extended_modules(singleton.class_name) || []
|
|
3070
|
+
return from_source if from_rbs.empty?
|
|
3071
|
+
return from_rbs if from_source.empty?
|
|
3072
|
+
|
|
3073
|
+
from_source | from_rbs
|
|
2522
3074
|
end
|
|
2523
3075
|
|
|
2524
3076
|
def narrow_singleton_not_class(singleton, class_name, context)
|
|
@@ -2534,6 +3086,22 @@ module Rigor
|
|
|
2534
3086
|
end
|
|
2535
3087
|
end
|
|
2536
3088
|
|
|
3089
|
+
# Whether the positive edge must decline the `Bot` that `subclass_of?` returning false would
|
|
3090
|
+
# otherwise authorise (#657). `subclass_of?` folds `:disjoint` and `:unknown` into one `false`, and
|
|
3091
|
+
# only the first is evidence that the guard can never match; the second is the environment saying it
|
|
3092
|
+
# cannot tell, which any file reopening either side can make true.
|
|
3093
|
+
#
|
|
3094
|
+
# This only ever ADDS a decline: every caller keeps its own `subclass_of?` test first, so the
|
|
3095
|
+
# preserving edge is untouched, and a `:disjoint`, `:equal`, `:subclass` or `:superclass` verdict
|
|
3096
|
+
# never reaches here. There is no ordering it turns INTO a match — the honest answer for
|
|
3097
|
+
# "cannot tell" is Dynamic, never the asked class.
|
|
3098
|
+
#
|
|
3099
|
+
# `instance_of?` is exempt because it compares the exact class: a module is never an object's class,
|
|
3100
|
+
# so that edge's `Bot` rests on the guard's own semantics rather than on the ordering.
|
|
3101
|
+
def declines_bot?(rigor_class_name, target_class_name, context)
|
|
3102
|
+
!context.exact && class_ordering(rigor_class_name, target_class_name, context) == :unknown
|
|
3103
|
+
end
|
|
3104
|
+
|
|
2537
3105
|
# Returns `true` when an instance of `rigor_class_name` satisfies
|
|
2538
3106
|
# `is_a?(target_class_name)` (or `instance_of?(target_class_name)` when `exact: true`).
|
|
2539
3107
|
# Falls back to the safe `false` when either name does not resolve through the analyzer
|
|
@@ -2555,25 +3123,15 @@ module Rigor
|
|
|
2555
3123
|
context.environment.class_ordering(lhs, rhs)
|
|
2556
3124
|
end
|
|
2557
3125
|
|
|
3126
|
+
# `x.nil?` on a local, instance variable, global or constant (#1429) read.
|
|
2558
3127
|
def analyse_nil_predicate(receiver, scope)
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
current = scope.local(receiver.name)
|
|
2562
|
-
return nil if current.nil?
|
|
2563
|
-
|
|
2564
|
-
[
|
|
2565
|
-
scope.with_local(receiver.name, narrow_nil(current)),
|
|
2566
|
-
scope.with_local(receiver.name, narrow_non_nil(current))
|
|
2567
|
-
]
|
|
2568
|
-
when Prism::InstanceVariableReadNode
|
|
2569
|
-
current = scope.ivar(receiver.name)
|
|
2570
|
-
return nil if current.nil?
|
|
3128
|
+
slot = receiver_slot(receiver, scope)
|
|
3129
|
+
return nil if slot.nil?
|
|
2571
3130
|
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
end
|
|
3131
|
+
[
|
|
3132
|
+
narrow_slot(scope, slot, narrow_nil(slot.current)),
|
|
3133
|
+
narrow_slot(scope, slot, narrow_non_nil(slot.current))
|
|
3134
|
+
]
|
|
2577
3135
|
end
|
|
2578
3136
|
|
|
2579
3137
|
# The three value-pinned classes a union arm can name whose instances are the single
|
|
@@ -2692,21 +3250,39 @@ module Rigor
|
|
|
2692
3250
|
def analyse_safe_nav_receiver(node, scope)
|
|
2693
3251
|
return nil unless node.safe_navigation?
|
|
2694
3252
|
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
when Prism::LocalVariableReadNode then %i[local with_local]
|
|
2699
|
-
when Prism::InstanceVariableReadNode then %i[ivar with_ivar]
|
|
2700
|
-
else return nil
|
|
2701
|
-
end
|
|
3253
|
+
narrowed = non_nil_slot_scope(node.receiver, scope)
|
|
3254
|
+
narrowed && [narrowed, scope]
|
|
3255
|
+
end
|
|
2702
3256
|
|
|
2703
|
-
|
|
2704
|
-
|
|
3257
|
+
# `scope` with the receiver `node` reads (a local, instance variable, global or constant, #1429) narrowed to
|
|
3258
|
+
# its non-nil fragment, or nil when there is no such receiver or it holds no `nil` to remove.
|
|
3259
|
+
def non_nil_slot_scope(node, scope)
|
|
3260
|
+
slot = receiver_slot(node, scope)
|
|
3261
|
+
return nil if slot.nil?
|
|
2705
3262
|
|
|
2706
|
-
non_nil = narrow_non_nil(current)
|
|
2707
|
-
return nil if non_nil.equal?(current)
|
|
3263
|
+
non_nil = narrow_non_nil(slot.current)
|
|
3264
|
+
return nil if non_nil.equal?(slot.current)
|
|
2708
3265
|
|
|
2709
|
-
|
|
3266
|
+
narrow_slot(scope, slot, non_nil)
|
|
3267
|
+
end
|
|
3268
|
+
|
|
3269
|
+
# The scope {.safe_navigation_scope} answers for a receiver, seen through the `&.` links of a chain whose
|
|
3270
|
+
# operands leave the root's binding alone, or nil. Unlike {.non_nil_slot_scope} it compares the fragment
|
|
3271
|
+
# structurally: `narrow_non_nil` rebuilds a union it removed nothing from, and a receiver that cannot be nil
|
|
3272
|
+
# must leave the call's operands on the scope they are typed from.
|
|
3273
|
+
def non_nil_receiver_scope(node, scope)
|
|
3274
|
+
if node.is_a?(Prism::CallNode)
|
|
3275
|
+
return nil unless node.safe_navigation?
|
|
3276
|
+
return nil if OperandEffects.any?(node.arguments) || OperandEffects.any?(node.block)
|
|
3277
|
+
|
|
3278
|
+
return non_nil_receiver_scope(node.receiver, scope)
|
|
3279
|
+
end
|
|
3280
|
+
|
|
3281
|
+
slot = receiver_slot(node, scope)
|
|
3282
|
+
return nil if slot.nil?
|
|
3283
|
+
|
|
3284
|
+
non_nil = narrow_non_nil(slot.current)
|
|
3285
|
+
non_nil == slot.current ? nil : narrow_slot(scope, slot, non_nil)
|
|
2710
3286
|
end
|
|
2711
3287
|
|
|
2712
3288
|
# Issue #606 slice 1 — the SAFE-NAV CHAIN forms. {.analyse_safe_nav_receiver} above proves
|
|
@@ -2778,12 +3354,17 @@ module Rigor
|
|
|
2778
3354
|
return nil if suffixes.empty? && !allow_bare
|
|
2779
3355
|
|
|
2780
3356
|
receiver = current.receiver
|
|
2781
|
-
return nil unless receiver.is_a?(
|
|
2782
|
-
receiver.is_a?(Prism::InstanceVariableReadNode)
|
|
3357
|
+
return nil unless SLOT_READ_NODES.any? { |klass| receiver.is_a?(klass) }
|
|
2783
3358
|
|
|
2784
3359
|
[receiver, suffixes]
|
|
2785
3360
|
end
|
|
2786
3361
|
|
|
3362
|
+
SLOT_READ_NODES = [
|
|
3363
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::GlobalVariableReadNode,
|
|
3364
|
+
Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
3365
|
+
].freeze
|
|
3366
|
+
private_constant :SLOT_READ_NODES
|
|
3367
|
+
|
|
2787
3368
|
# Which edge — if either — a chain's outcome narrows on. The inner suffixes (everything
|
|
2788
3369
|
# below the outermost) only ever have to NOT return, so they are held to the raise test
|
|
2789
3370
|
# alone; the outermost decides the edge per the table in {NIL_FALSEY_SUFFIXES}'s comment.
|
|
@@ -2798,20 +3379,7 @@ module Rigor
|
|
|
2798
3379
|
end
|
|
2799
3380
|
|
|
2800
3381
|
def safe_nav_chain_narrowed_scope(receiver, scope)
|
|
2801
|
-
|
|
2802
|
-
case receiver
|
|
2803
|
-
when Prism::LocalVariableReadNode then %i[local with_local]
|
|
2804
|
-
when Prism::InstanceVariableReadNode then %i[ivar with_ivar]
|
|
2805
|
-
else return nil
|
|
2806
|
-
end
|
|
2807
|
-
|
|
2808
|
-
current = scope.public_send(reader, receiver.name)
|
|
2809
|
-
return nil if current.nil?
|
|
2810
|
-
|
|
2811
|
-
non_nil = narrow_non_nil(current)
|
|
2812
|
-
return nil if non_nil.equal?(current)
|
|
2813
|
-
|
|
2814
|
-
scope.public_send(writer, receiver.name, non_nil)
|
|
3382
|
+
non_nil_slot_scope(receiver, scope)
|
|
2815
3383
|
end
|
|
2816
3384
|
|
|
2817
3385
|
# Issue #606 slice 1, second form — a safe-nav chain COMPARED to a value that cannot be
|
|
@@ -2921,14 +3489,23 @@ module Rigor
|
|
|
2921
3489
|
return edges unless node.safe_navigation? && edges
|
|
2922
3490
|
|
|
2923
3491
|
truthy, falsey = edges
|
|
2924
|
-
|
|
2925
|
-
return edges
|
|
3492
|
+
slot = receiver_slot(node.receiver, scope)
|
|
3493
|
+
return edges if slot.nil?
|
|
2926
3494
|
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
3495
|
+
non_nil = narrow_non_nil(slot.current)
|
|
3496
|
+
return edges if non_nil.equal?(slot.current)
|
|
3497
|
+
|
|
3498
|
+
[narrow_slot(truthy, slot, non_nil), falsey]
|
|
3499
|
+
end
|
|
3500
|
+
|
|
3501
|
+
# The predicates composed from other predicates, whose edges are built from their
|
|
3502
|
+
# operands' edges.
|
|
3503
|
+
def analyse_composition(node, scope)
|
|
3504
|
+
case node
|
|
3505
|
+
when Prism::AndNode then analyse_and(node, scope)
|
|
3506
|
+
when Prism::OrNode then analyse_or(node, scope)
|
|
3507
|
+
else analyse_conditional(node, scope)
|
|
3508
|
+
end
|
|
2932
3509
|
end
|
|
2933
3510
|
|
|
2934
3511
|
# `a && b` short-circuits: the truthy edge is the truthy edge of `b` evaluated under
|
|
@@ -2938,7 +3515,7 @@ module Rigor
|
|
|
2938
3515
|
# output scopes.
|
|
2939
3516
|
def analyse_and(node, scope)
|
|
2940
3517
|
truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
|
|
2941
|
-
truthy_b, falsey_b =
|
|
3518
|
+
truthy_b, falsey_b = operand_edges(node.right, truthy_a)
|
|
2942
3519
|
[truthy_b, falsey_a.join(falsey_b)]
|
|
2943
3520
|
end
|
|
2944
3521
|
|
|
@@ -2947,9 +3524,105 @@ module Rigor
|
|
|
2947
3524
|
# falsey scope evaluated under `a`'s falsey scope.
|
|
2948
3525
|
def analyse_or(node, scope)
|
|
2949
3526
|
truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
|
|
2950
|
-
truthy_b, falsey_b =
|
|
3527
|
+
truthy_b, falsey_b = operand_edges(node.right, falsey_a)
|
|
2951
3528
|
[truthy_a.join(truthy_b), falsey_b]
|
|
2952
3529
|
end
|
|
3530
|
+
|
|
3531
|
+
# The edges of an operand that runs only on one edge of an earlier operand, which is the
|
|
3532
|
+
# step `&&` and `||` both take for their right operand. Issue #1359 — the operand runs after
|
|
3533
|
+
# that edge, so a `$_` the earlier operand narrowed is forgotten when the operand may set it
|
|
3534
|
+
# (`gets && log(items.each { gets })`), before the operand's own edges narrow it again.
|
|
3535
|
+
# Issue #1429 — likewise a guard's narrowing of a global or constant the operand may rebind
|
|
3536
|
+
# (`$sep && reset_sep && $sep.length`).
|
|
3537
|
+
def operand_edges(node, edge_scope)
|
|
3538
|
+
edge_scope = LastLine.forget_if_set(edge_scope, node)
|
|
3539
|
+
if edge_scope.guard_narrowed? && GuardRebinding.may_rebind?(node, edge_scope)
|
|
3540
|
+
edge_scope = edge_scope.forget_guard_narrowings
|
|
3541
|
+
end
|
|
3542
|
+
analyse(node, edge_scope) || [edge_scope, edge_scope]
|
|
3543
|
+
end
|
|
3544
|
+
|
|
3545
|
+
# Issue #1017 — `p ? q : r` (and `if` / `unless` in condition position) is truthy through
|
|
3546
|
+
# `p ∧ q` or `¬p ∧ r` and falsey through `p ∧ ¬q` or `¬p ∧ ¬r`. Each conjunction is the
|
|
3547
|
+
# `&&` step — the arm's edges under the predicate's edge ({#operand_edges}) — and each
|
|
3548
|
+
# disjunction is the `||` merge (`Scope#join`), so `p ? false : q` narrows as `!p && q`
|
|
3549
|
+
# does without a literal-arm rule: an arm whose value is settled contributes only its
|
|
3550
|
+
# live edge ({#settled_edges}). Beyond {CONDITIONAL_GUARD_DEPTH} the guard declines.
|
|
3551
|
+
#
|
|
3552
|
+
# The predicate's own edges are never dropped as settled. `scope` is the scope after the
|
|
3553
|
+
# whole conditional ran, so it has already joined the arms, and a carrier every arm
|
|
3554
|
+
# narrowed has lost its optimistic mark there (`Scope#with_local` drops it): a
|
|
3555
|
+
# `missable.nil?` predicate reads as a proof-looking `false` it is not. An arm is judged
|
|
3556
|
+
# under the predicate's edge instead, where a settled value follows from that edge.
|
|
3557
|
+
def analyse_conditional(node, scope)
|
|
3558
|
+
return nil if conditional_guard_height(node) > CONDITIONAL_GUARD_DEPTH
|
|
3559
|
+
|
|
3560
|
+
predicate, then_arm, else_arm = conditional_parts(node)
|
|
3561
|
+
truthy_p, falsey_p = operand_edges(predicate, scope)
|
|
3562
|
+
truthy_q, falsey_q = settled_edges(then_arm, truthy_p)
|
|
3563
|
+
truthy_r, falsey_r = settled_edges(else_arm, falsey_p)
|
|
3564
|
+
truthy = join_live_edges(truthy_q, truthy_r)
|
|
3565
|
+
falsey = join_live_edges(falsey_q, falsey_r)
|
|
3566
|
+
return nil if truthy.nil? && falsey.nil?
|
|
3567
|
+
|
|
3568
|
+
[truthy || scope, falsey || scope]
|
|
3569
|
+
end
|
|
3570
|
+
|
|
3571
|
+
# `[predicate, arm run when it is truthy, arm run when it is falsey]`. An arm is a body or
|
|
3572
|
+
# `nil` for an absent / empty one; an `elsif` is the nested conditional itself.
|
|
3573
|
+
def conditional_parts(node)
|
|
3574
|
+
if node.is_a?(Prism::UnlessNode)
|
|
3575
|
+
[node.predicate, else_body(node.else_clause), node.statements]
|
|
3576
|
+
else
|
|
3577
|
+
subsequent = node.subsequent
|
|
3578
|
+
[node.predicate, node.statements, subsequent.is_a?(Prism::ElseNode) ? else_body(subsequent) : subsequent]
|
|
3579
|
+
end
|
|
3580
|
+
end
|
|
3581
|
+
|
|
3582
|
+
def else_body(else_node)
|
|
3583
|
+
else_node&.statements
|
|
3584
|
+
end
|
|
3585
|
+
|
|
3586
|
+
# An operand's `[truthy, falsey]` edges with a dead edge as `nil`. An absent arm evaluates
|
|
3587
|
+
# to `nil`, so only its falsey edge is live. Otherwise the edge the operand's type proves
|
|
3588
|
+
# unreachable is dropped — unless that proof rests on an optimistically nil-free lookup
|
|
3589
|
+
# (ADR-101, issue #313), which `branch_certainty` refuses to elide an arm on as well.
|
|
3590
|
+
def settled_edges(node, scope)
|
|
3591
|
+
return [nil, scope] if node.nil?
|
|
3592
|
+
|
|
3593
|
+
truthy, falsey = operand_edges(node, scope)
|
|
3594
|
+
case predicate_certainty(scope.type_of(node))
|
|
3595
|
+
when :truthy then optimistic_operand?(node, scope) ? [truthy, falsey] : [truthy, nil]
|
|
3596
|
+
when :falsey then optimistic_operand?(node, scope) ? [truthy, falsey] : [nil, falsey]
|
|
3597
|
+
else [truthy, falsey]
|
|
3598
|
+
end
|
|
3599
|
+
end
|
|
3600
|
+
|
|
3601
|
+
def optimistic_operand?(node, scope)
|
|
3602
|
+
node = node.body.last while node.is_a?(Prism::StatementsNode) && !node.body.empty?
|
|
3603
|
+
!OptimisticOrigin.resolve(node, scope).nil?
|
|
3604
|
+
end
|
|
3605
|
+
|
|
3606
|
+
def join_live_edges(left, right)
|
|
3607
|
+
return right if left.nil?
|
|
3608
|
+
return left if right.nil?
|
|
3609
|
+
|
|
3610
|
+
left.join(right)
|
|
3611
|
+
end
|
|
3612
|
+
|
|
3613
|
+
# The number of conditionals stacked in `node` along the shapes {.analyse} walks.
|
|
3614
|
+
def conditional_guard_height(node)
|
|
3615
|
+
case node
|
|
3616
|
+
when Prism::IfNode, Prism::UnlessNode
|
|
3617
|
+
1 + conditional_parts(node).map { |part| conditional_guard_height(part) }.max
|
|
3618
|
+
when Prism::ParenthesesNode then conditional_guard_height(node.body)
|
|
3619
|
+
when Prism::StatementsNode then conditional_guard_height(node.body.last)
|
|
3620
|
+
when Prism::AndNode, Prism::OrNode
|
|
3621
|
+
[conditional_guard_height(node.left), conditional_guard_height(node.right)].max
|
|
3622
|
+
when Prism::CallNode then node.name == :! ? conditional_guard_height(node.receiver) : 0
|
|
3623
|
+
else 0
|
|
3624
|
+
end
|
|
3625
|
+
end
|
|
2953
3626
|
end
|
|
2954
3627
|
end
|
|
2955
3628
|
end
|