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,360 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prism'
|
|
4
|
+
|
|
5
|
+
module Ryac
|
|
6
|
+
module Pipeline
|
|
7
|
+
# Stage 1: File Collection
|
|
8
|
+
# Discovers all dependencies via static analysis of require/require_relative/autoload
|
|
9
|
+
class FileCollector
|
|
10
|
+
# @param entry_path [String, Array<String>] Path(s) to entry point file(s)
|
|
11
|
+
# @return [DependencyGraph] Graph of all discovered files
|
|
12
|
+
# @raise [FileNotFoundError] If a required file doesn't exist
|
|
13
|
+
# @raise [NoFilesError] If entry_path is nil or empty
|
|
14
|
+
# @raise [DynamicRequireError] If a dynamic require is detected
|
|
15
|
+
def call(entry_path, project_root: nil, gem_names: [], gem_require_paths: [])
|
|
16
|
+
raise NoFilesError.new if entry_path.nil?
|
|
17
|
+
|
|
18
|
+
entry_paths = Array(entry_path) #: Array[String]
|
|
19
|
+
raise NoFilesError.new if entry_paths.empty?
|
|
20
|
+
|
|
21
|
+
@graph = DependencyGraph.new
|
|
22
|
+
@visited = Set.new
|
|
23
|
+
@gem_names = gem_names
|
|
24
|
+
@project_roots = if project_root
|
|
25
|
+
# Array() leaves only Strings whichever of the two shapes came in
|
|
26
|
+
Array(project_root).map { |p| File.expand_path(p) } # steep:ignore ArgumentTypeMismatch
|
|
27
|
+
else
|
|
28
|
+
root = find_project_root(entry_paths)
|
|
29
|
+
root ? [root] : []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
ensure_load_paths(gem_require_paths)
|
|
33
|
+
|
|
34
|
+
entry_paths.each do |path|
|
|
35
|
+
expanded = File.expand_path(path)
|
|
36
|
+
raise FileNotFoundError.new(expanded) unless File.exist?(expanded)
|
|
37
|
+
collect_file(expanded)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
collect_rbs_files(entry_paths)
|
|
41
|
+
@graph
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Recursively collect a file and its dependencies
|
|
47
|
+
def collect_file(file_path, required_from: nil, line: nil)
|
|
48
|
+
return if @visited.include?(file_path)
|
|
49
|
+
|
|
50
|
+
unless File.exist?(file_path)
|
|
51
|
+
raise FileNotFoundError.new(file_path, required_from: required_from, line: line)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
@visited.add(file_path)
|
|
55
|
+
# Ruby source defaults to UTF-8; reading with the ambient locale instead
|
|
56
|
+
# tags non-ASCII sources US-ASCII under a POSIX locale and every later
|
|
57
|
+
# String operation raises on the invalid bytes.
|
|
58
|
+
content = File.read(file_path, encoding: Encoding::UTF_8)
|
|
59
|
+
|
|
60
|
+
# Parse and extract require statements
|
|
61
|
+
require_nodes = extract_require_nodes(file_path, content)
|
|
62
|
+
dependencies = [] #: Array[String]
|
|
63
|
+
in_class_dependencies = [] #: Array[String]
|
|
64
|
+
|
|
65
|
+
require_nodes.each do |node_info|
|
|
66
|
+
dep_path = node_info[:resolved_path]
|
|
67
|
+
next unless dep_path
|
|
68
|
+
|
|
69
|
+
if node_info[:in_class]
|
|
70
|
+
in_class_dependencies << dep_path
|
|
71
|
+
else
|
|
72
|
+
dependencies << dep_path
|
|
73
|
+
end
|
|
74
|
+
collect_file(dep_path, required_from: file_path, line: node_info[:line])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
entry = FileEntry.new(
|
|
78
|
+
path: file_path,
|
|
79
|
+
content: content,
|
|
80
|
+
dependencies: dependencies,
|
|
81
|
+
in_class_dependencies: in_class_dependencies,
|
|
82
|
+
require_nodes: require_nodes
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@graph.add_file(entry)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Extract require/require_relative/autoload nodes from source
|
|
89
|
+
def extract_require_nodes(file_path, content)
|
|
90
|
+
result = Prism.parse(content)
|
|
91
|
+
# Collection is the only point that still knows which file a byte
|
|
92
|
+
# came from — a syntax error surfaces here with real coordinates,
|
|
93
|
+
# or downstream as a nameless internal failure.
|
|
94
|
+
error = result.errors[0]
|
|
95
|
+
if error
|
|
96
|
+
raise Ryac::SyntaxError.new(error.message, path: file_path,
|
|
97
|
+
line: error.location.start_line,
|
|
98
|
+
column: error.location.start_column)
|
|
99
|
+
end
|
|
100
|
+
nodes = [] #: Array[require_node_info]
|
|
101
|
+
|
|
102
|
+
traverse_for_requires(result.value, nodes, file_path)
|
|
103
|
+
|
|
104
|
+
nodes
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Traverse AST to find require statements
|
|
108
|
+
# @param in_method [Boolean] true when inside a DefNode body; dynamic requires are skipped
|
|
109
|
+
def traverse_for_requires(node, nodes, file_path, in_method: false, in_class: false)
|
|
110
|
+
return unless node
|
|
111
|
+
|
|
112
|
+
case node
|
|
113
|
+
when Prism::CallNode
|
|
114
|
+
handle_call_node(node, nodes, file_path, in_method: in_method, in_class: in_class)
|
|
115
|
+
when Prism::ProgramNode
|
|
116
|
+
traverse_for_requires(node.statements, nodes, file_path, in_method: in_method, in_class: in_class)
|
|
117
|
+
when Prism::StatementsNode
|
|
118
|
+
node.body.each { |child| traverse_for_requires(child, nodes, file_path, in_method: in_method, in_class: in_class) }
|
|
119
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
120
|
+
traverse_for_requires(node.body, nodes, file_path, in_method: in_method, in_class: true)
|
|
121
|
+
when Prism::DefNode
|
|
122
|
+
traverse_for_requires(node.body, nodes, file_path, in_method: true, in_class: in_class)
|
|
123
|
+
when Prism::IfNode
|
|
124
|
+
traverse_for_requires(node.statements, nodes, file_path, in_method: in_method, in_class: in_class)
|
|
125
|
+
traverse_for_requires(node.subsequent, nodes, file_path, in_method: in_method, in_class: in_class)
|
|
126
|
+
when Prism::BeginNode
|
|
127
|
+
traverse_for_requires(node.statements, nodes, file_path, in_method: in_method, in_class: in_class)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def handle_call_node(node, nodes, file_path, in_method: false, in_class: false)
|
|
132
|
+
method_name = node.name
|
|
133
|
+
|
|
134
|
+
case method_name
|
|
135
|
+
when :require_relative
|
|
136
|
+
handle_require_relative(node, nodes, file_path, in_method: in_method, in_class: in_class)
|
|
137
|
+
when :require
|
|
138
|
+
handle_require(node, nodes, file_path, in_method: in_method, in_class: in_class)
|
|
139
|
+
when :autoload
|
|
140
|
+
handle_autoload(node, nodes, file_path, in_method: in_method, in_class: in_class)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Continue traversing for nested calls
|
|
144
|
+
node.arguments&.arguments&.each do |arg|
|
|
145
|
+
traverse_for_requires(arg, nodes, file_path, in_method: in_method, in_class: in_class)
|
|
146
|
+
end
|
|
147
|
+
traverse_for_requires(node.block, nodes, file_path, in_method: in_method, in_class: in_class) if node.block
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def handle_require_relative(node, nodes, file_path, in_method: false, in_class: false)
|
|
151
|
+
arg = node.arguments&.arguments&.first
|
|
152
|
+
return unless arg
|
|
153
|
+
|
|
154
|
+
if arg.is_a?(Prism::StringNode)
|
|
155
|
+
nodes << {
|
|
156
|
+
type: :require_relative,
|
|
157
|
+
path: arg.unescaped,
|
|
158
|
+
line: node.location.start_line,
|
|
159
|
+
start_offset: node.location.start_offset,
|
|
160
|
+
length: node.location.length,
|
|
161
|
+
in_class: in_class,
|
|
162
|
+
in_method: in_method,
|
|
163
|
+
resolved_path: resolve_relative_path(arg.unescaped, file_path)
|
|
164
|
+
}
|
|
165
|
+
else
|
|
166
|
+
return if in_method
|
|
167
|
+
|
|
168
|
+
raise DynamicRequireError.new(
|
|
169
|
+
file_path,
|
|
170
|
+
line: node.location.start_line,
|
|
171
|
+
expression: node.slice
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def handle_require(node, nodes, file_path, in_method: false, in_class: false)
|
|
177
|
+
arg = node.arguments&.arguments&.first
|
|
178
|
+
return unless arg
|
|
179
|
+
|
|
180
|
+
if arg.is_a?(Prism::StringNode)
|
|
181
|
+
path = arg.unescaped
|
|
182
|
+
if path.start_with?('./', '../')
|
|
183
|
+
nodes << {
|
|
184
|
+
type: :require,
|
|
185
|
+
path: path,
|
|
186
|
+
line: node.location.start_line,
|
|
187
|
+
start_offset: node.location.start_offset,
|
|
188
|
+
length: node.location.length,
|
|
189
|
+
in_class: in_class,
|
|
190
|
+
in_method: in_method,
|
|
191
|
+
resolved_path: resolve_relative_path(path, file_path)
|
|
192
|
+
}
|
|
193
|
+
elsif (resolved = resolve_bare_require(path))
|
|
194
|
+
nodes << {
|
|
195
|
+
type: :require,
|
|
196
|
+
path: path,
|
|
197
|
+
line: node.location.start_line,
|
|
198
|
+
start_offset: node.location.start_offset,
|
|
199
|
+
length: node.location.length,
|
|
200
|
+
in_class: in_class,
|
|
201
|
+
in_method: in_method,
|
|
202
|
+
resolved_path: resolved
|
|
203
|
+
}
|
|
204
|
+
else
|
|
205
|
+
nodes << {
|
|
206
|
+
type: :require_stdlib,
|
|
207
|
+
path: path,
|
|
208
|
+
line: node.location.start_line,
|
|
209
|
+
start_offset: node.location.start_offset,
|
|
210
|
+
length: node.location.length,
|
|
211
|
+
in_class: in_class,
|
|
212
|
+
in_method: in_method
|
|
213
|
+
}
|
|
214
|
+
end
|
|
215
|
+
else
|
|
216
|
+
return if in_method
|
|
217
|
+
|
|
218
|
+
raise DynamicRequireError.new(
|
|
219
|
+
file_path,
|
|
220
|
+
line: node.location.start_line,
|
|
221
|
+
expression: node.slice
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def handle_autoload(node, nodes, file_path, in_method: false, in_class: false)
|
|
227
|
+
args = node.arguments&.arguments
|
|
228
|
+
return unless args && args.size >= 2
|
|
229
|
+
|
|
230
|
+
path_arg = args[1]
|
|
231
|
+
path = if path_arg.is_a?(Prism::StringNode)
|
|
232
|
+
path_arg.unescaped
|
|
233
|
+
else
|
|
234
|
+
dir_interpolated_path(path_arg)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
if path
|
|
238
|
+
# Treat autoload paths like require_relative for local files
|
|
239
|
+
if path.start_with?('./', '../') || !path.include?('/')
|
|
240
|
+
nodes << {
|
|
241
|
+
type: :autoload,
|
|
242
|
+
path: path,
|
|
243
|
+
line: node.location.start_line,
|
|
244
|
+
start_offset: node.location.start_offset,
|
|
245
|
+
length: node.location.length,
|
|
246
|
+
in_class: in_class,
|
|
247
|
+
in_method: in_method,
|
|
248
|
+
resolved_path: resolve_relative_path(path, file_path)
|
|
249
|
+
}
|
|
250
|
+
elsif (resolved = resolve_bare_require(path))
|
|
251
|
+
nodes << {
|
|
252
|
+
type: :autoload,
|
|
253
|
+
path: path,
|
|
254
|
+
line: node.location.start_line,
|
|
255
|
+
start_offset: node.location.start_offset,
|
|
256
|
+
length: node.location.length,
|
|
257
|
+
in_class: in_class,
|
|
258
|
+
in_method: in_method,
|
|
259
|
+
resolved_path: resolved
|
|
260
|
+
}
|
|
261
|
+
end
|
|
262
|
+
else
|
|
263
|
+
return if in_method
|
|
264
|
+
|
|
265
|
+
raise DynamicRequireError.new(
|
|
266
|
+
file_path,
|
|
267
|
+
line: node.location.start_line,
|
|
268
|
+
expression: node.slice
|
|
269
|
+
)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# "#{__dir__}/mixin/foo" reads as dynamic but is a constant at
|
|
274
|
+
# collection time: __dir__ is the directory of the file being
|
|
275
|
+
# collected. Recognizes exactly that shape — a sole receiverless,
|
|
276
|
+
# argument-less __dir__ interpolation, then a /-prefixed literal
|
|
277
|
+
# tail — and returns it as the file-relative "./mixin/foo" so the
|
|
278
|
+
# ordinary relative resolution runs; nil for every other
|
|
279
|
+
# interpolation.
|
|
280
|
+
def dir_interpolated_path(path_arg)
|
|
281
|
+
return nil unless path_arg.is_a?(Prism::InterpolatedStringNode)
|
|
282
|
+
|
|
283
|
+
parts = path_arg.parts
|
|
284
|
+
return nil unless parts.size == 2
|
|
285
|
+
|
|
286
|
+
interp, rest = parts
|
|
287
|
+
return nil unless interp.is_a?(Prism::EmbeddedStatementsNode) && rest.is_a?(Prism::StringNode)
|
|
288
|
+
|
|
289
|
+
call = AstUtils.unwrap_statements(interp.statements)
|
|
290
|
+
return nil unless call.is_a?(Prism::CallNode) &&
|
|
291
|
+
call.name == :__dir__ && call.receiver.nil? && call.arguments.nil?
|
|
292
|
+
|
|
293
|
+
tail = rest.unescaped
|
|
294
|
+
tail.start_with?('/') ? ".#{tail}" : nil
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def ensure_load_paths(require_paths)
|
|
298
|
+
require_paths.each do |path|
|
|
299
|
+
$LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def collect_rbs_files(_entry_paths)
|
|
304
|
+
@project_roots.each do |root|
|
|
305
|
+
load_rbs_from(File.join(root, "sig"))
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
collect_rbs_stdlib_files
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def collect_rbs_stdlib_files
|
|
312
|
+
return if @gem_names.empty?
|
|
313
|
+
|
|
314
|
+
stdlib_root = RBS::Repository::DEFAULT_STDLIB_ROOT
|
|
315
|
+
|
|
316
|
+
@gem_names.each do |gem_name|
|
|
317
|
+
gem_rbs_dir = File.join(stdlib_root, gem_name)
|
|
318
|
+
versions = Dir.children(gem_rbs_dir) rescue next
|
|
319
|
+
next if versions.empty?
|
|
320
|
+
|
|
321
|
+
latest = versions.max_by { |v| Gem::Version.new(v) } #: String
|
|
322
|
+
load_rbs_from(File.join(gem_rbs_dir, latest))
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def load_rbs_from(dir)
|
|
327
|
+
Dir.glob(File.join(dir, "**", "*.rbs")).each do |path|
|
|
328
|
+
@graph.rbs_files[path] = File.read(path, encoding: Encoding::UTF_8)
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def find_project_root(entry_paths)
|
|
333
|
+
dir = File.dirname(File.expand_path(entry_paths.first))
|
|
334
|
+
until dir == "/"
|
|
335
|
+
return dir if File.exist?(File.join(dir, "Gemfile")) || File.directory?(File.join(dir, ".git"))
|
|
336
|
+
dir = File.dirname(dir)
|
|
337
|
+
end
|
|
338
|
+
nil
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def resolve_relative_path(path, from_file)
|
|
342
|
+
path += '.rb' unless path.end_with?('.rb')
|
|
343
|
+
File.expand_path(path, File.dirname(from_file))
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Resolve a bare require path (e.g., "foo") via $LOAD_PATH.
|
|
347
|
+
# Returns absolute path if the file is under any project root, nil otherwise.
|
|
348
|
+
def resolve_bare_require(path)
|
|
349
|
+
result = $LOAD_PATH.resolve_feature_path(path)
|
|
350
|
+
return nil unless result
|
|
351
|
+
|
|
352
|
+
type, abs_path = result
|
|
353
|
+
return nil unless type == :rb
|
|
354
|
+
return nil unless @project_roots.any? { |root| abs_path.start_with?("#{root}/") }
|
|
355
|
+
|
|
356
|
+
abs_path
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
module Pipeline
|
|
5
|
+
# Method renaming: renames method definitions and call sites.
|
|
6
|
+
class MethodRenamer < Stage
|
|
7
|
+
include RenamePatcher
|
|
8
|
+
|
|
9
|
+
def needs_analysis? = true
|
|
10
|
+
|
|
11
|
+
def collect(ctx, patches)
|
|
12
|
+
analysis = analysis(ctx)
|
|
13
|
+
rename_map = analysis.rename_map
|
|
14
|
+
method_alias_map = analysis.method_alias_map
|
|
15
|
+
@negated_transforms = {}
|
|
16
|
+
@source_bytes = analysis.source.content
|
|
17
|
+
collect_patches(ctx.ast, patches, analysis, rename_map, method_alias_map)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def collect_patches(node, patches, analysis, rename_map, method_alias_map)
|
|
23
|
+
# @type var callback: ^(Prism::Node) -> void
|
|
24
|
+
callback = proc { |subnode|
|
|
25
|
+
handle_node(subnode, patches, analysis, rename_map, method_alias_map)
|
|
26
|
+
}
|
|
27
|
+
walk_prism(node, &callback)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def handle_node(subnode, patches, analysis, rename_map, method_alias_map)
|
|
31
|
+
case subnode
|
|
32
|
+
when Prism::DefNode
|
|
33
|
+
patch_def_name(subnode, patches, rename_map)
|
|
34
|
+
|
|
35
|
+
when Prism::CallNode
|
|
36
|
+
patch_call_node(subnode, patches, rename_map, method_alias_map, analysis)
|
|
37
|
+
|
|
38
|
+
when Prism::CallOperatorWriteNode,
|
|
39
|
+
Prism::CallOrWriteNode,
|
|
40
|
+
Prism::CallAndWriteNode
|
|
41
|
+
patch_call_operator_write(subnode, patches, rename_map, method_alias_map)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def patch_def_name(node, patches, rename_map)
|
|
46
|
+
key = prism_location_key(node)
|
|
47
|
+
short = rename_map[key]
|
|
48
|
+
return unless short
|
|
49
|
+
|
|
50
|
+
name_loc = node.name_loc
|
|
51
|
+
patches << { start: name_loc.start_offset, end: name_loc.end_offset, replacement: short }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def patch_call_node(node, patches, rename_map, method_alias_map, analysis)
|
|
55
|
+
# Detect negated transforms: !receiver.empty? → receiver!=[]
|
|
56
|
+
# The ! CallNode is visited before its children, so we mark the inner
|
|
57
|
+
# node here and negate the transform when it's applied later.
|
|
58
|
+
if node.name == :"!" && node.receiver.is_a?(Prism::CallNode)
|
|
59
|
+
inner = node.receiver
|
|
60
|
+
inner_key = prism_location_key(inner)
|
|
61
|
+
transform = analysis.method_transform_map[inner_key]
|
|
62
|
+
if transform&.start_with?('==') && inner.call_operator_loc && !inner.safe_navigation?
|
|
63
|
+
@negated_transforms[inner_key] = true
|
|
64
|
+
patches << { start: node.location.start_offset, end: inner.location.start_offset, replacement: '' }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
key = prism_location_key(node)
|
|
69
|
+
|
|
70
|
+
# Meta calls are structural, not plain calls: attr declarations are
|
|
71
|
+
# rewritten wholesale by AttrDeclShorten, and include must keep its
|
|
72
|
+
# name. Renaming or transforming them here would corrupt them.
|
|
73
|
+
return if analysis.meta_node_map[key]
|
|
74
|
+
|
|
75
|
+
# Structural transforms (e.g. .first → [0], .empty? → ==[])
|
|
76
|
+
transform = analysis.method_transform_map[key]
|
|
77
|
+
if transform && node.call_operator_loc && !node.safe_navigation?
|
|
78
|
+
replacement = @negated_transforms[key] ? "!#{transform[1..]}" : transform
|
|
79
|
+
end_offset = node.location.end_offset
|
|
80
|
+
# Consume trailing spaces around ternary ? when no longer needed
|
|
81
|
+
# e.g. .empty? ? "x" → ==[]?"x" since ] is not a name char
|
|
82
|
+
if @source_bytes.getbyte(end_offset) == 0x20 && @source_bytes.getbyte(end_offset + 1) == 0x3F # ' ' and '?'
|
|
83
|
+
end_offset += 2 # consume space + ternary ?
|
|
84
|
+
end_offset += 1 if @source_bytes.getbyte(end_offset) == 0x20 # consume space after ?
|
|
85
|
+
replacement = "#{replacement}?"
|
|
86
|
+
end
|
|
87
|
+
# call_operator_loc was checked at the top of this branch
|
|
88
|
+
patches << { start: node.call_operator_loc.start_offset, end: end_offset, replacement: replacement } # steep:ignore NoMethod
|
|
89
|
+
return
|
|
90
|
+
elsif transform && !node.call_operator_loc && node.receiver.is_a?(Prism::CallNode)
|
|
91
|
+
# A comparison-shape transform (.size==0 → ==[]), registered on
|
|
92
|
+
# the operator call whose receiver is the size query: the patch
|
|
93
|
+
# swallows the query and the comparison together.
|
|
94
|
+
receiver_end = node.receiver.receiver.location.end_offset # steep:ignore NoMethod
|
|
95
|
+
patches << { start: receiver_end, end: node.location.end_offset, replacement: transform }
|
|
96
|
+
return
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
return unless node.message_loc
|
|
100
|
+
|
|
101
|
+
short = rename_map[key]
|
|
102
|
+
|
|
103
|
+
# send/public_send/__send__: patch the symbol argument, not the method name
|
|
104
|
+
if short && SEND_METHODS.include?(node.name)
|
|
105
|
+
patch_send_symbol(node, patches, short)
|
|
106
|
+
return
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
alias_name = method_alias_map[key]&.to_s
|
|
110
|
+
replacement = short || alias_name
|
|
111
|
+
return unless replacement
|
|
112
|
+
|
|
113
|
+
if replacement.end_with?('=') && !replacement.end_with?('==') &&
|
|
114
|
+
node.name.to_s.end_with?('=') && !node.name.to_s.end_with?('==')
|
|
115
|
+
msg_slice = node.message_loc.slice
|
|
116
|
+
replacement = replacement.chomp('=') unless msg_slice.end_with?('=')
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
end_offset = node.message_loc.end_offset
|
|
120
|
+
# The compactor separates a ?/!-ending name from an =-starting
|
|
121
|
+
# operator with one protective space. A rename that drops the ?/!
|
|
122
|
+
# must take the space with it — left behind, re-minification removes
|
|
123
|
+
# it and the self-host fixed point drifts by a byte per site.
|
|
124
|
+
if node.name.to_s.end_with?('?', '!') && !replacement.end_with?('?', '!') &&
|
|
125
|
+
@source_bytes.getbyte(end_offset) == 0x20 && @source_bytes.getbyte(end_offset + 1) == 0x3D # ' ' then '='
|
|
126
|
+
end_offset += 1
|
|
127
|
+
end
|
|
128
|
+
patches << { start: node.message_loc.start_offset, end: end_offset, replacement: replacement }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
SEND_METHODS = %i[send __send__ public_send].freeze
|
|
132
|
+
|
|
133
|
+
def patch_send_symbol(node, patches, short_name)
|
|
134
|
+
first_arg = node.arguments&.arguments&.first
|
|
135
|
+
return unless first_arg.is_a?(Prism::SymbolNode)
|
|
136
|
+
|
|
137
|
+
loc = first_arg.location
|
|
138
|
+
patches << { start: loc.start_offset, end: loc.end_offset, replacement: ":#{short_name}" }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def patch_call_operator_write(node, patches, rename_map, method_alias_map)
|
|
142
|
+
return unless node.message_loc
|
|
143
|
+
key = prism_location_key(node)
|
|
144
|
+
short = rename_map[key] || method_alias_map[key]&.to_s
|
|
145
|
+
return unless short
|
|
146
|
+
short = short.chomp('=') if short.end_with?('=') && !short.end_with?('==')
|
|
147
|
+
patches << { start: node.message_loc.start_offset, end: node.message_loc.end_offset, replacement: short }
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
module Pipeline
|
|
5
|
+
# Removes unnecessary parentheses from method calls at statement level.
|
|
6
|
+
# Operates as a source patcher: Prism.parse → walk AST → collect patches → apply.
|
|
7
|
+
# Only removes parens; never adds them.
|
|
8
|
+
class ParenOptimizer < Stage
|
|
9
|
+
def collect(ctx, patches)
|
|
10
|
+
walk(ctx.ast, ctx.source, patches, statement_level: true)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def walk(node, source, patches, statement_level:)
|
|
16
|
+
case node
|
|
17
|
+
when Prism::ProgramNode
|
|
18
|
+
walk(node.statements, source, patches, statement_level: true)
|
|
19
|
+
|
|
20
|
+
when Prism::StatementsNode
|
|
21
|
+
node.body.each { |child| walk(child, source, patches, statement_level: statement_level) }
|
|
22
|
+
|
|
23
|
+
when Prism::CallNode
|
|
24
|
+
try_remove_call_parens(node, source, patches) if statement_level
|
|
25
|
+
walk_call_children(node, source, patches)
|
|
26
|
+
|
|
27
|
+
when Prism::YieldNode
|
|
28
|
+
try_remove_yield_parens(node, patches) if statement_level
|
|
29
|
+
node.arguments&.arguments&.each { |arg| walk(arg, source, patches, statement_level: false) }
|
|
30
|
+
|
|
31
|
+
when Prism::IfNode
|
|
32
|
+
walk_if(node, source, patches)
|
|
33
|
+
|
|
34
|
+
when Prism::UnlessNode
|
|
35
|
+
walk(node.predicate, source, patches, statement_level: false)
|
|
36
|
+
walk(node.statements, source, patches, statement_level: true) if node.statements
|
|
37
|
+
walk(node.else_clause, source, patches, statement_level: true) if node.else_clause
|
|
38
|
+
|
|
39
|
+
when Prism::ElseNode
|
|
40
|
+
walk(node.statements, source, patches, statement_level: statement_level) if node.statements
|
|
41
|
+
|
|
42
|
+
when Prism::WhileNode, Prism::UntilNode
|
|
43
|
+
walk(node.predicate, source, patches, statement_level: false)
|
|
44
|
+
walk(node.statements, source, patches, statement_level: true) if node.statements
|
|
45
|
+
|
|
46
|
+
when Prism::ForNode
|
|
47
|
+
walk(node.collection, source, patches, statement_level: false)
|
|
48
|
+
walk(node.statements, source, patches, statement_level: true) if node.statements
|
|
49
|
+
|
|
50
|
+
when Prism::DefNode
|
|
51
|
+
walk(node.body, source, patches, statement_level: true) if node.body
|
|
52
|
+
|
|
53
|
+
when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
54
|
+
walk(node.body, source, patches, statement_level: true) if node.body
|
|
55
|
+
|
|
56
|
+
when Prism::BeginNode
|
|
57
|
+
walk(node.statements, source, patches, statement_level: true) if node.statements
|
|
58
|
+
walk(node.rescue_clause, source, patches, statement_level: true) if node.rescue_clause
|
|
59
|
+
walk(node.else_clause, source, patches, statement_level: true) if node.else_clause
|
|
60
|
+
walk(node.ensure_clause, source, patches, statement_level: true) if node.ensure_clause
|
|
61
|
+
|
|
62
|
+
when Prism::RescueNode
|
|
63
|
+
walk(node.statements, source, patches, statement_level: true) if node.statements
|
|
64
|
+
walk(node.subsequent, source, patches, statement_level: true) if node.subsequent
|
|
65
|
+
|
|
66
|
+
when Prism::EnsureNode
|
|
67
|
+
walk(node.statements, source, patches, statement_level: true) if node.statements
|
|
68
|
+
|
|
69
|
+
when Prism::CaseNode
|
|
70
|
+
walk(node.predicate, source, patches, statement_level: false) if node.predicate
|
|
71
|
+
node.conditions.each { |cond| walk(cond, source, patches, statement_level: true) }
|
|
72
|
+
walk(node.else_clause, source, patches, statement_level: true) if node.else_clause
|
|
73
|
+
|
|
74
|
+
when Prism::WhenNode
|
|
75
|
+
node.conditions.each { |c| walk(c, source, patches, statement_level: false) }
|
|
76
|
+
walk(node.statements, source, patches, statement_level: true) if node.statements
|
|
77
|
+
|
|
78
|
+
when Prism::CaseMatchNode
|
|
79
|
+
walk(node.predicate, source, patches, statement_level: false) if node.predicate
|
|
80
|
+
node.conditions.each { |cond| walk(cond, source, patches, statement_level: true) }
|
|
81
|
+
walk(node.else_clause, source, patches, statement_level: true) if node.else_clause
|
|
82
|
+
|
|
83
|
+
when Prism::InNode
|
|
84
|
+
walk(node.statements, source, patches, statement_level: true) if node.statements
|
|
85
|
+
|
|
86
|
+
when Prism::BlockNode, Prism::LambdaNode
|
|
87
|
+
walk(node.body, source, patches, statement_level: true) if node.body
|
|
88
|
+
|
|
89
|
+
when Prism::ParenthesesNode
|
|
90
|
+
walk(node.body, source, patches, statement_level: statement_level) if node.body
|
|
91
|
+
|
|
92
|
+
when Prism::LocalVariableWriteNode
|
|
93
|
+
walk(node.value, source, patches, statement_level: statement_level)
|
|
94
|
+
|
|
95
|
+
when Prism::PostExecutionNode
|
|
96
|
+
walk(node.statements, source, patches, statement_level: true) if node.statements
|
|
97
|
+
|
|
98
|
+
else
|
|
99
|
+
node.compact_child_nodes.each { |child| walk(child, source, patches, statement_level: false) }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def walk_if(node, source, patches)
|
|
104
|
+
walk(node.predicate, source, patches, statement_level: false)
|
|
105
|
+
|
|
106
|
+
if node.if_keyword_loc
|
|
107
|
+
# Block-if or modifier-if: body is at statement level
|
|
108
|
+
walk(node.statements, source, patches, statement_level: true) if node.statements
|
|
109
|
+
walk(node.subsequent, source, patches, statement_level: true) if node.subsequent
|
|
110
|
+
else
|
|
111
|
+
# Ternary (no if_keyword_loc): arms are NOT at statement level
|
|
112
|
+
walk(node.statements, source, patches, statement_level: false) if node.statements
|
|
113
|
+
walk(node.subsequent, source, patches, statement_level: false) if node.subsequent
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def walk_call_children(node, source, patches)
|
|
118
|
+
walk(node.receiver, source, patches, statement_level: false) if node.receiver
|
|
119
|
+
node.arguments&.arguments&.each { |arg| walk(arg, source, patches, statement_level: false) }
|
|
120
|
+
if node.block
|
|
121
|
+
if node.block.is_a?(Prism::BlockNode)
|
|
122
|
+
walk(node.block, source, patches, statement_level: true)
|
|
123
|
+
else
|
|
124
|
+
walk(node.block, source, patches, statement_level: false)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def try_remove_call_parens(node, source, patches)
|
|
130
|
+
return unless node.opening_loc
|
|
131
|
+
return unless AstUtils.can_omit_parens?(node)
|
|
132
|
+
|
|
133
|
+
# Don't remove parens from calls with keyword args in modifier context.
|
|
134
|
+
# After hash shorthand (`a:` for `a:a`), `foo a: if cond` is ambiguous.
|
|
135
|
+
raw_args = node.arguments&.arguments || []
|
|
136
|
+
if raw_args.any? { |a| a.is_a?(Prism::KeywordHashNode) }
|
|
137
|
+
# opening_loc presence (checked above) implies the paired closing_loc
|
|
138
|
+
after = source.byteslice(node.closing_loc.end_offset, 10) # steep:ignore NoMethod
|
|
139
|
+
return if after&.match?(/\A (?:if|unless|while|until) /)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
add_paren_removal_patches(node.opening_loc, node.closing_loc, patches) # steep:ignore ArgumentTypeMismatch
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def try_remove_yield_parens(node, patches)
|
|
146
|
+
return unless node.lparen_loc
|
|
147
|
+
args = node.arguments&.arguments || []
|
|
148
|
+
return if args.empty?
|
|
149
|
+
|
|
150
|
+
# lparen_loc presence (checked above) implies the paired rparen_loc
|
|
151
|
+
add_paren_removal_patches(node.lparen_loc, node.rparen_loc, patches) # steep:ignore ArgumentTypeMismatch
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def add_paren_removal_patches(open_loc, close_loc, patches)
|
|
155
|
+
patches << { start: open_loc.start_offset, end: open_loc.end_offset, replacement: ' ' }
|
|
156
|
+
patches << { start: close_loc.start_offset, end: close_loc.end_offset, replacement: '' }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|