ryac 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +145 -0
- data/Rakefile +45 -0
- data/Steepfile +9 -0
- data/bin/console +11 -0
- data/bin/ryac +134 -0
- data/bin/setup +8 -0
- data/lib/ryac/analysis/constant/collection.rb +332 -0
- data/lib/ryac/analysis/constant/rename_mapping.rb +346 -0
- data/lib/ryac/analysis/cvar/collection.rb +37 -0
- data/lib/ryac/analysis/gvar/collection.rb +36 -0
- data/lib/ryac/analysis/gvar/rename_mapping.rb +82 -0
- data/lib/ryac/analysis/ivar/collection.rb +281 -0
- data/lib/ryac/analysis/keyword/collection.rb +213 -0
- data/lib/ryac/analysis/keyword/rename_mapping.rb +167 -0
- data/lib/ryac/analysis/local_scopes.rb +582 -0
- data/lib/ryac/analysis/method/collection.rb +298 -0
- data/lib/ryac/analysis/method/rename_mapping.rb +337 -0
- data/lib/ryac/analysis/method_aliases.rb +83 -0
- data/lib/ryac/analysis/method_aliasing.rb +65 -0
- data/lib/ryac/analysis/nesting.rb +123 -0
- data/lib/ryac/analysis/scope_management.rb +25 -0
- data/lib/ryac/analysis/site_bucket_mapping.rb +127 -0
- data/lib/ryac/analysis/type_oracle.rb +386 -0
- data/lib/ryac/ast_utils.rb +321 -0
- data/lib/ryac/errors.rb +32 -0
- data/lib/ryac/gem_resolver.rb +55 -0
- data/lib/ryac/minifier.rb +169 -0
- data/lib/ryac/name_generator.rb +68 -0
- data/lib/ryac/pipeline/analyzer.rb +264 -0
- data/lib/ryac/pipeline/attr_decl_shorten.rb +61 -0
- data/lib/ryac/pipeline/boolean_shorten.rb +26 -0
- data/lib/ryac/pipeline/char_shorten.rb +20 -0
- data/lib/ryac/pipeline/compactor.rb +1202 -0
- data/lib/ryac/pipeline/concatenator.rb +248 -0
- data/lib/ryac/pipeline/constant_aliaser.rb +240 -0
- data/lib/ryac/pipeline/constant_fold.rb +93 -0
- data/lib/ryac/pipeline/control_flow_simplify.rb +352 -0
- data/lib/ryac/pipeline/data_types.rb +179 -0
- data/lib/ryac/pipeline/endless_method.rb +60 -0
- data/lib/ryac/pipeline/errors.rb +94 -0
- data/lib/ryac/pipeline/file_collector.rb +360 -0
- data/lib/ryac/pipeline/method_renamer.rb +151 -0
- data/lib/ryac/pipeline/paren_optimizer.rb +161 -0
- data/lib/ryac/pipeline/rename_patcher.rb +274 -0
- data/lib/ryac/pipeline/source_patcher.rb +54 -0
- data/lib/ryac/pipeline/spelling_shorten.rb +60 -0
- data/lib/ryac/pipeline/stage.rb +72 -0
- data/lib/ryac/pipeline/stage_runner.rb +84 -0
- data/lib/ryac/pipeline/variable_renamer.rb +125 -0
- data/lib/ryac/union_find.rb +42 -0
- data/lib/ryac/version.rb +5 -0
- data/lib/ryac.rb +29 -0
- data/rbs_collection.lock.yaml +224 -0
- data/rbs_collection.yaml +25 -0
- data/sig/ryac/analysis/constant/collection.rbs +30 -0
- data/sig/ryac/analysis/constant/rename_mapping.rbs +60 -0
- data/sig/ryac/analysis/cvar/collection.rbs +11 -0
- data/sig/ryac/analysis/gvar/collection.rbs +9 -0
- data/sig/ryac/analysis/gvar/rename_mapping.rbs +23 -0
- data/sig/ryac/analysis/ivar/collection.rbs +26 -0
- data/sig/ryac/analysis/keyword/collection.rbs +27 -0
- data/sig/ryac/analysis/keyword/rename_mapping.rbs +31 -0
- data/sig/ryac/analysis/local_scopes.rbs +77 -0
- data/sig/ryac/analysis/method/collection.rbs +25 -0
- data/sig/ryac/analysis/method/rename_mapping.rbs +82 -0
- data/sig/ryac/analysis/method_aliases.rbs +10 -0
- data/sig/ryac/analysis/method_aliasing.rbs +9 -0
- data/sig/ryac/analysis/nesting.rbs +9 -0
- data/sig/ryac/analysis/scope_management.rbs +5 -0
- data/sig/ryac/analysis/site_bucket_mapping.rbs +26 -0
- data/sig/ryac/analysis/type_oracle.rbs +61 -0
- data/sig/ryac/ast_utils.rbs +34 -0
- data/sig/ryac/errors.rbs +21 -0
- data/sig/ryac/gem_resolver.rbs +28 -0
- data/sig/ryac/minifier.rbs +33 -0
- data/sig/ryac/name_generator.rbs +14 -0
- data/sig/ryac/pipeline/analyzer.rbs +34 -0
- data/sig/ryac/pipeline/attr_decl_shorten.rbs +17 -0
- data/sig/ryac/pipeline/boolean_shorten.rbs +11 -0
- data/sig/ryac/pipeline/char_shorten.rbs +11 -0
- data/sig/ryac/pipeline/compactor.rbs +125 -0
- data/sig/ryac/pipeline/concatenator.rbs +28 -0
- data/sig/ryac/pipeline/constant_aliaser.rbs +35 -0
- data/sig/ryac/pipeline/constant_fold.rbs +20 -0
- data/sig/ryac/pipeline/control_flow_simplify.rbs +34 -0
- data/sig/ryac/pipeline/data_types.rbs +151 -0
- data/sig/ryac/pipeline/endless_method.rbs +14 -0
- data/sig/ryac/pipeline/errors.rbs +46 -0
- data/sig/ryac/pipeline/file_collector.rbs +34 -0
- data/sig/ryac/pipeline/method_renamer.rbs +25 -0
- data/sig/ryac/pipeline/paren_optimizer.rbs +16 -0
- data/sig/ryac/pipeline/rename_patcher.rbs +38 -0
- data/sig/ryac/pipeline/source_patcher.rbs +12 -0
- data/sig/ryac/pipeline/spelling_shorten.rbs +14 -0
- data/sig/ryac/pipeline/stage.rbs +28 -0
- data/sig/ryac/pipeline/stage_runner.rbs +18 -0
- data/sig/ryac/pipeline/variable_renamer.rbs +21 -0
- data/sig/ryac/union_find.rbs +17 -0
- data/sig/ryac.rbs +13 -0
- data/sig/shims/env.rbs +19 -0
- data/sig/shims/typeprof/core/ast.rbs +299 -0
- data/sig/shims/typeprof/core/type.rbs +22 -0
- data/sig/shims/typeprof/core.rbs +73 -0
- data/tools/regen_pins.rb +45 -0
- metadata +201 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'typeprof'
|
|
4
|
+
|
|
5
|
+
module Ryac
|
|
6
|
+
# The one place allowed to talk to TypeProf.
|
|
7
|
+
#
|
|
8
|
+
# Everything the renamer needs from type analysis is phrased as a handful
|
|
9
|
+
# of questions — which methods a call can reach, who calls a method, what
|
|
10
|
+
# a receiver is, what a constant resolves to, what a class inherits. The
|
|
11
|
+
# collections compute every lexical fact themselves on the Prism tree and
|
|
12
|
+
# come here only for those questions, so a change in TypeProf's internals
|
|
13
|
+
# breaks this file and nothing else.
|
|
14
|
+
#
|
|
15
|
+
# Answers refer to source positions (location keys) or plain values, never
|
|
16
|
+
# to TypeProf objects.
|
|
17
|
+
class TypeOracle
|
|
18
|
+
# Boots TypeProf over the program and its RBS. This owns the two
|
|
19
|
+
# internals the boot requires — the Service construction and the
|
|
20
|
+
# private @rb_text_nodes table behind update_rb_file — so an upstream
|
|
21
|
+
# change to either breaks here and nowhere else.
|
|
22
|
+
def self.boot(content, rbs_files)
|
|
23
|
+
path = '(minify_concat)'
|
|
24
|
+
service = TypeProf::Core::Service.new({})
|
|
25
|
+
rbs_files.each do |rbs_path, rbs_content|
|
|
26
|
+
service.update_rbs_file(rbs_path, rbs_content)
|
|
27
|
+
end
|
|
28
|
+
service.update_rb_file(path, content)
|
|
29
|
+
new(service.genv, service.instance_variable_get(:@rb_text_nodes)[path])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# TypeProf keeps the backing Prism node private because an editor has no
|
|
33
|
+
# reason to ask; the coordinate join (tp_key below) asks constantly,
|
|
34
|
+
# since Prism is where the syntax being rewritten lives.
|
|
35
|
+
def self.raw_prism_node(node)
|
|
36
|
+
node.instance_variable_get(:@raw_node)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize(genv, tp_root)
|
|
40
|
+
@genv = genv
|
|
41
|
+
@tp_root = tp_root
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Yields every ancestor cpath of the class, including its own.
|
|
45
|
+
# Unresolvable classes yield nothing.
|
|
46
|
+
def each_ancestor_cpath(cpath, singleton, &block)
|
|
47
|
+
@ancestor_cpaths ||= {} #: Hash[MethodRenameMapping::class_key, Array[Array[Symbol]]]
|
|
48
|
+
cached = @ancestor_cpaths[[cpath, singleton]] ||= begin
|
|
49
|
+
list = [] #: Array[Array[Symbol]]
|
|
50
|
+
mod = @genv.resolve_cpath(cpath) rescue nil
|
|
51
|
+
if mod
|
|
52
|
+
@genv.each_superclass(mod, singleton) do |ancestor_mod, _singleton|
|
|
53
|
+
list << ancestor_mod.cpath
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
list
|
|
57
|
+
end
|
|
58
|
+
cached.each(&block)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Yields [ancestor_cpath, singleton, method_names] along the ancestor
|
|
62
|
+
# chain, own class first — the namespace a new method name must not
|
|
63
|
+
# collide with.
|
|
64
|
+
def each_ancestor_methods(cpath, singleton)
|
|
65
|
+
mod = @genv.resolve_cpath(cpath) rescue nil
|
|
66
|
+
return unless mod
|
|
67
|
+
|
|
68
|
+
@genv.each_superclass(mod, singleton) do |ancestor_mod, s|
|
|
69
|
+
names = [] #: Array[String]
|
|
70
|
+
ancestor_mod.methods[s]&.each_key { |mid| names << mid.to_s }
|
|
71
|
+
yield ancestor_mod.cpath, s, names
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# True when type analysis has any record of the method at all.
|
|
76
|
+
def method_known?(cpath, singleton, mid)
|
|
77
|
+
!resolve_method(cpath, singleton, mid).nil?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# True when the method has at least one definition in the analyzed source.
|
|
81
|
+
def method_defined?(cpath, singleton, mid)
|
|
82
|
+
entity = resolve_method(cpath, singleton, mid)
|
|
83
|
+
!entity.nil? && entity.defs.size > 0
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def method_call_count(cpath, singleton, mid)
|
|
87
|
+
entity = resolve_method(cpath, singleton, mid)
|
|
88
|
+
entity ? entity.method_call_boxes.size : 0
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Location keys of every call site that dispatches to the method.
|
|
92
|
+
def each_call_site_key(cpath, singleton, mid)
|
|
93
|
+
entity = resolve_method(cpath, singleton, mid)
|
|
94
|
+
return unless entity
|
|
95
|
+
|
|
96
|
+
entity.method_call_boxes.each do |call_box|
|
|
97
|
+
yield tp_key(call_box.node)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Location keys of the method's definition sites.
|
|
102
|
+
def method_definition_keys(cpath, singleton, mid)
|
|
103
|
+
entity = resolve_method(cpath, singleton, mid)
|
|
104
|
+
return [] unless entity
|
|
105
|
+
|
|
106
|
+
entity.defs.to_a.map { |d| tp_key(d.node) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# One record per call site that dispatches to the method, as plain data:
|
|
110
|
+
#
|
|
111
|
+
# prism_node the call in the syntax tree (nil for a super with no
|
|
112
|
+
# syntactic call node of its own, and for a dispatch
|
|
113
|
+
# synthesized inside an RBS declaration)
|
|
114
|
+
# super true for `super` — dispatch from a subclass override
|
|
115
|
+
# caller_cpath the class nesting the call sits in
|
|
116
|
+
# receiver whether the call is written with an explicit receiver
|
|
117
|
+
# keyword_entries [[symbol_node, value_node], ...] for literal keyword
|
|
118
|
+
# arguments, nil when the call passes none
|
|
119
|
+
# keyword_splat true when keywords arrive via **splat
|
|
120
|
+
CallerInfo = Struct.new(:prism_node, :super, :caller_cpath, :receiver,
|
|
121
|
+
:keyword_entries, :keyword_splat, keyword_init: true)
|
|
122
|
+
|
|
123
|
+
def each_caller(cpath, singleton, mid)
|
|
124
|
+
entity = resolve_method(cpath, singleton, mid)
|
|
125
|
+
return unless entity
|
|
126
|
+
|
|
127
|
+
entity.method_call_boxes.each do |call_box|
|
|
128
|
+
node = call_box.node
|
|
129
|
+
|
|
130
|
+
if node.is_a?(TypeProf::Core::AST::SuperNode) ||
|
|
131
|
+
node.is_a?(TypeProf::Core::AST::ForwardingSuperNode)
|
|
132
|
+
yield CallerInfo.new(prism_node: TypeOracle.raw_prism_node(node), super: true,
|
|
133
|
+
caller_cpath: node.lenv.cref.cpath, receiver: false,
|
|
134
|
+
keyword_entries: nil, keyword_splat: false)
|
|
135
|
+
next
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# A box whose node is not a syntactic call names no site in the
|
|
139
|
+
# program — e.g. one synthesized while TypeProf evaluated an RBS
|
|
140
|
+
# declaration: `[x].map(&:foo)` dispatches to foo from Array#map's
|
|
141
|
+
# declared signature. Nothing in the source can be rewritten to
|
|
142
|
+
# follow a rename, so the record carries only the fact that an
|
|
143
|
+
# unrewritable caller exists.
|
|
144
|
+
unless node.is_a?(TypeProf::Core::AST::CallBaseNode)
|
|
145
|
+
yield CallerInfo.new(prism_node: nil, super: false, caller_cpath: nil,
|
|
146
|
+
receiver: false, keyword_entries: nil, keyword_splat: false)
|
|
147
|
+
next
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
entries = nil
|
|
151
|
+
splat = false
|
|
152
|
+
kw = node.respond_to?(:keyword_args) ? node.keyword_args : nil
|
|
153
|
+
if kw.is_a?(TypeProf::Core::AST::HashNode)
|
|
154
|
+
splat = kw.keys.any?(&:nil?)
|
|
155
|
+
entries = kw.keys.zip(kw.vals).filter_map do |sym_node, val_node|
|
|
156
|
+
# @type var val_node: TypeProf::Core::AST::Node
|
|
157
|
+
next unless sym_node.is_a?(TypeProf::Core::AST::SymbolNode)
|
|
158
|
+
[TypeOracle.raw_prism_node(sym_node), TypeOracle.raw_prism_node(val_node)] #: [Prism::Node, Prism::Node]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
yield CallerInfo.new(prism_node: TypeOracle.raw_prism_node(node), super: false,
|
|
163
|
+
caller_cpath: nil, receiver: !node.recv.nil?,
|
|
164
|
+
keyword_entries: entries, keyword_splat: splat)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# The method keys a call could dispatch to, per type inference. nil when
|
|
169
|
+
# inference has no answer at all — distinct from an empty list.
|
|
170
|
+
def resolved_targets(prism_call_node, mid)
|
|
171
|
+
tp_node = tp_call_for(prism_call_node, mid)
|
|
172
|
+
return nil unless tp_node
|
|
173
|
+
|
|
174
|
+
keys = [] #: Array[method_key]
|
|
175
|
+
tp_node.boxes(:mcall) do |box|
|
|
176
|
+
box.resolve(@genv, nil) do |entity, ty, _mid, _orig_ty|
|
|
177
|
+
next unless entity
|
|
178
|
+
singleton = ty.is_a?(TypeProf::Core::Type::Singleton)
|
|
179
|
+
keys << [ty.mod.cpath, singleton, mid]
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
keys.any? ? keys : nil
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Symbol values inference assigns to the call's first positional argument
|
|
186
|
+
# — how `method(:foo)`-style dynamic references name their target.
|
|
187
|
+
def first_argument_symbols(prism_call_node)
|
|
188
|
+
tp_node = tp_call_for(prism_call_node, prism_call_node.name)
|
|
189
|
+
return [] unless tp_node
|
|
190
|
+
|
|
191
|
+
sym_arg = tp_node.positional_args&.first #: TypeProf::Core::AST::Node
|
|
192
|
+
ret = sym_arg.ret rescue nil
|
|
193
|
+
return [] unless ret
|
|
194
|
+
|
|
195
|
+
ret.types.each_key.filter_map do |ty|
|
|
196
|
+
ty.sym if ty.is_a?(TypeProf::Core::Type::Symbol)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# True when every inferred type of the call's receiver responds to mid —
|
|
201
|
+
# the safety condition for rewriting the call to a stdlib alias.
|
|
202
|
+
def receiver_responds_to?(prism_call_node, mid)
|
|
203
|
+
every_receiver_base_type(prism_call_node) do |base|
|
|
204
|
+
singleton = base.is_a?(TypeProf::Core::Type::Singleton)
|
|
205
|
+
type_responds_to?(base.mod, singleton, mid)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Resolves a constant reference to its fully-qualified path, using type
|
|
210
|
+
# analysis to see through lexical-scope lookup and value constants.
|
|
211
|
+
# nil when the reference is not statically resolvable. Memoized — the
|
|
212
|
+
# counting, external-reference, and precompute passes each ask about the
|
|
213
|
+
# same nodes.
|
|
214
|
+
def resolve_constant_read(prism_node)
|
|
215
|
+
@const_resolution_cache ||= {} #: Hash[location_key, Array[Symbol]?]
|
|
216
|
+
key = AstUtils.location_key(prism_node)
|
|
217
|
+
return @const_resolution_cache[key] if @const_resolution_cache.key?(key)
|
|
218
|
+
|
|
219
|
+
tp_node = tp_const_for(prism_node)
|
|
220
|
+
@const_resolution_cache[key] = tp_node ? resolve_tp_const(tp_node) : nil
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# How many read sites type analysis records for the constant. Can exceed
|
|
224
|
+
# the syntactic count when reads reach it through resolution the text
|
|
225
|
+
# does not show.
|
|
226
|
+
def constant_read_count(cpath)
|
|
227
|
+
# @type var entity: untyped
|
|
228
|
+
entity = begin
|
|
229
|
+
@genv.resolve_const(cpath)
|
|
230
|
+
rescue StandardError
|
|
231
|
+
nil
|
|
232
|
+
end
|
|
233
|
+
entity.respond_to?(:read_boxes) ? entity.read_boxes.size : 0
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# True when every inferred type of the call's receiver is type_name or a
|
|
237
|
+
# subclass — the safety condition for type-specific rewrites like
|
|
238
|
+
# `.empty?` → `=={}` on a Hash.
|
|
239
|
+
def receiver_within_type?(prism_call_node, type_name)
|
|
240
|
+
target_mod = @genv.resolve_cpath([type_name])
|
|
241
|
+
return false unless target_mod
|
|
242
|
+
|
|
243
|
+
every_receiver_base_type(prism_call_node) do |base|
|
|
244
|
+
mod_is_or_inherits?(base.mod, target_mod)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
private
|
|
249
|
+
|
|
250
|
+
# Location key of a TypeProf node, via the Prism node it was built from.
|
|
251
|
+
# TypeProf models source it cannot point back at — a node may carry no
|
|
252
|
+
# @raw_node at all, or an RBS node instead of a Prism one on sig-built
|
|
253
|
+
# nodes. Either way a node we cannot locate is one we cannot rename, so
|
|
254
|
+
# this raises rather than inventing a key that could never match. The
|
|
255
|
+
# conversion stays inside the oracle: everything outside it holds Prism
|
|
256
|
+
# nodes, and AstUtils keys those.
|
|
257
|
+
def tp_key(node)
|
|
258
|
+
prism_node = TypeOracle.raw_prism_node(node)
|
|
259
|
+
raise ArgumentError, "no source location behind #{node.class}" unless prism_node.is_a?(Prism::Node)
|
|
260
|
+
|
|
261
|
+
AstUtils.location_key(prism_node)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def resolve_method(cpath, singleton, mid)
|
|
265
|
+
@method_cache ||= {} #: Hash[method_key, TypeProf::Core::MethodEntity?]
|
|
266
|
+
key = [cpath, singleton, mid] #: method_key
|
|
267
|
+
return @method_cache[key] if @method_cache.key?(key)
|
|
268
|
+
|
|
269
|
+
@method_cache[key] = begin
|
|
270
|
+
@genv.resolve_method(cpath, singleton, mid)
|
|
271
|
+
rescue StandardError
|
|
272
|
+
nil
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# The shared scaffold of the receiver-safety questions: false unless the
|
|
277
|
+
# receiver has inferred types at all, then the block must hold for every
|
|
278
|
+
# type's base.
|
|
279
|
+
def every_receiver_base_type(prism_call_node)
|
|
280
|
+
recv = tp_call_for(prism_call_node, prism_call_node.name)&.recv #: untyped
|
|
281
|
+
return false unless recv.respond_to?(:ret) && recv.ret
|
|
282
|
+
|
|
283
|
+
types = recv.ret.types
|
|
284
|
+
return false if types.empty?
|
|
285
|
+
|
|
286
|
+
types.all? do |ty, _|
|
|
287
|
+
base = ty.base_type(@genv)
|
|
288
|
+
next false unless base.respond_to?(:mod)
|
|
289
|
+
yield base
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def type_responds_to?(mod, singleton, mid)
|
|
294
|
+
@genv.each_superclass(mod, singleton) do |ancestor_mod, s|
|
|
295
|
+
entity = ancestor_mod.methods[s]&.[](mid)
|
|
296
|
+
return true if entity && (entity.exist? || entity.aliases.any?)
|
|
297
|
+
end
|
|
298
|
+
false
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def mod_is_or_inherits?(mod, target)
|
|
302
|
+
return true if mod == target
|
|
303
|
+
@genv.each_superclass(mod, false) do |ancestor, _|
|
|
304
|
+
return true if ancestor == target
|
|
305
|
+
end
|
|
306
|
+
false
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
TP_CALL_NODES = [
|
|
310
|
+
TypeProf::Core::AST::CallNode,
|
|
311
|
+
TypeProf::Core::AST::CallReadNode,
|
|
312
|
+
TypeProf::Core::AST::CallWriteNode
|
|
313
|
+
].freeze
|
|
314
|
+
|
|
315
|
+
# TypeProf's node for a call at a given source position. TypeProf answers
|
|
316
|
+
# type questions per node in its own tree; this index is how a question
|
|
317
|
+
# about a Prism node finds its counterpart.
|
|
318
|
+
#
|
|
319
|
+
# Indexed by position *and* method name: a compound write like
|
|
320
|
+
# `self.count += 1` is one position but two TypeProf nodes — a read of
|
|
321
|
+
# `count` and a write of `count=` — and a question about either name has
|
|
322
|
+
# to reach its own node.
|
|
323
|
+
def tp_call_for(prism_node, mid)
|
|
324
|
+
build_tp_indexes
|
|
325
|
+
@tp_calls_by_loc[AstUtils.location_key(prism_node)]&.[](mid)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# TypeProf's constant-read node at a given source position. Covers plain
|
|
329
|
+
# reads, every level of a qualified chain, and the read half of compound
|
|
330
|
+
# writes (`X ||= 1` reads at the whole expression's position).
|
|
331
|
+
def tp_const_for(prism_node)
|
|
332
|
+
build_tp_indexes
|
|
333
|
+
@tp_consts_by_loc[AstUtils.location_key(prism_node)]
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Both position indexes come from the same walk; a real run always needs
|
|
337
|
+
# both, so one traversal fills them together.
|
|
338
|
+
def build_tp_indexes
|
|
339
|
+
return if @tp_calls_by_loc
|
|
340
|
+
|
|
341
|
+
calls = {} #: Hash[location_key, Hash[Symbol, TypeProf::Core::AST::CallBaseNode]]
|
|
342
|
+
consts = {} #: Hash[location_key, TypeProf::Core::AST::ConstantReadNode]
|
|
343
|
+
@tp_root.traverse do |event, node|
|
|
344
|
+
next unless event == :enter
|
|
345
|
+
case node
|
|
346
|
+
when *TP_CALL_NODES
|
|
347
|
+
# @type var node: TypeProf::Core::AST::CallBaseNode
|
|
348
|
+
(calls[tp_key(node)] ||= {})[node.mid] = node
|
|
349
|
+
when TypeProf::Core::AST::ConstantReadNode
|
|
350
|
+
consts[tp_key(node)] = node
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
@tp_calls_by_loc = calls
|
|
354
|
+
@tp_consts_by_loc = consts
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# A class/module reference resolves through its own analysis result; a
|
|
358
|
+
# value constant through its definition's location; a qualified chain
|
|
359
|
+
# (Foo::CONST) through its prefix, recursively.
|
|
360
|
+
def resolve_tp_const(node)
|
|
361
|
+
return nil unless node.is_a?(TypeProf::Core::AST::ConstantReadNode)
|
|
362
|
+
|
|
363
|
+
static_ret = begin
|
|
364
|
+
node.static_ret
|
|
365
|
+
rescue StandardError
|
|
366
|
+
nil
|
|
367
|
+
end
|
|
368
|
+
return nil unless static_ret.respond_to?(:cpath)
|
|
369
|
+
return static_ret.cpath if static_ret.cpath
|
|
370
|
+
|
|
371
|
+
cdef = static_ret.respond_to?(:cdef) ? static_ret.cdef : nil #: untyped
|
|
372
|
+
if cdef.respond_to?(:defs)
|
|
373
|
+
cdef.defs.each do |d|
|
|
374
|
+
return d.static_cpath if d.respond_to?(:static_cpath) && d.static_cpath
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
if node.cbase
|
|
379
|
+
base_cpath = resolve_tp_const(node.cbase)
|
|
380
|
+
return base_cpath + [node.cname] if base_cpath
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
nil
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
module AstUtils
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# The plain-symbol arguments of a call, as symbols — how attr_*,
|
|
8
|
+
# private_constant and friends name their subjects.
|
|
9
|
+
def symbol_arguments(call_node)
|
|
10
|
+
call_node.arguments&.arguments&.filter_map { |arg|
|
|
11
|
+
arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode)
|
|
12
|
+
} || []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def unwrap_statements(node)
|
|
16
|
+
return node unless node
|
|
17
|
+
node = node.body if node.is_a?(Prism::ParenthesesNode)
|
|
18
|
+
if node.is_a?(Prism::StatementsNode) && node.body.size == 1
|
|
19
|
+
node.body.first
|
|
20
|
+
else
|
|
21
|
+
node
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
MIDDLE_METHODS = %i[+ - * / ** % ^ > < <= >= <=> == === != & | << >> =~ !~].freeze
|
|
26
|
+
|
|
27
|
+
def middle_method?(method)
|
|
28
|
+
MIDDLE_METHODS.include?(method)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def logical_op?(node)
|
|
32
|
+
node.is_a?(Prism::OrNode) || node.is_a?(Prism::AndNode)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def has_block?(node)
|
|
36
|
+
node.block != nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def single_statement_body?(body)
|
|
40
|
+
return false if body.nil?
|
|
41
|
+
return body.body.size == 1 if body.is_a?(Prism::StatementsNode)
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def setter_def_name?(name)
|
|
46
|
+
s = name.to_s
|
|
47
|
+
s.end_with?('=') && !%w[== != <= >= ===].include?(s)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def simple_negatable?(node)
|
|
51
|
+
inner = unwrap_statements(node)
|
|
52
|
+
return false if logical_op?(inner)
|
|
53
|
+
|
|
54
|
+
inner.is_a?(Prism::CallNode) ? !middle_method?(inner.name) : true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def first_arg_starts_with_brace?(arg)
|
|
58
|
+
inner = unwrap_statements(arg)
|
|
59
|
+
return true if inner.is_a?(Prism::HashNode)
|
|
60
|
+
return first_arg_starts_with_brace?(inner.receiver) if inner.is_a?(Prism::CallNode) && inner.receiver
|
|
61
|
+
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Does this node's string representation end with a character
|
|
66
|
+
# that could form `name!` or `name?` if `!` or `?` is appended?
|
|
67
|
+
# Used for spacing before `!~` operator and ternary `?`/`:`.
|
|
68
|
+
# Recursively follows rightmost sub-expressions for compound nodes.
|
|
69
|
+
def ends_with_name_char?(node)
|
|
70
|
+
inner = unwrap_statements(node)
|
|
71
|
+
case inner
|
|
72
|
+
# Empty parens unwrap to nothing; the rendered `()` ends with ')'.
|
|
73
|
+
when nil
|
|
74
|
+
false
|
|
75
|
+
# nil renders as `()` — ends with ')', not a name char.
|
|
76
|
+
when Prism::NilNode
|
|
77
|
+
false
|
|
78
|
+
# Terminals: definitely safe (end with delimiter/sigil/literal)
|
|
79
|
+
when Prism::ParenthesesNode,
|
|
80
|
+
Prism::StringNode, Prism::InterpolatedStringNode,
|
|
81
|
+
Prism::InterpolatedSymbolNode,
|
|
82
|
+
Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode,
|
|
83
|
+
Prism::ArrayNode, Prism::HashNode, Prism::LambdaNode,
|
|
84
|
+
Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode,
|
|
85
|
+
Prism::GlobalVariableReadNode,
|
|
86
|
+
Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode,
|
|
87
|
+
Prism::DefinedNode, Prism::RescueModifierNode
|
|
88
|
+
false
|
|
89
|
+
# Bare symbol :name ends with name char; quoted :"name" ends with delimiter
|
|
90
|
+
when Prism::SymbolNode
|
|
91
|
+
!inner.closing_loc
|
|
92
|
+
# Compound: follow rightmost sub-expression
|
|
93
|
+
when Prism::OrNode, Prism::AndNode
|
|
94
|
+
ends_with_name_char?(inner.right)
|
|
95
|
+
when Prism::RangeNode
|
|
96
|
+
inner.right ? ends_with_name_char?(inner.right) : false
|
|
97
|
+
when Prism::CallNode
|
|
98
|
+
return false if inner.block
|
|
99
|
+
if middle_method?(inner.name)
|
|
100
|
+
# Binary op: a+b → ends with right operand
|
|
101
|
+
arg = inner.arguments&.arguments&.first
|
|
102
|
+
arg ? ends_with_name_char?(arg) : true
|
|
103
|
+
elsif inner.name == :[]=
|
|
104
|
+
# Index assignment: recv[k]=val → ends with value
|
|
105
|
+
last_arg = inner.arguments&.arguments&.last
|
|
106
|
+
last_arg ? ends_with_name_char?(last_arg) : false
|
|
107
|
+
elsif inner.receiver && !inner.arguments && (inner.name == :! || inner.name == :~)
|
|
108
|
+
# Unary prefix operators: !x, ~x → output ends with operand
|
|
109
|
+
ends_with_name_char?(inner.receiver)
|
|
110
|
+
elsif inner.opening_loc
|
|
111
|
+
false # foo(args) → ends with )
|
|
112
|
+
elsif inner.name.to_s.end_with?('?', '!')
|
|
113
|
+
false
|
|
114
|
+
else
|
|
115
|
+
true # bare method: foo.bar → ends with ident char
|
|
116
|
+
end
|
|
117
|
+
when Prism::YieldNode, Prism::SuperNode
|
|
118
|
+
inner.arguments.nil?
|
|
119
|
+
# Assignment: var=value → ends with value
|
|
120
|
+
when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
|
|
121
|
+
Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode,
|
|
122
|
+
Prism::ConstantWriteNode, Prism::ConstantPathWriteNode
|
|
123
|
+
ends_with_name_char?(inner.value)
|
|
124
|
+
when Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
|
|
125
|
+
Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
126
|
+
Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
|
|
127
|
+
Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
|
|
128
|
+
Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode
|
|
129
|
+
ends_with_name_char?(inner.value)
|
|
130
|
+
else
|
|
131
|
+
true # conservative: local var, const, nil, true, false, self, etc.
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Does this node's output end with ? or ! method suffix?
|
|
136
|
+
# Recursively follows rightmost sub-expressions.
|
|
137
|
+
def ends_with_method_suffix?(node)
|
|
138
|
+
inner = unwrap_statements(node)
|
|
139
|
+
case inner
|
|
140
|
+
when Prism::OrNode, Prism::AndNode
|
|
141
|
+
ends_with_method_suffix?(inner.right)
|
|
142
|
+
when Prism::CallNode
|
|
143
|
+
return false if inner.block
|
|
144
|
+
return false if inner.opening_loc && inner.name != :[]=
|
|
145
|
+
if middle_method?(inner.name)
|
|
146
|
+
arg = inner.arguments&.arguments&.first
|
|
147
|
+
arg ? ends_with_method_suffix?(arg) : false
|
|
148
|
+
elsif inner.name == :[]=
|
|
149
|
+
last_arg = inner.arguments&.arguments&.last
|
|
150
|
+
last_arg ? ends_with_method_suffix?(last_arg) : false
|
|
151
|
+
elsif inner.receiver && !inner.arguments && (inner.name == :! || inner.name == :~)
|
|
152
|
+
# Unary prefix operators (!x, ~x) — output ends with operand, not suffix
|
|
153
|
+
ends_with_method_suffix?(inner.receiver)
|
|
154
|
+
else
|
|
155
|
+
inner.name.to_s.end_with?('?', '!')
|
|
156
|
+
end
|
|
157
|
+
when Prism::SymbolNode
|
|
158
|
+
!inner.closing_loc && inner.value.to_s.end_with?('?', '!')
|
|
159
|
+
when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
|
|
160
|
+
Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode,
|
|
161
|
+
Prism::ConstantWriteNode, Prism::ConstantPathWriteNode,
|
|
162
|
+
Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
|
|
163
|
+
Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
164
|
+
Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
|
|
165
|
+
Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
|
|
166
|
+
Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode
|
|
167
|
+
ends_with_method_suffix?(inner.value)
|
|
168
|
+
else
|
|
169
|
+
false
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Needs space before ternary `?`?
|
|
174
|
+
# True when condition ends with: name char, `?`, `!`, or digit.
|
|
175
|
+
def needs_ternary_q_space?(node)
|
|
176
|
+
ends_with_name_char?(node) || ends_with_method_suffix?(node)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def modifier_control_flow?(node)
|
|
180
|
+
modifier_conditional?(node) || modifier_loop?(node)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def modifier_conditional?(node)
|
|
184
|
+
case node
|
|
185
|
+
when Prism::IfNode then node.if_keyword_loc && !node.end_keyword_loc
|
|
186
|
+
when Prism::UnlessNode then node.keyword_loc && !node.end_keyword_loc
|
|
187
|
+
else false
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def modifier_loop?(node)
|
|
192
|
+
case node
|
|
193
|
+
when Prism::WhileNode, Prism::UntilNode then !node.closing_loc
|
|
194
|
+
else false
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Bytes before which a ternary does NOT need wrapping parens
|
|
199
|
+
TERNARY_SAFE_PREV = [';'.ord, "\n".ord, "\r".ord, '('.ord, '['.ord, '{'.ord].freeze
|
|
200
|
+
COMPARISON_PREV = ['<'.ord, '>'.ord, '!'.ord, '='.ord].freeze
|
|
201
|
+
PIPE_BREAK_BYTES = [';'.ord, "\n".ord, '{'.ord].freeze
|
|
202
|
+
|
|
203
|
+
def ternary_needs_parens?(node, source)
|
|
204
|
+
start = node.location.start_offset
|
|
205
|
+
return false if start == 0
|
|
206
|
+
prev = source.getbyte(start - 1)
|
|
207
|
+
return false if TERNARY_SAFE_PREV.include?(prev)
|
|
208
|
+
if prev == '='.ord
|
|
209
|
+
return false if start < 2
|
|
210
|
+
prev2 = source.getbyte(start - 2)
|
|
211
|
+
return COMPARISON_PREV.include?(prev2)
|
|
212
|
+
end
|
|
213
|
+
if prev == '|'.ord
|
|
214
|
+
return true if start < 3
|
|
215
|
+
return true if source.getbyte(start - 2) == '|'.ord
|
|
216
|
+
pos = start - 2
|
|
217
|
+
while pos >= 0
|
|
218
|
+
byte = source.getbyte(pos)
|
|
219
|
+
if byte == '|'.ord
|
|
220
|
+
return !(pos > 0 && source.getbyte(pos - 1) == '{'.ord)
|
|
221
|
+
end
|
|
222
|
+
break if PIPE_BREAK_BYTES.include?(byte)
|
|
223
|
+
pos -= 1
|
|
224
|
+
end
|
|
225
|
+
return true
|
|
226
|
+
end
|
|
227
|
+
true
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def can_omit_parens?(node, method_name = nil)
|
|
231
|
+
return false if has_block?(node)
|
|
232
|
+
# `.()` proc-call sugar has no message; its parens ARE the call.
|
|
233
|
+
return false if node.message_loc.nil?
|
|
234
|
+
raw_args = node.arguments&.arguments || []
|
|
235
|
+
positional_args = raw_args.reject { |a| a.is_a?(Prism::KeywordHashNode) }
|
|
236
|
+
return false if positional_args.empty?
|
|
237
|
+
return false if raw_args.any? { |a| a.is_a?(Prism::ForwardingArgumentsNode) }
|
|
238
|
+
return false if node.name == :[] || node.name == :[]=
|
|
239
|
+
name_to_check = method_name || node.name.to_s
|
|
240
|
+
return false if name_to_check.end_with?('?')
|
|
241
|
+
return false if first_arg_starts_with_brace?(positional_args.first)
|
|
242
|
+
return false if first_arg_is_regex?(positional_args.first)
|
|
243
|
+
return false if raw_args.any? { |a| contains_bare_block?(a) }
|
|
244
|
+
|
|
245
|
+
true
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def first_arg_is_regex?(arg)
|
|
249
|
+
arg.is_a?(Prism::RegularExpressionNode) || arg.is_a?(Prism::InterpolatedRegularExpressionNode)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def contains_bare_block?(node)
|
|
253
|
+
case node
|
|
254
|
+
when Prism::CallNode
|
|
255
|
+
return true if node.block.is_a?(Prism::BlockNode)
|
|
256
|
+
return true if node.receiver && contains_bare_block?(node.receiver)
|
|
257
|
+
return false if node.opening_loc
|
|
258
|
+
node.arguments&.arguments&.any? { |a| contains_bare_block?(a) } || false
|
|
259
|
+
when Prism::ParenthesesNode, Prism::ArrayNode, Prism::HashNode, Prism::LambdaNode
|
|
260
|
+
false
|
|
261
|
+
when Prism::KeywordHashNode
|
|
262
|
+
node.elements.any? { |e| e.is_a?(Prism::AssocNode) && contains_bare_block?(e.value) }
|
|
263
|
+
when Prism::SplatNode
|
|
264
|
+
contains_bare_block?(node.expression)
|
|
265
|
+
else
|
|
266
|
+
false
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Depth-first over every node. The shared primitive for analyses that
|
|
271
|
+
# need the whole tree without caring about scope or nesting context.
|
|
272
|
+
def self.each_node(node, &block)
|
|
273
|
+
return unless node.is_a?(Prism::Node)
|
|
274
|
+
yield node
|
|
275
|
+
node.compact_child_nodes.each { |child| each_node(child, &block) }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Expressions that rebind to the surrounding statement when their parens
|
|
279
|
+
# are stripped: `def f =expr in pat` parses as `(def f =expr) in pat`.
|
|
280
|
+
# Every "needs parens" list must include these.
|
|
281
|
+
MATCH_REBIND_NODES = [Prism::MatchPredicateNode, Prism::MatchRequiredNode].freeze
|
|
282
|
+
|
|
283
|
+
def self.match_rebind?(node)
|
|
284
|
+
MATCH_REBIND_NODES.any? { |t| node.is_a?(t) }
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# The shorthand-pun values of a hash-ish node: implicit assoc values that
|
|
288
|
+
# are NOT plain local reads (`{ label: }` calling method label). The one
|
|
289
|
+
# definition of "pun" shared by the keyword and method exclusion passes.
|
|
290
|
+
def self.shorthand_pun_values(node)
|
|
291
|
+
return [] unless node.is_a?(Prism::KeywordHashNode) || node.is_a?(Prism::HashNode)
|
|
292
|
+
|
|
293
|
+
node.elements.filter_map do |el|
|
|
294
|
+
next unless el.is_a?(Prism::AssocNode) && el.value.is_a?(Prism::ImplicitNode)
|
|
295
|
+
|
|
296
|
+
inner = el.value.value
|
|
297
|
+
inner unless inner.is_a?(Prism::LocalVariableReadNode)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# The single place the analysis and the patchers agree on what "the same
|
|
302
|
+
# piece of source" is: every map between phases is keyed by this. Prism
|
|
303
|
+
# nodes only — TypeOracle converts its own tree's nodes before keying,
|
|
304
|
+
# so the seam between the two trees stays inside the oracle.
|
|
305
|
+
#
|
|
306
|
+
# Byte offsets, because that is the coordinate system patches are applied
|
|
307
|
+
# in — deriving a separate line/column space for the join only gave the two
|
|
308
|
+
# sides a way to disagree.
|
|
309
|
+
def self.location_key(node)
|
|
310
|
+
loc = node.location
|
|
311
|
+
[loc.start_offset, loc.end_offset]
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# The line/column coordinate space (syntax_data and friends) — kept
|
|
315
|
+
# separate from byte-offset location_key on purpose.
|
|
316
|
+
def self.line_col_key(node)
|
|
317
|
+
loc = node.location
|
|
318
|
+
[loc.start_line, loc.start_column]
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|