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,274 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
module Pipeline
|
|
5
|
+
# Shared infrastructure for L2-L5 rename pipeline stages.
|
|
6
|
+
# Each stage includes this module and implements its own collect_patches.
|
|
7
|
+
module RenamePatcher
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def walk_prism(node, &block)
|
|
11
|
+
return unless node
|
|
12
|
+
result = yield node
|
|
13
|
+
return if result == :skip_children
|
|
14
|
+
|
|
15
|
+
children = node.compact_child_nodes
|
|
16
|
+
if node.is_a?(Prism::MatchWriteNode)
|
|
17
|
+
children = children.reject { |c| c.is_a?(Prism::LocalVariableTargetNode) }
|
|
18
|
+
end
|
|
19
|
+
children.each { |child| walk_prism(child, &block) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Same key the analysis stored the rename under. Kept as its own name
|
|
23
|
+
# because the patchers only ever pass Prism nodes, but it must not grow a
|
|
24
|
+
# second implementation — the two sides sharing one definition is the
|
|
25
|
+
# point.
|
|
26
|
+
def prism_location_key(node)
|
|
27
|
+
AstUtils.location_key(node)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def patch_variable(node, patches, rename_map)
|
|
31
|
+
key = prism_location_key(node)
|
|
32
|
+
short = rename_map[key]
|
|
33
|
+
return unless short
|
|
34
|
+
|
|
35
|
+
loc = node.location
|
|
36
|
+
patches << { start: loc.start_offset, end: loc.end_offset, replacement: short }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def patch_variable_name_only(node, patches, rename_map)
|
|
40
|
+
key = prism_location_key(node)
|
|
41
|
+
short = rename_map[key]
|
|
42
|
+
return unless short
|
|
43
|
+
|
|
44
|
+
name_loc = node.name_loc
|
|
45
|
+
patches << { start: name_loc.start_offset, end: name_loc.end_offset, replacement: short }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def patch_def_params(node, patches, analysis, param_names_key)
|
|
49
|
+
return unless node.parameters
|
|
50
|
+
|
|
51
|
+
syntax_key = AstUtils.line_col_key(node)
|
|
52
|
+
data = analysis.syntax_data[syntax_key]
|
|
53
|
+
return unless data
|
|
54
|
+
|
|
55
|
+
# any? rather than empty?: `empty?` on a receiver TypeProf can prove
|
|
56
|
+
# is a Hash gets rewritten to `=={}` — but only when it can prove it,
|
|
57
|
+
# and whether it can differs between the original and the minified
|
|
58
|
+
# text. Self-hosting then never reaches a fixed point. `any?` has no
|
|
59
|
+
# such rewrite.
|
|
60
|
+
param_names = data[param_names_key] || {} #: Hash[Symbol, String]
|
|
61
|
+
return unless param_names.any?
|
|
62
|
+
|
|
63
|
+
params = node.parameters
|
|
64
|
+
patch_required_params(params.requireds, param_names, patches)
|
|
65
|
+
patch_optional_params(params.optionals, param_names, patches)
|
|
66
|
+
patch_rest_param(params.rest, param_names, patches)
|
|
67
|
+
patch_post_params(params.posts, param_names, patches)
|
|
68
|
+
patch_keyword_params(params.keywords, param_names, patches)
|
|
69
|
+
patch_keyword_rest(params.keyword_rest, param_names, patches)
|
|
70
|
+
patch_block_param_node(params.block, param_names, patches)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def patch_required_params(requireds, param_names, patches)
|
|
74
|
+
return unless requireds
|
|
75
|
+
requireds.each do |p|
|
|
76
|
+
if p.is_a?(Prism::MultiTargetNode)
|
|
77
|
+
patch_multi_target_params(p, param_names, patches)
|
|
78
|
+
elsif p.is_a?(Prism::RequiredParameterNode)
|
|
79
|
+
short = param_names[p.name]
|
|
80
|
+
next unless short && short != p.name.to_s
|
|
81
|
+
patches << { start: p.location.start_offset, end: p.location.end_offset, replacement: short }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def patch_multi_target_params(node, param_names, patches)
|
|
87
|
+
node.lefts.each do |p|
|
|
88
|
+
if p.is_a?(Prism::RequiredParameterNode)
|
|
89
|
+
short = param_names[p.name]
|
|
90
|
+
next unless short && short != p.name.to_s
|
|
91
|
+
patches << { start: p.location.start_offset, end: p.location.end_offset, replacement: short }
|
|
92
|
+
elsif p.is_a?(Prism::MultiTargetNode)
|
|
93
|
+
patch_multi_target_params(p, param_names, patches)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
if node.rest.is_a?(Prism::SplatNode) &&
|
|
97
|
+
node.rest.expression.is_a?(Prism::RequiredParameterNode)
|
|
98
|
+
p = node.rest.expression
|
|
99
|
+
short = param_names[p.name]
|
|
100
|
+
if short && short != p.name.to_s
|
|
101
|
+
patches << { start: p.location.start_offset, end: p.location.end_offset, replacement: short }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def patch_optional_params(optionals, param_names, patches)
|
|
107
|
+
return unless optionals
|
|
108
|
+
optionals.each do |p|
|
|
109
|
+
short = param_names[p.name]
|
|
110
|
+
next unless short && short != p.name.to_s
|
|
111
|
+
patches << { start: p.name_loc.start_offset, end: p.name_loc.end_offset, replacement: short }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Shared tail of the rest/**rest/&block patchers: Prism types their
|
|
116
|
+
# name_loc as nullable, but a named parameter always carries one.
|
|
117
|
+
def patch_named_param(name, name_loc, param_names, patches)
|
|
118
|
+
short = param_names[name]
|
|
119
|
+
return unless name_loc && short && short != name.to_s
|
|
120
|
+
patches << { start: name_loc.start_offset, end: name_loc.end_offset, replacement: short }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def patch_rest_param(rest, param_names, patches)
|
|
124
|
+
return unless rest.is_a?(Prism::RestParameterNode) && rest.name
|
|
125
|
+
patch_named_param(rest.name, rest.name_loc, param_names, patches)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def patch_post_params(posts, param_names, patches)
|
|
129
|
+
return unless posts
|
|
130
|
+
posts.each do |p|
|
|
131
|
+
next unless p.is_a?(Prism::RequiredParameterNode)
|
|
132
|
+
short = param_names[p.name]
|
|
133
|
+
next unless short && short != p.name.to_s
|
|
134
|
+
patches << { start: p.location.start_offset, end: p.location.end_offset, replacement: short }
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def patch_keyword_params(keywords, param_names, patches)
|
|
139
|
+
return unless keywords
|
|
140
|
+
keywords.each do |p|
|
|
141
|
+
short = param_names[p.name]
|
|
142
|
+
next unless short && short != p.name.to_s
|
|
143
|
+
patches << { start: p.name_loc.start_offset, end: p.name_loc.end_offset, replacement: "#{short}:" }
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def patch_keyword_rest(keyword_rest, param_names, patches)
|
|
148
|
+
return unless keyword_rest.is_a?(Prism::KeywordRestParameterNode) && keyword_rest.name
|
|
149
|
+
patch_named_param(keyword_rest.name, keyword_rest.name_loc, param_names, patches)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def patch_block_param_node(block, param_names, patches)
|
|
153
|
+
return unless block.is_a?(Prism::BlockParameterNode) && block.name
|
|
154
|
+
patch_named_param(block.name, block.name_loc, param_names, patches)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def patch_block_params(node, patches, block_param_names_map)
|
|
158
|
+
return unless node.block.is_a?(Prism::BlockNode)
|
|
159
|
+
block = node.block
|
|
160
|
+
return unless block.parameters.is_a?(Prism::BlockParametersNode)
|
|
161
|
+
|
|
162
|
+
key = prism_location_key(node)
|
|
163
|
+
param_map = block_param_names_map[key]
|
|
164
|
+
return unless param_map
|
|
165
|
+
|
|
166
|
+
mangled_values = param_map.values
|
|
167
|
+
block_params = block.parameters.parameters
|
|
168
|
+
has_non_required = block_params && (
|
|
169
|
+
block_params.optionals&.any? ||
|
|
170
|
+
block_params.rest ||
|
|
171
|
+
block_params.posts&.any? ||
|
|
172
|
+
block_params.keywords&.any? ||
|
|
173
|
+
block_params.keyword_rest ||
|
|
174
|
+
block_params.block
|
|
175
|
+
)
|
|
176
|
+
if mangled_values.any? { |v| v.match?(/^_\d+$/) } && !has_non_required
|
|
177
|
+
bp_loc = block.parameters.location
|
|
178
|
+
patches << { start: bp_loc.start_offset, end: bp_loc.end_offset, replacement: '' }
|
|
179
|
+
return
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
block_params = block.parameters.parameters
|
|
183
|
+
return unless block_params
|
|
184
|
+
|
|
185
|
+
patch_required_params(block_params.requireds, param_map, patches)
|
|
186
|
+
patch_optional_params(block_params.optionals, param_map, patches)
|
|
187
|
+
patch_rest_param(block_params.rest, param_map, patches)
|
|
188
|
+
patch_post_params(block_params.posts, param_map, patches)
|
|
189
|
+
patch_keyword_params(block_params.keywords, param_map, patches)
|
|
190
|
+
patch_keyword_rest(block_params.keyword_rest, param_map, patches)
|
|
191
|
+
patch_block_param_node(block_params.block, param_map, patches)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def patch_for_node(node, patches, analysis, mangled_key)
|
|
195
|
+
syntax_key = AstUtils.line_col_key(node)
|
|
196
|
+
data = analysis.syntax_data[syntax_key]
|
|
197
|
+
return unless data
|
|
198
|
+
|
|
199
|
+
target_name = data[mangled_key] #: String?
|
|
200
|
+
return unless target_name
|
|
201
|
+
|
|
202
|
+
idx = node.index
|
|
203
|
+
return unless idx.is_a?(Prism::LocalVariableTargetNode)
|
|
204
|
+
patches << { start: idx.location.start_offset, end: idx.location.end_offset, replacement: target_name }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def patch_assoc(node, patches, rename_map, keyword_arg: false)
|
|
208
|
+
key_node = node.key
|
|
209
|
+
val_node = node.value
|
|
210
|
+
|
|
211
|
+
return unless key_node.is_a?(Prism::SymbolNode)
|
|
212
|
+
return unless key_node.value.to_s.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
|
|
213
|
+
|
|
214
|
+
original_key = key_node.value.to_s
|
|
215
|
+
renamed_key = rename_map[prism_location_key(key_node)]
|
|
216
|
+
|
|
217
|
+
if val_node.is_a?(Prism::ImplicitNode)
|
|
218
|
+
inner = val_node.value
|
|
219
|
+
if inner.is_a?(Prism::LocalVariableReadNode)
|
|
220
|
+
mangled_var = rename_map[prism_location_key(inner)] || inner.name.to_s
|
|
221
|
+
key_name = keyword_arg ? (renamed_key || original_key) : original_key
|
|
222
|
+
if key_name == mangled_var
|
|
223
|
+
if key_name != original_key
|
|
224
|
+
patches << { start: key_node.location.start_offset, end: val_node.location.end_offset,
|
|
225
|
+
replacement: "#{key_name}:" }
|
|
226
|
+
end
|
|
227
|
+
else
|
|
228
|
+
sep = mangled_var.start_with?(':') ? ': ' : ':'
|
|
229
|
+
patches << { start: key_node.location.start_offset, end: val_node.location.end_offset,
|
|
230
|
+
replacement: "#{key_name}#{sep}#{mangled_var}" }
|
|
231
|
+
end
|
|
232
|
+
else
|
|
233
|
+
if keyword_arg && renamed_key
|
|
234
|
+
patches << { start: key_node.location.start_offset, end: val_node.location.end_offset,
|
|
235
|
+
replacement: "#{renamed_key}:" }
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
return :skip_children
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
key_name = renamed_key || original_key
|
|
242
|
+
|
|
243
|
+
if val_node.is_a?(Prism::LocalVariableReadNode)
|
|
244
|
+
mangled_var = rename_map[prism_location_key(val_node)] || val_node.name.to_s
|
|
245
|
+
if key_name == mangled_var
|
|
246
|
+
patches << { start: key_node.location.start_offset, end: val_node.location.end_offset,
|
|
247
|
+
replacement: "#{key_name}:" }
|
|
248
|
+
return :skip_children
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
if renamed_key
|
|
253
|
+
loc = key_node.location
|
|
254
|
+
suffix = loc.slice.end_with?(':') ? ':' : ''
|
|
255
|
+
patches << { start: loc.start_offset, end: loc.end_offset, replacement: "#{renamed_key}#{suffix}" }
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def patch_keyword_hash(node, patches, rename_map, &block)
|
|
260
|
+
node.elements.each do |elem|
|
|
261
|
+
if elem.is_a?(Prism::AssocNode)
|
|
262
|
+
result = patch_assoc(elem, patches, rename_map, keyword_arg: true)
|
|
263
|
+
if result != :skip_children
|
|
264
|
+
elem.compact_child_nodes.each { |child| walk_prism(child, &block) }
|
|
265
|
+
end
|
|
266
|
+
else
|
|
267
|
+
walk_prism(elem, &block)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
module Pipeline
|
|
5
|
+
module SourcePatcher
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def mk(node, replacement)
|
|
9
|
+
{ start: node.location.start_offset, end: node.location.end_offset, replacement: replacement }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# The one patch applier: every stage splices byte patches through
|
|
13
|
+
# here. Patches must be disjoint — an overlap means two rewrites
|
|
14
|
+
# fought over the same bytes, and applying either order silently
|
|
15
|
+
# corrupts the output, so it raises with both patches named. The
|
|
16
|
+
# end-position tie-break keeps same-start insertions deterministic.
|
|
17
|
+
def apply_patches(source, patches)
|
|
18
|
+
return source if patches.none?
|
|
19
|
+
sorted = patches.sort_by { |p| [-p[:start], -p[:end]] }
|
|
20
|
+
sorted.each_cons(2) do |after, before|
|
|
21
|
+
# each_cons(2) yields only full pairs; the destructuring alone is
|
|
22
|
+
# what makes the elements look nilable.
|
|
23
|
+
# @type var after: { start: Integer, end: Integer, replacement: String }
|
|
24
|
+
# @type var before: { start: Integer, end: Integer, replacement: String }
|
|
25
|
+
if before[:end] > after[:start]
|
|
26
|
+
raise MinifyError, "overlapping patches: #{before.inspect} / #{after.inspect}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
result = source.b.dup
|
|
30
|
+
sorted.each do |patch|
|
|
31
|
+
result[patch[:start]...patch[:end]] = patch[:replacement].b
|
|
32
|
+
end
|
|
33
|
+
result.force_encoding(source.encoding)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# byteslice is typed String? but node offsets always lie inside the
|
|
37
|
+
# source the node was parsed from, so the result is never nil.
|
|
38
|
+
def src(source, node) # steep:ignore MethodBodyTypeMismatch
|
|
39
|
+
source.byteslice(node.location.start_offset, node.location.length)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The output of every pipeline permutation must at minimum be valid
|
|
43
|
+
# Ruby. Prism also rejects duplicated parameter names, so a rename that
|
|
44
|
+
# collides inside a signature is caught here, at the stage that
|
|
45
|
+
# produced it.
|
|
46
|
+
def verify_parses(text, label)
|
|
47
|
+
return if text.empty?
|
|
48
|
+
|
|
49
|
+
errors = Prism.parse(text).errors
|
|
50
|
+
raise InvalidOutputError.new(label, errors) unless errors.empty?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
module Pipeline
|
|
5
|
+
# Same value, shorter spelling: symbol and string array literals collapse
|
|
6
|
+
# to their percent forms when that is strictly smaller, and the English
|
|
7
|
+
# spellings of Ruby's special globals collapse to their punctuation
|
|
8
|
+
# forms — the same storage cell under either name. The pairing lives in
|
|
9
|
+
# GvarRenameMapping::PERL_SPELLINGS, which also shields both spellings
|
|
10
|
+
# from the gvar renamer.
|
|
11
|
+
class SpellingShorten < Stage
|
|
12
|
+
# Words a percent literal can carry verbatim: nothing that needs
|
|
13
|
+
# escaping and nothing that could close or balance the delimiter.
|
|
14
|
+
SAFE_WORD = %r{\A[A-Za-z0-9_/.:=!?-]+\z}
|
|
15
|
+
|
|
16
|
+
def collect(ctx, patches)
|
|
17
|
+
AstUtils.each_node(ctx.ast) do |node|
|
|
18
|
+
case node
|
|
19
|
+
when Prism::ArrayNode
|
|
20
|
+
try_percent_array(node, patches)
|
|
21
|
+
when Prism::GlobalVariableReadNode, Prism::GlobalVariableWriteNode
|
|
22
|
+
short = GvarRenameMapping::PERL_SPELLINGS[node.name]
|
|
23
|
+
if short
|
|
24
|
+
loc = node.is_a?(Prism::GlobalVariableWriteNode) ? node.name_loc : node.location
|
|
25
|
+
patches << { start: loc.start_offset, end: loc.end_offset, replacement: short }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def try_percent_array(node, patches)
|
|
34
|
+
return unless node.opening == '['
|
|
35
|
+
|
|
36
|
+
elements = node.elements
|
|
37
|
+
return if elements.empty?
|
|
38
|
+
|
|
39
|
+
sigil = if elements.all? { |e| plain?(e, Prism::SymbolNode) }
|
|
40
|
+
'%i'
|
|
41
|
+
elsif elements.all? { |e| plain?(e, Prism::StringNode) }
|
|
42
|
+
'%w'
|
|
43
|
+
end
|
|
44
|
+
return unless sigil
|
|
45
|
+
|
|
46
|
+
# Both node types carry their text as unescaped; SAFE_WORD already
|
|
47
|
+
# guaranteed the text survives a percent literal verbatim.
|
|
48
|
+
words = elements.map { |e| e.unescaped } # steep:ignore NoMethod
|
|
49
|
+
new_size = 4 + words.sum(&:bytesize) + words.size - 1
|
|
50
|
+
return unless new_size < node.location.length
|
|
51
|
+
|
|
52
|
+
patches << mk(node, "#{sigil}[#{words.join(' ')}]")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def plain?(node, klass)
|
|
56
|
+
node.is_a?(klass) && SAFE_WORD.match?(node.unescaped) # steep:ignore NoMethod
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Stage#call applies patches at load-definition time via SourcePatcher —
|
|
4
|
+
# the module must be concatenated in front of this file.
|
|
5
|
+
require_relative 'source_patcher'
|
|
6
|
+
|
|
7
|
+
module Ryac
|
|
8
|
+
module Pipeline
|
|
9
|
+
# Everything a stage sees while collecting: the current text, its parse,
|
|
10
|
+
# and — inside an analysis batch — the one AnalysisResult. aliases and
|
|
11
|
+
# preamble accumulate finish contributions in list order.
|
|
12
|
+
StageContext = Struct.new(:source, :ast, :analysis, :aliases, :preamble)
|
|
13
|
+
|
|
14
|
+
# The one stage contract.
|
|
15
|
+
#
|
|
16
|
+
# A stage declares what it needs (needs_analysis?), whether it must be
|
|
17
|
+
# re-run to a fixed point (fixpoint?), collects byte patches against the
|
|
18
|
+
# context, and contributes aliases/preamble in finish. Parsing, patch
|
|
19
|
+
# application and iteration belong to the runner, never to the stage.
|
|
20
|
+
class Stage
|
|
21
|
+
include SourcePatcher
|
|
22
|
+
|
|
23
|
+
# true → member of an analysis batch: the runner parses and runs
|
|
24
|
+
# TypeProf once per maximal run of consecutive analysis stages, and
|
|
25
|
+
# every member collects against that single AnalysisResult.
|
|
26
|
+
def needs_analysis? = false
|
|
27
|
+
|
|
28
|
+
# true → the runner re-parses and re-collects until no patches remain.
|
|
29
|
+
def fixpoint? = false
|
|
30
|
+
|
|
31
|
+
def collect(ctx, patches)
|
|
32
|
+
raise NotImplementedError, "#{self.class}#collect must be implemented"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Post-application contribution (aliases/preamble). Stages with
|
|
36
|
+
# nothing to add do not override this — no identity fakes. Only the
|
|
37
|
+
# runner collects the contribution; the standalone driver below
|
|
38
|
+
# returns text alone.
|
|
39
|
+
def finish(ctx) = nil
|
|
40
|
+
|
|
41
|
+
# Driver for a standalone syntactic stage: the per-stage public API.
|
|
42
|
+
def call(input)
|
|
43
|
+
if needs_analysis?
|
|
44
|
+
raise InternalError, "#{self.class} needs analysis; run it through StageRunner"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
source = input
|
|
48
|
+
loop do
|
|
49
|
+
ctx = StageContext.new(source, Prism.parse(source).value, nil, '', '')
|
|
50
|
+
patches = [] #: Array[patch_entry]
|
|
51
|
+
collect(ctx, patches)
|
|
52
|
+
break if patches.none?
|
|
53
|
+
|
|
54
|
+
new_source = apply_patches(source, patches)
|
|
55
|
+
break if fixpoint? && new_source == source
|
|
56
|
+
|
|
57
|
+
source = new_source
|
|
58
|
+
break unless fixpoint?
|
|
59
|
+
end
|
|
60
|
+
source
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Analysis stages run only inside the runner's batch, which always
|
|
66
|
+
# supplies the AnalysisResult.
|
|
67
|
+
def analysis(ctx)
|
|
68
|
+
ctx.analysis #: AnalysisResult
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
module Pipeline
|
|
5
|
+
# The one place that executes a stage list.
|
|
6
|
+
#
|
|
7
|
+
# The list is ordered — phase is position, nothing else. Compaction is
|
|
8
|
+
# the fixed first step. A maximal run of consecutive needs_analysis?
|
|
9
|
+
# stages forms the analysis batch: TypeProf runs once for it, every
|
|
10
|
+
# member collects into one patch pool, the pool is applied in a single
|
|
11
|
+
# splice, and each member's finish then contributes aliases/preamble in
|
|
12
|
+
# list order. A list that would need a second batch implies a second
|
|
13
|
+
# TypeProf run — that is a composition error, and it raises before
|
|
14
|
+
# running anything half-way.
|
|
15
|
+
class StageRunner
|
|
16
|
+
include SourcePatcher
|
|
17
|
+
|
|
18
|
+
def initialize(stdlib_requires: [], rbs_files: {})
|
|
19
|
+
@stdlib_requires = stdlib_requires
|
|
20
|
+
@rbs_files = rbs_files
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(code, stage_defs)
|
|
24
|
+
stages = stage_defs.map { |entry| instantiate(entry) }
|
|
25
|
+
|
|
26
|
+
batches = stages.chunk_while { |a, b| a.needs_analysis? == b.needs_analysis? }.to_a
|
|
27
|
+
analysis_batches = batches.count { |chunk| chunk.fetch(0).needs_analysis? }
|
|
28
|
+
if analysis_batches > 1
|
|
29
|
+
raise ArgumentError,
|
|
30
|
+
"stage list needs #{analysis_batches} analysis batches; " \
|
|
31
|
+
"TypeProf runs once, so all analysis stages must be consecutive"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
result = Compactor.new.call(code)
|
|
35
|
+
# The compacted text is the dialect every stage downstream assumes.
|
|
36
|
+
# If it does not parse, the Compactor is the culprit — name it here
|
|
37
|
+
# instead of letting whichever stage trips first inherit the blame.
|
|
38
|
+
verify_parses(result, 'compacted')
|
|
39
|
+
aliases = ''
|
|
40
|
+
preamble = ''
|
|
41
|
+
|
|
42
|
+
batches.each do |chunk|
|
|
43
|
+
if chunk.fetch(0).needs_analysis?
|
|
44
|
+
result, aliases, preamble = run_analysis_batch(result, chunk)
|
|
45
|
+
else
|
|
46
|
+
chunk.each { |stage| result = stage.call(result) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
verify_parses(result, 'code')
|
|
51
|
+
verify_parses(aliases, 'aliases')
|
|
52
|
+
RenameResult.new(code: result, aliases: aliases, preamble: preamble)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def instantiate(entry)
|
|
58
|
+
klass, kwargs = entry
|
|
59
|
+
# A stage entry is data ([class] or [class, kwargs]); configuration
|
|
60
|
+
# becomes constructor state here and nowhere else.
|
|
61
|
+
kwargs ? klass.new(**kwargs) : klass.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def run_analysis_batch(code, batch)
|
|
65
|
+
source = ConcatenatedSource.new(
|
|
66
|
+
content: code,
|
|
67
|
+
stdlib_requires: @stdlib_requires,
|
|
68
|
+
rbs_files: @rbs_files
|
|
69
|
+
)
|
|
70
|
+
analysis = Analyzer.new.call(source)
|
|
71
|
+
|
|
72
|
+
ctx = StageContext.new(code, analysis.prism_ast, analysis, '', '')
|
|
73
|
+
patches = [] #: Array[patch_entry]
|
|
74
|
+
batch.each { |stage| stage.collect(ctx, patches) }
|
|
75
|
+
|
|
76
|
+
result = apply_patches(code, patches)
|
|
77
|
+
ctx.source = result
|
|
78
|
+
batch.each { |stage| stage.finish(ctx) }
|
|
79
|
+
|
|
80
|
+
[result, ctx.aliases, ctx.preamble]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
module Pipeline
|
|
5
|
+
# Configurable variable renaming stage.
|
|
6
|
+
# Default (no features): renames locals only.
|
|
7
|
+
# features: { keywords: true } → locals + keywords (Level 3)
|
|
8
|
+
# features: { keywords: true, ivars: true, cvars: true, gvars: true } → all variables (Level 4)
|
|
9
|
+
class VariableRenamer < Stage
|
|
10
|
+
include RenamePatcher
|
|
11
|
+
|
|
12
|
+
FEATURES = { locals: true, keywords: false, ivars: false, cvars: false, gvars: false }.freeze
|
|
13
|
+
|
|
14
|
+
def needs_analysis? = true
|
|
15
|
+
|
|
16
|
+
def initialize(features: {})
|
|
17
|
+
@features = FEATURES.merge(features)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def collect(ctx, patches)
|
|
21
|
+
analysis = analysis(ctx)
|
|
22
|
+
rename_map = build_rename_map(analysis)
|
|
23
|
+
block_param_names_map = analysis.block_param_names_map
|
|
24
|
+
collect_patches(ctx.ast, patches, analysis, rename_map, block_param_names_map)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def build_rename_map(analysis)
|
|
30
|
+
map = {} #: Hash[location_key, String]
|
|
31
|
+
map.merge!(analysis.local_rename_entries) if @features[:locals]
|
|
32
|
+
map.merge!(analysis.keyword_rename_entries) if @features[:keywords]
|
|
33
|
+
if @features[:ivars]
|
|
34
|
+
map.merge!(analysis.ivar_rename_entries)
|
|
35
|
+
map.merge!(analysis.attr_ivar_entries) if @features[:attr_ivars]
|
|
36
|
+
end
|
|
37
|
+
map.merge!(analysis.cvar_rename_entries) if @features[:cvars]
|
|
38
|
+
map.merge!(analysis.gvar_rename_entries) if @features[:gvars]
|
|
39
|
+
map
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def collect_patches(node, patches, analysis, rename_map, block_param_names_map)
|
|
43
|
+
callback = proc { |subnode| handle_node(subnode, patches, analysis, rename_map, block_param_names_map) } #: ^(Prism::Node) -> (Symbol | nil)
|
|
44
|
+
walk_prism(node, &callback)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def handle_node(subnode, patches, analysis, rename_map, block_param_names_map)
|
|
48
|
+
case subnode
|
|
49
|
+
# Local variables
|
|
50
|
+
when Prism::LocalVariableReadNode
|
|
51
|
+
patch_variable(subnode, patches, rename_map)
|
|
52
|
+
when Prism::LocalVariableWriteNode
|
|
53
|
+
patch_variable_name_only(subnode, patches, rename_map)
|
|
54
|
+
when Prism::LocalVariableTargetNode
|
|
55
|
+
patch_variable(subnode, patches, rename_map)
|
|
56
|
+
when Prism::LocalVariableOperatorWriteNode,
|
|
57
|
+
Prism::LocalVariableOrWriteNode,
|
|
58
|
+
Prism::LocalVariableAndWriteNode
|
|
59
|
+
patch_variable_name_only(subnode, patches, rename_map)
|
|
60
|
+
|
|
61
|
+
# Instance variables
|
|
62
|
+
when Prism::InstanceVariableReadNode
|
|
63
|
+
patch_variable(subnode, patches, rename_map) if @features[:ivars]
|
|
64
|
+
when Prism::InstanceVariableWriteNode
|
|
65
|
+
patch_variable_name_only(subnode, patches, rename_map) if @features[:ivars]
|
|
66
|
+
when Prism::InstanceVariableOperatorWriteNode,
|
|
67
|
+
Prism::InstanceVariableOrWriteNode,
|
|
68
|
+
Prism::InstanceVariableAndWriteNode
|
|
69
|
+
patch_variable_name_only(subnode, patches, rename_map) if @features[:ivars]
|
|
70
|
+
when Prism::InstanceVariableTargetNode
|
|
71
|
+
patch_variable(subnode, patches, rename_map) if @features[:ivars]
|
|
72
|
+
|
|
73
|
+
# Class variables
|
|
74
|
+
when Prism::ClassVariableReadNode
|
|
75
|
+
patch_variable(subnode, patches, rename_map) if @features[:cvars]
|
|
76
|
+
when Prism::ClassVariableWriteNode
|
|
77
|
+
patch_variable_name_only(subnode, patches, rename_map) if @features[:cvars]
|
|
78
|
+
when Prism::ClassVariableOperatorWriteNode,
|
|
79
|
+
Prism::ClassVariableOrWriteNode,
|
|
80
|
+
Prism::ClassVariableAndWriteNode
|
|
81
|
+
patch_variable_name_only(subnode, patches, rename_map) if @features[:cvars]
|
|
82
|
+
when Prism::ClassVariableTargetNode
|
|
83
|
+
patch_variable(subnode, patches, rename_map) if @features[:cvars]
|
|
84
|
+
|
|
85
|
+
# Global variables
|
|
86
|
+
when Prism::GlobalVariableReadNode
|
|
87
|
+
patch_variable(subnode, patches, rename_map) if @features[:gvars]
|
|
88
|
+
when Prism::GlobalVariableWriteNode
|
|
89
|
+
patch_variable_name_only(subnode, patches, rename_map) if @features[:gvars]
|
|
90
|
+
when Prism::GlobalVariableOperatorWriteNode,
|
|
91
|
+
Prism::GlobalVariableOrWriteNode,
|
|
92
|
+
Prism::GlobalVariableAndWriteNode
|
|
93
|
+
patch_variable_name_only(subnode, patches, rename_map) if @features[:gvars]
|
|
94
|
+
when Prism::GlobalVariableTargetNode
|
|
95
|
+
patch_variable(subnode, patches, rename_map) if @features[:gvars]
|
|
96
|
+
|
|
97
|
+
# Def params
|
|
98
|
+
when Prism::DefNode
|
|
99
|
+
patch_def_params(subnode, patches, analysis, :param_names)
|
|
100
|
+
|
|
101
|
+
# Block params
|
|
102
|
+
when Prism::CallNode
|
|
103
|
+
patch_block_params(subnode, patches, block_param_names_map)
|
|
104
|
+
|
|
105
|
+
# For node — patch index and walk body/collection manually to avoid
|
|
106
|
+
# double-patching the index via LocalVariableTargetNode handler
|
|
107
|
+
when Prism::ForNode
|
|
108
|
+
patch_for_node(subnode, patches, analysis, :for_index_mangled)
|
|
109
|
+
callback = proc { |n| handle_node(n, patches, analysis, rename_map, block_param_names_map) } #: ^(Prism::Node) -> (Symbol | nil)
|
|
110
|
+
walk_prism(subnode.collection, &callback)
|
|
111
|
+
walk_prism(subnode.statements, &callback)
|
|
112
|
+
:skip_children
|
|
113
|
+
|
|
114
|
+
# Hash assoc shorthand
|
|
115
|
+
when Prism::KeywordHashNode
|
|
116
|
+
callback = proc { |n| handle_node(n, patches, analysis, rename_map, block_param_names_map) } #: ^(Prism::Node) -> (Symbol | nil)
|
|
117
|
+
patch_keyword_hash(subnode, patches, rename_map, &callback)
|
|
118
|
+
:skip_children
|
|
119
|
+
when Prism::AssocNode
|
|
120
|
+
patch_assoc(subnode, patches, rename_map)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|