ryac 0.2.0 → 0.3.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 +4 -4
- data/README.md +70 -16
- data/Steepfile +2 -0
- data/bin/ryac +41 -2
- data/lib/ryac/analysis/constant/collection.rb +49 -0
- data/lib/ryac/analysis/constant/rename_mapping.rb +15 -1
- data/lib/ryac/analysis/ivar/collection.rb +28 -2
- data/lib/ryac/analysis/lazy_regions.rb +87 -0
- data/lib/ryac/analysis/local_scopes.rb +110 -12
- data/lib/ryac/analysis/method/collection.rb +100 -3
- data/lib/ryac/analysis/method/rename_mapping.rb +17 -12
- data/lib/ryac/analysis/type_oracle.rb +7 -2
- data/lib/ryac/driver_file.rb +47 -0
- data/lib/ryac/minifier.rb +27 -26
- data/lib/ryac/packer.rb +127 -0
- data/lib/ryac/pipeline/analyzer.rb +25 -3
- data/lib/ryac/pipeline/concatenator.rb +127 -8
- data/lib/ryac/pipeline/data_types.rb +13 -5
- data/lib/ryac/pipeline/file_collector.rb +64 -4
- data/lib/ryac/pipeline/method_renamer.rb +15 -0
- data/lib/ryac/pipeline/stage.rb +4 -0
- data/lib/ryac/pipeline/stage_runner.rb +7 -3
- data/lib/ryac/version.rb +1 -1
- data/lib/ryac.rb +3 -0
- data/sig/ryac/analysis/constant/collection.rbs +3 -0
- data/sig/ryac/analysis/constant/rename_mapping.rbs +2 -1
- data/sig/ryac/analysis/ivar/collection.rbs +1 -0
- data/sig/ryac/analysis/lazy_regions.rbs +10 -0
- data/sig/ryac/analysis/local_scopes.rbs +9 -1
- data/sig/ryac/analysis/method/collection.rbs +3 -0
- data/sig/ryac/analysis/method/rename_mapping.rbs +3 -2
- data/sig/ryac/analysis/type_oracle.rbs +1 -1
- data/sig/ryac/driver_file.rbs +8 -0
- data/sig/ryac/minifier.rbs +1 -2
- data/sig/ryac/packer.rbs +12 -0
- data/sig/ryac/pipeline/analyzer.rbs +6 -0
- data/sig/ryac/pipeline/concatenator.rbs +16 -1
- data/sig/ryac/pipeline/data_types.rbs +8 -5
- data/sig/ryac/pipeline/file_collector.rbs +8 -3
- data/sig/ryac/pipeline/method_renamer.rbs +3 -0
- data/sig/ryac/pipeline/stage.rbs +1 -0
- data/sig/ryac/pipeline/stage_runner.rbs +2 -1
- metadata +15 -8
- data/bin/console +0 -11
- data/bin/setup +0 -8
|
@@ -42,9 +42,19 @@ module Ryac
|
|
|
42
42
|
)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
def initialize(method_policy: :aggressive)
|
|
46
|
+
@method_policy = method_policy
|
|
47
|
+
end
|
|
48
|
+
|
|
45
49
|
def call(source)
|
|
46
50
|
prism_result, @oracle = without_stdout_pollution { setup_typeprof(source) }
|
|
47
51
|
@prism_root = prism_result.value
|
|
52
|
+
@lazy_regions = LazyRegions.collect(@prism_root)
|
|
53
|
+
# A program that loads files dynamically had enumerated its external
|
|
54
|
+
# readers; bundled as lazy regions, they are all inside now, and what
|
|
55
|
+
# is left outside is a launcher — which spells the class/module
|
|
56
|
+
# skeleton, never a value constant.
|
|
57
|
+
@alias_surface = source.lazy_files.empty? ? :full : :skeleton
|
|
48
58
|
@boot_constant_roots = BootConstants.for(source.stdlib_requires)
|
|
49
59
|
@syntax_data = collect_syntax_data(@prism_root)
|
|
50
60
|
|
|
@@ -99,7 +109,7 @@ module Ryac
|
|
|
99
109
|
raise InvalidOutputError.new('pre-rename source', prism_result.errors)
|
|
100
110
|
end
|
|
101
111
|
|
|
102
|
-
[prism_result, TypeOracle.boot(source.content, source.rbs_files)]
|
|
112
|
+
[prism_result, TypeOracle.boot(source.content, source.rbs_files, prism_result.value)]
|
|
103
113
|
end
|
|
104
114
|
|
|
105
115
|
def analyze_keywords_and_scopes
|
|
@@ -120,18 +130,25 @@ module Ryac
|
|
|
120
130
|
)
|
|
121
131
|
@local_scopes.resolve
|
|
122
132
|
@scope_mappings = @local_scopes.scope_mappings
|
|
133
|
+
@scope_visible_names = @local_scopes.visible_local_names
|
|
123
134
|
end
|
|
124
135
|
|
|
125
136
|
def analyze_methods_phase
|
|
126
137
|
@method_rename_mapping = MethodRenameMapping.new
|
|
127
138
|
collect_method_definitions(@prism_root)
|
|
139
|
+
collect_dynamic_ivar_attr_exclusions(@prism_root)
|
|
140
|
+
collect_inherited_attr_exclusions(@prism_root)
|
|
128
141
|
resolve_method_calls
|
|
129
142
|
collect_alias_undef_methods(@prism_root)
|
|
130
143
|
scan_dynamic_method_references(@prism_root)
|
|
131
144
|
collect_visibility_modifier_methods(@prism_root)
|
|
132
145
|
collect_attr_write_exclusions(@prism_root)
|
|
133
146
|
collect_shorthand_pun_methods(@prism_root)
|
|
134
|
-
@
|
|
147
|
+
if @method_policy == :safe
|
|
148
|
+
collect_string_literal_mentions(@prism_root)
|
|
149
|
+
@method_rename_mapping.exclude_uncalled_methods
|
|
150
|
+
end
|
|
151
|
+
@method_rename_mapping.assign_short_names(@scope_visible_names, @oracle)
|
|
135
152
|
end
|
|
136
153
|
|
|
137
154
|
def analyze_variables_phase
|
|
@@ -156,9 +173,14 @@ module Ryac
|
|
|
156
173
|
end
|
|
157
174
|
|
|
158
175
|
def analyze_constants_phase
|
|
159
|
-
@constant_mapping = ConstantRenameMapping.new(
|
|
176
|
+
@constant_mapping = ConstantRenameMapping.new(
|
|
177
|
+
boot_roots: @boot_constant_roots,
|
|
178
|
+
alias_surface: @alias_surface
|
|
179
|
+
)
|
|
160
180
|
collect_constants(@prism_root)
|
|
161
181
|
exclude_private_constants(@prism_root)
|
|
182
|
+
exclude_lazy_definitions(@prism_root)
|
|
183
|
+
exclude_dynamic_root_reads(@prism_root)
|
|
162
184
|
count_constant_references(@prism_root)
|
|
163
185
|
augment_constant_counts_via_oracle
|
|
164
186
|
collect_external_references(@prism_root)
|
|
@@ -7,6 +7,12 @@ module Ryac
|
|
|
7
7
|
# Stage 2: File Concatenation
|
|
8
8
|
# Performs topological sort and concatenates files in dependency order
|
|
9
9
|
class Concatenator
|
|
10
|
+
# The registry lazy regions register into and the loader that runs
|
|
11
|
+
# them: ordinary source, minified with everything else (both get short
|
|
12
|
+
# names), spelled so as not to collide with the program's own names.
|
|
13
|
+
REGISTRY_NAME = 'RYAC_LAZY'
|
|
14
|
+
LOADER_NAME = 'ryac_require'
|
|
15
|
+
|
|
10
16
|
# @param graph [DependencyGraph] From Stage 1
|
|
11
17
|
# @return [ConcatenatedSource] Ordered, concatenated source
|
|
12
18
|
# @raise [CircularDependencyError] If cycle detected in graph
|
|
@@ -102,21 +108,35 @@ module Ryac
|
|
|
102
108
|
inlined = Set.new
|
|
103
109
|
current_line = 1
|
|
104
110
|
|
|
111
|
+
lazy_paths = graph.lazy_paths
|
|
112
|
+
@root = common_root(graph.paths)
|
|
113
|
+
@registry, @loader = lazy_paths.empty? ? [nil, nil] : bundle_names(graph)
|
|
114
|
+
|
|
105
115
|
# Pre-clean all files: resolve in-class requires by inlining
|
|
106
116
|
cleaned_cache = {} #: Hash[String, String]
|
|
107
117
|
sorted_paths.each do |path|
|
|
108
118
|
entry = graph[path]
|
|
109
119
|
next unless entry
|
|
110
|
-
collect_stdlib_requires(entry, stdlib_requires)
|
|
120
|
+
collect_stdlib_requires(entry, stdlib_requires) unless entry.lazy
|
|
111
121
|
cleaned_cache[path] = process_require_statements(entry, graph, inlined, cleaned_cache)
|
|
112
122
|
end
|
|
113
123
|
|
|
114
|
-
|
|
124
|
+
if (registry = @registry) && (loader = @loader)
|
|
125
|
+
prelude = loader_prelude(registry, loader)
|
|
126
|
+
content_parts << prelude
|
|
127
|
+
current_line += prelude.count("\n") + 1
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Regions first: registering is all a region does at load, and it
|
|
131
|
+
# has to be registered before any code that could ask for it runs.
|
|
132
|
+
lazy, flat = sorted_paths.partition { |path| graph[path]&.lazy }
|
|
133
|
+
(lazy + flat).each do |path|
|
|
115
134
|
next if inlined.include?(path)
|
|
116
135
|
entry = graph[path]
|
|
117
136
|
next unless entry
|
|
118
137
|
|
|
119
138
|
cleaned_content = cleaned_cache[path]
|
|
139
|
+
cleaned_content = lazy_registration(path, cleaned_content) if entry.lazy
|
|
120
140
|
lines = cleaned_content.count("\n") + 1
|
|
121
141
|
|
|
122
142
|
file_boundaries << FileBoundary.new(
|
|
@@ -136,10 +156,76 @@ module Ryac
|
|
|
136
156
|
file_boundaries: file_boundaries,
|
|
137
157
|
original_size: original_size,
|
|
138
158
|
stdlib_requires: stdlib_requires.uniq,
|
|
139
|
-
rbs_files: graph.rbs_files
|
|
159
|
+
rbs_files: graph.rbs_files,
|
|
160
|
+
lazy_files: lazy_paths
|
|
140
161
|
)
|
|
141
162
|
end
|
|
142
163
|
|
|
164
|
+
# The directory every bundled file sits under. Region keys and the
|
|
165
|
+
# respelled prefix of a dynamic require site are both relative to it,
|
|
166
|
+
# which is what makes a key and the string the site builds at runtime
|
|
167
|
+
# agree.
|
|
168
|
+
def common_root(paths)
|
|
169
|
+
root = File.dirname(paths.fetch(0))
|
|
170
|
+
root = File.dirname(root) until root == '/' || paths.all? { |p| p.start_with?("#{root}/") }
|
|
171
|
+
root
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def relative_to_root(path)
|
|
175
|
+
path.delete_prefix(@root == '/' ? '/' : "#{@root}/")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def lazy_key(path)
|
|
179
|
+
relative_to_root(path).delete_suffix('.rb')
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Names the program does not spell anywhere.
|
|
183
|
+
def bundle_names(graph)
|
|
184
|
+
contents = graph.files.each_value.map(&:content)
|
|
185
|
+
[REGISTRY_NAME, LOADER_NAME].map { |base| unused_name(base, contents) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def unused_name(base, contents)
|
|
189
|
+
name = base
|
|
190
|
+
suffix = 0
|
|
191
|
+
while contents.any? { |c| c.match?(/\b#{Regexp.escape(name)}\b/) }
|
|
192
|
+
suffix += 1
|
|
193
|
+
name = "#{base}_#{suffix}"
|
|
194
|
+
end
|
|
195
|
+
name
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# The loader keeps Ruby's require contract for the regions it owns: a
|
|
199
|
+
# region runs once and answers true, a repeat answers false, a region
|
|
200
|
+
# whose run raised (its `require "ffi"` had no ffi) is restored so a
|
|
201
|
+
# later require can try it again, and a path no region was registered
|
|
202
|
+
# under falls through to a real require_relative — relative to the
|
|
203
|
+
# bundle, the one file all this code now lives in.
|
|
204
|
+
def loader_prelude(registry, loader)
|
|
205
|
+
<<~RUBY
|
|
206
|
+
#{registry} = {}
|
|
207
|
+
def #{loader}(path)
|
|
208
|
+
return require_relative(path) unless #{registry}.key?(path)
|
|
209
|
+
body = #{registry}[path]
|
|
210
|
+
return false unless body
|
|
211
|
+
#{registry}[path] = false
|
|
212
|
+
begin
|
|
213
|
+
body.call
|
|
214
|
+
rescue Exception
|
|
215
|
+
#{registry}[path] = body
|
|
216
|
+
raise
|
|
217
|
+
end
|
|
218
|
+
true
|
|
219
|
+
end
|
|
220
|
+
RUBY
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# The file as a region (see LazyRegions): its own line for the closing
|
|
224
|
+
# brace, so a trailing comment cannot swallow it.
|
|
225
|
+
def lazy_registration(path, content)
|
|
226
|
+
"#{@registry}[#{lazy_key(path).dump}] = -> {\n#{content}\n}"
|
|
227
|
+
end
|
|
228
|
+
|
|
143
229
|
# Hoisting a require to the top of the output makes it run at load time.
|
|
144
230
|
# That is fine for one the file already ran at load time, but a require
|
|
145
231
|
# inside a method body runs only when the method is called and is often
|
|
@@ -163,13 +249,13 @@ module Ryac
|
|
|
163
249
|
nodes_with_offsets = require_nodes.select { |n| n[:start_offset] }
|
|
164
250
|
|
|
165
251
|
if nodes_with_offsets.size == require_nodes.size
|
|
166
|
-
offset_based_processing(content, nodes_with_offsets, graph, in_class_deps, inlined, cleaned_cache)
|
|
252
|
+
offset_based_processing(content, nodes_with_offsets, graph, in_class_deps, inlined, cleaned_cache, entry.lazy)
|
|
167
253
|
else
|
|
168
254
|
line_based_processing(content, require_nodes)
|
|
169
255
|
end
|
|
170
256
|
end
|
|
171
257
|
|
|
172
|
-
def offset_based_processing(content, nodes, graph, in_class_deps, inlined, cleaned_cache)
|
|
258
|
+
def offset_based_processing(content, nodes, graph, in_class_deps, inlined, cleaned_cache, lazy_entry)
|
|
173
259
|
sorted_nodes = nodes.sort_by { |n| n[:start_offset] }.reverse
|
|
174
260
|
# Prism offsets are byte offsets: splice on bytes, or any multibyte
|
|
175
261
|
# character before a require shifts every slice after it.
|
|
@@ -178,8 +264,22 @@ module Ryac
|
|
|
178
264
|
start_pos = node[:start_offset]
|
|
179
265
|
end_pos = start_pos + node[:length]
|
|
180
266
|
|
|
267
|
+
if node[:type] == :require_lazy
|
|
268
|
+
result[start_pos...end_pos] = lazy_site_call(result, node).b
|
|
269
|
+
next
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
dep_path = resolve_node_path(node, graph)
|
|
273
|
+
# A require that names a region asks the loader for it, whether
|
|
274
|
+
# the site is a static sibling require inside another region or
|
|
275
|
+
# an autoload — which then loads at that point, as it does for a
|
|
276
|
+
# flat file.
|
|
277
|
+
if dep_path && graph[dep_path]&.lazy
|
|
278
|
+
result[start_pos...end_pos] = "#{@loader}(#{lazy_key(dep_path).dump})".b
|
|
279
|
+
next
|
|
280
|
+
end
|
|
281
|
+
|
|
181
282
|
if node[:in_class] && !node[:in_method] && node[:type] != :require_stdlib
|
|
182
|
-
dep_path = resolve_node_path(node, graph)
|
|
183
283
|
if dep_path && graph[dep_path]
|
|
184
284
|
# the `graph[dep_path]` check above guarantees the entry exists
|
|
185
285
|
dep_content = cleaned_cache[dep_path] || graph[dep_path].content # steep:ignore NoMethod
|
|
@@ -195,8 +295,9 @@ module Ryac
|
|
|
195
295
|
end
|
|
196
296
|
|
|
197
297
|
# An in-method stdlib require is not hoisted, so it has to stay where
|
|
198
|
-
# it is — deleting it here would drop the require altogether.
|
|
199
|
-
|
|
298
|
+
# it is — deleting it here would drop the require altogether. Nothing
|
|
299
|
+
# in a region is hoisted: its requires run when the region does.
|
|
300
|
+
next if node[:type] == :require_stdlib && (node[:in_method] || lazy_entry)
|
|
200
301
|
|
|
201
302
|
# For removal: consume trailing semicolons and newlines
|
|
202
303
|
while end_pos < result.size && (result[end_pos] == ';' || result[end_pos] == "\n")
|
|
@@ -207,6 +308,24 @@ module Ryac
|
|
|
207
308
|
result.force_encoding(content.encoding)
|
|
208
309
|
end
|
|
209
310
|
|
|
311
|
+
# `require_relative "driver/#{name}_#{type}"` becomes
|
|
312
|
+
# `ryac_require("optcarrot/driver/#{name}_#{type}")`: the literal keeps
|
|
313
|
+
# its interpolation, its static prefix is respelled relative to the
|
|
314
|
+
# bundle root — the spelling the regions were registered under — and a
|
|
315
|
+
# literal ".rb" tail goes, as keys carry no extension. The bytes are
|
|
316
|
+
# read before this node's own edit, so every offset is still original.
|
|
317
|
+
def lazy_site_call(bytes, node)
|
|
318
|
+
site = node.fetch(:lazy)
|
|
319
|
+
arg = bytes[site[:arg_start_offset]...site[:arg_end_offset]] #: String
|
|
320
|
+
base = site[:arg_start_offset]
|
|
321
|
+
if (suffix_start = site[:suffix_start_offset])
|
|
322
|
+
arg[(suffix_start - base), 3] = ''
|
|
323
|
+
end
|
|
324
|
+
dir = site[:dir]
|
|
325
|
+
arg[(site[:prefix_start_offset] - base), site[:prefix_length]] = (dir == @root ? '' : "#{relative_to_root(dir)}/").b
|
|
326
|
+
"#{@loader}(#{arg})"
|
|
327
|
+
end
|
|
328
|
+
|
|
210
329
|
def line_based_processing(content, require_nodes)
|
|
211
330
|
lines = content.lines
|
|
212
331
|
lines_to_remove = Set.new
|
|
@@ -9,10 +9,11 @@ module Ryac
|
|
|
9
9
|
:content, # String: Raw file content
|
|
10
10
|
:dependencies, # Array<String>: Absolute paths of required files (top-level requires only)
|
|
11
11
|
:require_nodes, # Array<Hash>: {type:, path:, line:, in_class:} for require/autoload nodes
|
|
12
|
-
:in_class_dependencies
|
|
12
|
+
:in_class_dependencies, # Array<String>: Absolute paths of files required inside class/module bodies
|
|
13
|
+
:lazy # Boolean: reached only through a dynamic require — bundled as a lazy region, run on demand
|
|
13
14
|
) do
|
|
14
15
|
# declared on the Data class itself in the RBS
|
|
15
|
-
def initialize(path:, content:, dependencies:, require_nodes:, in_class_dependencies: []) # steep:ignore UndeclaredMethodDefinition
|
|
16
|
+
def initialize(path:, content:, dependencies:, require_nodes:, in_class_dependencies: [], lazy: false) # steep:ignore UndeclaredMethodDefinition
|
|
16
17
|
super
|
|
17
18
|
end
|
|
18
19
|
end
|
|
@@ -29,6 +30,11 @@ module Ryac
|
|
|
29
30
|
@rbs_files = {} # Hash<String, String>: path -> RBS content
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
# The files bundled as lazy regions, in path order.
|
|
34
|
+
def lazy_paths
|
|
35
|
+
@files.each_value.select(&:lazy).map(&:path).sort
|
|
36
|
+
end
|
|
37
|
+
|
|
32
38
|
# Add a file entry to the graph
|
|
33
39
|
# @param entry [FileEntry] The file entry to add
|
|
34
40
|
def add_file(entry)
|
|
@@ -87,15 +93,17 @@ module Ryac
|
|
|
87
93
|
:file_boundaries, # Array<FileBoundary>: Line ranges are true at concatenation only; the pipeline rewrites the text from compaction on
|
|
88
94
|
:original_size, # Integer: Bytes of the input the content descends from
|
|
89
95
|
:stdlib_requires, # Array<String>: Standard library requires to preserve
|
|
90
|
-
:rbs_files
|
|
96
|
+
:rbs_files, # Hash<String, String>: path -> RBS content for TypeProf
|
|
97
|
+
:lazy_files # Array<String>: paths of the files bundled as lazy regions (see LazyRegions)
|
|
91
98
|
) do
|
|
92
99
|
# A source built straight from a string (tests, sub-pipelines) has no
|
|
93
100
|
# file ancestry: everything but the content defaults away, and its
|
|
94
101
|
# original size is the content itself.
|
|
95
|
-
def initialize(content:, file_boundaries: [], original_size: nil, stdlib_requires: [], rbs_files: {}) # steep:ignore UndeclaredMethodDefinition
|
|
102
|
+
def initialize(content:, file_boundaries: [], original_size: nil, stdlib_requires: [], rbs_files: {}, lazy_files: []) # steep:ignore UndeclaredMethodDefinition
|
|
96
103
|
super(content: content, file_boundaries: file_boundaries,
|
|
97
104
|
original_size: original_size || content.bytesize,
|
|
98
|
-
stdlib_requires: stdlib_requires, rbs_files: rbs_files
|
|
105
|
+
stdlib_requires: stdlib_requires, rbs_files: rbs_files,
|
|
106
|
+
lazy_files: lazy_files)
|
|
99
107
|
end
|
|
100
108
|
end
|
|
101
109
|
|
|
@@ -20,6 +20,7 @@ module Ryac
|
|
|
20
20
|
|
|
21
21
|
@graph = DependencyGraph.new
|
|
22
22
|
@visited = Set.new
|
|
23
|
+
@pending_lazy = [] #: Array[String]
|
|
23
24
|
@gem_names = gem_names
|
|
24
25
|
@project_roots = if project_root
|
|
25
26
|
# Array() leaves only Strings whichever of the two shapes came in
|
|
@@ -37,6 +38,11 @@ module Ryac
|
|
|
37
38
|
collect_file(expanded)
|
|
38
39
|
end
|
|
39
40
|
|
|
41
|
+
# Only once the static world is complete: a candidate it reached is
|
|
42
|
+
# bundled flat and already visited; the rest — and whatever they
|
|
43
|
+
# require that nothing static did — become lazy regions.
|
|
44
|
+
collect_file(@pending_lazy.shift, lazy: true) until @pending_lazy.empty?
|
|
45
|
+
|
|
40
46
|
collect_rbs_files(entry_paths)
|
|
41
47
|
@graph
|
|
42
48
|
end
|
|
@@ -44,7 +50,7 @@ module Ryac
|
|
|
44
50
|
private
|
|
45
51
|
|
|
46
52
|
# Recursively collect a file and its dependencies
|
|
47
|
-
def collect_file(file_path, required_from: nil, line: nil)
|
|
53
|
+
def collect_file(file_path, required_from: nil, line: nil, lazy: false)
|
|
48
54
|
return if @visited.include?(file_path)
|
|
49
55
|
|
|
50
56
|
unless File.exist?(file_path)
|
|
@@ -71,7 +77,7 @@ module Ryac
|
|
|
71
77
|
else
|
|
72
78
|
dependencies << dep_path
|
|
73
79
|
end
|
|
74
|
-
collect_file(dep_path, required_from: file_path, line: node_info[:line])
|
|
80
|
+
collect_file(dep_path, required_from: file_path, line: node_info[:line], lazy: lazy)
|
|
75
81
|
end
|
|
76
82
|
|
|
77
83
|
entry = FileEntry.new(
|
|
@@ -79,7 +85,8 @@ module Ryac
|
|
|
79
85
|
content: content,
|
|
80
86
|
dependencies: dependencies,
|
|
81
87
|
in_class_dependencies: in_class_dependencies,
|
|
82
|
-
require_nodes: require_nodes
|
|
88
|
+
require_nodes: require_nodes,
|
|
89
|
+
lazy: lazy
|
|
83
90
|
)
|
|
84
91
|
|
|
85
92
|
@graph.add_file(entry)
|
|
@@ -123,6 +130,11 @@ module Ryac
|
|
|
123
130
|
when Prism::IfNode
|
|
124
131
|
traverse_for_requires(node.statements, nodes, file_path, in_method: in_method, in_class: in_class)
|
|
125
132
|
traverse_for_requires(node.subsequent, nodes, file_path, in_method: in_method, in_class: in_class)
|
|
133
|
+
when Prism::UnlessNode
|
|
134
|
+
traverse_for_requires(node.statements, nodes, file_path, in_method: in_method, in_class: in_class)
|
|
135
|
+
traverse_for_requires(node.else_clause, nodes, file_path, in_method: in_method, in_class: in_class)
|
|
136
|
+
when Prism::ElseNode
|
|
137
|
+
traverse_for_requires(node.statements, nodes, file_path, in_method: in_method, in_class: in_class)
|
|
126
138
|
when Prism::BeginNode
|
|
127
139
|
traverse_for_requires(node.statements, nodes, file_path, in_method: in_method, in_class: in_class)
|
|
128
140
|
end
|
|
@@ -163,7 +175,10 @@ module Ryac
|
|
|
163
175
|
resolved_path: resolve_relative_path(arg.unescaped, file_path)
|
|
164
176
|
}
|
|
165
177
|
else
|
|
166
|
-
|
|
178
|
+
if in_method
|
|
179
|
+
collect_lazy_candidates(node, arg, nodes, file_path, in_class: in_class)
|
|
180
|
+
return
|
|
181
|
+
end
|
|
167
182
|
|
|
168
183
|
raise DynamicRequireError.new(
|
|
169
184
|
file_path,
|
|
@@ -173,6 +188,51 @@ module Ryac
|
|
|
173
188
|
end
|
|
174
189
|
end
|
|
175
190
|
|
|
191
|
+
# A dynamic require inside a method stays a runtime require. When its
|
|
192
|
+
# path starts with a static directory ("driver/#{name}"), the files it
|
|
193
|
+
# can load exist on disk now, so they join the bundle — as lazy
|
|
194
|
+
# regions, each run at the moment the require would have run — and
|
|
195
|
+
# the site is recorded for the Concatenator to point at the loader.
|
|
196
|
+
def collect_lazy_candidates(node, arg, nodes, file_path, in_class:)
|
|
197
|
+
return unless arg.is_a?(Prism::InterpolatedStringNode)
|
|
198
|
+
|
|
199
|
+
head = arg.parts.first
|
|
200
|
+
return unless head.is_a?(Prism::StringNode)
|
|
201
|
+
|
|
202
|
+
slash = head.unescaped.rindex('/')
|
|
203
|
+
return unless slash
|
|
204
|
+
|
|
205
|
+
prefix = head.unescaped[0..slash] #: String
|
|
206
|
+
# The prefix is respelled in place, which needs the source to spell
|
|
207
|
+
# it as it reads.
|
|
208
|
+
return unless head.content.start_with?(prefix)
|
|
209
|
+
|
|
210
|
+
dir = File.expand_path(prefix, File.dirname(file_path))
|
|
211
|
+
candidates = Dir.glob(File.join(dir, '*.rb')).sort
|
|
212
|
+
return if candidates.empty?
|
|
213
|
+
|
|
214
|
+
tail = arg.parts.last
|
|
215
|
+
suffix_start = tail.is_a?(Prism::StringNode) && tail.content.end_with?('.rb') ? tail.content_loc.end_offset - 3 : nil
|
|
216
|
+
nodes << {
|
|
217
|
+
type: :require_lazy,
|
|
218
|
+
path: prefix,
|
|
219
|
+
line: node.location.start_line,
|
|
220
|
+
start_offset: node.location.start_offset,
|
|
221
|
+
length: node.location.length,
|
|
222
|
+
in_class: in_class,
|
|
223
|
+
in_method: true,
|
|
224
|
+
lazy: {
|
|
225
|
+
dir: dir,
|
|
226
|
+
arg_start_offset: arg.location.start_offset,
|
|
227
|
+
arg_end_offset: arg.location.end_offset,
|
|
228
|
+
prefix_start_offset: head.content_loc.start_offset,
|
|
229
|
+
prefix_length: prefix.bytesize,
|
|
230
|
+
suffix_start_offset: suffix_start
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
@pending_lazy.concat(candidates)
|
|
234
|
+
end
|
|
235
|
+
|
|
176
236
|
def handle_require(node, nodes, file_path, in_method: false, in_class: false)
|
|
177
237
|
arg = node.arguments&.arguments&.first
|
|
178
238
|
return unless arg
|
|
@@ -3,9 +3,24 @@
|
|
|
3
3
|
module Ryac
|
|
4
4
|
module Pipeline
|
|
5
5
|
# Method renaming: renames method definitions and call sites.
|
|
6
|
+
#
|
|
7
|
+
# policy: :aggressive (default) renames every group the exclusion
|
|
8
|
+
# passes leave standing, folding unresolved same-name calls and blind
|
|
9
|
+
# defs into the groups they probably belong to. :safe renames only
|
|
10
|
+
# groups whose every caller type inference actually resolved — an
|
|
11
|
+
# unresolved call excludes its name, an uncalled def keeps its name
|
|
12
|
+
# (it is either dead or someone outside the program calls it), and a
|
|
13
|
+
# name that appears inside a string literal keeps its spelling (eval
|
|
14
|
+
# and send-by-string read strings, not the syntax tree).
|
|
6
15
|
class MethodRenamer < Stage
|
|
7
16
|
include RenamePatcher
|
|
8
17
|
|
|
18
|
+
def initialize(policy: :aggressive)
|
|
19
|
+
@policy = policy
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def analysis_options = { method_policy: @policy }
|
|
23
|
+
|
|
9
24
|
def needs_analysis? = true
|
|
10
25
|
|
|
11
26
|
def collect(ctx, patches)
|
data/lib/ryac/pipeline/stage.rb
CHANGED
|
@@ -28,6 +28,10 @@ module Ryac
|
|
|
28
28
|
# true → the runner re-parses and re-collects until no patches remain.
|
|
29
29
|
def fixpoint? = false
|
|
30
30
|
|
|
31
|
+
# Keyword options this stage needs the analysis batch's Analyzer to
|
|
32
|
+
# honor. The runner merges every batch member's contribution.
|
|
33
|
+
def analysis_options = {}
|
|
34
|
+
|
|
31
35
|
def collect(ctx, patches)
|
|
32
36
|
raise NotImplementedError, "#{self.class}#collect must be implemented"
|
|
33
37
|
end
|
|
@@ -15,9 +15,10 @@ module Ryac
|
|
|
15
15
|
class StageRunner
|
|
16
16
|
include SourcePatcher
|
|
17
17
|
|
|
18
|
-
def initialize(stdlib_requires: [], rbs_files: {})
|
|
18
|
+
def initialize(stdlib_requires: [], rbs_files: {}, lazy_files: [])
|
|
19
19
|
@stdlib_requires = stdlib_requires
|
|
20
20
|
@rbs_files = rbs_files
|
|
21
|
+
@lazy_files = lazy_files
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def call(code, stage_defs)
|
|
@@ -65,9 +66,12 @@ module Ryac
|
|
|
65
66
|
source = ConcatenatedSource.new(
|
|
66
67
|
content: code,
|
|
67
68
|
stdlib_requires: @stdlib_requires,
|
|
68
|
-
rbs_files: @rbs_files
|
|
69
|
+
rbs_files: @rbs_files,
|
|
70
|
+
lazy_files: @lazy_files
|
|
69
71
|
)
|
|
70
|
-
|
|
72
|
+
options = {} #: Hash[Symbol, untyped]
|
|
73
|
+
batch.each { |stage| options.merge!(stage.analysis_options) }
|
|
74
|
+
analysis = Analyzer.new(**options).call(source)
|
|
71
75
|
|
|
72
76
|
ctx = StageContext.new(code, analysis.prism_ast, analysis, '', '')
|
|
73
77
|
patches = [] #: Array[patch_entry]
|
data/lib/ryac/version.rb
CHANGED
data/lib/ryac.rb
CHANGED
|
@@ -12,6 +12,7 @@ require_relative "ryac/analysis/constant/rename_mapping"
|
|
|
12
12
|
require_relative "ryac/analysis/scope_management"
|
|
13
13
|
require_relative "ryac/analysis/local_scopes"
|
|
14
14
|
require_relative "ryac/analysis/nesting"
|
|
15
|
+
require_relative "ryac/analysis/lazy_regions"
|
|
15
16
|
require_relative "ryac/analysis/type_oracle"
|
|
16
17
|
require_relative "ryac/analysis/constant/collection"
|
|
17
18
|
require_relative "ryac/analysis/method_aliasing"
|
|
@@ -26,4 +27,6 @@ require_relative "ryac/analysis/gvar/collection"
|
|
|
26
27
|
require_relative "ryac/analysis/keyword/rename_mapping"
|
|
27
28
|
require_relative "ryac/analysis/keyword/collection"
|
|
28
29
|
require_relative "ryac/minifier"
|
|
30
|
+
require_relative "ryac/packer"
|
|
31
|
+
require_relative "ryac/driver_file"
|
|
29
32
|
require_relative "ryac/gem_resolver"
|
|
@@ -5,6 +5,9 @@ module Ryac
|
|
|
5
5
|
def qualified_write_cpath: (Prism::Node target, Array[Symbol] nesting, ?strip_doubled_nesting: bool) -> Array[Symbol]?
|
|
6
6
|
def syntactic_const_segments: (Prism::Node node) -> Array[Symbol]
|
|
7
7
|
def struct_definition_write?: (Prism::Node node) -> bool
|
|
8
|
+
def exclude_lazy_definitions: (Prism::Node prism_root) -> void
|
|
9
|
+
def exclude_dynamic_root_reads: (Prism::Node prism_root) -> void
|
|
10
|
+
def dynamic_root?: (Prism::ConstantPathNode node) -> bool
|
|
8
11
|
def collect_constants: (Prism::Node prism_root) -> void
|
|
9
12
|
def exclude_private_constants: (Prism::Node prism_root) -> void
|
|
10
13
|
def count_constant_references: (Prism::Node prism_root) -> void
|
|
@@ -29,8 +29,9 @@ module Ryac
|
|
|
29
29
|
@state: Symbol
|
|
30
30
|
@by_name: Hash[Symbol, Array[ConstantInfo]]
|
|
31
31
|
@external_prefixes: Hash[Array[Symbol], ExternalPrefixInfo]
|
|
32
|
+
@alias_surface: Symbol
|
|
32
33
|
|
|
33
|
-
def initialize: (?boot_roots: Set[Symbol]?) -> void
|
|
34
|
+
def initialize: (?boot_roots: Set[Symbol]?, ?alias_surface: Symbol) -> void
|
|
34
35
|
def empty?: () -> bool
|
|
35
36
|
def finalized?: () -> bool
|
|
36
37
|
def add_definition_with_path: (Array[Symbol] static_cpath, definition_type: Symbol) -> void
|
|
@@ -6,6 +6,7 @@ module Ryac
|
|
|
6
6
|
type ivar_write_node = Prism::InstanceVariableWriteNode | Prism::InstanceVariableTargetNode | Prism::InstanceVariableOperatorWriteNode | Prism::InstanceVariableOrWriteNode | Prism::InstanceVariableAndWriteNode
|
|
7
7
|
|
|
8
8
|
def collect_ivar_definitions: (Prism::Node prism_root, Hash[Array[Symbol], Set[Symbol]] attr_backed) -> void
|
|
9
|
+
def attr_backed_ivar?: (Hash[Array[Symbol], Set[Symbol]] attr_backed, Array[Symbol] cpath, Symbol name) -> bool
|
|
9
10
|
def collect_attr_backed_ivars: (Prism::Node prism_root) -> Hash[Array[Symbol], Set[Symbol]]
|
|
10
11
|
def scan_dynamic_ivar_access: (Prism::Node prism_root) -> void
|
|
11
12
|
def merge_inherited_ivars: () -> void
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Ryac
|
|
2
|
+
module LazyRegions
|
|
3
|
+
def self?.collect: (Prism::ProgramNode prism_root) -> Array[Prism::LambdaNode]
|
|
4
|
+
def self?.contains?: (Array[Prism::LambdaNode] lambdas, Prism::Node node) -> bool
|
|
5
|
+
def self?.typeprof_view: (String content, Prism::ProgramNode prism_root) -> String
|
|
6
|
+
def self?.each_registration: (Prism::ProgramNode prism_root) { (Prism::Node, Prism::LambdaNode) -> void } -> void
|
|
7
|
+
def self?.registration_lambda: (Prism::Node statement) -> Prism::LambdaNode?
|
|
8
|
+
def self?.blank: (String bytes, Integer from, Integer to) -> void
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -29,6 +29,8 @@ module Ryac
|
|
|
29
29
|
|
|
30
30
|
@program: Prism::ProgramNode
|
|
31
31
|
@scope_by_node: Hash[Integer, Scope]
|
|
32
|
+
@children: Hash[Integer, Array[Scope]]
|
|
33
|
+
@constraints: Hash[scope_id, [bool, Set[Symbol]]]
|
|
32
34
|
|
|
33
35
|
attr_reader scopes: Array[Scope]
|
|
34
36
|
attr_reader rename_entries: Hash[location_key, String]
|
|
@@ -41,6 +43,7 @@ module Ryac
|
|
|
41
43
|
def scope_id_of: (Prism::Node node) -> scope_id?
|
|
42
44
|
def allocate: (?kw_def_map: Hash[location_key, Hash[Symbol, String]], ?var_hints: Hash[scope_id, Hash[Symbol, String]]) -> self
|
|
43
45
|
def scope_mappings: () -> scope_mapping_table
|
|
46
|
+
def visible_local_names: () -> Hash[scope_id, Set[String]]
|
|
44
47
|
def resolve: () -> self
|
|
45
48
|
|
|
46
49
|
private
|
|
@@ -51,7 +54,12 @@ module Ryac
|
|
|
51
54
|
def allocate_top: (Scope scope) -> void
|
|
52
55
|
def allocate_def: (Scope scope, Hash[location_key, Hash[Symbol, String]] kw_def_map, Hash[scope_id, Hash[Symbol, String]] var_hints) -> void
|
|
53
56
|
def allocate_block: (Scope scope) -> void
|
|
54
|
-
def
|
|
57
|
+
def reserved_names: (Scope scope) -> Array[String]
|
|
58
|
+
def ancestor_visible_names: (Scope scope) -> Array[String]
|
|
59
|
+
def descendant_kept_names: (Scope scope) -> Array[String]
|
|
60
|
+
def kept_local_names: (Scope scope) -> Array[String]
|
|
61
|
+
def renamed_block_param_names: (Prism::BlockNode node) -> Set[Symbol]
|
|
62
|
+
def constraints_for: (Scope scope) -> [bool, Set[Symbol]]
|
|
55
63
|
def scope_constraints: (Prism::Node? body) -> [bool, Set[Symbol]]
|
|
56
64
|
|
|
57
65
|
def allocated_name: (Symbol var, bool unsafe, Set[Symbol] pinned, NameGenerator generator) -> String
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
module Ryac
|
|
2
2
|
module AnalysisPhases
|
|
3
3
|
def collect_method_definitions: (Prism::Node prism_root) -> void
|
|
4
|
+
def collect_dynamic_ivar_attr_exclusions: (Prism::Node prism_root) -> void
|
|
5
|
+
def collect_inherited_attr_exclusions: (Prism::Node prism_root) -> void
|
|
6
|
+
def collect_string_literal_mentions: (Prism::Node prism_root) -> void
|
|
4
7
|
def resolve_method_calls: () -> void
|
|
5
8
|
def scan_dynamic_method_references: (Prism::Node prism_root) -> void
|
|
6
9
|
def collect_visibility_modifier_methods: (Prism::Node prism_root) -> void
|
|
@@ -42,13 +42,15 @@ module Ryac
|
|
|
42
42
|
|
|
43
43
|
def merge_blind_def_groups: () -> void
|
|
44
44
|
|
|
45
|
+
def exclude_uncalled_methods: () -> void
|
|
46
|
+
|
|
45
47
|
def add_unresolved_sites_for_mid: (Symbol mid, Array[Prism::Node] call_nodes) -> void
|
|
46
48
|
|
|
47
49
|
def group_keys: (method_key method_key) -> Array[method_key]
|
|
48
50
|
|
|
49
51
|
def each_group_call_site: (method_key method_key) { (Prism::Node, scope_id?) -> void } -> void
|
|
50
52
|
|
|
51
|
-
def assign_short_names: (
|
|
53
|
+
def assign_short_names: (Hash[scope_id, Set[String]] scope_vars, ?TypeOracle? oracle) -> void
|
|
52
54
|
def verify_no_shadowing!: (?hierarchy hierarchy) -> void
|
|
53
55
|
|
|
54
56
|
def short_name_for: ([Integer, Integer] node_location_key) -> String?
|
|
@@ -67,7 +69,6 @@ module Ryac
|
|
|
67
69
|
|
|
68
70
|
def build_group_entries: (Hash[method_key, Array[method_key]] groups) -> Array[MethodGroupEntry]
|
|
69
71
|
|
|
70
|
-
def self.build_scope_vars: (scope_mapping_table scope_mappings) -> Hash[scope_id, Set[String]]
|
|
71
72
|
|
|
72
73
|
def groups_by_root: () -> Hash[method_key, Array[method_key]]
|
|
73
74
|
|
|
@@ -4,7 +4,7 @@ module Ryac
|
|
|
4
4
|
type tp_genv = TypeProf::Core::GlobalEnv
|
|
5
5
|
|
|
6
6
|
class TypeOracle
|
|
7
|
-
def self.boot: (String content, Hash[String, String] rbs_files) -> TypeOracle
|
|
7
|
+
def self.boot: (String content, Hash[String, String] rbs_files, Prism::ProgramNode prism_root) -> TypeOracle
|
|
8
8
|
def self.raw_prism_node: (tp_node node) -> Prism::Node?
|
|
9
9
|
|
|
10
10
|
class CallerInfo
|