rigortype 0.3.9 → 0.4.1
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 +5 -4
- 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 +127 -4
- data/docs/handbook/03-narrowing.md +95 -10
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/07-rbs-and-extended.md +16 -11
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +454 -13
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +65 -4
- data/docs/manual/03-configuration.md +12 -0
- data/docs/manual/04-diagnostics.md +4 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/docs/manual/16-rbs-extended-annotations.md +40 -7
- 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 +243 -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 +110 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
- 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 +212 -25
- 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 +1 -1
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +427 -0
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
- data/lib/rigor/analysis/check_rules.rb +638 -136
- data/lib/rigor/analysis/dependency_recorder.rb +222 -17
- data/lib/rigor/analysis/fact_store.rb +9 -0
- data/lib/rigor/analysis/incremental.rb +26 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
- data/lib/rigor/analysis/incremental_session.rb +309 -53
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
- data/lib/rigor/analysis/reachability/scan_cache.rb +3 -1
- data/lib/rigor/analysis/rule_catalog.rb +127 -0
- data/lib/rigor/analysis/run_cache_key.rb +46 -18
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
- data/lib/rigor/analysis/runner.rb +603 -54
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +93 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +409 -0
- data/lib/rigor/analysis/worker_session.rb +57 -13
- data/lib/rigor/bleeding_edge.rb +0 -2
- data/lib/rigor/builtins/hkt_builtins.rb +1 -0
- data/lib/rigor/builtins/imported_refinements.rb +4 -0
- data/lib/rigor/builtins/regex_refinement.rb +17 -9
- data/lib/rigor/builtins/static_return_refinements.rb +2 -0
- data/lib/rigor/cache/descriptor.rb +45 -1
- data/lib/rigor/cache/engine_source.rb +29 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +102 -8
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_descriptor.rb +37 -7
- data/lib/rigor/cache/store.rb +63 -4
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/check_command.rb +83 -13
- data/lib/rigor/cli/doc_links.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +4 -4
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/plugin_command.rb +3 -3
- data/lib/rigor/cli/prism_colorizer.rb +0 -1
- data/lib/rigor/cli/sig_gen_command.rb +210 -29
- data/lib/rigor/cli/skill_command.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +0 -1
- data/lib/rigor/cli/type_of_command.rb +19 -5
- 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/unused_command.rb +3 -3
- data/lib/rigor/cli.rb +21 -3
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +24 -3
- data/lib/rigor/configuration.rb +71 -7
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +11 -2
- 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 +9 -2
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/effect_table.rb +11 -3
- data/lib/rigor/effects/envelope_check.rb +1 -1
- data/lib/rigor/effects/envelope_index.rb +15 -0
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/identity.rb +16 -0
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +21 -0
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +49 -37
- data/lib/rigor/effects/propagator.rb +295 -16
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +121 -72
- data/lib/rigor/effects/signature_sources.rb +1 -1
- data/lib/rigor/effects/snapshot.rb +2 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +385 -30
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/lockfile_resolver.rb +27 -1
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_loader.rb +400 -134
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +104 -96
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +139 -9
- 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 +88 -26
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +2 -1
- data/lib/rigor/inference/budget_trace.rb +2 -1
- 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 +393 -15
- data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +3 -2
- 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/definer_resolution.rb +266 -0
- data/lib/rigor/inference/element_read_widening.rb +22 -10
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +1665 -538
- data/lib/rigor/inference/external_ancestor_resolution.rb +264 -0
- 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 +488 -0
- data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
- data/lib/rigor/inference/index_write_widening.rb +16 -3
- 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 +179 -17
- 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 +85 -24
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
- 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/rbs_dispatch.rb +901 -38
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +214 -13
- data/lib/rigor/inference/method_parameter_binder.rb +8 -3
- data/lib/rigor/inference/module_function_state.rb +213 -0
- data/lib/rigor/inference/multi_target_binder.rb +345 -52
- data/lib/rigor/inference/mutation_rejoin.rb +4 -1
- data/lib/rigor/inference/mutation_widening.rb +70 -48
- data/lib/rigor/inference/narrowing.rb +674 -124
- 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 +152 -9
- data/lib/rigor/inference/parameter_inference_collector.rb +13 -11
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +7 -2
- data/lib/rigor/inference/project_patched_scanner.rb +7 -3
- data/lib/rigor/inference/receiver_alias.rb +90 -1
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +15 -11
- 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/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +5913 -701
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +3249 -497
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +44 -7
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
- data/lib/rigor/language_server/hover_renderer.rb +3 -3
- data/lib/rigor/language_server/project_context.rb +5 -3
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +196 -13
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +1 -1
- data/lib/rigor/plugin/effect_attribution.rb +58 -4
- data/lib/rigor/plugin/io_boundary.rb +82 -4
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/loader.rb +2 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/manifest.rb +71 -10
- data/lib/rigor/plugin/registry.rb +35 -1
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/discovery_seed.rb +20 -9
- data/lib/rigor/protection/kill_signature.rb +0 -1
- data/lib/rigor/protection/mutation_cache.rb +4 -5
- data/lib/rigor/rbs_extended.rb +27 -0
- data/lib/rigor/reflection/constant_ancestors.rb +146 -0
- data/lib/rigor/reflection/constant_path.rb +41 -13
- data/lib/rigor/reflection.rb +73 -107
- data/lib/rigor/scope/discovery_index.rb +351 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +884 -0
- data/lib/rigor/scope.rb +981 -198
- 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 +580 -47
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +124 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +166 -35
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +1 -1
- data/lib/rigor/source/node_walker.rb +0 -3
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/combinator.rb +112 -12
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/hash_shape.rb +1 -1
- data/lib/rigor/type/refined.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
- 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 +239 -4
- 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/effects.rb +1 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -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 +380 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -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.rb +72 -2
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
- 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 +217 -36
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -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/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 +14 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +6 -4
- 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 +2 -1
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +50 -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 +7 -6
- data/sig/rigor/scope.rbs +177 -20
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +6 -0
- data/sig/rigor.rbs +38 -20
- data/skills/rigor-ask/SKILL.md +8 -5
- 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 +12 -25
- 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 +46 -18
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +40 -10
- 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-protection-uplift/SKILL.md +4 -6
- data/skills/rigor-rbs-setup/SKILL.md +4 -2
- data/skills/rigor-type-oracle/SKILL.md +4 -6
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
- data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +132 -1
|
@@ -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
|
|
@@ -50,8 +56,27 @@ module Rigor
|
|
|
50
56
|
# a value-pinned `Constant[Regexp]` share this carrier so the participation walk and the
|
|
51
57
|
# extended-mode bail read one shape.
|
|
52
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
|
|
53
72
|
private_constant :TRUSTED_EQUALITY_LITERAL_CLASSES, :SINGLETON_LITERAL_CLASSES, :ClassNarrowingContext,
|
|
54
|
-
: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
|
|
55
80
|
|
|
56
81
|
module_function
|
|
57
82
|
|
|
@@ -120,10 +145,9 @@ module Rigor
|
|
|
120
145
|
# falsey fragments above: `:truthy` when no inhabitant is falsey (the falsey fragment is
|
|
121
146
|
# `Bot`), `:falsey` when no inhabitant is truthy, nil when both fragments are inhabited —
|
|
122
147
|
# or when the type itself is nil / `Bot` (dead code is not a certainty claim). This is the
|
|
123
|
-
# single owner of the judgment
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
# 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.
|
|
127
151
|
def predicate_certainty(type)
|
|
128
152
|
return nil if type.nil? || type.is_a?(Type::Bot)
|
|
129
153
|
|
|
@@ -320,6 +344,22 @@ module Rigor
|
|
|
320
344
|
narrow_class_dispatch(type, class_name, context)
|
|
321
345
|
end
|
|
322
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
|
+
|
|
323
363
|
# Mirror of {.narrow_class} for the falsey edge of `is_a?`/`kind_of?`/`instance_of?`.
|
|
324
364
|
# Inhabitants that DO satisfy the predicate are removed; inhabitants that do not are
|
|
325
365
|
# preserved. Conservative on Top/Dynamic/Bot (preserved unchanged) because the analyzer
|
|
@@ -390,6 +430,30 @@ module Rigor
|
|
|
390
430
|
fact.type
|
|
391
431
|
end
|
|
392
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
|
+
|
|
443
|
+
# Issue #1446 — the classes a class guard narrowed the receiver `node` to where the guard left it `bot`
|
|
444
|
+
# ({Scope#bot_guard_classes_for}), or nil: a node that is not a local, instance-variable, global or constant
|
|
445
|
+
# read, or one no such guard narrowed.
|
|
446
|
+
def bot_guard_classes_of(node, scope)
|
|
447
|
+
case node
|
|
448
|
+
when Prism::LocalVariableReadNode then scope.bot_guard_classes_for(:local, node.name)
|
|
449
|
+
when Prism::InstanceVariableReadNode then scope.bot_guard_classes_for(:ivar, node.name)
|
|
450
|
+
when Prism::GlobalVariableReadNode then scope.bot_guard_classes_for(:global, node.name)
|
|
451
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
452
|
+
key = constant_key(node)
|
|
453
|
+
key && scope.bot_guard_classes_for(:constant, key)
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
393
457
|
# Public predicate analyser. Returns `[truthy_scope, falsey_scope]`, always; when no
|
|
394
458
|
# narrowing rule matches the predicate node both entries are the receiver scope unchanged.
|
|
395
459
|
def predicate_scopes(node, scope)
|
|
@@ -399,6 +463,37 @@ module Rigor
|
|
|
399
463
|
result || [scope, scope]
|
|
400
464
|
end
|
|
401
465
|
|
|
466
|
+
# Issue #1468 — the scope the arguments and block of a safe-navigation call `recv&.m(…)` run under. Ruby
|
|
467
|
+
# evaluates them only once `recv` is known non-nil, so `klass&.new(klass.flag?)` reads `klass` without its
|
|
468
|
+
# `nil`, as `if klass then klass.new(klass.flag?) end` does. Only `nil` is removed: `false&.m(x)` still calls
|
|
469
|
+
# `m`. The call's own value keeps its skipped-call `nil` (`ExpressionTyper#safe_navigation_call_type`).
|
|
470
|
+
#
|
|
471
|
+
# The receiver is one a guard narrows ({.receiver_slot}: a local, instance variable, global or constant), or a
|
|
472
|
+
# safe-navigation call on such a receiver whose own arguments and block hold no write or jump
|
|
473
|
+
# ({OperandEffects}): `a&.b(a.c)&.d(a.e)` runs `d` only when `a&.b(…)` answered non-nil, which it cannot on a
|
|
474
|
+
# nil `a`, and nothing between the two reads rebinds `a`. `scope` itself when nothing narrows, and for a call
|
|
475
|
+
# that is not `&.`.
|
|
476
|
+
def safe_navigation_scope(call_node, scope)
|
|
477
|
+
return scope unless call_node.safe_navigation?
|
|
478
|
+
|
|
479
|
+
non_nil_receiver_scope(call_node.receiver, scope) || scope
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# The scope the block of a safe-navigation call is entered from: {.safe_navigation_scope}, unless the narrowing
|
|
483
|
+
# may not hold for the whole body. An argument that writes may rebind the receiver before the block runs
|
|
484
|
+
# (`f&.two(f = nil) { f.run }`), and a block that writes the receiver hands the next iteration what it wrote
|
|
485
|
+
# (`a&.each { a.size; a = nil }`); either keeps `scope`.
|
|
486
|
+
def safe_navigation_block_scope(call_node, scope)
|
|
487
|
+
return scope unless call_node.safe_navigation?
|
|
488
|
+
return scope if OperandEffects.any?(call_node.arguments)
|
|
489
|
+
|
|
490
|
+
root = call_node.receiver
|
|
491
|
+
root = root.receiver while root.is_a?(Prism::CallNode) && root.safe_navigation?
|
|
492
|
+
return scope if root.respond_to?(:name) && OperandEffects.written_variables(call_node.block).include?(root.name)
|
|
493
|
+
|
|
494
|
+
safe_navigation_scope(call_node, scope)
|
|
495
|
+
end
|
|
496
|
+
|
|
402
497
|
# Slice 7 phase 5 — `case`/`when` narrowing.
|
|
403
498
|
#
|
|
404
499
|
# Given the subject of a `case` (the expression after the `case` keyword) and an array of
|
|
@@ -416,10 +511,6 @@ module Rigor
|
|
|
416
511
|
# scope. The catalogue mirrors {.case_equality_target_class}: static class/module
|
|
417
512
|
# constants narrow as `is_a?`; integer/float-endpoint ranges narrow to `Numeric`;
|
|
418
513
|
# string-endpoint ranges and regexp literals narrow to `String`.
|
|
419
|
-
#
|
|
420
|
-
# @param subject — the `case` subject.
|
|
421
|
-
# @param conditions — the `when`
|
|
422
|
-
# clause's `conditions` array.
|
|
423
514
|
def case_when_scopes(subject, conditions, scope)
|
|
424
515
|
# C1 — `case x when /re/` runs `/re/ === x`, which sets the regex match-data globals
|
|
425
516
|
# exactly as a successful `=~` does. Narrow `$~`/`$&`/`$1..$N` on the clause body (the
|
|
@@ -427,17 +518,22 @@ module Rigor
|
|
|
427
518
|
# a different regex. Applied even when the subject is not a narrowable local read.
|
|
428
519
|
body_scope = apply_when_regex_globals(conditions, scope)
|
|
429
520
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
return [body_scope, scope] if current.nil?
|
|
521
|
+
# Issue #1429 — a global or constant subject (`case $stdout when StringIO`) narrows as a local does, and so
|
|
522
|
+
# does an instance variable (#1446).
|
|
523
|
+
slot = receiver_slot(subject, scope, kinds: CASE_SUBJECT_KINDS)
|
|
524
|
+
return [body_scope, scope] if slot.nil?
|
|
435
525
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
526
|
+
truthy_type, falsey_type = case_when_types(scope, slot.current, conditions)
|
|
527
|
+
classes = conditions.map do |condition|
|
|
528
|
+
lexical_class_name(condition, scope) || case_equality_target_class(condition)
|
|
529
|
+
end
|
|
530
|
+
body_scope = record_bot_guard(narrow_guarded_slot(body_scope, slot, truthy_type), slot, truthy_type, classes)
|
|
531
|
+
[body_scope, narrow_guarded_slot(scope, slot, falsey_type)]
|
|
439
532
|
end
|
|
440
533
|
|
|
534
|
+
CASE_SUBJECT_KINDS = %i[local ivar global constant].freeze
|
|
535
|
+
private_constant :CASE_SUBJECT_KINDS
|
|
536
|
+
|
|
441
537
|
# When the clause has exactly one `RegularExpressionNode` literal condition, narrow the
|
|
442
538
|
# match-data globals on the body edge (same rule as `analyse_regex_match_predicate`'s
|
|
443
539
|
# truthy edge). With multiple regex conditions (`when /a/, /b/`) the body is reachable
|
|
@@ -460,8 +556,29 @@ module Rigor
|
|
|
460
556
|
|
|
461
557
|
# Internal analyser. Returns `[truthy_scope, falsey_scope]` when the predicate shape is
|
|
462
558
|
# recognised, or `nil` to signal "no narrowing" so the public surface can fall back to the
|
|
463
|
-
# entry scope.
|
|
559
|
+
# entry scope. Issue #1359 — a condition whose value is a `gets`-family reader's, the bare call
|
|
560
|
+
# or a variable written with it (`while gets`, `if (line = gets)`), also binds `$_` on each edge
|
|
561
|
+
# to the line the reader returned ({LastLine.predicate_scopes}).
|
|
464
562
|
def analyse(node, scope)
|
|
563
|
+
edges = analyse_shape(node, scope)
|
|
564
|
+
reader = last_line_condition(node)
|
|
565
|
+
reader ? LastLine.predicate_scopes(reader, scope, edges) : edges
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
# The reader call a condition's value is, or nil.
|
|
569
|
+
def last_line_condition(node)
|
|
570
|
+
call =
|
|
571
|
+
case node
|
|
572
|
+
when Prism::CallNode then node
|
|
573
|
+
when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::ClassVariableWriteNode,
|
|
574
|
+
Prism::GlobalVariableWriteNode
|
|
575
|
+
node.value
|
|
576
|
+
end
|
|
577
|
+
call if call.is_a?(Prism::CallNode) && LastLine::READERS.include?(call.name)
|
|
578
|
+
end
|
|
579
|
+
private_class_method :last_line_condition
|
|
580
|
+
|
|
581
|
+
def analyse_shape(node, scope)
|
|
465
582
|
case node
|
|
466
583
|
when Prism::ParenthesesNode
|
|
467
584
|
analyse_parentheses(node, scope)
|
|
@@ -473,20 +590,19 @@ module Rigor
|
|
|
473
590
|
analyse_local_write(node, scope)
|
|
474
591
|
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode
|
|
475
592
|
analyse_ivar(node, scope)
|
|
476
|
-
when Prism::ClassVariableWriteNode
|
|
477
|
-
|
|
478
|
-
when Prism::
|
|
479
|
-
|
|
593
|
+
when Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode
|
|
594
|
+
analyse_cvar_or_global_write(node, scope)
|
|
595
|
+
when Prism::GlobalVariableReadNode, Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
596
|
+
analyse_slot_read(node, scope)
|
|
480
597
|
when Prism::CallNode
|
|
481
|
-
analyse_call(node, scope)
|
|
482
|
-
when Prism::AndNode
|
|
483
|
-
|
|
484
|
-
when Prism::OrNode
|
|
485
|
-
analyse_or(node, scope)
|
|
598
|
+
analyse_call(write_receiver_as_read(node) || node, scope)
|
|
599
|
+
when Prism::AndNode, Prism::OrNode, Prism::IfNode, Prism::UnlessNode
|
|
600
|
+
analyse_composition(node, scope)
|
|
486
601
|
when Prism::MatchWriteNode
|
|
487
602
|
analyse_match_write(node, scope)
|
|
488
603
|
end
|
|
489
604
|
end
|
|
605
|
+
private_class_method :analyse_shape
|
|
490
606
|
|
|
491
607
|
# rubocop:disable-next Metrics/ClassLength
|
|
492
608
|
class << self
|
|
@@ -843,6 +959,12 @@ module Rigor
|
|
|
843
959
|
]
|
|
844
960
|
end
|
|
845
961
|
|
|
962
|
+
def analyse_cvar_or_global_write(node, scope)
|
|
963
|
+
return analyse_cvar_write(node, scope) if node.is_a?(Prism::ClassVariableWriteNode)
|
|
964
|
+
|
|
965
|
+
analyse_global_write(node, scope)
|
|
966
|
+
end
|
|
967
|
+
|
|
846
968
|
def analyse_cvar_write(node, scope)
|
|
847
969
|
current = scope.cvar(node.name)
|
|
848
970
|
return nil if current.nil?
|
|
@@ -863,6 +985,96 @@ module Rigor
|
|
|
863
985
|
]
|
|
864
986
|
end
|
|
865
987
|
|
|
988
|
+
# Issue #1429 — `if $g`, `return unless $g`, `if CONFIG`: the truthiness guard on a global or constant
|
|
989
|
+
# read, the counterpart of {#analyse_local_read} ({#narrow_slot}).
|
|
990
|
+
def analyse_slot_read(node, scope)
|
|
991
|
+
slot = receiver_slot(node, scope, kinds: GLOBAL_AND_CONSTANT)
|
|
992
|
+
return nil if slot.nil?
|
|
993
|
+
|
|
994
|
+
[
|
|
995
|
+
narrow_slot(scope, slot, narrow_truthy(slot.current)),
|
|
996
|
+
narrow_slot(scope, slot, narrow_falsey(slot.current))
|
|
997
|
+
]
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
GLOBAL_AND_CONSTANT = %i[global constant].freeze
|
|
1001
|
+
CLASS_GUARD_SLOT_KINDS = %i[ivar global constant].freeze
|
|
1002
|
+
ALL_SLOT_KINDS = %i[local ivar global constant].freeze
|
|
1003
|
+
private_constant :GLOBAL_AND_CONSTANT, :CLASS_GUARD_SLOT_KINDS, :ALL_SLOT_KINDS
|
|
1004
|
+
|
|
1005
|
+
# The {ReceiverSlot} `node` reads, among `kinds`, or nil. A local or instance variable needs a binding. A
|
|
1006
|
+
# global the scope does not bind is read as it reads unbound, and a constant reference as it resolves
|
|
1007
|
+
# (through any narrowing already on it), so a guard narrows it from that type.
|
|
1008
|
+
def receiver_slot(node, scope, kinds: ALL_SLOT_KINDS)
|
|
1009
|
+
case node
|
|
1010
|
+
when Prism::LocalVariableReadNode then variable_slot(:local, node.name, scope.local(node.name), kinds)
|
|
1011
|
+
when Prism::InstanceVariableReadNode then variable_slot(:ivar, node.name, scope.ivar(node.name), kinds)
|
|
1012
|
+
when Prism::GlobalVariableReadNode then global_slot(node, scope, kinds)
|
|
1013
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then constant_slot(node, scope, kinds)
|
|
1014
|
+
end
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
def variable_slot(kind, name, current, kinds)
|
|
1018
|
+
return nil if current.nil? || !kinds.include?(kind)
|
|
1019
|
+
|
|
1020
|
+
ReceiverSlot.new(kind: kind, name: name, current: current)
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
def global_slot(node, scope, kinds)
|
|
1024
|
+
return nil unless kinds.include?(:global)
|
|
1025
|
+
|
|
1026
|
+
ReceiverSlot.new(kind: :global, name: node.name, current: scope.global(node.name) || scope.type_of(node))
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
def constant_slot(node, scope, kinds)
|
|
1030
|
+
return nil unless kinds.include?(:constant)
|
|
1031
|
+
|
|
1032
|
+
key = constant_key(node)
|
|
1033
|
+
return nil if key.nil?
|
|
1034
|
+
|
|
1035
|
+
ReceiverSlot.new(kind: :constant, name: key, current: scope.type_of(node))
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
# `scope` with `slot`'s receiver read as `type` on one edge. A local or instance variable rebinds as its
|
|
1039
|
+
# analysers always did. A global or constant is left alone when the edge learns nothing (`type` is what
|
|
1040
|
+
# it read), and otherwise narrows with its pre-guard type recorded, so code that may rebind it restores
|
|
1041
|
+
# the binding ({Scope#forget_guard_narrowings}); the frame-local specials skip the record.
|
|
1042
|
+
def narrow_slot(scope, slot, type)
|
|
1043
|
+
case slot.kind
|
|
1044
|
+
when :local then scope.with_local(slot.name, type)
|
|
1045
|
+
when :ivar then scope.with_ivar(slot.name, type)
|
|
1046
|
+
when :global then narrow_global_slot(scope, slot, type)
|
|
1047
|
+
else
|
|
1048
|
+
type == slot.current ? scope : scope.with_constant_narrowing(slot.name, type, slot.current)
|
|
1049
|
+
end
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1052
|
+
# Issue #1446 — {#narrow_slot} for a class guard: an instance variable narrows with its pre-guard type
|
|
1053
|
+
# recorded, as a global does, since code that may reach `self` may rebind it. The other guards on an instance
|
|
1054
|
+
# variable keep {#narrow_slot}'s unrecorded binding.
|
|
1055
|
+
def narrow_guarded_slot(scope, slot, type)
|
|
1056
|
+
return narrow_slot(scope, slot, type) unless slot.kind == :ivar
|
|
1057
|
+
return scope if type == slot.current
|
|
1058
|
+
|
|
1059
|
+
scope.with_guarded_ivar(slot.name, type, slot.current)
|
|
1060
|
+
end
|
|
1061
|
+
|
|
1062
|
+
# Issue #1446 — `scope`, the guard's truthy edge, with `class_names` recorded for `slot` when the guard left
|
|
1063
|
+
# it `bot` ({Scope#with_bot_guard_classes}): the value that passes it is one of those classes. Nothing is
|
|
1064
|
+
# recorded when a condition names no class.
|
|
1065
|
+
def record_bot_guard(scope, slot, type, class_names)
|
|
1066
|
+
return scope unless type.is_a?(Type::Bot) && !class_names.empty? && class_names.none?(&:nil?)
|
|
1067
|
+
|
|
1068
|
+
scope.with_bot_guard_classes(slot.kind, slot.name, class_names)
|
|
1069
|
+
end
|
|
1070
|
+
|
|
1071
|
+
def narrow_global_slot(scope, slot, type)
|
|
1072
|
+
return scope if type == slot.current
|
|
1073
|
+
|
|
1074
|
+
record = !UNRECORDED_GUARD_GLOBALS.include?(slot.name)
|
|
1075
|
+
scope.with_guarded_global(slot.name, type, slot.current, record: record)
|
|
1076
|
+
end
|
|
1077
|
+
|
|
866
1078
|
# `if /(?<x>...)/ =~ str` — Prism wraps the `=~` call in a `MatchWriteNode` listing the
|
|
867
1079
|
# named-capture targets. The parent `eval_match_write` has already bound each target to
|
|
868
1080
|
# `String | nil`; in the truthy branch (the regex matched) every named capture is
|
|
@@ -918,6 +1130,25 @@ module Rigor
|
|
|
918
1130
|
# literals (Slice 6 phase 2 sub-phase 2).
|
|
919
1131
|
# Anything else returns nil so the surrounding analyser falls through to the
|
|
920
1132
|
# no-narrowing fallback.
|
|
1133
|
+
# Issue #1223 — `(x = e).nil?`, `(x ||= e).empty?`, `(@x = e).is_a?(C)`. A write's value is the binding
|
|
1134
|
+
# it leaves, and a predicate is narrowed in the scope after its receiver ran, so the call narrows the
|
|
1135
|
+
# variable exactly as the same call on a read of it would. Every analyser keys its receiver on a read
|
|
1136
|
+
# node, so the call is analysed with its receiver replaced by one ({ReceiverAlias.read_of}). Before #1223
|
|
1137
|
+
# the statement evaluator did not thread a receiver's write at all, so the local was unbound and there
|
|
1138
|
+
# was nothing to narrow; nil when the receiver is not such a write, bare or parenthesised on its own.
|
|
1139
|
+
def write_receiver_as_read(node)
|
|
1140
|
+
write = written_receiver(node.receiver)
|
|
1141
|
+
write && node.copy(receiver: ReceiverAlias.read_of(write))
|
|
1142
|
+
end
|
|
1143
|
+
|
|
1144
|
+
def written_receiver(receiver)
|
|
1145
|
+
while receiver.is_a?(Prism::ParenthesesNode) && receiver.body.is_a?(Prism::StatementsNode) &&
|
|
1146
|
+
receiver.body.body.size == 1
|
|
1147
|
+
receiver = receiver.body.body.first
|
|
1148
|
+
end
|
|
1149
|
+
receiver if ReceiverAlias.variable_write?(receiver)
|
|
1150
|
+
end
|
|
1151
|
+
|
|
921
1152
|
def analyse_call(node, scope)
|
|
922
1153
|
return nil if node.block
|
|
923
1154
|
|
|
@@ -1042,7 +1273,7 @@ module Rigor
|
|
|
1042
1273
|
end
|
|
1043
1274
|
|
|
1044
1275
|
def simple_dispatch_name?(name)
|
|
1045
|
-
%i[nil? ! is_a? kind_of? instance_of? == != === =~ match? key? has_key? empty? any?
|
|
1276
|
+
%i[nil? ! is_a? kind_of? instance_of? == != === =~ !~ match? key? has_key? empty? any?
|
|
1046
1277
|
none? respond_to? nan? finite?].include?(name)
|
|
1047
1278
|
end
|
|
1048
1279
|
|
|
@@ -1053,7 +1284,7 @@ module Rigor
|
|
|
1053
1284
|
when :instance_of? then analyse_class_predicate(node, scope, exact: true)
|
|
1054
1285
|
when :==, :!= then analyse_equality_predicate(node, scope, equality: name)
|
|
1055
1286
|
when :=== then analyse_case_equality_predicate(node, scope)
|
|
1056
|
-
when
|
|
1287
|
+
when :=~, :!~ then dispatch_regex_match_predicate(node, scope, name)
|
|
1057
1288
|
when :match? then analyse_whole_regex_match_predicate(node, scope)
|
|
1058
1289
|
when :key?, :has_key? then analyse_key_presence_predicate(node, scope)
|
|
1059
1290
|
when :empty?, :any?, :none? then analyse_array_emptiness_predicate(node, scope, name)
|
|
@@ -1062,17 +1293,29 @@ module Rigor
|
|
|
1062
1293
|
end
|
|
1063
1294
|
end
|
|
1064
1295
|
|
|
1296
|
+
def dispatch_regex_match_predicate(node, scope, name)
|
|
1297
|
+
name == :=~ ? analyse_regex_match_predicate(node, scope) : analyse_regex_not_match_predicate(node, scope)
|
|
1298
|
+
end
|
|
1299
|
+
|
|
1065
1300
|
# T3 (template-corpora survey) — `recv.respond_to?(sym)` truthy edge narrows `recv`
|
|
1066
1301
|
# non-nil. `nil.respond_to?(m)` is `false` for every method `m` that `NilClass` does not
|
|
1067
1302
|
# define, so a truthy `respond_to?` proves the receiver was not `nil` UNLESS the queried
|
|
1068
1303
|
# symbol is one of `NilClass`'s own methods (`:to_s`, `:inspect`, `:nil?`, …) — `nil`
|
|
1069
1304
|
# DOES respond to those, so the truthy edge admits a nil receiver and we narrow nothing.
|
|
1070
1305
|
#
|
|
1306
|
+
# Issue #1429 — the truthy edge also admits `m` itself. `respond_to?` is a guard written in the code
|
|
1307
|
+
# (ADR-117 Decision point 3), so a member of the receiver's type whose class Rigor knows to lack `m`
|
|
1308
|
+
# is not what the edge holds: members that may respond keep their type, and when no member may, the
|
|
1309
|
+
# receiver reads `Dynamic[top]` ({#respond_to_admitted}). `$stdout.respond_to?(:string) ?
|
|
1310
|
+
# $stdout.string : ""` on an `IO`-typed `$stdout` therefore does not report `undefined method`.
|
|
1311
|
+
# `Dynamic[top]` rather than `Dynamic[IO]`: method availability on a `Dynamic[T]` is checked against
|
|
1312
|
+
# `T` (special-types.md), so only the untyped carrier is sure to keep the guarded call quiet.
|
|
1313
|
+
#
|
|
1071
1314
|
# Conservative floor: narrow only on a literal `Symbol`/`String` argument resolved
|
|
1072
1315
|
# against the RBS environment; a non-literal symbol, a missing argument, or a symbol
|
|
1073
1316
|
# that IS in `NilClass`'s method set declines. The falsey edge is always the no-op
|
|
1074
|
-
# ("does not respond" proves little about the receiver's type).
|
|
1075
|
-
#
|
|
1317
|
+
# ("does not respond" proves little about the receiver's type). A local, instance variable,
|
|
1318
|
+
# global or constant receiver narrows ({#receiver_slot}).
|
|
1076
1319
|
def analyse_respond_to_predicate(node, scope)
|
|
1077
1320
|
return nil if node.block
|
|
1078
1321
|
return nil if node.arguments.nil? || node.arguments.arguments.size != 1
|
|
@@ -1081,16 +1324,63 @@ module Rigor
|
|
|
1081
1324
|
return nil if sym.nil?
|
|
1082
1325
|
return nil if nilclass_method?(sym, scope)
|
|
1083
1326
|
|
|
1084
|
-
|
|
1085
|
-
return nil if
|
|
1327
|
+
slot = receiver_slot(node.receiver, scope)
|
|
1328
|
+
return nil if slot.nil?
|
|
1086
1329
|
|
|
1087
|
-
|
|
1088
|
-
return nil if current.
|
|
1330
|
+
admitted = respond_to_admitted(narrow_non_nil(slot.current), sym.to_sym, scope)
|
|
1331
|
+
return nil if admitted.equal?(slot.current) || admitted == slot.current
|
|
1089
1332
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1333
|
+
[narrow_slot(scope, slot, admitted), scope]
|
|
1334
|
+
end
|
|
1335
|
+
|
|
1336
|
+
# The members of `type` that may respond to `method_name`, or, when Rigor knows every member lacks it
|
|
1337
|
+
# ({#lacks_method?}), `Dynamic[top]`: the members Rigor read the edge from are wrong about the object there,
|
|
1338
|
+
# and no class is named to read it as instead. A member Rigor cannot judge (`Dynamic`, `Bot`, a class no
|
|
1339
|
+
# signature declares) may respond.
|
|
1340
|
+
#
|
|
1341
|
+
# When every member lacking `m` records what the file literally shows (a literal, a `Tuple`, a `HashShape`, a
|
|
1342
|
+
# class object, `nil` / `true` / `false`), the edge is `Bot`. For a literal value that is exact
|
|
1343
|
+
# (`1.respond_to?(:string)` is false in every run); a class object's arm goes unchecked when a gem without a
|
|
1344
|
+
# signature adds the method (`Time.respond_to?(:zone)` under ActiveSupport).
|
|
1345
|
+
def respond_to_admitted(type, method_name, scope)
|
|
1346
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
1347
|
+
kept = members.reject { |member| lacks_method?(member, method_name, scope) }
|
|
1348
|
+
return type if kept.size == members.size
|
|
1349
|
+
return Type::Combinator.union(*kept) unless kept.empty?
|
|
1350
|
+
return Type::Combinator.bot if members.all? { |member| literal_carrier?(member) }
|
|
1092
1351
|
|
|
1093
|
-
|
|
1352
|
+
Type::Combinator.untyped
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
LITERAL_CARRIERS = [Type::Constant, Type::Tuple, Type::HashShape, Type::Singleton].freeze
|
|
1356
|
+
private_constant :LITERAL_CARRIERS
|
|
1357
|
+
|
|
1358
|
+
def literal_carrier?(member)
|
|
1359
|
+
LITERAL_CARRIERS.any? { |carrier| member.is_a?(carrier) } ||
|
|
1360
|
+
(member.is_a?(Type::Nominal) && VALUE_PINNED_CLASSES.include?(member.class_name))
|
|
1361
|
+
end
|
|
1362
|
+
|
|
1363
|
+
# True only when every class `member` dispatches through is declared in RBS, is not a mixin module or
|
|
1364
|
+
# `Object` / `BasicObject` (whose instances may be of any class), is not a plugin's open receiver, and
|
|
1365
|
+
# neither its signature nor the project defines `method_name` on it.
|
|
1366
|
+
def lacks_method?(member, method_name, scope)
|
|
1367
|
+
targets = ProjectMethodOwnership.targets(member)
|
|
1368
|
+
return false if targets.nil? || targets.empty?
|
|
1369
|
+
|
|
1370
|
+
targets.all? { |class_name, kind| class_lacks_method?(class_name, method_name, kind, scope) }
|
|
1371
|
+
end
|
|
1372
|
+
|
|
1373
|
+
OPEN_SURFACE_CLASSES = %w[Object BasicObject].freeze
|
|
1374
|
+
private_constant :OPEN_SURFACE_CLASSES
|
|
1375
|
+
|
|
1376
|
+
def class_lacks_method?(class_name, method_name, kind, scope)
|
|
1377
|
+
return false if kind == :instance && OPEN_SURFACE_CLASSES.include?(class_name)
|
|
1378
|
+
return false unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
1379
|
+
return false if kind == :instance && scope.environment.rbs_module?(class_name)
|
|
1380
|
+
return false if scope.environment.plugin_registry&.open_receiver?(class_name)
|
|
1381
|
+
return false if ProjectMethodOwnership.defines?(class_name, method_name, kind, scope)
|
|
1382
|
+
|
|
1383
|
+
ExternalAncestorResolution.method_definition(class_name, method_name, kind, scope: scope).nil?
|
|
1094
1384
|
end
|
|
1095
1385
|
|
|
1096
1386
|
# True when `nil` responds to `sym` — i.e. `NilClass` (own, inherited
|
|
@@ -1285,7 +1575,9 @@ module Rigor
|
|
|
1285
1575
|
#
|
|
1286
1576
|
# Returns nil (no narrowing) when the receiver / argument pair does not resolve to exactly one
|
|
1287
1577
|
# regex pattern we can count (see {#regex_match_pattern}).
|
|
1288
|
-
|
|
1578
|
+
#
|
|
1579
|
+
# `refine: false` leaves out the whole-receiver refinement, which names a String.
|
|
1580
|
+
def analyse_regex_match_predicate(node, scope, refine: true)
|
|
1289
1581
|
return nil if node.arguments.nil?
|
|
1290
1582
|
return nil unless node.arguments.arguments.size == 1
|
|
1291
1583
|
|
|
@@ -1305,10 +1597,96 @@ module Rigor
|
|
|
1305
1597
|
# match-global logic above. A successful `str =~ /\A\d+\z/` proves the WHOLE string
|
|
1306
1598
|
# matched, so the string operand narrows to the imported refinement the anchored pattern
|
|
1307
1599
|
# names. The falsey edge is untouched: a failed match proves nothing about the shape.
|
|
1308
|
-
truthy = apply_whole_receiver_refinement(truthy, node.receiver, arg, pattern.source, scope)
|
|
1600
|
+
truthy = apply_whole_receiver_refinement(truthy, node.receiver, arg, pattern.source, scope) if refine
|
|
1309
1601
|
[truthy, falsey]
|
|
1310
1602
|
end
|
|
1311
1603
|
|
|
1604
|
+
# Issue #1379 — `subject !~ pattern` is `Kernel#!~`, which returns `!(subject =~ pattern)`. It runs the same
|
|
1605
|
+
# match in the caller's frame, so its edges are {#analyse_regex_match_predicate}'s swapped: the falsey edge is
|
|
1606
|
+
# the match edge (`$~` a `MatchData`, an unconditional `$N` a `String`, and the whole-receiver refinement), the
|
|
1607
|
+
# truthy edge the no-match edge (`$~` and `$N` nil). The same patterns are recognised and the same ones
|
|
1608
|
+
# declined (a non-regex operand, an ambiguous constant, `//x`).
|
|
1609
|
+
#
|
|
1610
|
+
# Unlike `=~`'s, an edge here is narrowed only when the receiver proves what the frame holds on it
|
|
1611
|
+
# ({#not_match_receiver_edges}). An edge it cannot vouch for keeps the match globals forgotten, since the call
|
|
1612
|
+
# may still have rebound them. The refinement is kept only when the local it would narrow is known to be a
|
|
1613
|
+
# String on the match edge ({#refinable_operand?}).
|
|
1614
|
+
def analyse_regex_not_match_predicate(node, scope)
|
|
1615
|
+
edges = analyse_regex_match_predicate(node, scope, refine: refinable_operand?(node, scope))
|
|
1616
|
+
return nil if edges.nil?
|
|
1617
|
+
|
|
1618
|
+
match, no_match = edges
|
|
1619
|
+
forgotten = scope.forget_match_globals
|
|
1620
|
+
case not_match_receiver_edges(node, scope)
|
|
1621
|
+
when :both then [no_match, match]
|
|
1622
|
+
when :match then [forgotten, match]
|
|
1623
|
+
else [forgotten, forgotten]
|
|
1624
|
+
end
|
|
1625
|
+
end
|
|
1626
|
+
|
|
1627
|
+
# Which edges of `recv !~ pattern` the receiver lets {#analyse_regex_not_match_predicate} narrow. `Kernel#!~`
|
|
1628
|
+
# dispatches `=~` on the receiver:
|
|
1629
|
+
#
|
|
1630
|
+
# - A Regexp receiver (the pattern on the left) runs `Regexp#=~`, which sets `$~` on both outcomes, a `nil`
|
|
1631
|
+
# argument included: `:both`.
|
|
1632
|
+
# - `String#=~` and `Symbol#=~` do the same: `:both` when every member of the receiver's type is a String or a
|
|
1633
|
+
# Symbol.
|
|
1634
|
+
# - `NilClass#=~` answers nil and leaves `$~` alone, so a nil receiver reaches the no-match edge with the
|
|
1635
|
+
# previous match still bound; it never reaches the match edge. A `Dynamic` receiver may be nil at run time
|
|
1636
|
+
# whatever its static facet says: `:match` when every member is a String, a Symbol, nil, or a `Dynamic`
|
|
1637
|
+
# that is untyped or whose facet holds only those.
|
|
1638
|
+
# - Any other receiver may define `=~` in Ruby, which binds `$~` in its own frame, not the caller's: nil.
|
|
1639
|
+
#
|
|
1640
|
+
# A safe-navigation `recv&.!~(pattern)` answers nil without matching on a nil receiver, so it declines too.
|
|
1641
|
+
def not_match_receiver_edges(node, scope)
|
|
1642
|
+
return nil if node.receiver.nil? || node.safe_navigation?
|
|
1643
|
+
return :both if regex_operand_pattern(node.receiver, scope).is_a?(RegexMatchPattern)
|
|
1644
|
+
|
|
1645
|
+
kinds = not_match_member_kinds(scope.type_of(node.receiver), scope)
|
|
1646
|
+
return :both if kinds.all? { |kind| %i[string symbol].include?(kind) }
|
|
1647
|
+
|
|
1648
|
+
:match if kinds.all? { |kind| %i[string symbol nil dynamic].include?(kind) }
|
|
1649
|
+
end
|
|
1650
|
+
|
|
1651
|
+
# Whether the local operand {#apply_whole_receiver_refinement} would narrow holds only Strings and nil, which
|
|
1652
|
+
# the match edge excludes. The refinement names a String, so a member that may be a Symbol — a Symbol, or an
|
|
1653
|
+
# untyped `Dynamic` that `Kernel#!~` accepts as the receiver or `Regexp#=~` as the argument — keeps it out:
|
|
1654
|
+
# `return if raw !~ /\A\d+\z/` on an untyped `raw` must not make `raw.to_proc` an undefined method.
|
|
1655
|
+
def refinable_operand?(node, scope)
|
|
1656
|
+
operand = [node.receiver, *node.arguments&.arguments].find { |o| o.is_a?(Prism::LocalVariableReadNode) }
|
|
1657
|
+
type = operand && scope.local(operand.name)
|
|
1658
|
+
return true if type.nil?
|
|
1659
|
+
|
|
1660
|
+
not_match_member_kinds(type, scope).all? { |kind| %i[string nil].include?(kind) }
|
|
1661
|
+
end
|
|
1662
|
+
|
|
1663
|
+
def not_match_member_kinds(type, scope)
|
|
1664
|
+
(type.is_a?(Type::Union) ? type.members : [type]).map { |member| not_match_member_kind(member, scope) }
|
|
1665
|
+
end
|
|
1666
|
+
|
|
1667
|
+
# `:string` or `:symbol` for a member {.narrow_not_class} proves a String or a Symbol (a value or refinement of
|
|
1668
|
+
# either, or a class the environment orders below it; a project subclass it cannot order reads as neither),
|
|
1669
|
+
# `:nil`, `:dynamic` for a `Dynamic` whose facet is untyped or holds only Strings, Symbols and nil, or nil for
|
|
1670
|
+
# anything else.
|
|
1671
|
+
def not_match_member_kind(member, scope)
|
|
1672
|
+
return not_match_dynamic_kind(member, scope) if member.is_a?(Type::Dynamic)
|
|
1673
|
+
return :nil if narrow_nil(member) == member && !member.is_a?(Type::Bot)
|
|
1674
|
+
|
|
1675
|
+
environment = scope.environment
|
|
1676
|
+
{ "String" => :string, "Symbol" => :symbol }.each do |class_name, kind|
|
|
1677
|
+
return kind if narrow_not_class(member, class_name, environment: environment, scope: scope).is_a?(Type::Bot)
|
|
1678
|
+
end
|
|
1679
|
+
nil
|
|
1680
|
+
end
|
|
1681
|
+
|
|
1682
|
+
def not_match_dynamic_kind(member, scope)
|
|
1683
|
+
facet = member.static_facet
|
|
1684
|
+
return :dynamic if facet.is_a?(Type::Top)
|
|
1685
|
+
|
|
1686
|
+
facet_kinds = not_match_member_kinds(facet, scope)
|
|
1687
|
+
:dynamic if facet_kinds.all? { |kind| %i[string symbol nil].include?(kind) }
|
|
1688
|
+
end
|
|
1689
|
+
|
|
1312
1690
|
# `str.match?(/\A\d+\z/)` — `String#match?` is a pure boolean predicate whose truthy edge
|
|
1313
1691
|
# proves a property of the ENTIRE receiver. When the argument is a fully `\A…\z`-anchored
|
|
1314
1692
|
# single-char-class pattern recognised by {RegexRefinement.for_whole_pattern}, narrow the
|
|
@@ -1409,13 +1787,17 @@ module Rigor
|
|
|
1409
1787
|
# `String`; every other numbered group present in the pattern stays `String | nil` on
|
|
1410
1788
|
# both edges (a truthy match leaves an optional group nil at runtime), so we do not
|
|
1411
1789
|
# narrow it on the truthy edge.
|
|
1790
|
+
#
|
|
1791
|
+
# Both edges start from `scope` with the match globals forgotten (#1385): the match rebinds every one of them,
|
|
1792
|
+
# so a `$N` an earlier match narrowed says nothing here. Without that, a `when /(x)/` arm, or the right operand
|
|
1793
|
+
# of `s =~ /(a)(b)(c)/ && t =~ /(x)/`, kept `$2` and `$3` narrowed to `String` where Ruby reads nil.
|
|
1412
1794
|
def regex_match_predicate_scopes(scope, unconditional)
|
|
1413
1795
|
string_t = Type::Combinator.nominal_of("String")
|
|
1414
1796
|
match_data_t = Type::Combinator.nominal_of("MatchData")
|
|
1415
1797
|
nil_t = Type::Combinator.constant_of(nil)
|
|
1416
1798
|
|
|
1417
|
-
truthy = scope
|
|
1418
|
-
falsey =
|
|
1799
|
+
truthy = scope.forget_match_globals
|
|
1800
|
+
falsey = truthy
|
|
1419
1801
|
truthy = truthy.with_global(:$~, match_data_t)
|
|
1420
1802
|
falsey = falsey.with_global(:$~, nil_t)
|
|
1421
1803
|
REGEX_MATCH_GLOBALS.each do |name|
|
|
@@ -1449,8 +1831,10 @@ module Rigor
|
|
|
1449
1831
|
# them to non-nil `String` would be unsound. The walker is intentionally light (char
|
|
1450
1832
|
# scan, not a regex-AST parse): backslash escapes are skipped; `(?:…)`, lookahead
|
|
1451
1833
|
# `(?=…)`/`(?!…)`, and lookbehind `(?<=…)`/`(?<!…)` do not capture; named groups
|
|
1452
|
-
# `(?<name>…)` do.
|
|
1453
|
-
#
|
|
1834
|
+
# `(?<name>…)` / `(?'name'…)` do. Once the pattern has a named group, Ruby numbers only
|
|
1835
|
+
# the named groups and a plain `(…)` does not capture (#1471): `/(?<x>a)(b)/` has one
|
|
1836
|
+
# group, so `$2` is nil. Conservatism is one-directional — when in doubt a group is
|
|
1837
|
+
# treated as conditional (dropped from the Set), never the reverse.
|
|
1454
1838
|
def unconditional_capture_groups(source)
|
|
1455
1839
|
# `unconditional` collects every capturing index; a group is later removed (with its
|
|
1456
1840
|
# whole subtree) when it is optionally quantified, nested under an optional ancestor,
|
|
@@ -1460,7 +1844,15 @@ module Rigor
|
|
|
1460
1844
|
# mutually exclusive, so its descendant captures may be absent on a successful match;
|
|
1461
1845
|
# the group itself still participates. Closing a frame rolls its subtree up to the
|
|
1462
1846
|
# parent so an optional / alternated ancestor disqualifies it.
|
|
1463
|
-
state = { unconditional: Set.new, stack: [[nil, [], false]], group_index: 0 }
|
|
1847
|
+
state = { unconditional: Set.new, stack: [[nil, [], false]], group_index: 0, named: named_groups?(source) }
|
|
1848
|
+
each_structural_char(source) { |pos, chr| scan_group_char(source, pos, chr, state) }
|
|
1849
|
+
# Drain the virtual root: a top-level `|` disqualifies all.
|
|
1850
|
+
finalize_frame(state, state[:stack].pop, optional: false)
|
|
1851
|
+
state[:unconditional]
|
|
1852
|
+
end
|
|
1853
|
+
|
|
1854
|
+
# Yields each position of `source` outside a backslash escape and a character class, with its char.
|
|
1855
|
+
def each_structural_char(source)
|
|
1464
1856
|
pos = 0
|
|
1465
1857
|
length = source.length
|
|
1466
1858
|
while pos < length
|
|
@@ -1473,12 +1865,13 @@ module Rigor
|
|
|
1473
1865
|
pos = skip_char_class(source, pos) + 1
|
|
1474
1866
|
next
|
|
1475
1867
|
end
|
|
1476
|
-
|
|
1868
|
+
yield pos, chr
|
|
1477
1869
|
pos += 1
|
|
1478
1870
|
end
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1871
|
+
end
|
|
1872
|
+
|
|
1873
|
+
def named_groups?(source)
|
|
1874
|
+
each_structural_char(source) { |pos, chr| break true if chr == "(" && named_group?(source, pos) } == true
|
|
1482
1875
|
end
|
|
1483
1876
|
|
|
1484
1877
|
# Updates the walk `state` at a group-relevant char during
|
|
@@ -1488,7 +1881,7 @@ module Rigor
|
|
|
1488
1881
|
case chr
|
|
1489
1882
|
when "("
|
|
1490
1883
|
idx = nil
|
|
1491
|
-
if capturing_group?(source, pos)
|
|
1884
|
+
if capturing_group?(source, pos, named: state[:named])
|
|
1492
1885
|
idx = (state[:group_index] += 1)
|
|
1493
1886
|
state[:unconditional] << idx
|
|
1494
1887
|
end
|
|
@@ -1527,11 +1920,20 @@ module Rigor
|
|
|
1527
1920
|
end
|
|
1528
1921
|
end
|
|
1529
1922
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1923
|
+
# A plain `(…)` captures unless the pattern has a named group (`named:`); of the `(?…)` forms only a named
|
|
1924
|
+
# group does.
|
|
1925
|
+
def capturing_group?(source, pos, named:)
|
|
1926
|
+
return !named unless source[pos + 1] == "?"
|
|
1927
|
+
|
|
1928
|
+
named_group?(source, pos)
|
|
1929
|
+
end
|
|
1930
|
+
|
|
1931
|
+
# `(?<name>…)` and `(?'name'…)` are named groups; `(?<=…)`/`(?<!…)` (lookbehind) and
|
|
1932
|
+
# `(?:…)`/`(?=…)`/`(?!…)` are not.
|
|
1933
|
+
def named_group?(source, pos)
|
|
1934
|
+
return false unless source[pos + 1] == "?"
|
|
1935
|
+
return true if source[pos + 2] == "'"
|
|
1532
1936
|
|
|
1533
|
-
# `(?<name>…)` captures; `(?<=…)`/`(?<!…)` (lookbehind) and
|
|
1534
|
-
# `(?:…)`/`(?=…)`/`(?!…)` do not.
|
|
1535
1937
|
source[pos + 2] == "<" && source[pos + 3] != "=" && source[pos + 3] != "!"
|
|
1536
1938
|
end
|
|
1537
1939
|
|
|
@@ -1962,9 +2364,29 @@ module Rigor
|
|
|
1962
2364
|
analyse_class_predicate_on_local(node, scope, class_name, exact)
|
|
1963
2365
|
when Prism::CallNode
|
|
1964
2366
|
analyse_class_predicate_on_chain(node, scope, class_name, exact)
|
|
2367
|
+
else
|
|
2368
|
+
analyse_class_predicate_on_slot(node.receiver, scope, class_name, exact)
|
|
1965
2369
|
end
|
|
1966
2370
|
end
|
|
1967
2371
|
|
|
2372
|
+
# Issue #1429 — `$stdout.is_a?(StringIO)`, `STDOUT.kind_of?(StringIO)`: a global or constant receiver
|
|
2373
|
+
# narrows as a local does ({#class_predicate_scopes}), with the pre-guard type recorded ({#narrow_slot}). So
|
|
2374
|
+
# does an instance variable (#1446, {#narrow_guarded_slot}).
|
|
2375
|
+
def analyse_class_predicate_on_slot(receiver, scope, class_name, exact)
|
|
2376
|
+
slot = receiver_slot(receiver, scope, kinds: CLASS_GUARD_SLOT_KINDS)
|
|
2377
|
+
return nil if slot.nil?
|
|
2378
|
+
|
|
2379
|
+
class_slot_scopes(scope, slot, class_name, exact: exact)
|
|
2380
|
+
end
|
|
2381
|
+
|
|
2382
|
+
def class_slot_scopes(scope, slot, class_name, exact:)
|
|
2383
|
+
environment = scope.environment
|
|
2384
|
+
truthy = narrow_class(slot.current, class_name, exact: exact, environment: environment, scope: scope)
|
|
2385
|
+
falsey = narrow_not_class(slot.current, class_name, exact: exact, environment: environment, scope: scope)
|
|
2386
|
+
truthy_scope = record_bot_guard(narrow_guarded_slot(scope, slot, truthy), slot, truthy, [class_name])
|
|
2387
|
+
[truthy_scope, narrow_guarded_slot(scope, slot, falsey)]
|
|
2388
|
+
end
|
|
2389
|
+
|
|
1968
2390
|
# The class name a `is_a?` / `kind_of?` / `instance_of?` argument denotes: the top-level
|
|
1969
2391
|
# `bare_name` when the reference is written `::Foo` (nil when nothing declares it there),
|
|
1970
2392
|
# the lexical resolution otherwise.
|
|
@@ -2096,16 +2518,8 @@ module Rigor
|
|
|
2096
2518
|
end
|
|
2097
2519
|
|
|
2098
2520
|
def class_predicate_scopes(scope, name, current, class_name, exact:)
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
name,
|
|
2102
|
-
narrow_class(current, class_name, exact: exact, environment: scope.environment, scope: scope)
|
|
2103
|
-
),
|
|
2104
|
-
scope.with_local(
|
|
2105
|
-
name,
|
|
2106
|
-
narrow_not_class(current, class_name, exact: exact, environment: scope.environment, scope: scope)
|
|
2107
|
-
)
|
|
2108
|
-
]
|
|
2521
|
+
slot = ReceiverSlot.new(kind: :local, name: name, current: current)
|
|
2522
|
+
class_slot_scopes(scope, slot, class_name, exact: exact)
|
|
2109
2523
|
end
|
|
2110
2524
|
|
|
2111
2525
|
# Slice 7 phase 4 — `===`-narrowing. The case-equality predicate `<receiver> === local`
|
|
@@ -2139,9 +2553,23 @@ module Rigor
|
|
|
2139
2553
|
analyse_case_equality_receiver(node.receiver, scope, arg.name, current)
|
|
2140
2554
|
when Prism::CallNode
|
|
2141
2555
|
analyse_case_equality_on_chain(node.receiver, arg, scope)
|
|
2556
|
+
else
|
|
2557
|
+
analyse_case_equality_on_slot(node.receiver, arg, scope)
|
|
2142
2558
|
end
|
|
2143
2559
|
end
|
|
2144
2560
|
|
|
2561
|
+
# Issue #1429 — `StringIO === $stdout`, `StringIO === STDOUT`: the class-constant receiver form on a global or
|
|
2562
|
+
# constant argument, and on an instance variable (#1446). The Range / Regexp receivers stay local-only.
|
|
2563
|
+
def analyse_case_equality_on_slot(receiver, arg, scope)
|
|
2564
|
+
class_name = lexical_class_name(receiver, scope)
|
|
2565
|
+
return nil if class_name.nil?
|
|
2566
|
+
|
|
2567
|
+
slot = receiver_slot(arg, scope, kinds: CLASS_GUARD_SLOT_KINDS)
|
|
2568
|
+
return nil if slot.nil?
|
|
2569
|
+
|
|
2570
|
+
class_slot_scopes(scope, slot, class_name, exact: false)
|
|
2571
|
+
end
|
|
2572
|
+
|
|
2145
2573
|
# `Class === <local/ivar>.<method>` — the case-equality counterpart of
|
|
2146
2574
|
# {.analyse_class_predicate_on_chain}. The `open3` idiom `if Hash === cmd.last` narrows
|
|
2147
2575
|
# `cmd.last` to the class inside the branch, recorded as a single-hop method-chain
|
|
@@ -2360,6 +2788,10 @@ module Rigor
|
|
|
2360
2788
|
# that position, or nil when the overload shape does not allow a precise match.
|
|
2361
2789
|
def lookup_positional_arg(call_node, method_def, target_name)
|
|
2362
2790
|
arguments = call_node.arguments&.arguments || []
|
|
2791
|
+
# A `(?)` overload (`RBS::Types::UntypedFunction`, #1430) names no parameter and accepts any argument list,
|
|
2792
|
+
# so a call may reach it with the target's position holding anything: no overload proves the target.
|
|
2793
|
+
return nil unless method_def.method_types.all? { |mt| mt.type.respond_to?(:required_positionals) }
|
|
2794
|
+
|
|
2363
2795
|
method_def.method_types.each do |mt|
|
|
2364
2796
|
params = mt.type.required_positionals + mt.type.optional_positionals
|
|
2365
2797
|
index = params.find_index { |param| param.name == target_name }
|
|
@@ -2375,7 +2807,9 @@ module Rigor
|
|
|
2375
2807
|
# cannot statically classify are treated as "no narrowing": the body falls back to the
|
|
2376
2808
|
# union of what we did learn (or the entry type when nothing learned), and the falsey
|
|
2377
2809
|
# edge is the entry type (because we cannot prove the unknown condition didn't match).
|
|
2378
|
-
|
|
2810
|
+
#
|
|
2811
|
+
# `[body type, next-clause type]` of a `when` clause's conditions over the subject type `current`.
|
|
2812
|
+
def case_when_types(scope, current, conditions)
|
|
2379
2813
|
truthy_members = []
|
|
2380
2814
|
falsey_type = current
|
|
2381
2815
|
fully_narrowable = true
|
|
@@ -2392,10 +2826,7 @@ module Rigor
|
|
|
2392
2826
|
end
|
|
2393
2827
|
|
|
2394
2828
|
truthy = truthy_members.empty? ? current : Type::Combinator.union(*truthy_members)
|
|
2395
|
-
[
|
|
2396
|
-
scope.with_local(local_name, truthy),
|
|
2397
|
-
scope.with_local(local_name, fully_narrowable ? falsey_type : current)
|
|
2398
|
-
]
|
|
2829
|
+
[truthy, fully_narrowable ? falsey_type : current]
|
|
2399
2830
|
end
|
|
2400
2831
|
|
|
2401
2832
|
# Per-condition rule. Returns `nil` when the condition shape is not recognised (caller
|
|
@@ -2647,9 +3078,12 @@ module Rigor
|
|
|
2647
3078
|
# `is_a?(Integer)`) narrow DOWN to `Nominal[class_name]`; otherwise (disjoint
|
|
2648
3079
|
# hierarchies under `is_a?`, mismatch under `instance_of?`) collapse to `Bot`.
|
|
2649
3080
|
# Conservative when the analyzer environment cannot resolve either class.
|
|
3081
|
+
#
|
|
3082
|
+
# A disjoint `Bot` narrows the edge but is not always a verdict that the code is dead: see
|
|
3083
|
+
# {.disjoint_nominal_guard?} (issue #1429).
|
|
2650
3084
|
def narrow_nominal_to_class(nominal, class_name, context)
|
|
2651
3085
|
return nominal if nominal.class_name == class_name
|
|
2652
|
-
return
|
|
3086
|
+
return exact_nominal_to_class(nominal, class_name, context) if context.exact
|
|
2653
3087
|
|
|
2654
3088
|
case class_ordering(nominal.class_name, class_name, context)
|
|
2655
3089
|
when :superclass then Type::Combinator.nominal_of(class_name)
|
|
@@ -2664,6 +3098,17 @@ module Rigor
|
|
|
2664
3098
|
end
|
|
2665
3099
|
end
|
|
2666
3100
|
|
|
3101
|
+
# `instance_of?(class_name)` compares the exact class, so its truthy edge holds `class_name` itself. Issue
|
|
3102
|
+
# #1429: when `class_name` is a class below the nominal's (`Numeric` under `instance_of?(Integer)`), that is
|
|
3103
|
+
# the edge's type; the edge was `Bot` before, for every class but the nominal's own. A module is never an
|
|
3104
|
+
# object's class, and any other ordering leaves no class the object can have, so those stay `Bot`.
|
|
3105
|
+
def exact_nominal_to_class(nominal, class_name, context)
|
|
3106
|
+
return Type::Combinator.bot if context.environment.rbs_module?(class_name)
|
|
3107
|
+
return Type::Combinator.bot unless class_ordering(nominal.class_name, class_name, context) == :superclass
|
|
3108
|
+
|
|
3109
|
+
Type::Combinator.nominal_of(class_name)
|
|
3110
|
+
end
|
|
3111
|
+
|
|
2667
3112
|
def narrow_nominal_not_class(nominal, class_name, context)
|
|
2668
3113
|
return Type::Combinator.bot if nominal.class_name == class_name
|
|
2669
3114
|
return nominal if context.exact
|
|
@@ -2808,25 +3253,15 @@ module Rigor
|
|
|
2808
3253
|
context.environment.class_ordering(lhs, rhs)
|
|
2809
3254
|
end
|
|
2810
3255
|
|
|
3256
|
+
# `x.nil?` on a local, instance variable, global or constant (#1429) read.
|
|
2811
3257
|
def analyse_nil_predicate(receiver, scope)
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
current = scope.local(receiver.name)
|
|
2815
|
-
return nil if current.nil?
|
|
2816
|
-
|
|
2817
|
-
[
|
|
2818
|
-
scope.with_local(receiver.name, narrow_nil(current)),
|
|
2819
|
-
scope.with_local(receiver.name, narrow_non_nil(current))
|
|
2820
|
-
]
|
|
2821
|
-
when Prism::InstanceVariableReadNode
|
|
2822
|
-
current = scope.ivar(receiver.name)
|
|
2823
|
-
return nil if current.nil?
|
|
3258
|
+
slot = receiver_slot(receiver, scope)
|
|
3259
|
+
return nil if slot.nil?
|
|
2824
3260
|
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
end
|
|
3261
|
+
[
|
|
3262
|
+
narrow_slot(scope, slot, narrow_nil(slot.current)),
|
|
3263
|
+
narrow_slot(scope, slot, narrow_non_nil(slot.current))
|
|
3264
|
+
]
|
|
2830
3265
|
end
|
|
2831
3266
|
|
|
2832
3267
|
# The three value-pinned classes a union arm can name whose instances are the single
|
|
@@ -2945,21 +3380,39 @@ module Rigor
|
|
|
2945
3380
|
def analyse_safe_nav_receiver(node, scope)
|
|
2946
3381
|
return nil unless node.safe_navigation?
|
|
2947
3382
|
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
when Prism::LocalVariableReadNode then %i[local with_local]
|
|
2952
|
-
when Prism::InstanceVariableReadNode then %i[ivar with_ivar]
|
|
2953
|
-
else return nil
|
|
2954
|
-
end
|
|
3383
|
+
narrowed = non_nil_slot_scope(node.receiver, scope)
|
|
3384
|
+
narrowed && [narrowed, scope]
|
|
3385
|
+
end
|
|
2955
3386
|
|
|
2956
|
-
|
|
2957
|
-
|
|
3387
|
+
# `scope` with the receiver `node` reads (a local, instance variable, global or constant, #1429) narrowed to
|
|
3388
|
+
# its non-nil fragment, or nil when there is no such receiver or it holds no `nil` to remove.
|
|
3389
|
+
def non_nil_slot_scope(node, scope)
|
|
3390
|
+
slot = receiver_slot(node, scope)
|
|
3391
|
+
return nil if slot.nil?
|
|
2958
3392
|
|
|
2959
|
-
non_nil = narrow_non_nil(current)
|
|
2960
|
-
return nil if non_nil.equal?(current)
|
|
3393
|
+
non_nil = narrow_non_nil(slot.current)
|
|
3394
|
+
return nil if non_nil.equal?(slot.current)
|
|
3395
|
+
|
|
3396
|
+
narrow_slot(scope, slot, non_nil)
|
|
3397
|
+
end
|
|
3398
|
+
|
|
3399
|
+
# The scope {.safe_navigation_scope} answers for a receiver, seen through the `&.` links of a chain whose
|
|
3400
|
+
# operands leave the root's binding alone, or nil. Unlike {.non_nil_slot_scope} it compares the fragment
|
|
3401
|
+
# structurally: `narrow_non_nil` rebuilds a union it removed nothing from, and a receiver that cannot be nil
|
|
3402
|
+
# must leave the call's operands on the scope they are typed from.
|
|
3403
|
+
def non_nil_receiver_scope(node, scope)
|
|
3404
|
+
if node.is_a?(Prism::CallNode)
|
|
3405
|
+
return nil unless node.safe_navigation?
|
|
3406
|
+
return nil if OperandEffects.any?(node.arguments) || OperandEffects.any?(node.block)
|
|
3407
|
+
|
|
3408
|
+
return non_nil_receiver_scope(node.receiver, scope)
|
|
3409
|
+
end
|
|
2961
3410
|
|
|
2962
|
-
|
|
3411
|
+
slot = receiver_slot(node, scope)
|
|
3412
|
+
return nil if slot.nil?
|
|
3413
|
+
|
|
3414
|
+
non_nil = narrow_non_nil(slot.current)
|
|
3415
|
+
non_nil == slot.current ? nil : narrow_slot(scope, slot, non_nil)
|
|
2963
3416
|
end
|
|
2964
3417
|
|
|
2965
3418
|
# Issue #606 slice 1 — the SAFE-NAV CHAIN forms. {.analyse_safe_nav_receiver} above proves
|
|
@@ -3031,12 +3484,17 @@ module Rigor
|
|
|
3031
3484
|
return nil if suffixes.empty? && !allow_bare
|
|
3032
3485
|
|
|
3033
3486
|
receiver = current.receiver
|
|
3034
|
-
return nil unless receiver.is_a?(
|
|
3035
|
-
receiver.is_a?(Prism::InstanceVariableReadNode)
|
|
3487
|
+
return nil unless SLOT_READ_NODES.any? { |klass| receiver.is_a?(klass) }
|
|
3036
3488
|
|
|
3037
3489
|
[receiver, suffixes]
|
|
3038
3490
|
end
|
|
3039
3491
|
|
|
3492
|
+
SLOT_READ_NODES = [
|
|
3493
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::GlobalVariableReadNode,
|
|
3494
|
+
Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
3495
|
+
].freeze
|
|
3496
|
+
private_constant :SLOT_READ_NODES
|
|
3497
|
+
|
|
3040
3498
|
# Which edge — if either — a chain's outcome narrows on. The inner suffixes (everything
|
|
3041
3499
|
# below the outermost) only ever have to NOT return, so they are held to the raise test
|
|
3042
3500
|
# alone; the outermost decides the edge per the table in {NIL_FALSEY_SUFFIXES}'s comment.
|
|
@@ -3051,20 +3509,7 @@ module Rigor
|
|
|
3051
3509
|
end
|
|
3052
3510
|
|
|
3053
3511
|
def safe_nav_chain_narrowed_scope(receiver, scope)
|
|
3054
|
-
|
|
3055
|
-
case receiver
|
|
3056
|
-
when Prism::LocalVariableReadNode then %i[local with_local]
|
|
3057
|
-
when Prism::InstanceVariableReadNode then %i[ivar with_ivar]
|
|
3058
|
-
else return nil
|
|
3059
|
-
end
|
|
3060
|
-
|
|
3061
|
-
current = scope.public_send(reader, receiver.name)
|
|
3062
|
-
return nil if current.nil?
|
|
3063
|
-
|
|
3064
|
-
non_nil = narrow_non_nil(current)
|
|
3065
|
-
return nil if non_nil.equal?(current)
|
|
3066
|
-
|
|
3067
|
-
scope.public_send(writer, receiver.name, non_nil)
|
|
3512
|
+
non_nil_slot_scope(receiver, scope)
|
|
3068
3513
|
end
|
|
3069
3514
|
|
|
3070
3515
|
# Issue #606 slice 1, second form — a safe-nav chain COMPARED to a value that cannot be
|
|
@@ -3174,14 +3619,23 @@ module Rigor
|
|
|
3174
3619
|
return edges unless node.safe_navigation? && edges
|
|
3175
3620
|
|
|
3176
3621
|
truthy, falsey = edges
|
|
3177
|
-
|
|
3178
|
-
return edges
|
|
3622
|
+
slot = receiver_slot(node.receiver, scope)
|
|
3623
|
+
return edges if slot.nil?
|
|
3624
|
+
|
|
3625
|
+
non_nil = narrow_non_nil(slot.current)
|
|
3626
|
+
return edges if non_nil.equal?(slot.current)
|
|
3179
3627
|
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3628
|
+
[narrow_slot(truthy, slot, non_nil), falsey]
|
|
3629
|
+
end
|
|
3630
|
+
|
|
3631
|
+
# The predicates composed from other predicates, whose edges are built from their
|
|
3632
|
+
# operands' edges.
|
|
3633
|
+
def analyse_composition(node, scope)
|
|
3634
|
+
case node
|
|
3635
|
+
when Prism::AndNode then analyse_and(node, scope)
|
|
3636
|
+
when Prism::OrNode then analyse_or(node, scope)
|
|
3637
|
+
else analyse_conditional(node, scope)
|
|
3638
|
+
end
|
|
3185
3639
|
end
|
|
3186
3640
|
|
|
3187
3641
|
# `a && b` short-circuits: the truthy edge is the truthy edge of `b` evaluated under
|
|
@@ -3191,7 +3645,7 @@ module Rigor
|
|
|
3191
3645
|
# output scopes.
|
|
3192
3646
|
def analyse_and(node, scope)
|
|
3193
3647
|
truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
|
|
3194
|
-
truthy_b, falsey_b =
|
|
3648
|
+
truthy_b, falsey_b = operand_edges(node.right, truthy_a)
|
|
3195
3649
|
[truthy_b, falsey_a.join(falsey_b)]
|
|
3196
3650
|
end
|
|
3197
3651
|
|
|
@@ -3200,9 +3654,105 @@ module Rigor
|
|
|
3200
3654
|
# falsey scope evaluated under `a`'s falsey scope.
|
|
3201
3655
|
def analyse_or(node, scope)
|
|
3202
3656
|
truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
|
|
3203
|
-
truthy_b, falsey_b =
|
|
3657
|
+
truthy_b, falsey_b = operand_edges(node.right, falsey_a)
|
|
3204
3658
|
[truthy_a.join(truthy_b), falsey_b]
|
|
3205
3659
|
end
|
|
3660
|
+
|
|
3661
|
+
# The edges of an operand that runs only on one edge of an earlier operand, which is the
|
|
3662
|
+
# step `&&` and `||` both take for their right operand. Issue #1359 — the operand runs after
|
|
3663
|
+
# that edge, so a `$_` the earlier operand narrowed is forgotten when the operand may set it
|
|
3664
|
+
# (`gets && log(items.each { gets })`), before the operand's own edges narrow it again.
|
|
3665
|
+
# Issue #1429 — likewise a guard's narrowing of a global or constant the operand may rebind
|
|
3666
|
+
# (`$sep && reset_sep && $sep.length`).
|
|
3667
|
+
def operand_edges(node, edge_scope)
|
|
3668
|
+
edge_scope = LastLine.forget_if_set(edge_scope, node)
|
|
3669
|
+
if edge_scope.guard_narrowed? && GuardRebinding.may_rebind?(node, edge_scope)
|
|
3670
|
+
edge_scope = edge_scope.forget_guard_narrowings
|
|
3671
|
+
end
|
|
3672
|
+
analyse(node, edge_scope) || [edge_scope, edge_scope]
|
|
3673
|
+
end
|
|
3674
|
+
|
|
3675
|
+
# Issue #1017 — `p ? q : r` (and `if` / `unless` in condition position) is truthy through
|
|
3676
|
+
# `p ∧ q` or `¬p ∧ r` and falsey through `p ∧ ¬q` or `¬p ∧ ¬r`. Each conjunction is the
|
|
3677
|
+
# `&&` step — the arm's edges under the predicate's edge ({#operand_edges}) — and each
|
|
3678
|
+
# disjunction is the `||` merge (`Scope#join`), so `p ? false : q` narrows as `!p && q`
|
|
3679
|
+
# does without a literal-arm rule: an arm whose value is settled contributes only its
|
|
3680
|
+
# live edge ({#settled_edges}). Beyond {CONDITIONAL_GUARD_DEPTH} the guard declines.
|
|
3681
|
+
#
|
|
3682
|
+
# The predicate's own edges are never dropped as settled. `scope` is the scope after the
|
|
3683
|
+
# whole conditional ran, so it has already joined the arms, and a carrier every arm
|
|
3684
|
+
# narrowed has lost its optimistic mark there (`Scope#with_local` drops it): a
|
|
3685
|
+
# `missable.nil?` predicate reads as a proof-looking `false` it is not. An arm is judged
|
|
3686
|
+
# under the predicate's edge instead, where a settled value follows from that edge.
|
|
3687
|
+
def analyse_conditional(node, scope)
|
|
3688
|
+
return nil if conditional_guard_height(node) > CONDITIONAL_GUARD_DEPTH
|
|
3689
|
+
|
|
3690
|
+
predicate, then_arm, else_arm = conditional_parts(node)
|
|
3691
|
+
truthy_p, falsey_p = operand_edges(predicate, scope)
|
|
3692
|
+
truthy_q, falsey_q = settled_edges(then_arm, truthy_p)
|
|
3693
|
+
truthy_r, falsey_r = settled_edges(else_arm, falsey_p)
|
|
3694
|
+
truthy = join_live_edges(truthy_q, truthy_r)
|
|
3695
|
+
falsey = join_live_edges(falsey_q, falsey_r)
|
|
3696
|
+
return nil if truthy.nil? && falsey.nil?
|
|
3697
|
+
|
|
3698
|
+
[truthy || scope, falsey || scope]
|
|
3699
|
+
end
|
|
3700
|
+
|
|
3701
|
+
# `[predicate, arm run when it is truthy, arm run when it is falsey]`. An arm is a body or
|
|
3702
|
+
# `nil` for an absent / empty one; an `elsif` is the nested conditional itself.
|
|
3703
|
+
def conditional_parts(node)
|
|
3704
|
+
if node.is_a?(Prism::UnlessNode)
|
|
3705
|
+
[node.predicate, else_body(node.else_clause), node.statements]
|
|
3706
|
+
else
|
|
3707
|
+
subsequent = node.subsequent
|
|
3708
|
+
[node.predicate, node.statements, subsequent.is_a?(Prism::ElseNode) ? else_body(subsequent) : subsequent]
|
|
3709
|
+
end
|
|
3710
|
+
end
|
|
3711
|
+
|
|
3712
|
+
def else_body(else_node)
|
|
3713
|
+
else_node&.statements
|
|
3714
|
+
end
|
|
3715
|
+
|
|
3716
|
+
# An operand's `[truthy, falsey]` edges with a dead edge as `nil`. An absent arm evaluates
|
|
3717
|
+
# to `nil`, so only its falsey edge is live. Otherwise the edge the operand's type proves
|
|
3718
|
+
# unreachable is dropped — unless that proof rests on an optimistically nil-free lookup
|
|
3719
|
+
# (ADR-101, issue #313), which `branch_certainty` refuses to elide an arm on as well.
|
|
3720
|
+
def settled_edges(node, scope)
|
|
3721
|
+
return [nil, scope] if node.nil?
|
|
3722
|
+
|
|
3723
|
+
truthy, falsey = operand_edges(node, scope)
|
|
3724
|
+
case predicate_certainty(scope.type_of(node))
|
|
3725
|
+
when :truthy then optimistic_operand?(node, scope) ? [truthy, falsey] : [truthy, nil]
|
|
3726
|
+
when :falsey then optimistic_operand?(node, scope) ? [truthy, falsey] : [nil, falsey]
|
|
3727
|
+
else [truthy, falsey]
|
|
3728
|
+
end
|
|
3729
|
+
end
|
|
3730
|
+
|
|
3731
|
+
def optimistic_operand?(node, scope)
|
|
3732
|
+
node = node.body.last while node.is_a?(Prism::StatementsNode) && !node.body.empty?
|
|
3733
|
+
!OptimisticOrigin.resolve(node, scope).nil?
|
|
3734
|
+
end
|
|
3735
|
+
|
|
3736
|
+
def join_live_edges(left, right)
|
|
3737
|
+
return right if left.nil?
|
|
3738
|
+
return left if right.nil?
|
|
3739
|
+
|
|
3740
|
+
left.join(right)
|
|
3741
|
+
end
|
|
3742
|
+
|
|
3743
|
+
# The number of conditionals stacked in `node` along the shapes {.analyse} walks.
|
|
3744
|
+
def conditional_guard_height(node)
|
|
3745
|
+
case node
|
|
3746
|
+
when Prism::IfNode, Prism::UnlessNode
|
|
3747
|
+
1 + conditional_parts(node).map { |part| conditional_guard_height(part) }.max
|
|
3748
|
+
when Prism::ParenthesesNode then conditional_guard_height(node.body)
|
|
3749
|
+
when Prism::StatementsNode then conditional_guard_height(node.body.last)
|
|
3750
|
+
when Prism::AndNode, Prism::OrNode
|
|
3751
|
+
[conditional_guard_height(node.left), conditional_guard_height(node.right)].max
|
|
3752
|
+
when Prism::CallNode then node.name == :! ? conditional_guard_height(node.receiver) : 0
|
|
3753
|
+
else 0
|
|
3754
|
+
end
|
|
3755
|
+
end
|
|
3206
3756
|
end
|
|
3207
3757
|
end
|
|
3208
3758
|
end
|