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.
Files changed (182) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/11-sig-gen.md +6 -0
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/03-configuration.md +1 -1
  27. data/docs/manual/04-diagnostics.md +2 -1
  28. data/docs/manual/06-baseline.md +2 -0
  29. data/docs/manual/07-plugins.md +1 -1
  30. data/docs/manual/18-removing-dead-code.md +32 -2
  31. data/docs/manual/plugins/README.md +6 -1
  32. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  33. data/docs/manual/plugins/rigor-alba.md +60 -0
  34. data/docs/manual/plugins/rigor-minitest.md +22 -4
  35. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  36. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  37. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  38. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  39. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  40. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  41. data/lib/rigor/analysis/check_rules.rb +289 -57
  42. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  43. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  44. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  45. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  46. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  47. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  48. data/lib/rigor/analysis/incremental.rb +29 -9
  49. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  50. data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
  51. data/lib/rigor/analysis/incremental_session.rb +14 -1
  52. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  53. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  54. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  55. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  56. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
  57. data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
  58. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  59. data/lib/rigor/analysis/runner.rb +34 -3
  60. data/lib/rigor/analysis/worker_session.rb +7 -4
  61. data/lib/rigor/cache/incremental_snapshot.rb +22 -0
  62. data/lib/rigor/cli/annotate_command.rb +5 -0
  63. data/lib/rigor/cli/baseline_command.rb +7 -18
  64. data/lib/rigor/cli/check_command.rb +2 -1
  65. data/lib/rigor/cli/trace_command.rb +5 -0
  66. data/lib/rigor/cli/type_of_command.rb +5 -3
  67. data/lib/rigor/cli/unused_command.rb +13 -7
  68. data/lib/rigor/cli.rb +10 -2
  69. data/lib/rigor/configuration/severity_profile.rb +6 -0
  70. data/lib/rigor/configuration.rb +46 -2
  71. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  72. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  73. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  74. data/lib/rigor/environment/rbs_loader.rb +283 -14
  75. data/lib/rigor/environment/required_features.rb +206 -0
  76. data/lib/rigor/environment.rb +21 -3
  77. data/lib/rigor/inference/acceptance.rb +28 -3
  78. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  79. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  81. data/lib/rigor/inference/expression_typer.rb +215 -16
  82. data/lib/rigor/inference/global_write_census.rb +7 -4
  83. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  84. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  85. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  86. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  87. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  88. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  89. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  90. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  91. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  92. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  93. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  94. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  95. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  96. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  97. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  98. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  99. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  100. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
  101. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  102. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  103. data/lib/rigor/inference/module_function_state.rb +2 -1
  104. data/lib/rigor/inference/narrowing.rb +20 -6
  105. data/lib/rigor/inference/object_mixins.rb +180 -0
  106. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  107. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  108. data/lib/rigor/inference/refine_census.rb +239 -0
  109. data/lib/rigor/inference/refine_self.rb +317 -0
  110. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  111. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  112. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  113. data/lib/rigor/inference/scope_indexer.rb +687 -60
  114. data/lib/rigor/inference/statement_evaluator.rb +71 -20
  115. data/lib/rigor/inference/string_mutation.rb +9 -3
  116. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  117. data/lib/rigor/inference/version_guard.rb +19 -10
  118. data/lib/rigor/language_server/project_context.rb +31 -0
  119. data/lib/rigor/language_server/server.rb +18 -0
  120. data/lib/rigor/plugin/inflector.rb +1 -1
  121. data/lib/rigor/plugin/io_boundary.rb +42 -3
  122. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  123. data/lib/rigor/project_environment.rb +29 -6
  124. data/lib/rigor/protection/mutation_cache.rb +1 -1
  125. data/lib/rigor/protection/mutator.rb +1 -1
  126. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  127. data/lib/rigor/reflection/constant_path.rb +13 -2
  128. data/lib/rigor/reflection.rb +40 -8
  129. data/lib/rigor/scope/discovery_index.rb +40 -1
  130. data/lib/rigor/scope/resolution_chain.rb +159 -11
  131. data/lib/rigor/scope.rb +189 -10
  132. data/lib/rigor/sig_gen/classification.rb +4 -1
  133. data/lib/rigor/sig_gen/generator.rb +55 -5
  134. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  135. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  136. data/lib/rigor/source/alias_names.rb +52 -0
  137. data/lib/rigor/source/literals.rb +3 -2
  138. data/lib/rigor/source/symbol_name.rb +20 -0
  139. data/lib/rigor/type/combinator.rb +34 -2
  140. data/lib/rigor/type/float_range.rb +4 -1
  141. data/lib/rigor/type/nominal.rb +6 -1
  142. data/lib/rigor/version.rb +1 -1
  143. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  144. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  145. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  146. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  147. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  148. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  149. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  150. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  151. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  152. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  153. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  156. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  157. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  158. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  159. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  160. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  161. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  162. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  163. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  164. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  165. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  166. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  167. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  168. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  169. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  170. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  171. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  172. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  173. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  174. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  175. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  176. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  177. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  178. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  179. data/sig/rigor/scope.rbs +27 -4
  180. data/sig/rigor/source.rbs +4 -0
  181. data/sig/rigor.rbs +3 -1
  182. metadata +66 -1
@@ -486,7 +486,17 @@ module Rigor
486
486
  )
487
487
  end
488
488
 
489
- nil
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 only Constant[v'] with structurally equal value. Any other type is rejected
967
- # (modulo the universal Bot/Dynamic short-circuits already applied upstream).
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 = loop_completion_type(node, call_result_type_for(node, receiver_override: receiver_override))
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
- def infer_user_method_return(def_node, receiver, arg_types, self_fold_safe: false, yield_type: nil)
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
- narrowed_self_type: block_body_self_narrowing(call_node, receiver_type),
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
- narrowed_self_type: block_body_self_narrowing(call_node, receiver),
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`, or `nil` to leave the scope contract unchanged.
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.narrow_self_type_for(
4119
- scope: scope, call_node: call_node, receiver_type: receiver_type
4120
- ) || DefineMethodBlockSelf.narrow_self_type_for(scope: scope, call_node: call_node)
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, narrowed_self_type: nil, repeats: false)
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, narrowed_self_type: narrowed_self_type, captured: captured
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, narrowed_self_type: nil, captured: 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 narrowed_self_type
4356
+ return block_scope unless narrowing
4159
4357
 
4160
- block_scope.with_self_type(narrowed_self_type)
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 node.name == :refine && self_receiver?(node) && node.block.is_a?(Prism::BlockNode)
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