Class: Ryac::TypeOracle
- Inherits:
-
Object
- Object
- Ryac::TypeOracle
- 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 =
[ TypeProf::Core::AST::CallNode, TypeProf::Core::AST::CallReadNode, TypeProf::Core::AST::CallWriteNode ].freeze
Class Method Summary collapse
-
.boot(content, rbs_files, prism_root) ⇒ TypeOracle
Boots TypeProf over the program and its RBS.
-
.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.
Instance Method Summary collapse
-
#build_tp_indexes ⇒ void
Both position indexes come from the same walk; a real run always needs both, so one traversal fills them together.
-
#constant_read_count(cpath) ⇒ Integer
How many read sites type analysis records for the constant.
-
#each_ancestor_cpath(cpath, singleton) {|arg0| ... } ⇒ void
Yields every ancestor cpath of the class, including its own.
-
#each_ancestor_methods(cpath, singleton) {|arg0, arg1, arg2| ... } ⇒ void
Yields [ancestor_cpath, singleton, method_names] along the ancestor chain, own class first — the namespace a new method name must not collide with.
-
#each_call_site_key(cpath, singleton, mid) {|arg0| ... } ⇒ void
Location keys of every call site that dispatches to the method.
- #each_caller(cpath, singleton, mid) {|arg0| ... } ⇒ void
-
#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.
-
#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. -
#initialize(genv, tp_root) ⇒ TypeOracle
constructor
A new instance of TypeOracle.
- #method_call_count(cpath, singleton, mid) ⇒ Integer
-
#method_defined?(cpath, singleton, mid) ⇒ Boolean
True when the method has at least one definition in the analyzed source.
-
#method_definition_keys(cpath, singleton, mid) ⇒ Array[location_key]
Location keys of the method's definition sites.
-
#method_known?(cpath, singleton, mid) ⇒ Boolean
True when type analysis has any record of the method at all.
- #mod_is_or_inherits?(mod, target) ⇒ Boolean
-
#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.
-
#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. -
#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.
- #resolve_method(cpath, singleton, mid) ⇒ Object
-
#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.
-
#resolved_targets(prism_call_node, mid) ⇒ Array[method_key]?
The method keys a call could dispatch to, per type inference.
-
#tp_call_for(prism_node, mid) ⇒ TypeProf::Core::AST::CallBaseNode?
TypeProf's node for a call at a given source position.
-
#tp_const_for(prism_node) ⇒ TypeProf::Core::AST::ConstantReadNode?
TypeProf's constant-read node at a given source position.
-
#tp_key(node) ⇒ location_key
Location key of a TypeProf node, via the Prism node it was built from.
- #type_responds_to?(mod, singleton, mid) ⇒ Boolean
Constructor Details
#initialize(genv, tp_root) ⇒ TypeOracle
Returns a new instance of TypeOracle.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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
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.
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.
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.
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
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.
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.
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.
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).
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.
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
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 |