rigortype 0.4.4 → 0.4.5
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 -5
- data/docs/handbook/11-sig-gen.md +90 -17
- data/docs/manual/02-cli-reference.md +39 -9
- data/docs/manual/03-configuration.md +10 -8
- data/docs/manual/04-diagnostics.md +16 -5
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +228 -15
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +49 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +166 -24
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/rule_catalog.rb +17 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +69 -16
- data/lib/rigor/analysis/runner.rb +69 -65
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +12 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +26 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +60 -15
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +21 -1
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +87 -20
- data/lib/rigor/inference/expression_typer.rb +35 -5
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +137 -42
- data/lib/rigor/inference/narrowing.rb +54 -10
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +120 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +675 -105
- data/lib/rigor/inference/self_call_ivar_writes.rb +11 -6
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +446 -139
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +2 -2
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +45 -2
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +13 -1
|
@@ -526,12 +526,60 @@ module Rigor
|
|
|
526
526
|
registry = environment&.plugin_registry
|
|
527
527
|
each_source_ancestor_candidate(scope, class_name) do |candidate|
|
|
528
528
|
return nil if scope.discovered_method?(candidate, method_name, kind)
|
|
529
|
-
|
|
530
|
-
|
|
529
|
+
next unless ALLOWED_RBS_COMPLETE_ANCESTORS.include?(candidate) ||
|
|
530
|
+
registry&.rbs_complete_ancestor?(candidate)
|
|
531
|
+
|
|
532
|
+
return shadowed_in_ruby_order?(scope, class_name, kind, method_name, candidate) ? nil : candidate
|
|
531
533
|
end
|
|
532
534
|
nil
|
|
533
535
|
end
|
|
534
536
|
|
|
537
|
+
# Issue #1572 — the walk above is breadth-first, so it reaches the superclass before a module an included
|
|
538
|
+
# module includes, which Ruby's order puts ahead of it (`class P < Rigor::Plugin::Base; include A` with `A`
|
|
539
|
+
# including `M` calls `M#prepare`, not `Base#prepare`), and it passes over RBS-known modules. True when the
|
|
540
|
+
# bridged `ancestor` is not what Ruby calls: on `class_name`'s {Scope::ResolutionChain} in Ruby's order, a
|
|
541
|
+
# project entry ahead of it records the name ({#project_records_name?}), an external entry ahead of it is a
|
|
542
|
+
# module RBS knows and declares the name in, or the chain does not hold it. An RBS-unknown external ahead is
|
|
543
|
+
# passed over, as the breadth-first walk always did. An unsettled chain withholds too: a mixin edge the
|
|
544
|
+
# tables cannot place (a receiver-form `Demo.include(M)` after the body, an edge in a conditional, a block or
|
|
545
|
+
# a hook) may put a definer ahead of the ancestor.
|
|
546
|
+
def shadowed_in_ruby_order?(scope, class_name, kind, method_name, ancestor)
|
|
547
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, kind, :methods)
|
|
548
|
+
return true if chain.unsettled?
|
|
549
|
+
|
|
550
|
+
entries = chain.entries
|
|
551
|
+
stop = entries.index do |entry|
|
|
552
|
+
entry.external? ? entry.candidates.include?(ancestor) : entry.name == ancestor
|
|
553
|
+
end
|
|
554
|
+
return true if stop.nil?
|
|
555
|
+
|
|
556
|
+
entries.first(stop).any? do |entry|
|
|
557
|
+
if entry.external?
|
|
558
|
+
rbs_declares_ahead?(scope, entry, method_name)
|
|
559
|
+
else
|
|
560
|
+
project_records_name?(scope, entry.name, method_name, entry.side)
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Whether the project entry `owner` holds the name on `kind`'s side. `discovered_methods` carries the
|
|
566
|
+
# `attr_*` and `define_method` rows project-wide, but at dispatch it has no row for a `def` in a module
|
|
567
|
+
# declared in another file, so a `def` is also asked of the project-wide def tables. ADR-46:
|
|
568
|
+
# `user_def_for` / `singleton_def_for` file the method edge, positive or negative, so a file that adds or
|
|
569
|
+
# removes the `def` on an entry ahead of the ancestor re-checks the call.
|
|
570
|
+
def project_records_name?(scope, owner, method_name, kind)
|
|
571
|
+
return true if scope.discovered_method?(owner, method_name, kind)
|
|
572
|
+
|
|
573
|
+
return !scope.singleton_def_for(owner, method_name).nil? if kind == :singleton
|
|
574
|
+
|
|
575
|
+
!scope.user_def_for(owner, method_name).nil?
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def rbs_declares_ahead?(scope, entry, method_name)
|
|
579
|
+
entry.candidates.any? { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) } &&
|
|
580
|
+
!Scope::ResolutionChain::Relevance.external_lacks_on?(scope, entry.candidates, entry.side, method_name)
|
|
581
|
+
end
|
|
582
|
+
|
|
535
583
|
# Issue #527 slice 1 — the RBS instance definition a Ruby-source class inherits from a CORE or
|
|
536
584
|
# STDLIB ancestor, or nil. `Oj::EasyHash < Hash` answering `Dynamic[top]` to `has_key?` while
|
|
537
585
|
# `{}.has_key?` folds was the largest single opacity family in the 2026-09-01 corpus sweep.
|
|
@@ -220,7 +220,8 @@ module Rigor
|
|
|
220
220
|
# `call.undefined-method`.
|
|
221
221
|
patched_result =
|
|
222
222
|
unless skip_project_patched
|
|
223
|
-
try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment
|
|
223
|
+
try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment,
|
|
224
|
+
call_node: call_node, scope: scope)
|
|
224
225
|
end
|
|
225
226
|
if patched_result
|
|
226
227
|
scope&.record_dynamic_origin(call_node, DynamicOrigin::EXTERNAL_GEM_WITHOUT_RBS)
|
|
@@ -258,6 +259,22 @@ module Rigor
|
|
|
258
259
|
return discovered_result
|
|
259
260
|
end
|
|
260
261
|
|
|
262
|
+
# Issue #1708 — a `pre_eval:` patch on one of the receiver's ancestors (`class Object; def zz`,
|
|
263
|
+
# `module Enumerable; def sum_by`). Below the RBS tier, whose decline means no nearer RBS declares the
|
|
264
|
+
# method, and below the tiers that answer the receiver's own patch or a gem catalogue entry. A nearer
|
|
265
|
+
# `def` in the project's source (on an RBS-known receiver, which the discovered-method tier leaves to
|
|
266
|
+
# user-method inference) stops the walk: see {#project_source_defines?}. That probe reads the scope's def
|
|
267
|
+
# index, so a single-file scope (`type-of`, `annotate`, an LSP buffer) cannot see a `def` in another file.
|
|
268
|
+
ancestor_patched =
|
|
269
|
+
if !skip_project_patched && scope
|
|
270
|
+
try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment,
|
|
271
|
+
call_node: call_node, scope: scope, through_ancestors: true)
|
|
272
|
+
end
|
|
273
|
+
if ancestor_patched
|
|
274
|
+
scope&.record_dynamic_origin(call_node, DynamicOrigin::EXTERNAL_GEM_WITHOUT_RBS)
|
|
275
|
+
return ancestor_patched
|
|
276
|
+
end
|
|
277
|
+
|
|
261
278
|
# ADR-5 robustness — synthesized-stub-type tier. When the receiver is a type Rigor
|
|
262
279
|
# invented to make an otherwise-unbuildable project signature resolve (a
|
|
263
280
|
# missing-namespace module, or a stub for a referenced-but-undeclared type like an
|
|
@@ -644,7 +661,13 @@ module Rigor
|
|
|
644
661
|
# `def to_url; "hello"; end` patched onto `String` now resolves `s.to_url` to
|
|
645
662
|
# `Dynamic[Nominal[String]]` instead of the pre-3a `Dynamic[Top]`. Falls back to
|
|
646
663
|
# `Dynamic[Top]` when the heuristic declined (non-literal tail expression).
|
|
647
|
-
|
|
664
|
+
#
|
|
665
|
+
# `through_ancestors:` (issue #1708), with a scope, searches the receiver's ancestors as well, through
|
|
666
|
+
# {ProjectPatchedMethods#lookup_through_ancestors}, stopping at a nearer project-source `def`. A private or
|
|
667
|
+
# protected patch Ruby would not let the call reach ({ProjectPatchedMethods#callable?}) answers nothing, so
|
|
668
|
+
# `1.kp` on `module Kernel; private def kp` reads as the undefined call it is.
|
|
669
|
+
def try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment, # rubocop:disable Metrics/ParameterLists
|
|
670
|
+
call_node: nil, scope: nil, through_ancestors: false)
|
|
648
671
|
registry = environment&.project_patched_methods
|
|
649
672
|
return nil if registry.nil? || registry.empty?
|
|
650
673
|
|
|
@@ -652,14 +675,55 @@ module Rigor
|
|
|
652
675
|
return nil if class_name.nil?
|
|
653
676
|
|
|
654
677
|
kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
|
|
655
|
-
entry = registry
|
|
678
|
+
entry = project_patched_entry(registry, class_name, method_name, kind, environment,
|
|
679
|
+
scope: scope, through_ancestors: through_ancestors)
|
|
656
680
|
return nil if entry.nil?
|
|
681
|
+
|
|
682
|
+
verdict = registry.callable?(entry, explicit: explicit_non_self_receiver?(call_node),
|
|
683
|
+
self_type: scope ? scope.self_type : :unknown,
|
|
684
|
+
rbs_loader: environment.rbs_loader)
|
|
685
|
+
return nil if verdict == false
|
|
686
|
+
return Type::Combinator.untyped if verdict.nil?
|
|
657
687
|
return Type::Combinator.dynamic(entry.return_type) if entry.return_type
|
|
658
688
|
|
|
659
689
|
aliased = project_patched_alias_target(entry, receiver_type, arg_types, block_type, environment)
|
|
660
690
|
aliased ? Type::Combinator.dynamic(aliased) : Type::Combinator.untyped
|
|
661
691
|
end
|
|
662
692
|
|
|
693
|
+
def project_patched_entry(registry, class_name, method_name, kind, environment, scope:, through_ancestors:)
|
|
694
|
+
return registry.lookup(class_name: class_name, method_name: method_name, kind: kind) unless through_ancestors
|
|
695
|
+
|
|
696
|
+
registry.lookup_through_ancestors(
|
|
697
|
+
class_name: class_name, method_name: method_name, kind: kind, rbs_loader: environment.rbs_loader,
|
|
698
|
+
source_defines: ->(owner, side) { project_source_defines?(scope, owner, method_name, side) }
|
|
699
|
+
)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# Issue #1708 — does the project's own source define `method_name` on `owner` (`side` as the ancestor walk
|
|
703
|
+
# reaches it), or on a module the project prepends to or includes into it? Ruby runs that `def` ahead of a
|
|
704
|
+
# patch on a farther ancestor: `class Integer; def nn = "s"` beats `class Object; def nn = nil` in a
|
|
705
|
+
# `pre_eval:` file. The owner's level of its {Scope::ResolutionChain} (its prepends, itself, its includes
|
|
706
|
+
# and their own mixins, or a class object's singleton and its extends) holds those definers; whether one
|
|
707
|
+
# of them defines the name does not depend on their order, so the chain is searched without settling it.
|
|
708
|
+
def project_source_defines?(scope, owner, method_name, side)
|
|
709
|
+
return false unless scope.known_user_class?(owner)
|
|
710
|
+
|
|
711
|
+
chain = Scope::ResolutionChain.for(scope, owner, side, :methods)
|
|
712
|
+
own_level_end = chain.level_starts[1] || chain.entries.size
|
|
713
|
+
found = chain.search(scope, 0, own_level_end) do |entry|
|
|
714
|
+
!entry.external? && source_def?(scope, entry.name, method_name, entry.side)
|
|
715
|
+
end
|
|
716
|
+
!found.nil?
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def source_def?(scope, owner, method_name, side)
|
|
720
|
+
if side == :singleton
|
|
721
|
+
scope.discovered_method?(owner, method_name, :singleton) || !scope.singleton_def_for(owner, method_name).nil?
|
|
722
|
+
else
|
|
723
|
+
scope.discovered_method?(owner, method_name, :instance) || !scope.user_def_for(owner, method_name).nil?
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
663
727
|
# Issue #1702 — an alias in a `pre_eval:` patch whose old name no earlier patch binding defines
|
|
664
728
|
# (`alias old_plus +` on Integer, or `alias_method :orig_succ, :succ` ahead of the patch's own `def succ`)
|
|
665
729
|
# names the method the class had before the patch, so it answers what that method answers for the same
|
|
@@ -9,24 +9,36 @@ module Rigor
|
|
|
9
9
|
# The one home for what a receiverless `module_function` call does to the methods around it
|
|
10
10
|
# ([#1507](https://github.com/rigortype/rigor/issues/1507),
|
|
11
11
|
# [#1197](https://github.com/rigortype/rigor/issues/1197)). Four readers used to compute it in four
|
|
12
|
-
# places. They disagree with each other and, in places, with
|
|
13
|
-
# one reader's answer exactly: this module gives the readings one
|
|
14
|
-
# them agree. A change to one reading lands here, beside the
|
|
15
|
-
#
|
|
12
|
+
# places, and the method-visibility table reads it too. They disagree with each other and, in places, with
|
|
13
|
+
# Ruby. Each entry point below reproduces one reader's answer exactly: this module gives the readings one
|
|
14
|
+
# place to live, and does not yet make them agree. A change to one reading lands here, beside the others.
|
|
15
|
+
#
|
|
16
|
+
# In Ruby a bare `module_function` call turns the body's module_function mode on, and a later bare
|
|
17
|
+
# `public`, `private` or `protected` ({.visibility_reset?}) turns it off again (#1968). A named call
|
|
18
|
+
# (`public :x`, `private def y`) leaves the mode alone, and each `module` body starts with it off. A reset
|
|
19
|
+
# written in the body's own statements, or in a plain `begin` or `ensure` there, runs. One nested anywhere
|
|
20
|
+
# else (control flow, a block, a `rescue` clause, an argument) may not, so the mode after it is uncertain.
|
|
21
|
+
# A reset in a nested `class`, `module`, `class <<` or `def` body, a `*_eval` / `*_exec` block or a
|
|
22
|
+
# class-creating block (`Class.new do`, `Struct.new do`, ...) belongs to another scope and does not count.
|
|
16
23
|
#
|
|
17
24
|
# - The singleton def-node table (`ScopeIndexer#walk_singleton_body`) reads
|
|
18
25
|
# {.each_singleton_sibling} and {.each_singleton_copy}. A bare call covers the later direct
|
|
19
|
-
# statements of its own body
|
|
26
|
+
# statements of its own body, up to a reset; after an uncertain one the copy is possible.
|
|
20
27
|
# - The deferred ranges (`ScopeIndexer#walk_deferred_body`) read {.prescan_deferred} and
|
|
21
|
-
# {.deferred_module_function?}. A bare call covers every later def by offset,
|
|
22
|
-
# control flow or a block counts.
|
|
28
|
+
# {.deferred_module_function?}. A bare call covers every later def by offset, up to a later reset,
|
|
29
|
+
# and a call inside control flow or a block counts. An uncertain reset changes nothing here.
|
|
23
30
|
# - The extends table (`ScopeIndexer#record_extend_call`) reads {.extends_self?}. A bare call covers
|
|
24
|
-
# every instance def of the module, in any order.
|
|
31
|
+
# every instance def of the module, in any order, and is never reset.
|
|
25
32
|
# - sig-gen (`SigGen::Generator#walk_statements`) reads {.each_sig_gen_statement} and
|
|
26
33
|
# {.sig_gen_module_function?}. A bare call covers the later statements of its own statement list and
|
|
27
|
-
# what nests in them.
|
|
34
|
+
# what nests in them, up to a reset; after an uncertain one {.sig_gen_uncertain?} says so.
|
|
35
|
+
# - The method-visibility table (`ScopeIndexer#walk_method_visibilities`) reads {.instance_visibility}. It
|
|
36
|
+
# applies a `module_function` call as it applies a `private` one, bare or named: the call makes the instance
|
|
37
|
+
# copy private (#1569), and a later bare `public`, `private` or `protected` sets the running default as it
|
|
38
|
+
# would anyway, so the reset needs no reading of its own. A call or reset that may not run leaves the slots
|
|
39
|
+
# after it contested, as any visibility call there does.
|
|
28
40
|
#
|
|
29
|
-
#
|
|
41
|
+
# Another reader does not come here yet: `rigor-activerecord`'s `ModelDiscoverer#table_name_decorators`
|
|
30
42
|
# (`plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb`) reads a module body's
|
|
31
43
|
# direct statements for its table-name readers. It also accepts `self.module_function`, and there a bare
|
|
32
44
|
# call covers the instance methods written before it as well as after it, while a named call takes only a
|
|
@@ -47,23 +59,60 @@ module Rigor
|
|
|
47
59
|
node.name == :module_function && node.receiver.nil?
|
|
48
60
|
end
|
|
49
61
|
|
|
62
|
+
# The visibility a `module_function` call gives the instance method of each name it covers, bare or named:
|
|
63
|
+
# Ruby makes the instance copy private and adds a public singleton copy, which the instance-side
|
|
64
|
+
# method-visibility table does not hold (#1569).
|
|
65
|
+
def instance_visibility = :private
|
|
66
|
+
|
|
50
67
|
# The toggle form: a `module_function` call with no arguments.
|
|
51
68
|
def bare?(node)
|
|
52
69
|
node.arguments.nil? || node.arguments.arguments.empty?
|
|
53
70
|
end
|
|
54
71
|
|
|
72
|
+
# A bare `public`, `private` or `protected` call, receiverless or on `self`: Ruby ends the module_function
|
|
73
|
+
# mode there (#1968). A call with arguments sets the named methods' visibility and leaves the mode alone.
|
|
74
|
+
def visibility_reset?(node)
|
|
75
|
+
%i[public private protected].include?(node.name) && bare?(node) &&
|
|
76
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The mode after `stmt`, a direct statement of a body, for the readers that walk the statements: `mode`
|
|
80
|
+
# is `true`, `false` or `:uncertain` before it. A {.visibility_reset?} the statement runs ends the mode,
|
|
81
|
+
# and one it may not run (`:maybe_off`, below {.prescan_deferred}) makes an on mode uncertain. A
|
|
82
|
+
# `module_function` nested in the statement is not read here, as before. `nested` says the statement
|
|
83
|
+
# itself may not run, so every reset in it may not either.
|
|
84
|
+
def mode_after(mode, stmt, nested: false)
|
|
85
|
+
return mode unless mode
|
|
86
|
+
|
|
87
|
+
events = []
|
|
88
|
+
prescan_deferred(stmt, EMPTY_PREFIX, false, events, [], nested: nested)
|
|
89
|
+
events.sort_by(&:first).each do |_offset, event|
|
|
90
|
+
case event
|
|
91
|
+
when :off then mode = false
|
|
92
|
+
when :maybe_off then mode = :uncertain if mode == true
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
mode
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The owner prefix {.mode_after} hands the prescan, which records no row a reset needs.
|
|
99
|
+
EMPTY_PREFIX = [].freeze
|
|
100
|
+
|
|
55
101
|
# The singleton def-node table's reading, over one class-ish body's direct statements in source
|
|
56
102
|
# order. `ScopeIndexer#statements_of` builds that list, folding a body-level `rescue`, `else` or
|
|
57
103
|
# `ensure` clause into it. Yields `stmt, module_function_on, named_call` for every statement except a
|
|
58
104
|
# bare call:
|
|
59
105
|
#
|
|
60
|
-
# - `module_function_on`
|
|
61
|
-
#
|
|
106
|
+
# - `module_function_on` is `true` when a bare call precedes the statement with no reset between them,
|
|
107
|
+
# `:uncertain` when a reset that may not run is between them ({.mode_after}), and `false` otherwise.
|
|
62
108
|
# - `named_call` is true for a named call (`module_function :a` or `module_function def x`), which
|
|
63
109
|
# {.each_singleton_copy} resolves; the caller records nothing else for it.
|
|
64
110
|
#
|
|
65
|
-
# A call nested in control flow or a block is not seen.
|
|
66
|
-
|
|
111
|
+
# A `module_function` call nested in control flow or a block is not seen. `conditional` lists the
|
|
112
|
+
# statements of a body-level `rescue` or `else` clause ({.conditional_clause_statements}), which may not
|
|
113
|
+
# run, so a reset there makes the mode uncertain rather than ending it.
|
|
114
|
+
def each_singleton_sibling(statements, conditional = [])
|
|
115
|
+
conditional = Set.new(conditional).compare_by_identity
|
|
67
116
|
module_function_on = false
|
|
68
117
|
statements.each do |stmt|
|
|
69
118
|
if stmt.is_a?(Prism::CallNode) && call?(stmt)
|
|
@@ -75,9 +124,26 @@ module Rigor
|
|
|
75
124
|
next
|
|
76
125
|
end
|
|
77
126
|
yield stmt, module_function_on, false
|
|
127
|
+
module_function_on = mode_after(module_function_on, stmt, nested: conditional.include?(stmt))
|
|
78
128
|
end
|
|
79
129
|
end
|
|
80
130
|
|
|
131
|
+
# The statements of a body-level `rescue` or `else` clause, which `ScopeIndexer#statements_of` folds into
|
|
132
|
+
# the body's direct statements although they run only on an exception, or only without one. An `ensure`
|
|
133
|
+
# clause runs either way and is not listed.
|
|
134
|
+
def conditional_clause_statements(body)
|
|
135
|
+
return [] unless body.is_a?(Prism::BeginNode)
|
|
136
|
+
|
|
137
|
+
statements = []
|
|
138
|
+
clause = body.rescue_clause
|
|
139
|
+
while clause
|
|
140
|
+
statements.concat(ScopeIndexer.statements_of(clause.statements))
|
|
141
|
+
clause = clause.subsequent
|
|
142
|
+
end
|
|
143
|
+
statements.concat(ScopeIndexer.statements_of(body.else_clause.statements)) if body.else_clause
|
|
144
|
+
statements
|
|
145
|
+
end
|
|
146
|
+
|
|
81
147
|
# The defs a named call copies onto the module's singleton, for the singleton def-node table. Ruby
|
|
82
148
|
# copies the method in effect at the call (#1550), so a later def of the name redefines only the
|
|
83
149
|
# instance method. This reading approximates that from the body's direct `statements`: it yields
|
|
@@ -121,10 +187,11 @@ module Rigor
|
|
|
121
187
|
alias_method define_method attr attr_reader attr_writer attr_accessor remove_method undef_method
|
|
122
188
|
].freeze
|
|
123
189
|
|
|
124
|
-
# The deferred-ranges reading, over one class-ish body's subtree. Appends to `offsets`
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
190
|
+
# The deferred-ranges reading, over one class-ish body's subtree. Appends to `offsets` an
|
|
191
|
+
# `[offset, :on]` event for the start of every bare call, and for every {.visibility_reset?} an
|
|
192
|
+
# `[offset, :off]` event where it runs or a `[offset, :maybe_off]` one where it may not: `nested` is true
|
|
193
|
+
# once the walk has left the body's statements, a plain `begin` and an `ensure`. Appends to `ranges` one
|
|
194
|
+
# row per named call, in the table's `[start, end, name, kind, owner]` shape:
|
|
128
195
|
#
|
|
129
196
|
# - a `:name` argument gives a `:singleton` row over the call itself, because the copy happens at
|
|
130
197
|
# the call and not at the def;
|
|
@@ -134,33 +201,36 @@ module Rigor
|
|
|
134
201
|
# The prescan looks through control flow and blocks, because a call that ran there still flips the
|
|
135
202
|
# later defs. It stops at a nested class, module, `class <<` or def body, where the call targets
|
|
136
203
|
# another module. It skips an `END` body, which runs after every def, and a `*_eval` / `*_exec`
|
|
137
|
-
# block, whose self is the receiver's module
|
|
138
|
-
#
|
|
204
|
+
# block, whose self is the receiver's module, and a class-creating block (`Class.new do`, `Struct.new do`,
|
|
205
|
+
# `Data.define do`, `Module.new do`), whose self is the new class; such a block gets its own prescan as a
|
|
206
|
+
# body where the walk opens one.
|
|
139
207
|
#
|
|
140
208
|
# `qualified_prefix` names the owner, and `in_singleton_class` says whether the body is a
|
|
141
209
|
# `class <<` body; both feed `ScopeIndexer.def_singleton?`.
|
|
142
|
-
def prescan_deferred(node, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
210
|
+
def prescan_deferred(node, qualified_prefix, in_singleton_class, offsets, ranges, nested: false)
|
|
143
211
|
return unless node.is_a?(Prism::Node)
|
|
144
212
|
return if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode) ||
|
|
145
213
|
node.is_a?(Prism::SingletonClassNode) || node.is_a?(Prism::DefNode) ||
|
|
146
214
|
node.is_a?(Prism::PostExecutionNode)
|
|
215
|
+
return if node.is_a?(Prism::CallNode) &&
|
|
216
|
+
prescanned_call?(node, qualified_prefix, in_singleton_class, offsets, ranges, nested)
|
|
147
217
|
|
|
148
|
-
|
|
149
|
-
return prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges) if call?(node)
|
|
150
|
-
if ScopeIndexer.receiver_eval_call?(node)
|
|
151
|
-
return prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
218
|
+
nested ||= !RUN_IN_PLACE.include?(node.class)
|
|
155
219
|
node.rigor_each_child do |child|
|
|
156
|
-
prescan_deferred(child, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
220
|
+
prescan_deferred(child, qualified_prefix, in_singleton_class, offsets, ranges, nested: nested)
|
|
157
221
|
end
|
|
158
222
|
end
|
|
159
223
|
|
|
160
|
-
#
|
|
161
|
-
|
|
224
|
+
# The nodes whose statements run where they are written, so a reset there runs ({.prescan_deferred}).
|
|
225
|
+
RUN_IN_PLACE = [Prism::StatementsNode, Prism::BeginNode, Prism::EnsureNode].freeze
|
|
226
|
+
|
|
227
|
+
# Whether the deferred-ranges reading makes a def starting at `start` a module function: the last
|
|
228
|
+
# `:on` or `:off` event {.prescan_deferred} recorded before it, at any depth the prescan entered, is
|
|
229
|
+
# `:on`. A `:maybe_off` leaves the answer as it was: the row stays `:both`, the reading that counts the
|
|
230
|
+
# def as run on the singleton side too, since the table has no possible row for it.
|
|
162
231
|
def deferred_module_function?(offsets, start)
|
|
163
|
-
offsets.
|
|
232
|
+
last = offsets.select { |offset, event| offset < start && event != :maybe_off }.max_by(&:first)
|
|
233
|
+
last ? last[1] == :on : false
|
|
164
234
|
end
|
|
165
235
|
|
|
166
236
|
# The extends table's reading. A bare call makes the module extend itself, so every instance def the
|
|
@@ -174,10 +244,12 @@ module Rigor
|
|
|
174
244
|
end
|
|
175
245
|
|
|
176
246
|
# sig-gen's reading, over one statement list. Yields every statement except a bare call, with
|
|
177
|
-
# whether a bare call came before it in this list or `active` was already
|
|
247
|
+
# whether a bare call came before it in this list or `active` was already on at entry: `true`, `false`
|
|
248
|
+
# or `:uncertain` after a reset that may not run ({.mode_after}). The
|
|
178
249
|
# generator passes the enclosing list's answer into an `if`, a block or a `begin`, but not into a
|
|
179
250
|
# nested class, module or `class << self`. A bare call in a nested list covers only that list's
|
|
180
|
-
# later statements.
|
|
251
|
+
# later statements. A reset ends it for the statements after it, and a named call is not a directive at
|
|
252
|
+
# all.
|
|
181
253
|
def each_sig_gen_statement(statements, active)
|
|
182
254
|
statements.each do |stmt|
|
|
183
255
|
if sig_gen_directive?(stmt)
|
|
@@ -185,6 +257,7 @@ module Rigor
|
|
|
185
257
|
next
|
|
186
258
|
end
|
|
187
259
|
yield stmt, active
|
|
260
|
+
active = mode_after(active, stmt)
|
|
188
261
|
end
|
|
189
262
|
end
|
|
190
263
|
|
|
@@ -196,7 +269,13 @@ module Rigor
|
|
|
196
269
|
# Whether sig-gen renders a def as `def self?.name`: an instance-side def under an active directive,
|
|
197
270
|
# in a class or a module alike. `kind` is `:singleton` for `def self.x` and a `class << self` body.
|
|
198
271
|
def sig_gen_module_function?(active, kind)
|
|
199
|
-
active && kind == :instance
|
|
272
|
+
active == true && kind == :instance
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Whether sig-gen cannot tell a def's kind: an instance-side def after a reset that may not run, which
|
|
276
|
+
# is a module function only if the reset did not run.
|
|
277
|
+
def sig_gen_uncertain?(active, kind)
|
|
278
|
+
active == :uncertain && kind == :instance
|
|
200
279
|
end
|
|
201
280
|
|
|
202
281
|
# Yields each instance-method name `node` may rebind on the module whose body holds it, or
|
|
@@ -239,10 +318,24 @@ module Rigor
|
|
|
239
318
|
end
|
|
240
319
|
end
|
|
241
320
|
|
|
321
|
+
# The prescan's arms for a call; true when the arm took the call and its subtree.
|
|
322
|
+
def prescanned_call?(node, qualified_prefix, in_singleton_class, offsets, ranges, nested)
|
|
323
|
+
if call?(node)
|
|
324
|
+
prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
325
|
+
elsif visibility_reset?(node)
|
|
326
|
+
offsets << [node.location.start_offset, nested ? :maybe_off : :off]
|
|
327
|
+
elsif ScopeIndexer.receiver_eval_call?(node) || (node.block && ScopeIndexer.meta_new_constant_rvalue?(node))
|
|
328
|
+
prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
329
|
+
else
|
|
330
|
+
return false
|
|
331
|
+
end
|
|
332
|
+
true
|
|
333
|
+
end
|
|
334
|
+
|
|
242
335
|
# The prescan's call arm.
|
|
243
336
|
def prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
244
337
|
if bare?(node)
|
|
245
|
-
offsets << node.location.start_offset
|
|
338
|
+
offsets << [node.location.start_offset, :on]
|
|
246
339
|
else
|
|
247
340
|
owner = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
|
|
248
341
|
node.arguments&.arguments&.each do |arg|
|
|
@@ -252,20 +345,22 @@ module Rigor
|
|
|
252
345
|
elsif (name = symbol_argument_name(arg))
|
|
253
346
|
ranges << [node.location.start_offset, node.location.end_offset, name, :singleton, owner]
|
|
254
347
|
else
|
|
255
|
-
prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
348
|
+
prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
|
|
256
349
|
end
|
|
257
350
|
end
|
|
258
351
|
end
|
|
259
352
|
return unless node.block
|
|
260
353
|
|
|
261
|
-
prescan_deferred(node.block, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
354
|
+
prescan_deferred(node.block, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
|
|
262
355
|
end
|
|
263
356
|
|
|
264
357
|
# The prescan's eval arm: only the call's receiver and arguments keep this module's context.
|
|
265
358
|
def prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
266
|
-
|
|
359
|
+
if node.receiver
|
|
360
|
+
prescan_deferred(node.receiver, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
|
|
361
|
+
end
|
|
267
362
|
node.arguments&.arguments&.each do |arg|
|
|
268
|
-
prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
363
|
+
prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
|
|
269
364
|
end
|
|
270
365
|
end
|
|
271
366
|
|
|
@@ -274,8 +369,8 @@ module Rigor
|
|
|
274
369
|
Source::Literals.symbol_or_string(arg)
|
|
275
370
|
end
|
|
276
371
|
|
|
277
|
-
private_class_method :prescan_call, :prescan_eval_call, :symbol_argument_name,
|
|
278
|
-
:each_own_rebound_name, :each_rebinding_call_name
|
|
372
|
+
private_class_method :prescan_call, :prescanned_call?, :prescan_eval_call, :symbol_argument_name,
|
|
373
|
+
:each_rebound_name, :each_own_rebound_name, :each_rebinding_call_name
|
|
279
374
|
end
|
|
280
375
|
end
|
|
281
376
|
end
|
|
@@ -10,6 +10,7 @@ require_relative "../rbs_extended"
|
|
|
10
10
|
require_relative "../analysis/fact_store"
|
|
11
11
|
require_relative "../builtins/regex_refinement"
|
|
12
12
|
require_relative "guard_rebinding"
|
|
13
|
+
require_relative "intervening_ivar_reset"
|
|
13
14
|
require_relative "key_presence_guard"
|
|
14
15
|
require_relative "last_line"
|
|
15
16
|
require_relative "operand_effects"
|
|
@@ -511,8 +512,9 @@ module Rigor
|
|
|
511
512
|
# none of the condition shapes are recognised, both returned scopes equal the input
|
|
512
513
|
# scope. The catalogue mirrors {.case_equality_target_class}: static class/module
|
|
513
514
|
# constants narrow as `is_a?`; integer/float-endpoint ranges narrow to `Numeric`;
|
|
514
|
-
# string-endpoint ranges
|
|
515
|
-
# `false` literal narrows as its
|
|
515
|
+
# string-endpoint ranges narrow to `String`; regexp literals narrow as
|
|
516
|
+
# {.regexp_case_equality_type} says; a `nil`, `true` or `false` literal narrows as its
|
|
517
|
+
# class does ({.when_condition_class}).
|
|
516
518
|
def case_when_scopes(subject, conditions, scope)
|
|
517
519
|
# C1 — `case x when /re/` runs `/re/ === x`, which sets the regex match-data globals
|
|
518
520
|
# exactly as a successful `=~` does. Narrow `$~`/`$&`/`$1..$N` on the clause body (the
|
|
@@ -1797,9 +1799,13 @@ module Rigor
|
|
|
1797
1799
|
# passes through unchanged (failing one pattern proves nothing about the string's shape).
|
|
1798
1800
|
# A non-literal / unanchored / `\Z` / line-anchored / `//x` pattern falls through to no
|
|
1799
1801
|
# narrowing.
|
|
1802
|
+
#
|
|
1803
|
+
# The refinement names a String, so it shares `=~`'s gate (#1955): a receiver that may be a Symbol, or an
|
|
1804
|
+
# untyped one, is left as it is ({#refinable_operand?}).
|
|
1800
1805
|
def analyse_whole_regex_match_predicate(node, scope)
|
|
1801
1806
|
return nil unless node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
1802
1807
|
return nil if node.arguments.nil? || node.arguments.arguments.size != 1
|
|
1808
|
+
return nil unless refinable_operand?(node, scope)
|
|
1803
1809
|
|
|
1804
1810
|
pattern = regex_operand_pattern(node.arguments.arguments.first, scope)
|
|
1805
1811
|
return nil unless pattern.is_a?(RegexMatchPattern)
|
|
@@ -2636,10 +2642,10 @@ module Rigor
|
|
|
2636
2642
|
# iff the endpoints compare it. We conservatively narrow `x` to `Numeric` for
|
|
2637
2643
|
# integer-endpoint ranges and to `String` for string-endpoint ranges; other endpoint
|
|
2638
2644
|
# types fall through.
|
|
2639
|
-
# - **Regexp literal receiver** (`/foo/ === x`): the
|
|
2640
|
-
#
|
|
2641
|
-
# type unchanged because `Regexp#===` returns false for non-Strings AND
|
|
2642
|
-
# that simply do not match.
|
|
2645
|
+
# - **Regexp literal receiver** (`/foo/ === x`): the truthy edge narrows `x` as
|
|
2646
|
+
# {#regexp_case_equality_type} says (its String and Symbol parts, #1955). The falsey edge
|
|
2647
|
+
# keeps the entry type unchanged because `Regexp#===` returns false for non-Strings AND
|
|
2648
|
+
# for Strings that simply do not match.
|
|
2643
2649
|
#
|
|
2644
2650
|
# Anything else — non-local LHS argument, dynamic receiver, custom `===` method — falls
|
|
2645
2651
|
# through to the no-narrowing branch (nil), preserving the entry scope on both edges.
|
|
@@ -2703,6 +2709,9 @@ module Rigor
|
|
|
2703
2709
|
if (class_name = lexical_class_name(receiver, scope))
|
|
2704
2710
|
return class_predicate_scopes(scope, local_name, current, class_name, exact: false)
|
|
2705
2711
|
end
|
|
2712
|
+
if regexp_literal?(receiver)
|
|
2713
|
+
return [scope.with_local(local_name, regexp_case_equality_type(current, scope)), scope]
|
|
2714
|
+
end
|
|
2706
2715
|
|
|
2707
2716
|
target_class = case_equality_target_class(receiver)
|
|
2708
2717
|
return nil if target_class.nil?
|
|
@@ -2716,7 +2725,7 @@ module Rigor
|
|
|
2716
2725
|
end
|
|
2717
2726
|
|
|
2718
2727
|
# The class a `when` condition narrows the subject to, as `is_a?` would, or nil when the condition is no class
|
|
2719
|
-
# test Rigor reads: a class or module constant, a range
|
|
2728
|
+
# test Rigor reads: a class or module constant, a range literal ({.case_equality_target_class}),
|
|
2720
2729
|
# or (#1925) a `nil` / `true` / `false` literal. `nil === x` runs `NilClass#===`, which holds only for `nil`
|
|
2721
2730
|
# itself whatever the subject's class defines, and `nil` is `NilClass`'s only instance, so `when nil` is
|
|
2722
2731
|
# `when NilClass`; `true` and `false` likewise.
|
|
@@ -2733,8 +2742,9 @@ module Rigor
|
|
|
2733
2742
|
# Maps a case-equality literal receiver to the class whose membership is implied by the
|
|
2734
2743
|
# truthy edge. `Prism::ParenthesesNode` wrappers are transparently unwrapped (`(1..10)
|
|
2735
2744
|
# === x` is parsed with the range inside parentheses). Range literals: integer
|
|
2736
|
-
# endpoints → `Numeric`; string endpoints → `String`. Regexp literals → `String
|
|
2737
|
-
#
|
|
2745
|
+
# endpoints → `Numeric`; string endpoints → `String`. Regexp literals → `String`, read only
|
|
2746
|
+
# as the class a dead `when` arm names: their narrowing is {#regexp_case_equality_type}'s,
|
|
2747
|
+
# which the callers consult first. Other shapes return nil so the caller falls through.
|
|
2738
2748
|
def case_equality_target_class(receiver)
|
|
2739
2749
|
receiver = unwrap_parens(receiver)
|
|
2740
2750
|
case receiver
|
|
@@ -2743,6 +2753,33 @@ module Rigor
|
|
|
2743
2753
|
end
|
|
2744
2754
|
end
|
|
2745
2755
|
|
|
2756
|
+
def regexp_literal?(node)
|
|
2757
|
+
case unwrap_parens(node)
|
|
2758
|
+
when Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode then true
|
|
2759
|
+
else false
|
|
2760
|
+
end
|
|
2761
|
+
end
|
|
2762
|
+
|
|
2763
|
+
# Issue #1955 — the subject on the match edge of `/re/ === x` (and of `case x when /re/`). `Regexp#===`
|
|
2764
|
+
# matches a String, a Symbol by its name, and an object that converts with `to_str`, and answers false for
|
|
2765
|
+
# anything else. Each member narrows to its String and Symbol parts: a `String | nil` subject reads `String`, a
|
|
2766
|
+
# Symbol one stays `Symbol`. The `to_str` conversion is not modelled, so a member that defines `to_str` and is
|
|
2767
|
+
# neither drops out of the edge. A `Dynamic` member is left as it is, since it may be a Symbol at run time. The
|
|
2768
|
+
# no-match edge keeps the entry type: a String that does not match reaches it too.
|
|
2769
|
+
def regexp_case_equality_type(current, scope)
|
|
2770
|
+
members = current.is_a?(Type::Union) ? current.members : [current]
|
|
2771
|
+
Type::Combinator.union(*members.map { |member| regexp_case_equality_member(member, scope) })
|
|
2772
|
+
end
|
|
2773
|
+
|
|
2774
|
+
def regexp_case_equality_member(member, scope)
|
|
2775
|
+
return member if member.is_a?(Type::Dynamic)
|
|
2776
|
+
|
|
2777
|
+
parts = %w[String Symbol].map do |class_name|
|
|
2778
|
+
narrow_class(member, class_name, exact: false, environment: scope.environment, scope: scope)
|
|
2779
|
+
end
|
|
2780
|
+
Type::Combinator.union(*parts)
|
|
2781
|
+
end
|
|
2782
|
+
|
|
2746
2783
|
def unwrap_parens(node)
|
|
2747
2784
|
while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode) &&
|
|
2748
2785
|
node.body.body.size == 1
|
|
@@ -2971,6 +3008,10 @@ module Rigor
|
|
|
2971
3008
|
return integer_literal_when_result(current, int_literal, falsey_acc)
|
|
2972
3009
|
end
|
|
2973
3010
|
|
|
3011
|
+
if regexp_literal?(condition)
|
|
3012
|
+
return { truthy: regexp_case_equality_type(current, scope), falsey: falsey_acc, fully_narrowable: true }
|
|
3013
|
+
end
|
|
3014
|
+
|
|
2974
3015
|
target = when_condition_class(condition, scope)
|
|
2975
3016
|
return class_when_result(scope, current, target, falsey_acc) if target
|
|
2976
3017
|
|
|
@@ -3793,9 +3834,12 @@ module Rigor
|
|
|
3793
3834
|
# that edge, so a `$_` the earlier operand narrowed is forgotten when the operand may set it
|
|
3794
3835
|
# (`gets && log(items.each { gets })`), before the operand's own edges narrow it again.
|
|
3795
3836
|
# Issue #1429 — likewise a guard's narrowing of a global or constant the operand may rebind
|
|
3796
|
-
# (`$sep && reset_sep && $sep.length`).
|
|
3837
|
+
# (`$sep && reset_sep && $sep.length`). Issue #1963 — and an instance variable's narrowing an implicit-self
|
|
3838
|
+
# or `self.` call in the operand may write ({InterveningIvarReset.after_operand}): `@x.nil? && reset!` does
|
|
3839
|
+
# not leave `@x` `nil` when `reset!` assigns it.
|
|
3797
3840
|
def operand_edges(node, edge_scope)
|
|
3798
3841
|
edge_scope = LastLine.forget_if_set(edge_scope, node)
|
|
3842
|
+
edge_scope = InterveningIvarReset.after_operand(node, edge_scope)
|
|
3799
3843
|
if edge_scope.guard_narrowed? && GuardRebinding.may_rebind?(node, edge_scope)
|
|
3800
3844
|
edge_scope = edge_scope.forget_guard_narrowings
|
|
3801
3845
|
end
|