rigortype 0.3.9 → 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/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/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +454 -13
- data/docs/manual/02-cli-reference.md +63 -2
- data/docs/manual/03-configuration.md +7 -0
- data/docs/manual/04-diagnostics.md +4 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +3 -2
- 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 +323 -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 +520 -49
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/fact_store.rb +9 -0
- data/lib/rigor/analysis/incremental.rb +26 -0
- data/lib/rigor/analysis/incremental_session.rb +36 -4
- 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 +1 -0
- data/lib/rigor/analysis/rule_catalog.rb +127 -0
- data/lib/rigor/analysis/run_cache_key.rb +20 -11
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
- data/lib/rigor/analysis/runner.rb +278 -20
- 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 +53 -10
- 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 +6 -1
- data/lib/rigor/cache/engine_source.rb +29 -1
- data/lib/rigor/cache/incremental_snapshot.rb +33 -1
- 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 +0 -1
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/doc_links.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +4 -4
- 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.rb +21 -3
- data/lib/rigor/configuration/severity_profile.rb +19 -3
- data/lib/rigor/configuration.rb +66 -3
- 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 +43 -31
- 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/lockfile_resolver.rb +17 -0
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/rbs_loader.rb +399 -133
- data/lib/rigor/environment.rb +103 -38
- 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 +73 -27
- 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 +387 -15
- data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
- data/lib/rigor/inference/content_join.rb +200 -27
- 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/element_read_widening.rb +22 -10
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +1532 -496
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -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 +447 -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 +167 -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 +881 -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/multi_target_binder.rb +340 -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 +540 -120
- 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 +3 -2
- 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.rb +4615 -551
- data/lib/rigor/inference/statement_evaluator.rb +3176 -490
- 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 +168 -5
- 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/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.rb +1 -0
- data/lib/rigor/protection/discovery_seed.rb +3 -1
- data/lib/rigor/protection/kill_signature.rb +0 -1
- data/lib/rigor/protection/mutation_cache.rb +1 -2
- data/lib/rigor/rbs_extended.rb +27 -0
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +19 -6
- data/lib/rigor/reflection.rb +72 -105
- data/lib/rigor/scope/discovery_index.rb +135 -2
- data/lib/rigor/scope.rb +859 -49
- 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 +558 -31
- 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 +88 -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-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 +12 -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 +1 -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 +135 -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 +9 -7
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +33 -8
- 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 +108 -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,16 @@ 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
|
+
|
|
393
443
|
# Public predicate analyser. Returns `[truthy_scope, falsey_scope]`, always; when no
|
|
394
444
|
# narrowing rule matches the predicate node both entries are the receiver scope unchanged.
|
|
395
445
|
def predicate_scopes(node, scope)
|
|
@@ -399,6 +449,37 @@ module Rigor
|
|
|
399
449
|
result || [scope, scope]
|
|
400
450
|
end
|
|
401
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
|
+
|
|
402
483
|
# Slice 7 phase 5 — `case`/`when` narrowing.
|
|
403
484
|
#
|
|
404
485
|
# Given the subject of a `case` (the expression after the `case` keyword) and an array of
|
|
@@ -416,10 +497,6 @@ module Rigor
|
|
|
416
497
|
# scope. The catalogue mirrors {.case_equality_target_class}: static class/module
|
|
417
498
|
# constants narrow as `is_a?`; integer/float-endpoint ranges narrow to `Numeric`;
|
|
418
499
|
# 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
500
|
def case_when_scopes(subject, conditions, scope)
|
|
424
501
|
# C1 — `case x when /re/` runs `/re/ === x`, which sets the regex match-data globals
|
|
425
502
|
# exactly as a successful `=~` does. Narrow `$~`/`$&`/`$1..$N` on the clause body (the
|
|
@@ -427,17 +504,17 @@ module Rigor
|
|
|
427
504
|
# a different regex. Applied even when the subject is not a narrowable local read.
|
|
428
505
|
body_scope = apply_when_regex_globals(conditions, scope)
|
|
429
506
|
|
|
430
|
-
|
|
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?
|
|
431
510
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
return [body_scope, scope] if current.nil?
|
|
435
|
-
|
|
436
|
-
truthy, = accumulate_case_when_scopes(body_scope, local_name, current, conditions)
|
|
437
|
-
_, falsey = accumulate_case_when_scopes(scope, local_name, current, conditions)
|
|
438
|
-
[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)]
|
|
439
513
|
end
|
|
440
514
|
|
|
515
|
+
CASE_SUBJECT_KINDS = %i[local global constant].freeze
|
|
516
|
+
private_constant :CASE_SUBJECT_KINDS
|
|
517
|
+
|
|
441
518
|
# When the clause has exactly one `RegularExpressionNode` literal condition, narrow the
|
|
442
519
|
# match-data globals on the body edge (same rule as `analyse_regex_match_predicate`'s
|
|
443
520
|
# truthy edge). With multiple regex conditions (`when /a/, /b/`) the body is reachable
|
|
@@ -460,8 +537,29 @@ module Rigor
|
|
|
460
537
|
|
|
461
538
|
# Internal analyser. Returns `[truthy_scope, falsey_scope]` when the predicate shape is
|
|
462
539
|
# recognised, or `nil` to signal "no narrowing" so the public surface can fall back to the
|
|
463
|
-
# 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}).
|
|
464
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)
|
|
465
563
|
case node
|
|
466
564
|
when Prism::ParenthesesNode
|
|
467
565
|
analyse_parentheses(node, scope)
|
|
@@ -473,20 +571,19 @@ module Rigor
|
|
|
473
571
|
analyse_local_write(node, scope)
|
|
474
572
|
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode
|
|
475
573
|
analyse_ivar(node, scope)
|
|
476
|
-
when Prism::ClassVariableWriteNode
|
|
477
|
-
|
|
478
|
-
when Prism::
|
|
479
|
-
|
|
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)
|
|
480
578
|
when Prism::CallNode
|
|
481
|
-
analyse_call(node, scope)
|
|
482
|
-
when Prism::AndNode
|
|
483
|
-
|
|
484
|
-
when Prism::OrNode
|
|
485
|
-
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)
|
|
486
582
|
when Prism::MatchWriteNode
|
|
487
583
|
analyse_match_write(node, scope)
|
|
488
584
|
end
|
|
489
585
|
end
|
|
586
|
+
private_class_method :analyse_shape
|
|
490
587
|
|
|
491
588
|
# rubocop:disable-next Metrics/ClassLength
|
|
492
589
|
class << self
|
|
@@ -843,6 +940,12 @@ module Rigor
|
|
|
843
940
|
]
|
|
844
941
|
end
|
|
845
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
|
+
|
|
846
949
|
def analyse_cvar_write(node, scope)
|
|
847
950
|
current = scope.cvar(node.name)
|
|
848
951
|
return nil if current.nil?
|
|
@@ -863,6 +966,76 @@ module Rigor
|
|
|
863
966
|
]
|
|
864
967
|
end
|
|
865
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
|
+
|
|
866
1039
|
# `if /(?<x>...)/ =~ str` — Prism wraps the `=~` call in a `MatchWriteNode` listing the
|
|
867
1040
|
# named-capture targets. The parent `eval_match_write` has already bound each target to
|
|
868
1041
|
# `String | nil`; in the truthy branch (the regex matched) every named capture is
|
|
@@ -918,6 +1091,25 @@ module Rigor
|
|
|
918
1091
|
# literals (Slice 6 phase 2 sub-phase 2).
|
|
919
1092
|
# Anything else returns nil so the surrounding analyser falls through to the
|
|
920
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
|
+
|
|
921
1113
|
def analyse_call(node, scope)
|
|
922
1114
|
return nil if node.block
|
|
923
1115
|
|
|
@@ -1068,11 +1260,19 @@ module Rigor
|
|
|
1068
1260
|
# symbol is one of `NilClass`'s own methods (`:to_s`, `:inspect`, `:nil?`, …) — `nil`
|
|
1069
1261
|
# DOES respond to those, so the truthy edge admits a nil receiver and we narrow nothing.
|
|
1070
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
|
+
#
|
|
1071
1271
|
# Conservative floor: narrow only on a literal `Symbol`/`String` argument resolved
|
|
1072
1272
|
# against the RBS environment; a non-literal symbol, a missing argument, or a symbol
|
|
1073
1273
|
# 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
|
-
#
|
|
1274
|
+
# ("does not respond" proves little about the receiver's type). A local, instance variable,
|
|
1275
|
+
# global or constant receiver narrows ({#receiver_slot}).
|
|
1076
1276
|
def analyse_respond_to_predicate(node, scope)
|
|
1077
1277
|
return nil if node.block
|
|
1078
1278
|
return nil if node.arguments.nil? || node.arguments.arguments.size != 1
|
|
@@ -1081,16 +1281,63 @@ module Rigor
|
|
|
1081
1281
|
return nil if sym.nil?
|
|
1082
1282
|
return nil if nilclass_method?(sym, scope)
|
|
1083
1283
|
|
|
1084
|
-
|
|
1085
|
-
return nil if
|
|
1284
|
+
slot = receiver_slot(node.receiver, scope)
|
|
1285
|
+
return nil if slot.nil?
|
|
1086
1286
|
|
|
1087
|
-
|
|
1088
|
-
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
|
|
1089
1289
|
|
|
1090
|
-
|
|
1091
|
-
|
|
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) }
|
|
1092
1308
|
|
|
1093
|
-
|
|
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?
|
|
1094
1341
|
end
|
|
1095
1342
|
|
|
1096
1343
|
# True when `nil` responds to `sym` — i.e. `NilClass` (own, inherited
|
|
@@ -1409,13 +1656,17 @@ module Rigor
|
|
|
1409
1656
|
# `String`; every other numbered group present in the pattern stays `String | nil` on
|
|
1410
1657
|
# both edges (a truthy match leaves an optional group nil at runtime), so we do not
|
|
1411
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.
|
|
1412
1663
|
def regex_match_predicate_scopes(scope, unconditional)
|
|
1413
1664
|
string_t = Type::Combinator.nominal_of("String")
|
|
1414
1665
|
match_data_t = Type::Combinator.nominal_of("MatchData")
|
|
1415
1666
|
nil_t = Type::Combinator.constant_of(nil)
|
|
1416
1667
|
|
|
1417
|
-
truthy = scope
|
|
1418
|
-
falsey =
|
|
1668
|
+
truthy = scope.forget_match_globals
|
|
1669
|
+
falsey = truthy
|
|
1419
1670
|
truthy = truthy.with_global(:$~, match_data_t)
|
|
1420
1671
|
falsey = falsey.with_global(:$~, nil_t)
|
|
1421
1672
|
REGEX_MATCH_GLOBALS.each do |name|
|
|
@@ -1449,8 +1700,10 @@ module Rigor
|
|
|
1449
1700
|
# them to non-nil `String` would be unsound. The walker is intentionally light (char
|
|
1450
1701
|
# scan, not a regex-AST parse): backslash escapes are skipped; `(?:…)`, lookahead
|
|
1451
1702
|
# `(?=…)`/`(?!…)`, and lookbehind `(?<=…)`/`(?<!…)` do not capture; named groups
|
|
1452
|
-
# `(?<name>…)` do.
|
|
1453
|
-
#
|
|
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.
|
|
1454
1707
|
def unconditional_capture_groups(source)
|
|
1455
1708
|
# `unconditional` collects every capturing index; a group is later removed (with its
|
|
1456
1709
|
# whole subtree) when it is optionally quantified, nested under an optional ancestor,
|
|
@@ -1460,7 +1713,15 @@ module Rigor
|
|
|
1460
1713
|
# mutually exclusive, so its descendant captures may be absent on a successful match;
|
|
1461
1714
|
# the group itself still participates. Closing a frame rolls its subtree up to the
|
|
1462
1715
|
# parent so an optional / alternated ancestor disqualifies it.
|
|
1463
|
-
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)
|
|
1464
1725
|
pos = 0
|
|
1465
1726
|
length = source.length
|
|
1466
1727
|
while pos < length
|
|
@@ -1473,12 +1734,13 @@ module Rigor
|
|
|
1473
1734
|
pos = skip_char_class(source, pos) + 1
|
|
1474
1735
|
next
|
|
1475
1736
|
end
|
|
1476
|
-
|
|
1737
|
+
yield pos, chr
|
|
1477
1738
|
pos += 1
|
|
1478
1739
|
end
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1740
|
+
end
|
|
1741
|
+
|
|
1742
|
+
def named_groups?(source)
|
|
1743
|
+
each_structural_char(source) { |pos, chr| break true if chr == "(" && named_group?(source, pos) } == true
|
|
1482
1744
|
end
|
|
1483
1745
|
|
|
1484
1746
|
# Updates the walk `state` at a group-relevant char during
|
|
@@ -1488,7 +1750,7 @@ module Rigor
|
|
|
1488
1750
|
case chr
|
|
1489
1751
|
when "("
|
|
1490
1752
|
idx = nil
|
|
1491
|
-
if capturing_group?(source, pos)
|
|
1753
|
+
if capturing_group?(source, pos, named: state[:named])
|
|
1492
1754
|
idx = (state[:group_index] += 1)
|
|
1493
1755
|
state[:unconditional] << idx
|
|
1494
1756
|
end
|
|
@@ -1527,11 +1789,20 @@ module Rigor
|
|
|
1527
1789
|
end
|
|
1528
1790
|
end
|
|
1529
1791
|
|
|
1530
|
-
|
|
1531
|
-
|
|
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] == "'"
|
|
1532
1805
|
|
|
1533
|
-
# `(?<name>…)` captures; `(?<=…)`/`(?<!…)` (lookbehind) and
|
|
1534
|
-
# `(?:…)`/`(?=…)`/`(?!…)` do not.
|
|
1535
1806
|
source[pos + 2] == "<" && source[pos + 3] != "=" && source[pos + 3] != "!"
|
|
1536
1807
|
end
|
|
1537
1808
|
|
|
@@ -1962,9 +2233,30 @@ module Rigor
|
|
|
1962
2233
|
analyse_class_predicate_on_local(node, scope, class_name, exact)
|
|
1963
2234
|
when Prism::CallNode
|
|
1964
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)
|
|
1965
2238
|
end
|
|
1966
2239
|
end
|
|
1967
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
|
+
|
|
1968
2260
|
# The class name a `is_a?` / `kind_of?` / `instance_of?` argument denotes: the top-level
|
|
1969
2261
|
# `bare_name` when the reference is written `::Foo` (nil when nothing declares it there),
|
|
1970
2262
|
# the lexical resolution otherwise.
|
|
@@ -2096,16 +2388,8 @@ module Rigor
|
|
|
2096
2388
|
end
|
|
2097
2389
|
|
|
2098
2390
|
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
|
-
]
|
|
2391
|
+
slot = ReceiverSlot.new(kind: :local, name: name, current: current)
|
|
2392
|
+
class_slot_scopes(scope, slot, class_name, exact: exact)
|
|
2109
2393
|
end
|
|
2110
2394
|
|
|
2111
2395
|
# Slice 7 phase 4 — `===`-narrowing. The case-equality predicate `<receiver> === local`
|
|
@@ -2139,9 +2423,23 @@ module Rigor
|
|
|
2139
2423
|
analyse_case_equality_receiver(node.receiver, scope, arg.name, current)
|
|
2140
2424
|
when Prism::CallNode
|
|
2141
2425
|
analyse_case_equality_on_chain(node.receiver, arg, scope)
|
|
2426
|
+
else
|
|
2427
|
+
analyse_case_equality_on_slot(node.receiver, arg, scope)
|
|
2142
2428
|
end
|
|
2143
2429
|
end
|
|
2144
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
|
+
|
|
2145
2443
|
# `Class === <local/ivar>.<method>` — the case-equality counterpart of
|
|
2146
2444
|
# {.analyse_class_predicate_on_chain}. The `open3` idiom `if Hash === cmd.last` narrows
|
|
2147
2445
|
# `cmd.last` to the class inside the branch, recorded as a single-hop method-chain
|
|
@@ -2360,6 +2658,10 @@ module Rigor
|
|
|
2360
2658
|
# that position, or nil when the overload shape does not allow a precise match.
|
|
2361
2659
|
def lookup_positional_arg(call_node, method_def, target_name)
|
|
2362
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
|
+
|
|
2363
2665
|
method_def.method_types.each do |mt|
|
|
2364
2666
|
params = mt.type.required_positionals + mt.type.optional_positionals
|
|
2365
2667
|
index = params.find_index { |param| param.name == target_name }
|
|
@@ -2375,7 +2677,9 @@ module Rigor
|
|
|
2375
2677
|
# cannot statically classify are treated as "no narrowing": the body falls back to the
|
|
2376
2678
|
# union of what we did learn (or the entry type when nothing learned), and the falsey
|
|
2377
2679
|
# edge is the entry type (because we cannot prove the unknown condition didn't match).
|
|
2378
|
-
|
|
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)
|
|
2379
2683
|
truthy_members = []
|
|
2380
2684
|
falsey_type = current
|
|
2381
2685
|
fully_narrowable = true
|
|
@@ -2392,10 +2696,7 @@ module Rigor
|
|
|
2392
2696
|
end
|
|
2393
2697
|
|
|
2394
2698
|
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
|
-
]
|
|
2699
|
+
[truthy, fully_narrowable ? falsey_type : current]
|
|
2399
2700
|
end
|
|
2400
2701
|
|
|
2401
2702
|
# Per-condition rule. Returns `nil` when the condition shape is not recognised (caller
|
|
@@ -2647,9 +2948,12 @@ module Rigor
|
|
|
2647
2948
|
# `is_a?(Integer)`) narrow DOWN to `Nominal[class_name]`; otherwise (disjoint
|
|
2648
2949
|
# hierarchies under `is_a?`, mismatch under `instance_of?`) collapse to `Bot`.
|
|
2649
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).
|
|
2650
2954
|
def narrow_nominal_to_class(nominal, class_name, context)
|
|
2651
2955
|
return nominal if nominal.class_name == class_name
|
|
2652
|
-
return
|
|
2956
|
+
return exact_nominal_to_class(nominal, class_name, context) if context.exact
|
|
2653
2957
|
|
|
2654
2958
|
case class_ordering(nominal.class_name, class_name, context)
|
|
2655
2959
|
when :superclass then Type::Combinator.nominal_of(class_name)
|
|
@@ -2664,6 +2968,17 @@ module Rigor
|
|
|
2664
2968
|
end
|
|
2665
2969
|
end
|
|
2666
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
|
+
|
|
2667
2982
|
def narrow_nominal_not_class(nominal, class_name, context)
|
|
2668
2983
|
return Type::Combinator.bot if nominal.class_name == class_name
|
|
2669
2984
|
return nominal if context.exact
|
|
@@ -2808,25 +3123,15 @@ module Rigor
|
|
|
2808
3123
|
context.environment.class_ordering(lhs, rhs)
|
|
2809
3124
|
end
|
|
2810
3125
|
|
|
3126
|
+
# `x.nil?` on a local, instance variable, global or constant (#1429) read.
|
|
2811
3127
|
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?
|
|
3128
|
+
slot = receiver_slot(receiver, scope)
|
|
3129
|
+
return nil if slot.nil?
|
|
2824
3130
|
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
end
|
|
3131
|
+
[
|
|
3132
|
+
narrow_slot(scope, slot, narrow_nil(slot.current)),
|
|
3133
|
+
narrow_slot(scope, slot, narrow_non_nil(slot.current))
|
|
3134
|
+
]
|
|
2830
3135
|
end
|
|
2831
3136
|
|
|
2832
3137
|
# The three value-pinned classes a union arm can name whose instances are the single
|
|
@@ -2945,21 +3250,39 @@ module Rigor
|
|
|
2945
3250
|
def analyse_safe_nav_receiver(node, scope)
|
|
2946
3251
|
return nil unless node.safe_navigation?
|
|
2947
3252
|
|
|
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
|
|
3253
|
+
narrowed = non_nil_slot_scope(node.receiver, scope)
|
|
3254
|
+
narrowed && [narrowed, scope]
|
|
3255
|
+
end
|
|
2955
3256
|
|
|
2956
|
-
|
|
2957
|
-
|
|
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?
|
|
2958
3262
|
|
|
2959
|
-
non_nil = narrow_non_nil(current)
|
|
2960
|
-
return nil if non_nil.equal?(current)
|
|
3263
|
+
non_nil = narrow_non_nil(slot.current)
|
|
3264
|
+
return nil if non_nil.equal?(slot.current)
|
|
2961
3265
|
|
|
2962
|
-
|
|
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)
|
|
2963
3286
|
end
|
|
2964
3287
|
|
|
2965
3288
|
# Issue #606 slice 1 — the SAFE-NAV CHAIN forms. {.analyse_safe_nav_receiver} above proves
|
|
@@ -3031,12 +3354,17 @@ module Rigor
|
|
|
3031
3354
|
return nil if suffixes.empty? && !allow_bare
|
|
3032
3355
|
|
|
3033
3356
|
receiver = current.receiver
|
|
3034
|
-
return nil unless receiver.is_a?(
|
|
3035
|
-
receiver.is_a?(Prism::InstanceVariableReadNode)
|
|
3357
|
+
return nil unless SLOT_READ_NODES.any? { |klass| receiver.is_a?(klass) }
|
|
3036
3358
|
|
|
3037
3359
|
[receiver, suffixes]
|
|
3038
3360
|
end
|
|
3039
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
|
+
|
|
3040
3368
|
# Which edge — if either — a chain's outcome narrows on. The inner suffixes (everything
|
|
3041
3369
|
# below the outermost) only ever have to NOT return, so they are held to the raise test
|
|
3042
3370
|
# alone; the outermost decides the edge per the table in {NIL_FALSEY_SUFFIXES}'s comment.
|
|
@@ -3051,20 +3379,7 @@ module Rigor
|
|
|
3051
3379
|
end
|
|
3052
3380
|
|
|
3053
3381
|
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)
|
|
3382
|
+
non_nil_slot_scope(receiver, scope)
|
|
3068
3383
|
end
|
|
3069
3384
|
|
|
3070
3385
|
# Issue #606 slice 1, second form — a safe-nav chain COMPARED to a value that cannot be
|
|
@@ -3174,14 +3489,23 @@ module Rigor
|
|
|
3174
3489
|
return edges unless node.safe_navigation? && edges
|
|
3175
3490
|
|
|
3176
3491
|
truthy, falsey = edges
|
|
3177
|
-
|
|
3178
|
-
return edges
|
|
3492
|
+
slot = receiver_slot(node.receiver, scope)
|
|
3493
|
+
return edges if slot.nil?
|
|
3179
3494
|
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
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
|
|
3185
3509
|
end
|
|
3186
3510
|
|
|
3187
3511
|
# `a && b` short-circuits: the truthy edge is the truthy edge of `b` evaluated under
|
|
@@ -3191,7 +3515,7 @@ module Rigor
|
|
|
3191
3515
|
# output scopes.
|
|
3192
3516
|
def analyse_and(node, scope)
|
|
3193
3517
|
truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
|
|
3194
|
-
truthy_b, falsey_b =
|
|
3518
|
+
truthy_b, falsey_b = operand_edges(node.right, truthy_a)
|
|
3195
3519
|
[truthy_b, falsey_a.join(falsey_b)]
|
|
3196
3520
|
end
|
|
3197
3521
|
|
|
@@ -3200,9 +3524,105 @@ module Rigor
|
|
|
3200
3524
|
# falsey scope evaluated under `a`'s falsey scope.
|
|
3201
3525
|
def analyse_or(node, scope)
|
|
3202
3526
|
truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
|
|
3203
|
-
truthy_b, falsey_b =
|
|
3527
|
+
truthy_b, falsey_b = operand_edges(node.right, falsey_a)
|
|
3204
3528
|
[truthy_a.join(truthy_b), falsey_b]
|
|
3205
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
|
|
3206
3626
|
end
|
|
3207
3627
|
end
|
|
3208
3628
|
end
|