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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +70 -16
  3. data/Steepfile +2 -0
  4. data/bin/ryac +41 -2
  5. data/lib/ryac/analysis/constant/collection.rb +49 -0
  6. data/lib/ryac/analysis/constant/rename_mapping.rb +15 -1
  7. data/lib/ryac/analysis/ivar/collection.rb +28 -2
  8. data/lib/ryac/analysis/lazy_regions.rb +87 -0
  9. data/lib/ryac/analysis/local_scopes.rb +110 -12
  10. data/lib/ryac/analysis/method/collection.rb +100 -3
  11. data/lib/ryac/analysis/method/rename_mapping.rb +17 -12
  12. data/lib/ryac/analysis/type_oracle.rb +7 -2
  13. data/lib/ryac/driver_file.rb +47 -0
  14. data/lib/ryac/minifier.rb +27 -26
  15. data/lib/ryac/packer.rb +127 -0
  16. data/lib/ryac/pipeline/analyzer.rb +25 -3
  17. data/lib/ryac/pipeline/concatenator.rb +127 -8
  18. data/lib/ryac/pipeline/data_types.rb +13 -5
  19. data/lib/ryac/pipeline/file_collector.rb +64 -4
  20. data/lib/ryac/pipeline/method_renamer.rb +15 -0
  21. data/lib/ryac/pipeline/stage.rb +4 -0
  22. data/lib/ryac/pipeline/stage_runner.rb +7 -3
  23. data/lib/ryac/version.rb +1 -1
  24. data/lib/ryac.rb +3 -0
  25. data/sig/ryac/analysis/constant/collection.rbs +3 -0
  26. data/sig/ryac/analysis/constant/rename_mapping.rbs +2 -1
  27. data/sig/ryac/analysis/ivar/collection.rbs +1 -0
  28. data/sig/ryac/analysis/lazy_regions.rbs +10 -0
  29. data/sig/ryac/analysis/local_scopes.rbs +9 -1
  30. data/sig/ryac/analysis/method/collection.rbs +3 -0
  31. data/sig/ryac/analysis/method/rename_mapping.rbs +3 -2
  32. data/sig/ryac/analysis/type_oracle.rbs +1 -1
  33. data/sig/ryac/driver_file.rbs +8 -0
  34. data/sig/ryac/minifier.rbs +1 -2
  35. data/sig/ryac/packer.rbs +12 -0
  36. data/sig/ryac/pipeline/analyzer.rbs +6 -0
  37. data/sig/ryac/pipeline/concatenator.rbs +16 -1
  38. data/sig/ryac/pipeline/data_types.rbs +8 -5
  39. data/sig/ryac/pipeline/file_collector.rbs +8 -3
  40. data/sig/ryac/pipeline/method_renamer.rbs +3 -0
  41. data/sig/ryac/pipeline/stage.rbs +1 -0
  42. data/sig/ryac/pipeline/stage_runner.rbs +2 -1
  43. metadata +15 -8
  44. data/bin/console +0 -11
  45. 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
- @method_rename_mapping.assign_short_names(@scope_mappings, @oracle)
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(boot_roots: @boot_constant_roots)
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
- sorted_paths.each do |path|
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
- next if node[:type] == :require_stdlib && node[:in_method]
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 # Array<String>: Absolute paths of files required inside class/module bodies
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 # Hash<String, String>: path -> RBS content for TypeProf
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
- return if in_method
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)
@@ -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
- analysis = Analyzer.new.call(source)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ryac
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
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 ancestor_mapping_values: (Scope scope) -> Array[String]
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: (scope_mapping_table scope_mappings, ?TypeOracle? oracle) -> void
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
@@ -0,0 +1,8 @@
1
+ module Ryac
2
+ module DriverFile
3
+ HEADER: String
4
+ FOOTER: String
5
+
6
+ def self.split: (String full_content) -> [String, String]
7
+ end
8
+ end