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
@@ -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 "key_presence_guard"
13
14
  require_relative "last_line"
14
15
  require_relative "operand_effects"
15
16
  require_relative "optimistic_origin"
@@ -1273,7 +1274,7 @@ module Rigor
1273
1274
  end
1274
1275
 
1275
1276
  def simple_dispatch_name?(name)
1276
- %i[nil? ! is_a? kind_of? instance_of? == != === =~ !~ match? key? has_key? empty? any?
1277
+ %i[nil? ! is_a? kind_of? instance_of? == != === =~ !~ match? key? has_key? include? member? empty? any?
1277
1278
  none? respond_to? nan? finite?].include?(name)
1278
1279
  end
1279
1280
 
@@ -1286,7 +1287,7 @@ module Rigor
1286
1287
  when :=== then analyse_case_equality_predicate(node, scope)
1287
1288
  when :=~, :!~ then dispatch_regex_match_predicate(node, scope, name)
1288
1289
  when :match? then analyse_whole_regex_match_predicate(node, scope)
1289
- when :key?, :has_key? then analyse_key_presence_predicate(node, scope)
1290
+ when :key?, :has_key?, :include?, :member? then analyse_key_presence_predicate(node, scope)
1290
1291
  when :empty?, :any?, :none? then analyse_array_emptiness_predicate(node, scope, name)
1291
1292
  when :respond_to? then analyse_respond_to_predicate(node, scope)
1292
1293
  when :nan?, :finite? then analyse_float_class_predicate(node, scope, name)
@@ -1462,14 +1463,16 @@ module Rigor
1462
1463
  # `h[:foo]` nil-typed). The falsey edge is left unchanged — "key absent" is the
1463
1464
  # conservative no-op. Only literal `Symbol`/`String` arguments and
1464
1465
  # `LocalVariableReadNode`/`InstanceVariableReadNode` receivers narrow; everything else
1465
- # (Dynamic, `Nominal[Hash]`, method-chain receivers, dynamic keys) bails to no
1466
- # narrowing.
1466
+ # (Dynamic, `Nominal[Hash]`, method-chain receivers) bails to no narrowing. A non-literal
1467
+ # key goes to {#analyse_computed_key_presence_predicate} (issue #1703).
1467
1468
  def analyse_key_presence_predicate(node, scope)
1468
1469
  return nil if node.arguments.nil?
1469
1470
  return nil unless node.arguments.arguments.size == 1
1470
1471
 
1471
- key = static_hash_key(node.arguments.arguments.first)
1472
- return nil if key.nil?
1472
+ # A literal key promotes a shape's optional key only for `key?` / `has_key?`; `include?` / `member?` reach
1473
+ # the computed-key guard alone, so a String or Array receiver's `include?` keeps its own narrowings.
1474
+ key = static_hash_key(node.arguments.arguments.first) if %i[key? has_key?].include?(node.name)
1475
+ return analyse_computed_key_presence_predicate(node, scope) if key.nil?
1473
1476
 
1474
1477
  case node.receiver
1475
1478
  when Prism::LocalVariableReadNode
@@ -1479,6 +1482,17 @@ module Rigor
1479
1482
  end
1480
1483
  end
1481
1484
 
1485
+ # Issue #1703 — `H.key?(k)` / `has_key?` / `include?` / `member?` with a non-literal key (a local, an ivar,
1486
+ # or a reader chain such as `prop.column_type`) on a closed hash shape held in a local, an ivar or a
1487
+ # constant. The truthy edge records the guard, and a later `H[k]` of the same receiver and structurally the
1488
+ # same key drops the miss `nil` ({KeyPresenceGuard}). The falsey edge is the no-op: an
1489
+ # absent computed key says nothing a shape can record. Nil when the guard does not apply, so a collection's
1490
+ # `include?` still reaches the membership and String predicates.
1491
+ def analyse_computed_key_presence_predicate(node, scope)
1492
+ truthy = KeyPresenceGuard.record(node, scope)
1493
+ truthy && [truthy, scope]
1494
+ end
1495
+
1482
1496
  def key_presence_scopes(name, key, scope, reader:, writer:)
1483
1497
  current = scope.public_send(reader, name)
1484
1498
  return nil if current.nil?
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../reflection"
4
+ require_relative "../analysis/dependency_recorder"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # Issue #1697 — the modules the project mixes into `Object`, and what they say about a method name.
9
+ #
10
+ # A top-level `include M` is `main.include`, which is `Object.include`, so M's instance methods exist on
11
+ # every object: bare top-level calls (`main` is an `Object`) and calls on any receiver whose class
12
+ # descends from `Object`. `ScopeIndexer` records that statement under its own owner key
13
+ # ({TOPLEVEL_INCLUDE_KEY}), and an `include M` inside `class Object ... end` on `Object`. The RBS
14
+ # environment knows nothing of either: `Object`'s RBS ancestry is core's, so a direct lookup on an
15
+ # RBS-known receiver misses every one of M's methods. This module is the one place that asks the
16
+ # project's `Object` edges instead, for the
17
+ # `call.unresolved-toplevel` and `call.undefined-method` silences in `Analysis::CheckRules`. Nothing
18
+ # types a call through these edges yet: what Ruby reaches first, even for a bare top-level call, depends
19
+ # on more than the chain shows (a block that rebinds `self`, an `extend` or a singleton `def` on `main`),
20
+ # so typing is issue #1715's.
21
+ #
22
+ # The edges are read through `Scope::ResolutionChain`, the two owners' instance chains, whose search
23
+ # files the ADR-46 class edge of every project entry it passes, the declaring files among them (a file
24
+ # that wrote a top-level `include` is one), so a consumer re-checks when the include that answered for
25
+ # it is edited away. A project that never reopens `Object` and writes no top-level `include` returns
26
+ # before reading anything. The question is a union over the chain, not "which definer does Ruby call":
27
+ # any entry that may define the name answers, so the chain's `settle` verdict is not asked.
28
+ module ObjectMixins
29
+ OWNER = "Object"
30
+ # The owner key `ScopeIndexer` files a top-level `include` under, beside `Object`'s own key, so that
31
+ # no ancestry walk that types a call reaches it (see `ScopeIndexer#toplevel_include_owner`).
32
+ TOPLEVEL_INCLUDE_KEY = "<toplevel-include>"
33
+
34
+ module_function
35
+
36
+ # How the project's `Object` mixins answer `method_name`, nearest edge first:
37
+ #
38
+ # - `[:rbs, module_name, definition]` — an RBS-declared module declares it; `definition` is its
39
+ # `RBS::Definition::Method`. `:private_rbs` is the same for a private declaration, which a bare call
40
+ # reaches and a call with a receiver does not.
41
+ # - `[:source, module_name]` — a project module (or `Object` itself) defines it.
42
+ # - `[:singleton_function, module_name]` — an RBS-declared module declares it only as a singleton method.
43
+ # Core declares `Math`'s functions that way (`def self.sqrt`) although they are `module_function`s,
44
+ # whose private instance copies a bare call reaches after `include Math`. RBS cannot tell such a
45
+ # function from a plain `def self.`, so the answer silences a bare call only.
46
+ # - `[:opaque, module_name]` — a module neither RBS nor the project declares. Its surface cannot be
47
+ # enumerated, so it may define the name.
48
+ # - `[:truncated, "Object"]` — the chain was cut at its budget with no answer before the cut: a definer
49
+ # may sit past it, which is uncertainty, not absence.
50
+ # - `nil` — no edge, or every edge names a module known not to declare it.
51
+ #
52
+ # `skip:` names the kinds a caller cannot use; an entry of one of them is passed over and the search
53
+ # goes on to the next, so the answer is a union over the chain for those callers rather than the
54
+ # nearest entry only.
55
+ def answer(scope, method_name, skip: NO_SKIP)
56
+ return nil if scope.nil?
57
+
58
+ # ADR-46 — the answer depends on whether a top-level `include` exists anywhere, so a consumer checked
59
+ # while none did must re-check when one appears. The key appears as a declared "class" of the file that
60
+ # writes it (`discovered_class_sources`), which is what this name-keyed existence edge listens for.
61
+ if Analysis::DependencyRecorder.active?
62
+ Analysis::DependencyRecorder.read_last_segment(:class, TOPLEVEL_INCLUDE_KEY)
63
+ end
64
+ EDGE_OWNERS.each do |owner|
65
+ next unless scope.known_user_class?(owner)
66
+
67
+ found = answer_on_chain(scope, owner, method_name, skip)
68
+ return found if found
69
+ end
70
+ nil
71
+ end
72
+
73
+ # Where `Object`'s project edges are recorded: a top-level `include` under the indexer's own key, and
74
+ # `Object` itself for `include M` in `class Object` (and `Object`'s own definitions).
75
+ EDGE_OWNERS = [TOPLEVEL_INCLUDE_KEY, OWNER].freeze
76
+ private_constant :EDGE_OWNERS
77
+
78
+ def answer_on_chain(scope, owner, method_name, skip)
79
+ chain = Scope::ResolutionChain.for(scope, owner, :instance, :methods)
80
+ found = chain.search(scope, side: :instance) do |entry|
81
+ entry_answer = answer_for_entry(scope, entry, method_name)
82
+ entry_answer unless entry_answer.nil? || skip.include?(entry_answer.first)
83
+ end
84
+ return found if found
85
+
86
+ chain.truncated? ? [:truncated, owner] : nil
87
+ end
88
+ private_class_method :answer_on_chain
89
+
90
+ NO_SKIP = [].freeze
91
+ # An explicit receiver reaches neither a private method (a `module_function`'s instance copy among
92
+ # them) nor, on #746's
93
+ # reading of an ancestor's unknown include (the receiver's own includes only), a module whose surface
94
+ # is unknown: one top-level `include` of an undeclared module would otherwise silence
95
+ # `call.undefined-method` on every receiver in the project.
96
+ EXPLICIT_RECEIVER_SKIP = %i[private_rbs singleton_function opaque].freeze
97
+ private_constant :NO_SKIP, :EXPLICIT_RECEIVER_SKIP
98
+
99
+ # Whether some `Object` mixin may define `method_name`: the question the silences ask. A bare
100
+ # top-level call (`receiver: :implicit`, `call.unresolved-toplevel`) counts every answer, an unknown
101
+ # module's included: such an include is the likeliest source of a bare name, and silencing a typo
102
+ # there is the cheaper error. A call with a receiver (`receiver: :explicit`, `call.undefined-method`)
103
+ # counts only a declared, public definition.
104
+ def may_define?(scope, method_name, receiver:)
105
+ skip = receiver == :explicit ? EXPLICIT_RECEIVER_SKIP : NO_SKIP
106
+ !answer(scope, method_name, skip: skip).nil?
107
+ end
108
+
109
+ # Whether a receiver of `class_name` reaches `Object`'s mixins at all: `Object` itself, or an RBS
110
+ # class the environment orders below it. A `BasicObject` subclass does not; a class the environment
111
+ # cannot order is not claimed.
112
+ def reaches_object?(scope, class_name)
113
+ name = class_name.to_s.delete_prefix("::")
114
+ return true if name == OWNER
115
+
116
+ environment = scope&.environment
117
+ !environment.nil? && environment.class_ordering(name, OWNER) == :subclass
118
+ end
119
+
120
+ # A project entry (`Object` itself, or a project module mixed into it) answers when it defines the name;
121
+ # an external one when RBS declares the name on one of the names its spelling can denote (as an instance
122
+ # method, or as a module's singleton function), and is opaque when none of those names is declared at
123
+ # all.
124
+ def answer_for_entry(scope, entry, method_name)
125
+ unless entry.external?
126
+ return source_defines?(scope, entry.name, method_name) ? [:source, entry.name] : nil
127
+ end
128
+
129
+ entry.candidates.each do |candidate|
130
+ definition = Rigor::Reflection.instance_method_definition(candidate, method_name, scope: scope)
131
+ return [private_definition?(definition) ? :private_rbs : :rbs, candidate, definition] if definition
132
+ return [:singleton_function, candidate] if singleton_function?(scope, candidate, method_name)
133
+ end
134
+ return nil if entry.candidates.any? { |candidate| declared?(scope, candidate) }
135
+
136
+ [:opaque, entry.raw.to_s]
137
+ end
138
+ private_class_method :answer_for_entry
139
+
140
+ def private_definition?(definition)
141
+ definition.respond_to?(:accessibility) && definition.accessibility == :private
142
+ end
143
+ private_class_method :private_definition?
144
+
145
+ def singleton_function?(scope, name, method_name)
146
+ environment = scope.environment
147
+ return false if environment.nil? || !environment.rbs_module?(name)
148
+
149
+ definition = Rigor::Reflection.singleton_method_definition(name, method_name, scope: scope)
150
+ # Declared on the module itself: its singleton class also answers every `Module` method
151
+ # (`include`, `name`), which no `include` of it brings to the top level.
152
+ !definition.nil? && definition.defined_in.to_s.delete_prefix("::") == name
153
+ end
154
+ private_class_method :singleton_function?
155
+
156
+ # Asked of both tables a project method lands in, as `RbsDispatch#source_declares_through_ancestors?`
157
+ # does: `discovered_methods` withholds a plain `def` written in another file, which is exactly where a
158
+ # top-level `include` usually finds its module. The chain already expands each module's own ancestry, so
159
+ # one entry's own tables are asked.
160
+ def source_defines?(scope, name, method_name)
161
+ scope.discovered_method?(name, method_name, :instance) || !scope.user_def_for(name, method_name).nil?
162
+ end
163
+ private_class_method :source_defines?
164
+
165
+ # The same "is this mixin's surface enumerable" reading `Analysis::CheckRules#known_mixin?` gives a
166
+ # class's own includes (#746): a project class or module, or an RBS declaration Rigor did not
167
+ # synthesize to keep a broken signature buildable.
168
+ def declared?(scope, name)
169
+ return true if scope.discovered_classes.key?(name)
170
+ return false unless Rigor::Reflection.rbs_class_known?(name, scope: scope)
171
+
172
+ loader = scope.environment&.rbs_loader
173
+ return true if loader.nil? || !loader.respond_to?(:synthesized_type_names)
174
+
175
+ !loader.synthesized_type_names.include?(name)
176
+ end
177
+ private_class_method :declared?
178
+ end
179
+ end
180
+ end
@@ -7,9 +7,9 @@ module Rigor
7
7
  # ADR-17 § "Inference contract" — project-wide patched-method registry populated by the pre-eval
8
8
  # pre-pass (slice 2) from the user's `.rigor.yml` `pre_eval:` list.
9
9
  #
10
- # Each entry records one `def` declaration the pre-pass observed inside a class / module body. The
11
- # dispatcher's `try_project_patched_method` tier consults this registry between the plugin tier and
12
- # the dependency-source tier so project-side `lib/core_ext/string_extensions.rb` patches are visible
10
+ # Each entry records one `def`, `alias` or `alias_method` declaration the pre-pass observed inside a class /
11
+ # module body. The dispatcher's `try_project_patched_method` tier consults this registry between the plugin
12
+ # tier and the dependency-source tier so project-side `lib/core_ext/string_extensions.rb` patches are visible
13
13
  # to cross-file dispatch.
14
14
  #
15
15
  # The dispatcher answers `Dynamic[T]` (with a heuristic static facet) when `Entry#return_type` is
@@ -22,8 +22,13 @@ module Rigor
22
22
  # the {Analysis::DependencySourceInference::ReturnTypeHeuristic}-extracted static facet (a
23
23
  # `Rigor::Type::*`) or `nil` when the heuristic declined. The dispatcher wraps a non-nil
24
24
  # `return_type` in `Dynamic[T]`; a `nil` `return_type` falls back to `Dynamic[top]`.
25
- Entry = Data.define(:class_name, :method_name, :kind, :source_path, :source_line, :return_type) do
26
- def initialize(class_name:, method_name:, kind:, source_path:, source_line:, return_type: nil)
25
+ #
26
+ # Issue #1702 — an `alias new old` / `alias_method :new, :old` in a patch is recorded too. When a `def` or
27
+ # alias earlier in the `pre_eval:` files binds `old`, the alias's entry copies that entry's `return_type` (and
28
+ # its `alias_of`); otherwise `alias_of` names `old`, a method the class had before the patch, and the
29
+ # dispatcher answers the alias with what that method answers. `alias_of` is nil for a `def`.
30
+ Entry = Data.define(:class_name, :method_name, :kind, :source_path, :source_line, :return_type, :alias_of) do
31
+ def initialize(class_name:, method_name:, kind:, source_path:, source_line:, return_type: nil, alias_of: nil)
27
32
  super
28
33
  end
29
34
  end
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "project_patched_methods"
6
6
  require_relative "../analysis/dependency_source_inference/return_type_heuristic"
7
+ require_relative "../source/alias_names"
7
8
  require_relative "../source/constant_path"
8
9
  require_relative "../source/node_children"
9
10
 
@@ -11,7 +12,8 @@ module Rigor
11
12
  module Inference
12
13
  # ADR-17 slice 2 — pre-pass scanner. Walks every file the user listed under `pre_eval:` and
13
14
  # harvests every `def` / `def self.` declaration inside a class / module body into a
14
- # {ProjectPatchedMethods} registry the dispatcher consults below the plugin tier.
15
+ # {ProjectPatchedMethods} registry the dispatcher consults below the plugin tier, together with every
16
+ # `alias new old` / `alias_method :new, :old` there (issue #1702, read through {Source::AliasNames}).
15
17
  #
16
18
  # The walker is intentionally a strict subset of {Rigor::Inference::ScopeIndexer}'s machinery: it
17
19
  # only needs `class C; def m; end; end` shape recognition, not full inference. Parse errors degrade
@@ -29,6 +31,10 @@ module Rigor
29
31
  end
30
32
  end
31
33
 
34
+ # An alias the walk met, before {resolve_aliases} knows whether its old name is one the patch files define.
35
+ PendingAlias = Data.define(:class_name, :new_name, :old_name, :kind, :source_path, :source_line)
36
+ private_constant :PendingAlias
37
+
32
38
  module_function
33
39
 
34
40
  # @param paths — absolute paths to the pre-eval files. The runner has already
@@ -39,10 +45,11 @@ module Rigor
39
45
  # users editing a monkey-patch file see the in-flight version in their analysis.
40
46
  # @return the populated registry plus any per-file warnings.
41
47
  def scan(paths, buffer: nil)
42
- entries = []
48
+ collected = []
43
49
  diagnostics = []
44
50
  census = Set.new
45
- paths.each { |path| scan_file(path, entries, diagnostics, buffer, census) }
51
+ paths.each { |path| scan_file(path, collected, diagnostics, buffer, census) }
52
+ entries = resolve_aliases(collected)
46
53
  diagnostics.concat(duplicate_declaration_diagnostics(entries))
47
54
  Result.new(
48
55
  registry: ProjectPatchedMethods.new(entries: entries, write_census: census.freeze),
@@ -78,6 +85,46 @@ module Rigor
78
85
  end
79
86
  private_class_method :duplicate_declaration_diagnostics
80
87
 
88
+ # Turns each {PendingAlias} into the entry it publishes, walking the bindings in configuration-then-source
89
+ # order as Ruby runs them. An alias binds to the body its old name has WHEN IT RUNS, so only a `def` or alias
90
+ # earlier in that order may supply its return type: `alias to_m to_modint` after `def to_modint` answers what
91
+ # `to_modint` does, while `alias_method :orig_succ, :succ` ahead of a patch's `def succ` is the method
92
+ # Integer already had. An alias whose old name nothing earlier binds records that name as `alias_of`, for the
93
+ # dispatcher to answer with the class's existing method (`alias old_plus +` on Integer), or with
94
+ # `Dynamic[top]` when nothing knows the name: the alias is still published, since a call to it is not the
95
+ # undefined method a dropped entry would report.
96
+ #
97
+ # Within one file the later binding of a name replaces the earlier, as at runtime: the alias-method-chain
98
+ # idiom (`def foo`, `alias_method :foo_without_x, :foo`, `alias_method :foo, :foo_with_x`) leaves `foo` the
99
+ # alias. Bindings of one name in DIFFERENT files are what `pre-eval.duplicate-declaration` reports, and the
100
+ # registry keeps the first of those, as it always has for `def`s.
101
+ def resolve_aliases(collected)
102
+ bound = {}
103
+ per_file = collected.group_by(&:source_path).values.map do |items|
104
+ final = {}
105
+ items.each do |item|
106
+ entry = item.is_a?(PendingAlias) ? alias_entry(item, bound) : item
107
+ key = [entry.class_name, entry.method_name, entry.kind]
108
+ bound[key] = entry
109
+ final.delete(key)
110
+ final[key] = entry
111
+ end
112
+ final.values
113
+ end
114
+ per_file.flatten(1)
115
+ end
116
+ private_class_method :resolve_aliases
117
+
118
+ def alias_entry(item, bound)
119
+ target = bound[[item.class_name, item.old_name, item.kind]]
120
+ ProjectPatchedMethods::Entry.new(
121
+ class_name: item.class_name, method_name: item.new_name, kind: item.kind,
122
+ source_path: item.source_path, source_line: item.source_line,
123
+ return_type: target&.return_type, alias_of: target ? target.alias_of : item.old_name
124
+ )
125
+ end
126
+ private_class_method :alias_entry
127
+
81
128
  def scan_file(path, entries, diagnostics, buffer = nil, census = Set.new)
82
129
  physical = buffer ? buffer.resolve(path) : path
83
130
  parse_result =
@@ -139,6 +186,13 @@ module Rigor
139
186
  descend_singleton_class(node, qualified_prefix, source_path, entries)
140
187
  when Prism::DefNode
141
188
  record_def_node(node, qualified_prefix, in_singleton_class, source_path, entries)
189
+ when Prism::AliasMethodNode
190
+ record_alias(node, Source::AliasNames.keyword_names(node), qualified_prefix, in_singleton_class,
191
+ source_path, entries)
192
+ when Prism::CallNode
193
+ record_alias(node, Source::AliasNames.alias_method_call_names(node), qualified_prefix,
194
+ in_singleton_class, source_path, entries)
195
+ walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
142
196
  else
143
197
  walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
144
198
  end
@@ -186,6 +240,20 @@ module Rigor
186
240
  )
187
241
  end
188
242
  private_class_method :record_def_node
243
+
244
+ # `names` is the alias's `[new_name, old_name]`, or nil for a computed name or a call that is not an alias.
245
+ # The side follows the body it is written in, as a `def`'s does: `alias` and `alias_method` inside
246
+ # `class << self` both bind on the singleton.
247
+ def record_alias(node, names, qualified_prefix, in_singleton_class, source_path, entries)
248
+ return if names.nil? || qualified_prefix.empty?
249
+
250
+ entries << PendingAlias.new(
251
+ class_name: qualified_prefix.join("::"), new_name: names.first, old_name: names.last,
252
+ kind: in_singleton_class ? :singleton : :instance,
253
+ source_path: source_path, source_line: node.location&.start_line || 1
254
+ )
255
+ end
256
+ private_class_method :record_alias
189
257
  end
190
258
  end
191
259
  end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/alias_names"
6
+ require_relative "../source/node_children"
7
+
8
+ module Rigor
9
+ module Inference
10
+ # ADR-121 WD7 (issues #1796, #1799) — what a `refine` body tells the refinement table, read the one way the project
11
+ # walk (`ScopeIndexer#record_refinement_defs`) and the gem source walker
12
+ # (`Analysis::DependencySourceInference::Walker#walk_refine_body`) both use, so the two cannot drift. The table is
13
+ # `{refined class => {method => [refining modules]}}`; `Scope::DiscoveryIndex::REFINEMENT_WILDCARD` stands for
14
+ # what could not be read.
15
+ #
16
+ # A body is fully read when every call on its own `self` (the refinement module) is one that defines a name the
17
+ # walk can spell, or one that defines nothing: a `def`, an `alias` or `alias_method` of two literal names, a
18
+ # `define_method` of a literal name, an `attr_*` of literal names, an `undef`, a visibility call, or
19
+ # `Refinement#target`. Anything
20
+ # else (`import_methods`, `send`, a computed `define_method`, `include`, a nested `refine`, any other call on
21
+ # `self`) may define a name the walk cannot spell, so the body's names are incomplete and the table gets a
22
+ # names-wildcard row for each target. A call on another receiver cannot define a method on the refinement, and
23
+ # a call inside a `def` or `define_method` body runs on an instance of the refined class, so neither counts.
24
+ module RefineCensus
25
+ # `names`, every name the body is read to define, in textual order; `complete`, whether those are all of them;
26
+ # `defs`, the typed refined arm's bodies in textual order: `[name, Prism::DefNode]` for a `def` or an alias of
27
+ # an earlier `def`, and `[name, nil]` for any other definer (`define_method`, `attr_*`, `undef`, an alias of a
28
+ # name the body did not define earlier). A later definer replaces an earlier one, as Ruby's method table
29
+ # keeps the last, so a name whose last definer has no `DefNode` types as `Dynamic[top]`.
30
+ BodyReading = Struct.new(:names, :complete, :defs)
31
+
32
+ # Calls on the refinement that define nothing: visibility, with no argument, literal names, a `def`, or an
33
+ # allowlisted definer call.
34
+ VISIBILITY_CALLS = Set[:private, :public, :protected, :module_function, :private_constant].freeze
35
+ # `Refinement#target`, a reader of the refined class (the other `Refinement` method on Ruby 4.0.5 is
36
+ # `import_methods`).
37
+ READER_CALLS = Set[:target].freeze
38
+ # `attr_*` and whether each defines the reader and the writer.
39
+ ATTR_CALLS = {
40
+ attr_reader: [true, false].freeze, attr_writer: [false, true].freeze, attr_accessor: [true, true].freeze,
41
+ attr: [true, false].freeze
42
+ }.freeze
43
+
44
+ # A3 — the calls whose String argument may hold `refine` as code or as a method name, and the two families
45
+ # whose `self` is their receiver (so `M.send(:refine, …)` runs `refine` on `M`).
46
+ SEND_CALLS = Set[:send, :__send__, :public_send].freeze
47
+ SELF_EVAL_CALLS = Set[:instance_eval, :class_eval, :module_eval, :instance_exec, :class_exec, :module_exec].freeze
48
+ NAMING_CALLS = Set[
49
+ :method, :public_method, :singleton_method, :instance_method, :public_instance_method, :define_method,
50
+ :define_singleton_method, :alias_method
51
+ ].freeze
52
+ REFINE_WORD = /\brefine\b/
53
+
54
+ private_constant :VISIBILITY_CALLS, :READER_CALLS, :ATTR_CALLS, :REFINE_WORD
55
+
56
+ module_function
57
+
58
+ def wildcard = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
59
+
60
+ # §2.2 — what `body` (a refine block's body, or nil) defines on the refined class.
61
+ def read_body(body)
62
+ reading = BodyReading.new([], true, [])
63
+ scan_body(body, reading, {}, true) if body
64
+ reading.names.uniq!
65
+ reading
66
+ end
67
+
68
+ # Records `reading`'s names for each of `targets` under `refining` into `table` (nil starts one), plus a
69
+ # names-wildcard row per target when the body's names are incomplete. Returns the table.
70
+ def record_rows(table, targets, reading, refining)
71
+ table ||= {}
72
+ targets.each do |class_name|
73
+ reading.names.each { |name| add_row(table, class_name, name, refining) }
74
+ add_row(table, class_name, wildcard, refining) unless reading.complete
75
+ end
76
+ table
77
+ end
78
+
79
+ # One `{class_name => {method_key => [refining]}}` entry, `refining` once.
80
+ def add_row(table, class_name, method_key, refining)
81
+ modules = ((table[class_name] ||= {})[method_key] ||= [])
82
+ modules << refining unless modules.include?(refining)
83
+ table
84
+ end
85
+
86
+ # A3 — a `:refine` Symbol literal: it may name `Module#refine` for `send`, `alias_method`, `method`, ….
87
+ def refine_symbol?(node) = node.is_a?(Prism::SymbolNode) && node.unescaped == "refine"
88
+
89
+ # A3 — a String literal (or a heredoc's literal part) holding the word `refine`, as code an eval runs or as a
90
+ # method name.
91
+ def refine_string?(node)
92
+ case node
93
+ # As bytes: a literal whose bytes are not valid in its encoding (`"\xff …"`) must not raise.
94
+ when Prism::StringNode then REFINE_WORD.match?(node.unescaped.b)
95
+ when Prism::InterpolatedStringNode then node.parts.any? { |part| refine_string?(part) }
96
+ else false
97
+ end
98
+ end
99
+
100
+ # A3 — does a String argument of `call` count: an eval, `send` or method-naming call?
101
+ def string_literal_call?(call)
102
+ name = call.name
103
+ name == :eval || SELF_EVAL_CALLS.include?(name) || SEND_CALLS.include?(name) || NAMING_CALLS.include?(name)
104
+ end
105
+
106
+ # A3 — the `self` a refine literal argument of `call` runs `refine` on: `:receiver` for a `send` or `*_eval`
107
+ # / `*_exec` call (the receiver, or the caller's `self` when it has none or it is `self`), `:unknown` for an
108
+ # `eval` on a binding, `:self` for everything else (the caller's `self`).
109
+ def literal_target(call)
110
+ receiver = call.receiver
111
+ self_receiver = receiver.nil? || receiver.is_a?(Prism::SelfNode)
112
+ if SEND_CALLS.include?(call.name) || SELF_EVAL_CALLS.include?(call.name)
113
+ self_receiver ? :self : :receiver
114
+ elsif call.name == :eval
115
+ self_receiver ? :self : :unknown
116
+ else
117
+ :self
118
+ end
119
+ end
120
+
121
+ # An implicit- or `self`-receiver call: one whose `self` is the walk's `self`.
122
+ def self_call?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
123
+
124
+ # `classify` is false inside a `define_method` block or its arguments, whose calls run on an instance of the
125
+ # refined class (or before the method exists), so they define nothing on the refinement. `latest` maps each
126
+ # name to the last `def` the body wrote for it so far, which an alias copies.
127
+ def scan_body(node, reading, latest, classify)
128
+ case node
129
+ when Prism::DefNode
130
+ record_def(node, reading, latest) if node.receiver.nil?
131
+ return
132
+ when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
133
+ return
134
+ when Prism::AliasMethodNode
135
+ return record_alias(Source::AliasNames.keyword_names(node), reading, latest)
136
+ when Prism::UndefNode
137
+ return record_undef(node, reading, latest)
138
+ when Prism::CallNode
139
+ if classify && self_call?(node)
140
+ classify_call(node, reading, latest)
141
+ classify = node.name != :define_method
142
+ end
143
+ end
144
+
145
+ node.rigor_each_child { |child| scan_body(child, reading, latest, classify) }
146
+ end
147
+
148
+ def record_def(node, reading, latest)
149
+ reading.names << node.name
150
+ reading.defs << [node.name, node]
151
+ latest[node.name] = node
152
+ end
153
+
154
+ # `alias new old` / `alias_method :new, :old`: `new` is defined; it copies `old`'s body when the refine body
155
+ # wrote `old` earlier (Ruby copies at the alias, so a later `def old` does not move it), and otherwise it
156
+ # copies the refined class's own method, whose body the walk does not hold.
157
+ def record_alias(pair, reading, latest)
158
+ return reading.complete = false if pair.nil?
159
+
160
+ new_name, old_name = pair
161
+ reading.names << new_name
162
+ def_node = latest[old_name]
163
+ return record_bodiless(new_name, reading, latest) if def_node.nil?
164
+
165
+ reading.defs << [new_name, def_node]
166
+ latest[new_name] = def_node
167
+ end
168
+
169
+ # A name a definer with no `DefNode` defines: it replaces any earlier `def` of the name, so the typed arm
170
+ # finds no body for it (`refine(String) { def center(a) = 1; define_method(:center) { |a| "dm" } }` answers
171
+ # `"dm"` on Ruby 4.0.5), and an alias of it copies no body either.
172
+ def record_bodiless(name, reading, latest)
173
+ reading.names << name
174
+ reading.defs << [name, nil]
175
+ latest.delete(name)
176
+ end
177
+
178
+ def record_undef(node, reading, latest)
179
+ node.names.each do |name|
180
+ symbol = literal_name(name)
181
+ symbol ? record_bodiless(symbol, reading, latest) : reading.complete = false
182
+ end
183
+ end
184
+
185
+ # Classifies a call on the refinement.
186
+ def classify_call(node, reading, latest)
187
+ name = node.name
188
+ arguments = node.arguments&.arguments || []
189
+ if (attr = ATTR_CALLS[name])
190
+ record_attr(arguments, attr, reading, latest)
191
+ elsif name == :define_method
192
+ record_define_method(arguments, reading, latest)
193
+ elsif name == :alias_method
194
+ record_alias(Source::AliasNames.alias_method_call_names(node), reading, latest)
195
+ else
196
+ reading.complete = false unless defines_nothing?(node, arguments)
197
+ end
198
+ end
199
+
200
+ # A visibility call with no argument, literal names, a `def` or a call; `Refinement#target` with none.
201
+ def defines_nothing?(node, arguments)
202
+ return arguments.all? { |argument| visibility_argument?(argument) } if VISIBILITY_CALLS.include?(node.name)
203
+
204
+ READER_CALLS.include?(node.name) && arguments.empty? && node.block.nil?
205
+ end
206
+
207
+ def record_attr(arguments, (reader, writer), reading, latest)
208
+ arguments.each do |argument|
209
+ name = literal_name(argument)
210
+ next reading.complete = false if name.nil?
211
+
212
+ record_bodiless(name, reading, latest) if reader
213
+ record_bodiless(:"#{name}=", reading, latest) if writer
214
+ end
215
+ end
216
+
217
+ def record_define_method(arguments, reading, latest)
218
+ name = arguments.first && literal_name(arguments.first)
219
+ name ? record_bodiless(name, reading, latest) : reading.complete = false
220
+ end
221
+
222
+ # `private`, `private :a, "b"`, `private def x`, `private attr_reader :x`: the nested call is classified on
223
+ # its own when the walk descends into it.
224
+ def visibility_argument?(argument)
225
+ argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode) || argument.is_a?(Prism::DefNode) ||
226
+ argument.is_a?(Prism::CallNode)
227
+ end
228
+
229
+ # A literal's name, or nil for none and for bytes no Symbol holds: `define_method("\xff")` raises
230
+ # EncodingError on Ruby 4.0.5, so the body is not read.
231
+ def literal_name(node)
232
+ return nil unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
233
+
234
+ name = node.unescaped
235
+ name.to_sym if name&.valid_encoding?
236
+ end
237
+ end
238
+ end
239
+ end