rigortype 0.4.1 → 0.4.2
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 +1 -1
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/18-removing-dead-code.md +32 -2
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +289 -57
- data/lib/rigor/analysis/dependency_recorder.rb +15 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
- data/lib/rigor/analysis/incremental_session.rb +14 -1
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
- data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +34 -3
- data/lib/rigor/analysis/worker_session.rb +7 -4
- data/lib/rigor/cache/incremental_snapshot.rb +22 -0
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +13 -7
- data/lib/rigor/cli.rb +10 -2
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +46 -2
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +283 -14
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment.rb +21 -3
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +215 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +1 -1
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +59 -12
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- data/lib/rigor/inference/module_function_state.rb +2 -1
- data/lib/rigor/inference/narrowing.rb +20 -6
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/project_patched_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +687 -60
- data/lib/rigor/inference/statement_evaluator.rb +71 -20
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +42 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/mutation_cache.rb +1 -1
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +127 -11
- data/lib/rigor/reflection/constant_path.rb +13 -2
- data/lib/rigor/reflection.rb +40 -8
- data/lib/rigor/scope/discovery_index.rb +40 -1
- data/lib/rigor/scope/resolution_chain.rb +159 -11
- data/lib/rigor/scope.rb +189 -10
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +55 -5
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +34 -2
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/scope.rbs +27 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- metadata +66 -1
|
@@ -486,7 +486,17 @@ module Rigor
|
|
|
486
486
|
)
|
|
487
487
|
end
|
|
488
488
|
|
|
489
|
-
|
|
489
|
+
project_sequence_to_enumerator(self_type, other_type)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# `Enumerator::ArithmeticSequence[E]` carries only the element of its `Enumerator[Numeric, void]` superclass
|
|
493
|
+
# (#1794), so against a two-argument `Enumerator` it projects to `Enumerator[E, top]`, `top` being what RBS
|
|
494
|
+
# `void` translates to: `Enumerator[Integer, void]` and `[Integer, untyped]` accept it, as RBS would.
|
|
495
|
+
def project_sequence_to_enumerator(self_type, other_type)
|
|
496
|
+
return nil unless self_type.class_name == "Enumerator" && self_type.type_args.size == 2
|
|
497
|
+
return nil unless other_type.class_name == "Enumerator::ArithmeticSequence" && other_type.type_args.size == 1
|
|
498
|
+
|
|
499
|
+
Type::Combinator.nominal_of("Enumerator", type_args: [other_type.type_args.first, Type::Combinator.top])
|
|
490
500
|
end
|
|
491
501
|
|
|
492
502
|
def project_tuple_to_nominal(tuple)
|
|
@@ -963,11 +973,14 @@ module Rigor
|
|
|
963
973
|
end
|
|
964
974
|
end
|
|
965
975
|
|
|
966
|
-
# Constant[v] accepts
|
|
967
|
-
# (modulo the universal Bot/Dynamic short-circuits
|
|
976
|
+
# Constant[v] accepts Constant[v'] with structurally equal value, and the plain nominal of a class whose
|
|
977
|
+
# only instance is `v`. Any other type is rejected (modulo the universal Bot/Dynamic short-circuits
|
|
978
|
+
# already applied upstream).
|
|
968
979
|
def accepts_constant(self_type, other_type, mode)
|
|
969
980
|
if other_type.is_a?(Type::Constant) && self_type == other_type
|
|
970
981
|
Type::AcceptsResult.yes(mode: mode, reasons: "structural literal match")
|
|
982
|
+
elsif sole_instance_nominal?(self_type, other_type)
|
|
983
|
+
Type::AcceptsResult.yes(mode: mode, reasons: "#{other_type.class_name} has no instance but the literal")
|
|
971
984
|
else
|
|
972
985
|
Type::AcceptsResult.no(
|
|
973
986
|
mode: mode,
|
|
@@ -976,6 +989,18 @@ module Rigor
|
|
|
976
989
|
end
|
|
977
990
|
end
|
|
978
991
|
|
|
992
|
+
# `true`, `false` and `nil` are the only instances of their classes, so `FalseClass` holds exactly the
|
|
993
|
+
# values `false` holds. Read apart, `Array.new(n, false)`, typed `Array[FalseClass]` through the
|
|
994
|
+
# `[T] (Integer, T)` binding, failed a declared `-> Array[bool]` and fired `def.return-type-mismatch` on
|
|
995
|
+
# correct code.
|
|
996
|
+
SOLE_INSTANCE_CLASSES = { true => "TrueClass", false => "FalseClass", nil => "NilClass" }.freeze
|
|
997
|
+
private_constant :SOLE_INSTANCE_CLASSES
|
|
998
|
+
|
|
999
|
+
def sole_instance_nominal?(constant, other_type)
|
|
1000
|
+
other_type.is_a?(Type::Nominal) && other_type.type_args.empty? &&
|
|
1001
|
+
SOLE_INSTANCE_CLASSES.key?(constant.value) && SOLE_INSTANCE_CLASSES[constant.value] == other_type.class_name
|
|
1002
|
+
end
|
|
1003
|
+
|
|
979
1004
|
# Tuple[A1..An] accepts:
|
|
980
1005
|
# - Tuple[B1..Bn] when arities match and each Ai accepts Bi (covariant per-position).
|
|
981
1006
|
# - Anything else: no (we cannot prove the arity from a generic nominal alone).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require_relative "method_catalog"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module Builtins
|
|
9
|
+
# Issue #1740 — which instance methods CRuby itself defines on a core class or module, read from the offline
|
|
10
|
+
# catalogues under `data/builtins/ruby_core/` (`tool/extract_builtin_catalog.rb` records each `rb_define_method`
|
|
11
|
+
# and prelude `def` of an `Init_*` function on the class it names, aliases included). A registration inside an
|
|
12
|
+
# `#if 0` block CRuby keeps for RDoc carries `rdoc_only: true` and is left out: `random.c`'s registers
|
|
13
|
+
# `Random#seed` on `Random`, though it runs on `Random::Base`.
|
|
14
|
+
# `spec/rigor/inference/builtins/c_ruby_definers_spec.rb` checks every listed method the running Ruby has
|
|
15
|
+
# against its runtime owner. RBS is not that record: core RBS redeclares some inherited methods on a subclass
|
|
16
|
+
# (`Integer#quo` and `File#to_path`, whose CRuby owners are `Numeric` and `IO`), and declares a few CRuby no
|
|
17
|
+
# longer defines (`Process::Status#&`). A class no catalogue covers answers false, as does a method its
|
|
18
|
+
# catalogue does not list: the caller reads both as "not proven". The catalogue is one CRuby snapshot.
|
|
19
|
+
module CRubyDefiners
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# Does the catalogue prove CRuby defines `method_name` on `class_name` itself?
|
|
23
|
+
def defines?(class_name, method_name)
|
|
24
|
+
names = INDEX[class_name.to_s.delete_prefix("::")]
|
|
25
|
+
!names.nil? && names.include?(method_name.to_s)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def build_index
|
|
29
|
+
index = Hash.new { |hash, key| hash[key] = Set.new }
|
|
30
|
+
MethodCatalog.topic_paths.each do |path|
|
|
31
|
+
data = YAML.safe_load_file(path, permitted_classes: [Symbol])
|
|
32
|
+
classes = data.is_a?(Hash) ? data["classes"] : nil
|
|
33
|
+
next unless classes.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
classes.each do |name, klass|
|
|
36
|
+
next if klass["rdoc_only"]
|
|
37
|
+
|
|
38
|
+
names = index[name]
|
|
39
|
+
names.merge(defined_names(klass["instance_methods"]))
|
|
40
|
+
names.merge(defined_names(klass["aliases"]))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
Ractor.make_shareable(index.to_h { |name, names| [name, names.freeze] })
|
|
44
|
+
rescue Psych::SyntaxError, SystemCallError
|
|
45
|
+
Ractor.make_shareable({})
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The names of `entries` CRuby defines at runtime: one the catalogue marks `rdoc_only` was registered only for
|
|
49
|
+
# RDoc, inside an `#if 0` block.
|
|
50
|
+
def defined_names(entries)
|
|
51
|
+
return [] unless entries.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
entries.filter_map { |method, entry| method unless entry.is_a?(Hash) && entry["rdoc_only"] }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
INDEX = build_index
|
|
57
|
+
private_constant :INDEX
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -48,6 +48,9 @@ module Rigor
|
|
|
48
48
|
new(path: File.join(DATA_ROOT, "#{topic}.yml"), mutating_selectors: mutating_selectors)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# Every catalogue file under {DATA_ROOT}, in a stable order.
|
|
52
|
+
def self.topic_paths = Dir[File.join(DATA_ROOT, "*.yml")]
|
|
53
|
+
|
|
51
54
|
def initialize(path:, mutating_selectors: {})
|
|
52
55
|
@path = path
|
|
53
56
|
@mutating_selectors = mutating_selectors.transform_values(&:freeze).freeze
|
|
@@ -33,7 +33,7 @@ module Rigor
|
|
|
33
33
|
# ({.rehydrated_nesting}).
|
|
34
34
|
def self.with_run
|
|
35
35
|
previous = Thread.current[MEMO_KEY]
|
|
36
|
-
Thread.current[MEMO_KEY] = { nodes: {}, indexes: {}, nestings: {}.compare_by_identity }
|
|
36
|
+
Thread.current[MEMO_KEY] = { nodes: {}, indexes: {}, nestings: {}.compare_by_identity, refinement_queries: {} }
|
|
37
37
|
yield
|
|
38
38
|
ensure
|
|
39
39
|
Thread.current[MEMO_KEY] = previous
|
|
@@ -113,6 +113,28 @@ module Rigor
|
|
|
113
113
|
end
|
|
114
114
|
private_class_method :record_nesting
|
|
115
115
|
|
|
116
|
+
# Issue #1664 — the {InEffectRefinements} over `path`'s own parse, once a run, or nil when the file cannot be
|
|
117
|
+
# read: the typed refined arm reads another file's refine body through it. The bodies' `Module.nesting`
|
|
118
|
+
# is recorded as a handle's is, so the re-typed body reads its constants where it is written.
|
|
119
|
+
def self.refinement_query(path)
|
|
120
|
+
memo = Thread.current[MEMO_KEY]
|
|
121
|
+
return build_refinement_query(path, nil) if memo.nil?
|
|
122
|
+
|
|
123
|
+
queries = memo[:refinement_queries]
|
|
124
|
+
return queries[path] if queries.key?(path)
|
|
125
|
+
|
|
126
|
+
queries[path] = build_refinement_query(path, memo)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def self.build_refinement_query(path, memo)
|
|
130
|
+
query = InEffectRefinements.new(Prism.parse(File.read(path)).value)
|
|
131
|
+
query.refine_def_nestings.each { |node, nesting| record_nesting(memo, node, nesting) } if memo
|
|
132
|
+
query
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
private_class_method :build_refinement_query
|
|
137
|
+
|
|
116
138
|
# Finds the node for `handle` using a per-file `{node_id => DefNode}` + `{name => DefNode}` index cache.
|
|
117
139
|
def self.locate(handle, index_cache)
|
|
118
140
|
index = index_cache[handle.path] ||= build_file_index(handle.path)
|
|
@@ -28,6 +28,7 @@ require_relative "fallback"
|
|
|
28
28
|
require_relative "flow_tracer"
|
|
29
29
|
require_relative "index_write_widening"
|
|
30
30
|
require_relative "indexed_narrowing"
|
|
31
|
+
require_relative "key_presence_guard"
|
|
31
32
|
require_relative "jump_targets"
|
|
32
33
|
require_relative "define_method_block_self"
|
|
33
34
|
require_relative "macro_block_self_type"
|
|
@@ -39,6 +40,7 @@ require_relative "optimistic_origin"
|
|
|
39
40
|
require_relative "receiver_alias"
|
|
40
41
|
require_relative "repeated_or_writes"
|
|
41
42
|
require_relative "singleton_object_constant"
|
|
43
|
+
require_relative "refined_dispatch"
|
|
42
44
|
require_relative "stored_block_call"
|
|
43
45
|
require_relative "struct_fold_safety"
|
|
44
46
|
require_relative "unknown_store_widening"
|
|
@@ -648,7 +650,12 @@ module Rigor
|
|
|
648
650
|
# `StatementEvaluator` at class-body and method-body boundaries) or `Dynamic[Top]` at the top level.
|
|
649
651
|
# Class-body `self` is `Singleton[<class>]`; instance-method `self` is `Nominal[<class>]`;
|
|
650
652
|
# singleton-method `self` is `Singleton[<class>]`.
|
|
653
|
+
#
|
|
654
|
+
# Issue #1763 — directly in a `refine X` block body `self` is the refinement module, which Rigor does not
|
|
655
|
+
# model; its `Singleton[X]` stand-in serves implicit-self calls only, so a value read of `self` is untyped.
|
|
651
656
|
def type_of_self_node(_node)
|
|
657
|
+
return dynamic_top if scope.refinement_block_self?
|
|
658
|
+
|
|
652
659
|
scope.self_type || dynamic_top
|
|
653
660
|
end
|
|
654
661
|
|
|
@@ -1811,7 +1818,8 @@ module Rigor
|
|
|
1811
1818
|
# that normal return is `untyped` — the result's declared type — so `loop { e.next }` is not `bot` and
|
|
1812
1819
|
# `loop { x = e.next; break x if x }` keeps its arm beside it.
|
|
1813
1820
|
def call_dispatch_type_for(node, receiver_override: nil)
|
|
1814
|
-
result =
|
|
1821
|
+
result = key_guarded_type(node, call_result_type_for(node, receiver_override: receiver_override))
|
|
1822
|
+
result = loop_completion_type(node, result)
|
|
1815
1823
|
arms = call_break_arm_types(node, receiver_override: receiver_override)
|
|
1816
1824
|
if exactly_once_block_never_completes?(node, receiver_override)
|
|
1817
1825
|
return arms.empty? ? Type::Combinator.bot : Type::Combinator.union(*arms)
|
|
@@ -1821,6 +1829,15 @@ module Rigor
|
|
|
1821
1829
|
widen_optimistic_predicate_constant(node, combined)
|
|
1822
1830
|
end
|
|
1823
1831
|
|
|
1832
|
+
# Issue #1703 — `H[k]` under a true `H.key?(k)` with a non-literal key drops the miss `nil`
|
|
1833
|
+
# ({KeyPresenceGuard.guarded_read}). Only inside the guarded re-walk `call.possible-nil-receiver` runs to
|
|
1834
|
+
# decide whether to withhold a report; the analysis every other rule reads never records a guard.
|
|
1835
|
+
def key_guarded_type(node, type)
|
|
1836
|
+
return type unless node.name == :[]
|
|
1837
|
+
|
|
1838
|
+
KeyPresenceGuard.guarded_read(node, type, scope) || type
|
|
1839
|
+
end
|
|
1840
|
+
|
|
1824
1841
|
# Issue #1172 — the nil-collapsing predicates (`nil?`, `!`, `x == nil`, …) answer a `Constant` that
|
|
1825
1842
|
# encodes the receiver's nil-freeness. When that nil-freeness is only *optimistic* — an
|
|
1826
1843
|
# `%a{implicitly-returns-nil}` read `RbsDispatch` deliberately reads past — the constant is a bet,
|
|
@@ -1896,6 +1913,9 @@ module Rigor
|
|
|
1896
1913
|
# asks nothing further of dispatch. Off (the default) this is one integer read.
|
|
1897
1914
|
Effects::Collector.record_call(node, receiver, scope) if Effects::Collector.active?
|
|
1898
1915
|
|
|
1916
|
+
refined = try_refined_dispatch(node, receiver, arg_types, block_type)
|
|
1917
|
+
return refined if refined
|
|
1918
|
+
|
|
1899
1919
|
literal_send = try_literal_send(node, receiver)
|
|
1900
1920
|
return literal_send if literal_send
|
|
1901
1921
|
|
|
@@ -1927,6 +1947,155 @@ module Rigor
|
|
|
1927
1947
|
dispatch_miss_result(node, receiver, arg_types, block_type)
|
|
1928
1948
|
end
|
|
1929
1949
|
|
|
1950
|
+
# ADR-121 WD2 (issue #1664) — a call through an in-effect Ruby refinement, typed from the winning refine body
|
|
1951
|
+
# with the call's receiver as `self`, ahead of every tier below: each answers from the method the refinement
|
|
1952
|
+
# replaces (a refined `String#upcase` must not fold, a refined `map` must not fold per element, a top-level
|
|
1953
|
+
# `def` must not bind ahead of a refinement of `Object`, a plugin models the class's own method). An unreadable
|
|
1954
|
+
# body, or a list that may hold any refinement, answers `Dynamic[top]`, never the replaced method's signature.
|
|
1955
|
+
# A union receiver is decided per member; a `Dynamic` receiver, a class object and a call no refinement wins
|
|
1956
|
+
# fall through. A call site no refinement is in effect at answers nil at once and records nothing: its answer
|
|
1957
|
+
# cannot change without an edit to its own file.
|
|
1958
|
+
#
|
|
1959
|
+
# ADR-121 WD7 (A2) — inside the span of an activation that puts an opaque module in effect
|
|
1960
|
+
# ({InEffectRefinements.opaque_module?}), any method of any instance may be replaced, so every instance-receiver
|
|
1961
|
+
# call answers `Dynamic[top]`; a class object falls through, as it does below.
|
|
1962
|
+
def try_refined_dispatch(node, receiver, arg_types, block_type)
|
|
1963
|
+
list = scope.in_effect_refinements(node)
|
|
1964
|
+
return nil if list.empty?
|
|
1965
|
+
|
|
1966
|
+
InEffectRefinements.record_refinement_names(node.name)
|
|
1967
|
+
return dynamic_top if refined_instance_receiver?(receiver) && opaque_refinement_span?(node, list)
|
|
1968
|
+
return nil if scope.discovered_refinements.empty?
|
|
1969
|
+
|
|
1970
|
+
refined_indirect_result(node, receiver, list) ||
|
|
1971
|
+
refined_receiver_result(node, receiver, list, arg_types, block_type)
|
|
1972
|
+
end
|
|
1973
|
+
|
|
1974
|
+
# ADR-121 WD4 — `send` / `__send__` / `public_send` with a literal name, `respond_to?` and `method` /
|
|
1975
|
+
# `public_method` with one, and a `&:name` block argument honour refinements too (CRuby `test_refinement.rb`;
|
|
1976
|
+
# probed on Ruby 4.0.5). When the name they reach is refined in effect for the receiver they answer
|
|
1977
|
+
# `Dynamic[top]`: typing them through the arm is deferred, and `respond_to?` must never fold to `false`. A
|
|
1978
|
+
# `&:name` block's receiver is the yielded element, which is not known here, so any in-effect refinement of the
|
|
1979
|
+
# name answers. Only `methods` ignores refinements and keeps its answer.
|
|
1980
|
+
def refined_indirect_result(node, receiver, list)
|
|
1981
|
+
name = REFINED_NAMING_CALLS.include?(node.name) ? literal_method_name_argument(node) : nil
|
|
1982
|
+
return dynamic_top if name && refined_name_for?(receiver, name, list)
|
|
1983
|
+
|
|
1984
|
+
block_name = symbol_block_argument(node)
|
|
1985
|
+
dynamic_top if block_name && refined_anywhere?(block_name, list)
|
|
1986
|
+
end
|
|
1987
|
+
|
|
1988
|
+
REFINED_NAMING_CALLS = Set[:send, :__send__, :public_send, :respond_to?, :method, :public_method].freeze
|
|
1989
|
+
private_constant :REFINED_NAMING_CALLS
|
|
1990
|
+
|
|
1991
|
+
def literal_method_name_argument(node)
|
|
1992
|
+
argument = node.arguments&.arguments&.first
|
|
1993
|
+
name = argument.unescaped if argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
|
|
1994
|
+
return nil if name.nil?
|
|
1995
|
+
|
|
1996
|
+
name = name.to_sym
|
|
1997
|
+
InEffectRefinements.record_refinement_names(name)
|
|
1998
|
+
name
|
|
1999
|
+
end
|
|
2000
|
+
|
|
2001
|
+
def symbol_block_argument(node)
|
|
2002
|
+
block = node.block
|
|
2003
|
+
return nil unless block.is_a?(Prism::BlockArgumentNode) && block.expression.is_a?(Prism::SymbolNode)
|
|
2004
|
+
|
|
2005
|
+
name = block.expression.unescaped.to_sym
|
|
2006
|
+
InEffectRefinements.record_refinement_names(name)
|
|
2007
|
+
name
|
|
2008
|
+
end
|
|
2009
|
+
|
|
2010
|
+
def refined_name_for?(receiver, name, list)
|
|
2011
|
+
members = receiver.is_a?(Type::Union) ? receiver.members : [receiver]
|
|
2012
|
+
members.any? do |member|
|
|
2013
|
+
class_name = refined_receiver_class_name(member)
|
|
2014
|
+
!class_name.nil? && !RefinedDispatch.winner(scope, class_name, name, list).nil?
|
|
2015
|
+
end
|
|
2016
|
+
end
|
|
2017
|
+
|
|
2018
|
+
# ADR-121 WD7 — a names-wildcard row counts as a refinement of every name.
|
|
2019
|
+
def refined_anywhere?(name, list)
|
|
2020
|
+
refinements = scope.discovered_refinements
|
|
2021
|
+
wildcard = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
|
|
2022
|
+
return true if list.include?(InEffectRefinements::UNKNOWN) &&
|
|
2023
|
+
refinements.any? { |_refined, methods| methods.key?(name) || methods.key?(wildcard) }
|
|
2024
|
+
|
|
2025
|
+
!RefinedDispatch.targets(refinements, name, list).nil? ||
|
|
2026
|
+
!RefinedDispatch.targets(refinements, wildcard, list).nil?
|
|
2027
|
+
end
|
|
2028
|
+
|
|
2029
|
+
# ADR-121 WD7 — does the call's lexical list (a plugin-declared module is the plugin's declaration and never
|
|
2030
|
+
# opaque) hold an opaque module? `list` is the full list, which is the lexical one when nothing is declared.
|
|
2031
|
+
def opaque_refinement_span?(node, list)
|
|
2032
|
+
lexical = scope.declared_refinements.empty? ? list : scope.in_effect_refinements(node, InEffectRefinements::EMPTY)
|
|
2033
|
+
InEffectRefinements.opaque_in?(scope, lexical)
|
|
2034
|
+
end
|
|
2035
|
+
|
|
2036
|
+
# Does `receiver` (or a member of a union) have a class the refined arm answers for: an instance, not a class
|
|
2037
|
+
# object or `Dynamic`?
|
|
2038
|
+
def refined_instance_receiver?(receiver)
|
|
2039
|
+
members = receiver.is_a?(Type::Union) ? receiver.members : [receiver]
|
|
2040
|
+
members.any? { |member| !refined_receiver_class_name(member).nil? }
|
|
2041
|
+
end
|
|
2042
|
+
|
|
2043
|
+
def refined_receiver_result(node, receiver, list, arg_types, block_type)
|
|
2044
|
+
return refined_member_result(node, receiver, list, arg_types, block_type) unless receiver.is_a?(Type::Union)
|
|
2045
|
+
|
|
2046
|
+
answers = receiver.members.map { |member| refined_member_result(node, member, list, arg_types, block_type) }
|
|
2047
|
+
return nil if answers.none?
|
|
2048
|
+
|
|
2049
|
+
members = receiver.members.each_with_index.map do |member, index|
|
|
2050
|
+
answers[index] || call_result_type_for(node, receiver_override: member)
|
|
2051
|
+
end
|
|
2052
|
+
Type::Combinator.union(*members)
|
|
2053
|
+
end
|
|
2054
|
+
|
|
2055
|
+
def refined_member_result(node, receiver, list, arg_types, block_type)
|
|
2056
|
+
class_name = refined_receiver_class_name(receiver)
|
|
2057
|
+
return nil if class_name.nil?
|
|
2058
|
+
|
|
2059
|
+
winner = RefinedDispatch.winner(scope, class_name, node.name, list)
|
|
2060
|
+
return nil if winner.nil?
|
|
2061
|
+
return dynamic_top if winner == RefinedDispatch::UNKNOWN
|
|
2062
|
+
|
|
2063
|
+
refined_body_return(winner, node.name, Type::Combinator.nominal_of(class_name), arg_types, block_type) ||
|
|
2064
|
+
dynamic_top
|
|
2065
|
+
end
|
|
2066
|
+
|
|
2067
|
+
# The winning refine body's inferred return, re-typed with the receiver's class as `self` and the call's
|
|
2068
|
+
# argument types bound as an undeclared method's are; nil when the body is not readable here.
|
|
2069
|
+
#
|
|
2070
|
+
# A body another file wrote is re-typed with that file's in-effect refinements at the `def` (its own module
|
|
2071
|
+
# among them), so a sibling override it calls answers from its refine body too.
|
|
2072
|
+
def refined_body_return(winner, method_name, self_type, arg_types, block_type)
|
|
2073
|
+
def_node, foreign =
|
|
2074
|
+
InEffectRefinements.refinement_def_with_query(scope, winner.module_name, winner.refined_class, method_name)
|
|
2075
|
+
return nil if def_node.nil?
|
|
2076
|
+
|
|
2077
|
+
refinements = foreign&.at(def_node.location.start_offset) do |name|
|
|
2078
|
+
InEffectRefinements.activated_modules(scope, name)
|
|
2079
|
+
end
|
|
2080
|
+
infer_user_method_return(def_node, self_type, arg_types, yield_type: block_type, refinements: refinements)
|
|
2081
|
+
rescue StandardError
|
|
2082
|
+
nil
|
|
2083
|
+
end
|
|
2084
|
+
|
|
2085
|
+
# The class an instance-side receiver is an instance of, or nil for one a refinement does not answer here: a
|
|
2086
|
+
# class object (refinements of singleton classes are not recorded), `Dynamic`, `top`, or a shape with no class.
|
|
2087
|
+
def refined_receiver_class_name(receiver)
|
|
2088
|
+
case receiver
|
|
2089
|
+
when Type::Nominal, Type::StructInstance, Type::DataInstance then receiver.class_name
|
|
2090
|
+
when Type::Constant then receiver.value.class.name
|
|
2091
|
+
when Type::Tuple then "Array"
|
|
2092
|
+
when Type::HashShape then "Hash"
|
|
2093
|
+
when Type::IntegerRange then "Integer"
|
|
2094
|
+
when Type::FloatRange then "Float"
|
|
2095
|
+
when Type::Refined, Type::Difference then refined_receiver_class_name(receiver.base)
|
|
2096
|
+
end
|
|
2097
|
+
end
|
|
2098
|
+
|
|
1930
2099
|
# The post-dispatch tiers for a call `MethodDispatcher` could not answer, in their historical
|
|
1931
2100
|
# order; extracted from {#call_dispatch_type_for} whole.
|
|
1932
2101
|
def dispatch_miss_result(node, receiver, arg_types, block_type = nil)
|
|
@@ -2959,14 +3128,37 @@ module Rigor
|
|
|
2959
3128
|
# not observable on the body scope, so it does need its own memo-key slot. It is dropped for a def
|
|
2960
3129
|
# that cannot reach a `yield` — nearly all of them — which keeps the key shape constant for the
|
|
2961
3130
|
# methods the memo actually carries and confines the extra dimension to yielding callees.
|
|
2962
|
-
|
|
3131
|
+
# Issue #1703 — inside the guarded re-walk a callee's return summary is still computed with `key?` guards off
|
|
3132
|
+
# ({KeyPresenceGuard.without_guards}): summaries are memoised for the whole run, and one computed with a guard
|
|
3133
|
+
# would reach the analysis every rule reads.
|
|
3134
|
+
#
|
|
3135
|
+
# Issue #1664 — `refinements` are the modules in effect in a refine body another file wrote, which the body scope
|
|
3136
|
+
# carries as declared refinements: its nodes are not this file's, so this file's lexical list cannot answer them.
|
|
3137
|
+
# The return memo and the recursion guard do not key on them, which holds because such a node comes only from
|
|
3138
|
+
# `DefNodeResolver.refinement_query`'s own parse and always carries that file's one list; a caller that types
|
|
3139
|
+
# the same node under another list must add it to the key.
|
|
3140
|
+
def infer_user_method_return(def_node, receiver, arg_types, self_fold_safe: false, yield_type: nil,
|
|
3141
|
+
refinements: nil)
|
|
2963
3142
|
return nil if def_node.body.nil?
|
|
3143
|
+
unless KeyPresenceGuard.active?
|
|
3144
|
+
return infer_user_method_return_unguarded(def_node, receiver, arg_types, self_fold_safe, yield_type,
|
|
3145
|
+
refinements)
|
|
3146
|
+
end
|
|
2964
3147
|
|
|
3148
|
+
KeyPresenceGuard.without_guards do
|
|
3149
|
+
infer_user_method_return_unguarded(def_node, receiver, arg_types, self_fold_safe, yield_type, refinements)
|
|
3150
|
+
end
|
|
3151
|
+
end
|
|
3152
|
+
|
|
3153
|
+
def infer_user_method_return_unguarded(def_node, receiver, arg_types, self_fold_safe, yield_type,
|
|
3154
|
+
refinements = nil)
|
|
2965
3155
|
yield_type = nil unless yield_type && body_yields?(def_node)
|
|
2966
3156
|
body_scope = build_user_method_body_scope(def_node, receiver, arg_types,
|
|
2967
3157
|
self_fold_safe: self_fold_safe)
|
|
2968
3158
|
return nil if body_scope.nil?
|
|
2969
3159
|
|
|
3160
|
+
body_scope = body_scope.with_declared_refinements(refinements) if refinements
|
|
3161
|
+
|
|
2970
3162
|
# Recursion-guard signature. Keyed on `(receiver, method)` only — NOT the argument types. ADR-24 WD5:
|
|
2971
3163
|
# a method whose summary is still being computed resolves to `Dynamic[top]` for that cycle. Keying on
|
|
2972
3164
|
# arg types would let mutual recursion through a `module_function` module (`Acceptance#accepts` →
|
|
@@ -4021,12 +4213,13 @@ module Rigor
|
|
|
4021
4213
|
method_name: call_node.name,
|
|
4022
4214
|
arg_types: arg_types,
|
|
4023
4215
|
environment: scope.environment,
|
|
4024
|
-
scope: scope
|
|
4216
|
+
scope: scope,
|
|
4217
|
+
call_node: call_node
|
|
4025
4218
|
)
|
|
4026
4219
|
block_return_for(
|
|
4027
4220
|
block_arg, expected,
|
|
4028
4221
|
call_node: call_node,
|
|
4029
|
-
|
|
4222
|
+
narrowing: block_body_self_narrowing(call_node, receiver_type),
|
|
4030
4223
|
repeats: !BLOCK_VALUE_DISCARDING.include?(call_node.name) && block_may_repeat?(call_node, receiver_type)
|
|
4031
4224
|
)
|
|
4032
4225
|
rescue StandardError
|
|
@@ -4093,7 +4286,7 @@ module Rigor
|
|
|
4093
4286
|
block_scope = block_entry_scope(
|
|
4094
4287
|
block_node, param_types,
|
|
4095
4288
|
call_node: call_node,
|
|
4096
|
-
|
|
4289
|
+
narrowing: block_body_self_narrowing(call_node, receiver),
|
|
4097
4290
|
captured: repeating_captured_bindings(block_node, param_types, block_may_repeat?(call_node, receiver))
|
|
4098
4291
|
)
|
|
4099
4292
|
_result, collected = StatementEvaluator.with_break_value_sink do
|
|
@@ -4102,10 +4295,12 @@ module Rigor
|
|
|
4102
4295
|
collected.filter_map { |jump, type| type if targets.key?(jump) }
|
|
4103
4296
|
end
|
|
4104
4297
|
|
|
4105
|
-
# The block body's narrowed `self_type
|
|
4298
|
+
# The block body's narrowing — a {MacroBlockSelfType::Match} carrying the narrowed `self_type` and the declared
|
|
4299
|
+
# refinements — or `nil` to leave the scope contract unchanged.
|
|
4106
4300
|
#
|
|
4107
4301
|
# ADR-16 Tier A: a registered plugin's `block_as_methods` entry matching `(receiver_type,
|
|
4108
|
-
# call_node.name)` narrows to the receiver class's instance type
|
|
4302
|
+
# call_node.name)` narrows to the receiver class's instance type, and puts its `refinements:` in effect in the
|
|
4303
|
+
# body (issue #1667).
|
|
4109
4304
|
#
|
|
4110
4305
|
# Issue #963: `define_method(:name) { ... }` installs its block as an instance method and runs it with
|
|
4111
4306
|
# `self` bound to the receiving instance, so the block body's `self` is the INSTANCE side of a class body's
|
|
@@ -4115,9 +4310,11 @@ module Rigor
|
|
|
4115
4310
|
# matters wherever the block's value is observable, e.g. a project-declared generic `define_method`
|
|
4116
4311
|
# signature that returns the block's own type.
|
|
4117
4312
|
def block_body_self_narrowing(call_node, receiver_type)
|
|
4118
|
-
MacroBlockSelfType.
|
|
4119
|
-
|
|
4120
|
-
|
|
4313
|
+
match = MacroBlockSelfType.match_for(scope: scope, call_node: call_node, receiver_type: receiver_type)
|
|
4314
|
+
return match if match
|
|
4315
|
+
|
|
4316
|
+
defined = DefineMethodBlockSelf.narrow_self_type_for(scope: scope, call_node: call_node)
|
|
4317
|
+
defined && MacroBlockSelfType::Match.new(self_type: defined, refinements: InEffectRefinements::EMPTY)
|
|
4121
4318
|
end
|
|
4122
4319
|
|
|
4123
4320
|
def break_arm_param_types(call_node, receiver)
|
|
@@ -4126,16 +4323,17 @@ module Rigor
|
|
|
4126
4323
|
method_name: call_node.name,
|
|
4127
4324
|
arg_types: call_arg_types(call_node),
|
|
4128
4325
|
environment: scope.environment,
|
|
4129
|
-
scope: scope
|
|
4326
|
+
scope: scope,
|
|
4327
|
+
call_node: call_node
|
|
4130
4328
|
)
|
|
4131
4329
|
end
|
|
4132
4330
|
|
|
4133
|
-
def block_return_for(block_arg, expected, call_node: nil,
|
|
4331
|
+
def block_return_for(block_arg, expected, call_node: nil, narrowing: nil, repeats: false)
|
|
4134
4332
|
case block_arg
|
|
4135
4333
|
when Prism::BlockNode
|
|
4136
4334
|
captured = repeating_captured_bindings(block_arg, expected, repeats)
|
|
4137
4335
|
entry = block_entry_scope(
|
|
4138
|
-
block_arg, expected, call_node: call_node,
|
|
4336
|
+
block_arg, expected, call_node: call_node, narrowing: narrowing, captured: captured
|
|
4139
4337
|
)
|
|
4140
4338
|
type_block_body(block_arg, entry, captured: captured)
|
|
4141
4339
|
when Prism::BlockArgumentNode
|
|
@@ -4151,13 +4349,14 @@ module Rigor
|
|
|
4151
4349
|
# Issue #316 — mirrors `StatementEvaluator#build_block_entry_scope`: the block body's `self` is the
|
|
4152
4350
|
# yielding method's business, so the return-typing pass must see the same unmodelled-self mark. Issue #1358
|
|
4153
4351
|
# — and the same match-global view ({MatchRebinding.block_entry}), which reads the owning `call_node`.
|
|
4154
|
-
def block_entry_scope(block_node, expected, call_node: nil,
|
|
4352
|
+
def block_entry_scope(block_node, expected, call_node: nil, narrowing: nil, captured: nil)
|
|
4155
4353
|
entry = MatchRebinding.block_entry(scope.entering_opaque_block, block_node, call_node)
|
|
4156
4354
|
entry = captured.lay(entry) if captured
|
|
4157
4355
|
block_scope = BlockParameterBinder.new(expected_param_types: expected).bind_onto(block_node, entry)
|
|
4158
|
-
return block_scope unless
|
|
4356
|
+
return block_scope unless narrowing
|
|
4159
4357
|
|
|
4160
|
-
|
|
4358
|
+
keeps_unknown = call_node ? scope.block_self_narrowing_unknown?(call_node) : false
|
|
4359
|
+
MacroBlockSelfType.apply(block_scope, narrowing, keeps_unknown: keeps_unknown)
|
|
4161
4360
|
end
|
|
4162
4361
|
|
|
4163
4362
|
# The #587 (b) captured binding when the call may run the block more than once ({#block_may_repeat?}), and
|
|
@@ -82,7 +82,7 @@ module Rigor
|
|
|
82
82
|
|
|
83
83
|
# A standalone walk, for a file no existing descent visits (a `pre_eval:` entry).
|
|
84
84
|
def scan(root)
|
|
85
|
-
collector = Collector.new
|
|
85
|
+
collector = Collector.new(InEffectRefinements.new(root))
|
|
86
86
|
walk(root, collector, true)
|
|
87
87
|
collector.census.freeze
|
|
88
88
|
end
|
|
@@ -98,13 +98,16 @@ module Rigor
|
|
|
98
98
|
|
|
99
99
|
# Accumulates one file's census, fed each node of a pre-order descent. `top_level` says whether the node sits
|
|
100
100
|
# outside every `class` / `module` body; it is the host walk's to track. `refine` blocks are remembered by
|
|
101
|
-
# offset as they are met, so a node inside one is known without the host walk tracking it.
|
|
101
|
+
# offset as they are met, so a node inside one is known without the host walk tracking it. `in_effect` is the
|
|
102
|
+
# file's {InEffectRefinements}: a `refine` it places where `self` is a class is the class's own method, so its
|
|
103
|
+
# block is not a refinement (issue #1689).
|
|
102
104
|
class Collector
|
|
103
105
|
attr_reader :census
|
|
104
106
|
|
|
105
|
-
def initialize
|
|
107
|
+
def initialize(in_effect)
|
|
106
108
|
@census = Set.new
|
|
107
109
|
@refine_ranges = []
|
|
110
|
+
@in_effect = in_effect
|
|
108
111
|
end
|
|
109
112
|
|
|
110
113
|
# A node the collector cannot read must not fail the analysis of its file, nor of every file the pre-pass
|
|
@@ -223,7 +226,7 @@ module Rigor
|
|
|
223
226
|
def self_receiver?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
224
227
|
|
|
225
228
|
def remember_refine(node)
|
|
226
|
-
return unless
|
|
229
|
+
return unless ScopeIndexer.refine_call?(node) && !@in_effect.class_body_refine?(node)
|
|
227
230
|
|
|
228
231
|
location = node.block.location
|
|
229
232
|
@refine_ranges << (location.start_offset...location.end_offset)
|
|
@@ -406,7 +406,7 @@ module Rigor
|
|
|
406
406
|
arguments = call_node.arguments&.arguments || []
|
|
407
407
|
MethodDispatcher.expected_block_param_types(
|
|
408
408
|
receiver_type: receiver, method_name: call_node.name, environment: scope.environment, scope: scope,
|
|
409
|
-
arg_types: arguments.map { |argument| scope.type_of(argument) }
|
|
409
|
+
arg_types: arguments.map { |argument| scope.type_of(argument) }, call_node: call_node
|
|
410
410
|
)
|
|
411
411
|
rescue StandardError
|
|
412
412
|
[]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
class InEffectRefinements
|
|
8
|
+
# Issue #1666 — the Proc literals whose body a `.refined` receiver call applies to, recognised from syntax.
|
|
9
|
+
module ProcLiterals
|
|
10
|
+
# The `Kernel` calls whose literal block is the Proc they return.
|
|
11
|
+
BARE_PROC_CALLS = %i[proc lambda].freeze
|
|
12
|
+
private_constant :BARE_PROC_CALLS
|
|
13
|
+
|
|
14
|
+
# Copies that keep a refined Proc's refinements (`test_refined_preserved_by_dup`): a chain reads through them.
|
|
15
|
+
COPY_CALLS = %i[dup clone].freeze
|
|
16
|
+
private_constant :COPY_CALLS
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# `[literal, refined calls]` for the outermost `.refined` call `node`: the Proc literal the chain starts from
|
|
21
|
+
# ({.refinable}, or nil), and every `.refined` call of the chain, innermost (first called) first. The chain
|
|
22
|
+
# reads through parentheses and through `dup` / `clone`.
|
|
23
|
+
def refined_chain(node)
|
|
24
|
+
calls = [node]
|
|
25
|
+
receiver = unwrap(node.receiver)
|
|
26
|
+
loop do
|
|
27
|
+
if refined_call?(receiver)
|
|
28
|
+
calls << receiver
|
|
29
|
+
elsif !copy_call?(receiver)
|
|
30
|
+
break
|
|
31
|
+
end
|
|
32
|
+
receiver = unwrap(receiver.receiver)
|
|
33
|
+
end
|
|
34
|
+
[refinable(receiver), calls.reverse]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def refined_call?(node) = node.is_a?(Prism::CallNode) && node.name == :refined
|
|
38
|
+
|
|
39
|
+
def copy_call?(node)
|
|
40
|
+
node.is_a?(Prism::CallNode) && COPY_CALLS.include?(node.name) && node.arguments.nil? && node.block.nil? &&
|
|
41
|
+
!node.receiver.nil?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# `( expr )` with one statement reads as `expr`.
|
|
45
|
+
def unwrap(node)
|
|
46
|
+
while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode) &&
|
|
47
|
+
node.body.body.size == 1
|
|
48
|
+
node = node.body.body.first
|
|
49
|
+
end
|
|
50
|
+
node
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The node whose span is the body `node.refined(…)` applies to: `node` itself for a lambda literal, or the
|
|
54
|
+
# literal block of a `proc` / `lambda` call on no receiver or on `Kernel`, or of `Proc.new`; nil for anything
|
|
55
|
+
# else. A Proc held in a variable, or one passed as `&blk`, is not one: which block it holds is not syntax.
|
|
56
|
+
def refinable(node)
|
|
57
|
+
case node
|
|
58
|
+
when Prism::LambdaNode then node
|
|
59
|
+
when Prism::CallNode
|
|
60
|
+
block = node.block
|
|
61
|
+
return nil unless block.is_a?(Prism::BlockNode)
|
|
62
|
+
|
|
63
|
+
block if bare_proc_call?(node) || proc_new_call?(node)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def bare_proc_call?(node)
|
|
68
|
+
BARE_PROC_CALLS.include?(node.name) && (node.receiver.nil? || top_constant?(node.receiver, :Kernel))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def proc_new_call?(node)
|
|
72
|
+
node.name == :new && top_constant?(node.receiver, :Proc)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# `Name` or `::Name`.
|
|
76
|
+
def top_constant?(node, name)
|
|
77
|
+
(node.is_a?(Prism::ConstantReadNode) || (node.is_a?(Prism::ConstantPathNode) && node.parent.nil?)) &&
|
|
78
|
+
node.name == name
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|