Class: Ryac::TypeOracle

Inherits:
Object
  • Object
show all
Defined in:
lib/ryac/analysis/type_oracle.rb,
sig/ryac/analysis/type_oracle.rbs

Overview

The one place allowed to talk to TypeProf.

Everything the renamer needs from type analysis is phrased as a handful of questions — which methods a call can reach, who calls a method, what a receiver is, what a constant resolves to, what a class inherits. The collections compute every lexical fact themselves on the Prism tree and come here only for those questions, so a change in TypeProf's internals breaks this file and nothing else.

Answers refer to source positions (location keys) or plain values, never to TypeProf objects.

Defined Under Namespace

Classes: CallerInfo

Constant Summary collapse

TP_CALL_NODES =

Returns:

  • (Array[Class])
[
  TypeProf::Core::AST::CallNode,
  TypeProf::Core::AST::CallReadNode,
  TypeProf::Core::AST::CallWriteNode
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(genv, tp_root) ⇒ TypeOracle

Returns a new instance of TypeOracle.

Parameters:

  • genv (tp_genv)
  • tp_root (tp_node)


44
45
46
47
# File 'lib/ryac/analysis/type_oracle.rb', line 44

def initialize(genv, tp_root)
  @genv = genv
  @tp_root = tp_root
end

Class Method Details

.boot(content, rbs_files, prism_root) ⇒ TypeOracle

Boots TypeProf over the program and its RBS. This owns the two internals the boot requires — the Service construction and the private @rb_text_nodes table behind update_rb_file — so an upstream change to either breaks here and nowhere else.

TypeProf does not register a class defined inside a block, and a lazy region (LazyRegions) is exactly that. It reads the view with the region wrappers blanked instead — same byte positions, which is what the coordinate join below relies on.

Parameters:

  • content (String)
  • rbs_files (Hash[String, String])
  • prism_root (Prism::ProgramNode)

Returns:



27
28
29
30
31
32
33
34
35
# File 'lib/ryac/analysis/type_oracle.rb', line 27

def self.boot(content, rbs_files, prism_root)
  path = '(minify_concat)'
  service = TypeProf::Core::Service.new({})
  rbs_files.each do |rbs_path, rbs_content|
    service.update_rbs_file(rbs_path, rbs_content)
  end
  service.update_rb_file(path, LazyRegions.typeprof_view(content, prism_root))
  new(service.genv, service.instance_variable_get(:@rb_text_nodes)[path])
end

.raw_prism_node(node) ⇒ Prism::Node?

TypeProf keeps the backing Prism node private because an editor has no reason to ask; the coordinate join (tp_key below) asks constantly, since Prism is where the syntax being rewritten lives.

Parameters:

  • node (tp_node)

Returns:

  • (Prism::Node, nil)


40
41
42
# File 'lib/ryac/analysis/type_oracle.rb', line 40

def self.raw_prism_node(node)
  node.instance_variable_get(:@raw_node)
end

Instance Method Details

#build_tp_indexes ⇒ void

This method returns an undefined value.

Both position indexes come from the same walk; a real run always needs both, so one traversal fills them together.



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/ryac/analysis/type_oracle.rb', line 343

def build_tp_indexes
  return if @tp_calls_by_loc

  calls = {} #: Hash[location_key, Hash[Symbol, TypeProf::Core::AST::CallBaseNode]]
  consts = {} #: Hash[location_key, TypeProf::Core::AST::ConstantReadNode]
  @tp_root.traverse do |event, node|
    next unless event == :enter
    case node
    when *TP_CALL_NODES
      # @type var node: TypeProf::Core::AST::CallBaseNode
      (calls[tp_key(node)] ||= {})[node.mid] = node
    when TypeProf::Core::AST::ConstantReadNode
      consts[tp_key(node)] = node
    end
  end
  @tp_calls_by_loc = calls
  @tp_consts_by_loc = consts
end

#constant_read_count(cpath) ⇒ Integer

How many read sites type analysis records for the constant. Can exceed the syntactic count when reads reach it through resolution the text does not show.

Parameters:

  • cpath (Array[Symbol])

Returns:

  • (Integer)


231
232
233
234
235
236
237
238
239
# File 'lib/ryac/analysis/type_oracle.rb', line 231

def constant_read_count(cpath)
  # @type var entity: untyped
  entity = begin
    @genv.resolve_const(cpath)
  rescue StandardError
    nil
  end
  entity.respond_to?(:read_boxes) ? entity.read_boxes.size : 0
end

#each_ancestor_cpath(cpath, singleton) {|arg0| ... } ⇒ void

This method returns an undefined value.

Yields every ancestor cpath of the class, including its own. Unresolvable classes yield nothing.

Parameters:

  • cpath (Array[Symbol])
  • singleton (Boolean)

Yields:

Yield Parameters:

  • arg0 (Array[Symbol])

Yield Returns:

  • (void)


51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/ryac/analysis/type_oracle.rb', line 51

def each_ancestor_cpath(cpath, singleton, &block)
  @ancestor_cpaths ||= {} #: Hash[MethodRenameMapping::class_key, Array[Array[Symbol]]]
  cached = @ancestor_cpaths[[cpath, singleton]] ||= begin
    list = [] #: Array[Array[Symbol]]
    mod = @genv.resolve_cpath(cpath) rescue nil
    if mod
      @genv.each_superclass(mod, singleton) do |ancestor_mod, _singleton|
        list << ancestor_mod.cpath
      end
    end
    list
  end
  cached.each(&block)
end

#each_ancestor_methods(cpath, singleton) {|arg0, arg1, arg2| ... } ⇒ void

This method returns an undefined value.

Yields [ancestor_cpath, singleton, method_names] along the ancestor chain, own class first — the namespace a new method name must not collide with.

Parameters:

  • cpath (Array[Symbol])
  • singleton (Boolean)

Yields:

Yield Parameters:

  • arg0 (Array[Symbol])
  • arg1 (Boolean)
  • arg2 (Array[String])

Yield Returns:

  • (void)


69
70
71
72
73
74
75
76
77
78
# File 'lib/ryac/analysis/type_oracle.rb', line 69

def each_ancestor_methods(cpath, singleton)
  mod = @genv.resolve_cpath(cpath) rescue nil
  return unless mod

  @genv.each_superclass(mod, singleton) do |ancestor_mod, s|
    names = [] #: Array[String]
    ancestor_mod.methods[s]&.each_key { |mid| names << mid.to_s }
    yield ancestor_mod.cpath, s, names
  end
end

#each_call_site_key(cpath, singleton, mid) {|arg0| ... } ⇒ void

This method returns an undefined value.

Location keys of every call site that dispatches to the method.

Parameters:

  • cpath (Array[Symbol])
  • singleton (Boolean)
  • mid (Symbol)

Yields:

Yield Parameters:

  • arg0 (location_key)

Yield Returns:

  • (void)


97
98
99
100
101
102
103
104
# File 'lib/ryac/analysis/type_oracle.rb', line 97

def each_call_site_key(cpath, singleton, mid)
  entity = resolve_method(cpath, singleton, mid)
  return unless entity

  entity.method_call_boxes.each do |call_box|
    yield tp_key(call_box.node)
  end
end

#each_caller(cpath, singleton, mid) {|arg0| ... } ⇒ void

This method returns an undefined value.

Parameters:

  • cpath (Array[Symbol])
  • singleton (Boolean)
  • mid (Symbol)

Yields:

Yield Parameters:

Yield Returns:

  • (void)


128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/ryac/analysis/type_oracle.rb', line 128

def each_caller(cpath, singleton, mid)
  entity = resolve_method(cpath, singleton, mid)
  return unless entity

  entity.method_call_boxes.each do |call_box|
    node = call_box.node

    if node.is_a?(TypeProf::Core::AST::SuperNode) ||
       node.is_a?(TypeProf::Core::AST::ForwardingSuperNode)
      yield CallerInfo.new(prism_node: TypeOracle.raw_prism_node(node), super: true,
                           caller_cpath: node.lenv.cref.cpath, receiver: false,
                           keyword_entries: nil, keyword_splat: false)
      next
    end

    # A box whose node is not a syntactic call names no site in the
    # program — e.g. one synthesized while TypeProf evaluated an RBS
    # declaration: `[x].map(&:foo)` dispatches to foo from Array#map's
    # declared signature. Nothing in the source can be rewritten to
    # follow a rename, so the record carries only the fact that an
    # unrewritable caller exists.
    unless node.is_a?(TypeProf::Core::AST::CallBaseNode)
      yield CallerInfo.new(prism_node: nil, super: false, caller_cpath: nil,
                           receiver: false, keyword_entries: nil, keyword_splat: false)
      next
    end

    entries = nil
    splat = false
    kw = node.respond_to?(:keyword_args) ? node.keyword_args : nil
    if kw.is_a?(TypeProf::Core::AST::HashNode)
      splat = kw.keys.any?(&:nil?)
      entries = kw.keys.zip(kw.vals).filter_map do |sym_node, val_node|
        # @type var val_node: TypeProf::Core::AST::Node
        next unless sym_node.is_a?(TypeProf::Core::AST::SymbolNode)
        [TypeOracle.raw_prism_node(sym_node), TypeOracle.raw_prism_node(val_node)] #: [Prism::Node, Prism::Node]
      end
    end

    yield CallerInfo.new(prism_node: TypeOracle.raw_prism_node(node), super: false,
                         caller_cpath: nil, receiver: !node.recv.nil?,
                         keyword_entries: entries, keyword_splat: splat)
  end
end

#every_receiver_base_type(prism_call_node) {|arg0| ... } ⇒ Boolean

The shared scaffold of the receiver-safety questions: false unless the receiver has inferred types at all, then the block must hold for every type's base.

Parameters:

  • prism_call_node (Prism::CallNode)

Yields:

Yield Parameters:

  • arg0 (Object)

Yield Returns:

  • (boolish)

Returns:

  • (Boolean)


284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/ryac/analysis/type_oracle.rb', line 284

def every_receiver_base_type(prism_call_node)
  recv = tp_call_for(prism_call_node, prism_call_node.name)&.recv #: untyped
  return false unless recv.respond_to?(:ret) && recv.ret

  types = recv.ret.types
  return false if types.empty?

  types.all? do |ty, _|
    base = ty.base_type(@genv)
    next false unless base.respond_to?(:mod)
    yield base
  end
end

#first_argument_symbols(prism_call_node) ⇒ Array[Symbol]

Symbol values inference assigns to the call's first positional argument — how method(:foo)-style dynamic references name their target.

Parameters:

  • prism_call_node (Prism::CallNode)

Returns:

  • (Array[Symbol])


192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/ryac/analysis/type_oracle.rb', line 192

def first_argument_symbols(prism_call_node)
  tp_node = tp_call_for(prism_call_node, prism_call_node.name)
  return [] unless tp_node

  sym_arg = tp_node.positional_args&.first #: TypeProf::Core::AST::Node
  ret = sym_arg.ret rescue nil
  return [] unless ret

  ret.types.each_key.filter_map do |ty|
    ty.sym if ty.is_a?(TypeProf::Core::Type::Symbol)
  end
end

#method_call_count(cpath, singleton, mid) ⇒ Integer

Parameters:

  • cpath (Array[Symbol])
  • singleton (Boolean)
  • mid (Symbol)

Returns:

  • (Integer)


91
92
93
94
# File 'lib/ryac/analysis/type_oracle.rb', line 91

def method_call_count(cpath, singleton, mid)
  entity = resolve_method(cpath, singleton, mid)
  entity ? entity.method_call_boxes.size : 0
end

#method_defined?(cpath, singleton, mid) ⇒ Boolean

True when the method has at least one definition in the analyzed source.

Parameters:

  • cpath (Array[Symbol])
  • singleton (Boolean)
  • mid (Symbol)

Returns:

  • (Boolean)


86
87
88
89
# File 'lib/ryac/analysis/type_oracle.rb', line 86

def method_defined?(cpath, singleton, mid)
  entity = resolve_method(cpath, singleton, mid)
  !entity.nil? && entity.defs.size > 0
end

#method_definition_keys(cpath, singleton, mid) ⇒ Array[location_key]

Location keys of the method's definition sites.

Parameters:

  • cpath (Array[Symbol])
  • singleton (Boolean)
  • mid (Symbol)

Returns:

  • (Array[location_key])


107
108
109
110
111
112
# File 'lib/ryac/analysis/type_oracle.rb', line 107

def method_definition_keys(cpath, singleton, mid)
  entity = resolve_method(cpath, singleton, mid)
  return [] unless entity

  entity.defs.to_a.map { |d| tp_key(d.node) }
end

#method_known?(cpath, singleton, mid) ⇒ Boolean

True when type analysis has any record of the method at all.

Parameters:

  • cpath (Array[Symbol])
  • singleton (Boolean)
  • mid (Symbol)

Returns:

  • (Boolean)


81
82
83
# File 'lib/ryac/analysis/type_oracle.rb', line 81

def method_known?(cpath, singleton, mid)
  !resolve_method(cpath, singleton, mid).nil?
end

#mod_is_or_inherits?(mod, target) ⇒ Boolean

Parameters:

  • mod (Object)
  • target (Object)

Returns:

  • (Boolean)


306
307
308
309
310
311
312
# File 'lib/ryac/analysis/type_oracle.rb', line 306

def mod_is_or_inherits?(mod, target)
  return true if mod == target
  @genv.each_superclass(mod, false) do |ancestor, _|
    return true if ancestor == target
  end
  false
end

#receiver_responds_to?(prism_call_node, mid) ⇒ Boolean

True when every inferred type of the call's receiver responds to mid — the safety condition for rewriting the call to a stdlib alias.

Parameters:

  • prism_call_node (Prism::CallNode)
  • mid (Symbol)

Returns:

  • (Boolean)


207
208
209
210
211
212
# File 'lib/ryac/analysis/type_oracle.rb', line 207

def receiver_responds_to?(prism_call_node, mid)
  every_receiver_base_type(prism_call_node) do |base|
    singleton = base.is_a?(TypeProf::Core::Type::Singleton)
    type_responds_to?(base.mod, singleton, mid)
  end
end

#receiver_within_type?(prism_call_node, type_name) ⇒ Boolean

True when every inferred type of the call's receiver is type_name or a subclass — the safety condition for type-specific rewrites like .empty? → =={} on a Hash.

Parameters:

  • prism_call_node (Prism::CallNode)
  • type_name (Symbol)

Returns:

  • (Boolean)


244
245
246
247
248
249
250
251
# File 'lib/ryac/analysis/type_oracle.rb', line 244

def receiver_within_type?(prism_call_node, type_name)
  target_mod = @genv.resolve_cpath([type_name])
  return false unless target_mod

  every_receiver_base_type(prism_call_node) do |base|
    mod_is_or_inherits?(base.mod, target_mod)
  end
end

#resolve_constant_read(prism_node) ⇒ Array[Symbol]?

Resolves a constant reference to its fully-qualified path, using type analysis to see through lexical-scope lookup and value constants. nil when the reference is not statically resolvable. Memoized — the counting, external-reference, and precompute passes each ask about the same nodes.

Parameters:

  • prism_node (Prism::Node)

Returns:

  • (Array[Symbol], nil)


219
220
221
222
223
224
225
226
# File 'lib/ryac/analysis/type_oracle.rb', line 219

def resolve_constant_read(prism_node)
  @const_resolution_cache ||= {} #: Hash[location_key, Array[Symbol]?]
  key = AstUtils.location_key(prism_node)
  return @const_resolution_cache[key] if @const_resolution_cache.key?(key)

  tp_node = tp_const_for(prism_node)
  @const_resolution_cache[key] = tp_node ? resolve_tp_const(tp_node) : nil
end

#resolve_method(cpath, singleton, mid) ⇒ Object

Parameters:

  • cpath (Array[Symbol])
  • singleton (Boolean)
  • mid (Symbol)

Returns:

  • (Object)


269
270
271
272
273
274
275
276
277
278
279
# File 'lib/ryac/analysis/type_oracle.rb', line 269

def resolve_method(cpath, singleton, mid)
  @method_cache ||= {} #: Hash[method_key, TypeProf::Core::MethodEntity?]
  key = [cpath, singleton, mid] #: method_key
  return @method_cache[key] if @method_cache.key?(key)

  @method_cache[key] = begin
    @genv.resolve_method(cpath, singleton, mid)
  rescue StandardError
    nil
  end
end

#resolve_tp_const(node) ⇒ Array[Symbol]?

A class/module reference resolves through its own analysis result; a value constant through its definition's location; a qualified chain (Foo::CONST) through its prefix, recursively.

Parameters:

  • node (tp_node)

Returns:

  • (Array[Symbol], nil)


365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/ryac/analysis/type_oracle.rb', line 365

def resolve_tp_const(node)
  return nil unless node.is_a?(TypeProf::Core::AST::ConstantReadNode)

  static_ret = begin
    node.static_ret
  rescue StandardError
    nil
  end
  return nil unless static_ret.respond_to?(:cpath)
  return static_ret.cpath if static_ret.cpath

  cdef = static_ret.respond_to?(:cdef) ? static_ret.cdef : nil #: untyped
  if cdef.respond_to?(:defs)
    cdef.defs.each do |d|
      return d.static_cpath if d.respond_to?(:static_cpath) && d.static_cpath
    end
  end

  if node.cbase
    base_cpath = resolve_tp_const(node.cbase)
    return base_cpath + [node.cname] if base_cpath
  end

  nil
end

#resolved_targets(prism_call_node, mid) ⇒ Array[method_key]?

The method keys a call could dispatch to, per type inference. nil when inference has no answer at all — distinct from an empty list.

Parameters:

  • prism_call_node (Prism::Node)
  • mid (Symbol)

Returns:

  • (Array[method_key], nil)


175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/ryac/analysis/type_oracle.rb', line 175

def resolved_targets(prism_call_node, mid)
  tp_node = tp_call_for(prism_call_node, mid)
  return nil unless tp_node

  keys = [] #: Array[method_key]
  tp_node.boxes(:mcall) do |box|
    box.resolve(@genv, nil) do |entity, ty, _mid, _orig_ty|
      next unless entity
      singleton = ty.is_a?(TypeProf::Core::Type::Singleton)
      keys << [ty.mod.cpath, singleton, mid]
    end
  end
  keys.any? ? keys : nil
end

#tp_call_for(prism_node, mid) ⇒ TypeProf::Core::AST::CallBaseNode?

TypeProf's node for a call at a given source position. TypeProf answers type questions per node in its own tree; this index is how a question about a Prism node finds its counterpart.

Indexed by position and method name: a compound write like self.count += 1 is one position but two TypeProf nodes — a read of count and a write of count= — and a question about either name has to reach its own node.

Parameters:

  • prism_node (Prism::Node)
  • mid (Symbol)

Returns:



328
329
330
331
# File 'lib/ryac/analysis/type_oracle.rb', line 328

def tp_call_for(prism_node, mid)
  build_tp_indexes
  @tp_calls_by_loc[AstUtils.location_key(prism_node)]&.[](mid)
end

#tp_const_for(prism_node) ⇒ TypeProf::Core::AST::ConstantReadNode?

TypeProf's constant-read node at a given source position. Covers plain reads, every level of a qualified chain, and the read half of compound writes (X ||= 1 reads at the whole expression's position).

Parameters:

  • prism_node (Prism::Node)

Returns:



336
337
338
339
# File 'lib/ryac/analysis/type_oracle.rb', line 336

def tp_const_for(prism_node)
  build_tp_indexes
  @tp_consts_by_loc[AstUtils.location_key(prism_node)]
end

#tp_key(node) ⇒ location_key

Location key of a TypeProf node, via the Prism node it was built from. TypeProf models source it cannot point back at — a node may carry no nodes. Either way a node we cannot locate is one we cannot rename, so this raises rather than inventing a key that could never match. The conversion stays inside the oracle: everything outside it holds Prism nodes, and AstUtils keys those.

Parameters:

  • node (tp_node)

Returns:

  • (location_key)

Raises:

  • (ArgumentError)


262
263
264
265
266
267
# File 'lib/ryac/analysis/type_oracle.rb', line 262

def tp_key(node)
  prism_node = TypeOracle.raw_prism_node(node)
  raise ArgumentError, "no source location behind #{node.class}" unless prism_node.is_a?(Prism::Node)

  AstUtils.location_key(prism_node)
end

#type_responds_to?(mod, singleton, mid) ⇒ Boolean

Parameters:

  • mod (Object)
  • singleton (Boolean)
  • mid (Symbol)

Returns:

  • (Boolean)


298
299
300
301
302
303
304
# File 'lib/ryac/analysis/type_oracle.rb', line 298

def type_responds_to?(mod, singleton, mid)
  @genv.each_superclass(mod, singleton) do |ancestor_mod, s|
    entity = ancestor_mod.methods[s]&.[](mid)
    return true if entity && (entity.exist? || entity.aliases.any?)
  end
  false
end