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.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +145 -0
  4. data/Rakefile +45 -0
  5. data/Steepfile +9 -0
  6. data/bin/console +11 -0
  7. data/bin/ryac +134 -0
  8. data/bin/setup +8 -0
  9. data/lib/ryac/analysis/constant/collection.rb +332 -0
  10. data/lib/ryac/analysis/constant/rename_mapping.rb +346 -0
  11. data/lib/ryac/analysis/cvar/collection.rb +37 -0
  12. data/lib/ryac/analysis/gvar/collection.rb +36 -0
  13. data/lib/ryac/analysis/gvar/rename_mapping.rb +82 -0
  14. data/lib/ryac/analysis/ivar/collection.rb +281 -0
  15. data/lib/ryac/analysis/keyword/collection.rb +213 -0
  16. data/lib/ryac/analysis/keyword/rename_mapping.rb +167 -0
  17. data/lib/ryac/analysis/local_scopes.rb +582 -0
  18. data/lib/ryac/analysis/method/collection.rb +298 -0
  19. data/lib/ryac/analysis/method/rename_mapping.rb +337 -0
  20. data/lib/ryac/analysis/method_aliases.rb +83 -0
  21. data/lib/ryac/analysis/method_aliasing.rb +65 -0
  22. data/lib/ryac/analysis/nesting.rb +123 -0
  23. data/lib/ryac/analysis/scope_management.rb +25 -0
  24. data/lib/ryac/analysis/site_bucket_mapping.rb +127 -0
  25. data/lib/ryac/analysis/type_oracle.rb +386 -0
  26. data/lib/ryac/ast_utils.rb +321 -0
  27. data/lib/ryac/errors.rb +32 -0
  28. data/lib/ryac/gem_resolver.rb +55 -0
  29. data/lib/ryac/minifier.rb +169 -0
  30. data/lib/ryac/name_generator.rb +68 -0
  31. data/lib/ryac/pipeline/analyzer.rb +264 -0
  32. data/lib/ryac/pipeline/attr_decl_shorten.rb +61 -0
  33. data/lib/ryac/pipeline/boolean_shorten.rb +26 -0
  34. data/lib/ryac/pipeline/char_shorten.rb +20 -0
  35. data/lib/ryac/pipeline/compactor.rb +1202 -0
  36. data/lib/ryac/pipeline/concatenator.rb +248 -0
  37. data/lib/ryac/pipeline/constant_aliaser.rb +240 -0
  38. data/lib/ryac/pipeline/constant_fold.rb +93 -0
  39. data/lib/ryac/pipeline/control_flow_simplify.rb +352 -0
  40. data/lib/ryac/pipeline/data_types.rb +179 -0
  41. data/lib/ryac/pipeline/endless_method.rb +60 -0
  42. data/lib/ryac/pipeline/errors.rb +94 -0
  43. data/lib/ryac/pipeline/file_collector.rb +360 -0
  44. data/lib/ryac/pipeline/method_renamer.rb +151 -0
  45. data/lib/ryac/pipeline/paren_optimizer.rb +161 -0
  46. data/lib/ryac/pipeline/rename_patcher.rb +274 -0
  47. data/lib/ryac/pipeline/source_patcher.rb +54 -0
  48. data/lib/ryac/pipeline/spelling_shorten.rb +60 -0
  49. data/lib/ryac/pipeline/stage.rb +72 -0
  50. data/lib/ryac/pipeline/stage_runner.rb +84 -0
  51. data/lib/ryac/pipeline/variable_renamer.rb +125 -0
  52. data/lib/ryac/union_find.rb +42 -0
  53. data/lib/ryac/version.rb +5 -0
  54. data/lib/ryac.rb +29 -0
  55. data/rbs_collection.lock.yaml +224 -0
  56. data/rbs_collection.yaml +25 -0
  57. data/sig/ryac/analysis/constant/collection.rbs +30 -0
  58. data/sig/ryac/analysis/constant/rename_mapping.rbs +60 -0
  59. data/sig/ryac/analysis/cvar/collection.rbs +11 -0
  60. data/sig/ryac/analysis/gvar/collection.rbs +9 -0
  61. data/sig/ryac/analysis/gvar/rename_mapping.rbs +23 -0
  62. data/sig/ryac/analysis/ivar/collection.rbs +26 -0
  63. data/sig/ryac/analysis/keyword/collection.rbs +27 -0
  64. data/sig/ryac/analysis/keyword/rename_mapping.rbs +31 -0
  65. data/sig/ryac/analysis/local_scopes.rbs +77 -0
  66. data/sig/ryac/analysis/method/collection.rbs +25 -0
  67. data/sig/ryac/analysis/method/rename_mapping.rbs +82 -0
  68. data/sig/ryac/analysis/method_aliases.rbs +10 -0
  69. data/sig/ryac/analysis/method_aliasing.rbs +9 -0
  70. data/sig/ryac/analysis/nesting.rbs +9 -0
  71. data/sig/ryac/analysis/scope_management.rbs +5 -0
  72. data/sig/ryac/analysis/site_bucket_mapping.rbs +26 -0
  73. data/sig/ryac/analysis/type_oracle.rbs +61 -0
  74. data/sig/ryac/ast_utils.rbs +34 -0
  75. data/sig/ryac/errors.rbs +21 -0
  76. data/sig/ryac/gem_resolver.rbs +28 -0
  77. data/sig/ryac/minifier.rbs +33 -0
  78. data/sig/ryac/name_generator.rbs +14 -0
  79. data/sig/ryac/pipeline/analyzer.rbs +34 -0
  80. data/sig/ryac/pipeline/attr_decl_shorten.rbs +17 -0
  81. data/sig/ryac/pipeline/boolean_shorten.rbs +11 -0
  82. data/sig/ryac/pipeline/char_shorten.rbs +11 -0
  83. data/sig/ryac/pipeline/compactor.rbs +125 -0
  84. data/sig/ryac/pipeline/concatenator.rbs +28 -0
  85. data/sig/ryac/pipeline/constant_aliaser.rbs +35 -0
  86. data/sig/ryac/pipeline/constant_fold.rbs +20 -0
  87. data/sig/ryac/pipeline/control_flow_simplify.rbs +34 -0
  88. data/sig/ryac/pipeline/data_types.rbs +151 -0
  89. data/sig/ryac/pipeline/endless_method.rbs +14 -0
  90. data/sig/ryac/pipeline/errors.rbs +46 -0
  91. data/sig/ryac/pipeline/file_collector.rbs +34 -0
  92. data/sig/ryac/pipeline/method_renamer.rbs +25 -0
  93. data/sig/ryac/pipeline/paren_optimizer.rbs +16 -0
  94. data/sig/ryac/pipeline/rename_patcher.rbs +38 -0
  95. data/sig/ryac/pipeline/source_patcher.rbs +12 -0
  96. data/sig/ryac/pipeline/spelling_shorten.rbs +14 -0
  97. data/sig/ryac/pipeline/stage.rbs +28 -0
  98. data/sig/ryac/pipeline/stage_runner.rbs +18 -0
  99. data/sig/ryac/pipeline/variable_renamer.rbs +21 -0
  100. data/sig/ryac/union_find.rbs +17 -0
  101. data/sig/ryac.rbs +13 -0
  102. data/sig/shims/env.rbs +19 -0
  103. data/sig/shims/typeprof/core/ast.rbs +299 -0
  104. data/sig/shims/typeprof/core/type.rbs +22 -0
  105. data/sig/shims/typeprof/core.rbs +73 -0
  106. data/tools/regen_pins.rb +45 -0
  107. metadata +201 -0
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ryac
4
+ module AnalysisPhases
5
+ DYNAMIC_CVAR_METHODS = %i[
6
+ class_variable_get class_variable_set
7
+ class_variable_defined? class_variables
8
+ remove_class_variable
9
+ ].freeze
10
+
11
+ CVAR_WRITE_NODES = [
12
+ Prism::ClassVariableWriteNode,
13
+ Prism::ClassVariableTargetNode,
14
+ Prism::ClassVariableOperatorWriteNode,
15
+ Prism::ClassVariableOrWriteNode,
16
+ Prism::ClassVariableAndWriteNode
17
+ ].freeze
18
+
19
+ def collect_cvar_definitions(prism_root)
20
+ Nesting.each(prism_root) do |node, cpath, _singleton|
21
+ case node
22
+ when Prism::ClassVariableReadNode, *CVAR_WRITE_NODES
23
+ # @type var node: Prism::ClassVariableReadNode | cvar_write_node
24
+ @cvar_rename_mapping.add_site(cpath, node.name, node)
25
+ end
26
+ end
27
+ end
28
+
29
+ def scan_dynamic_cvar_access(prism_root)
30
+ scan_dynamic_sigil_access(prism_root, DYNAMIC_CVAR_METHODS, @cvar_rename_mapping)
31
+ end
32
+
33
+ def merge_inherited_cvars
34
+ merge_inherited_sites(@cvar_rename_mapping)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ryac
4
+ module AnalysisPhases
5
+ GVAR_NODES = [
6
+ Prism::GlobalVariableReadNode,
7
+ Prism::GlobalVariableWriteNode,
8
+ Prism::GlobalVariableTargetNode,
9
+ Prism::GlobalVariableOperatorWriteNode,
10
+ Prism::GlobalVariableOrWriteNode,
11
+ Prism::GlobalVariableAndWriteNode
12
+ ].freeze
13
+
14
+ def collect_gvar_definitions(prism_root)
15
+ AstUtils.each_node(prism_root) do |node|
16
+ case node
17
+ when *GVAR_NODES
18
+ # @type var node: gvar_node
19
+ @gvar_rename_mapping.add_site(node.name, node,
20
+ write: !node.is_a?(Prism::GlobalVariableReadNode))
21
+ end
22
+ end
23
+ end
24
+
25
+ # `alias $new $old` aliases the *variable*, so the two names refer to one
26
+ # storage cell; renaming either side independently would sever them.
27
+ def scan_alias_globals(prism_root)
28
+ AstUtils.each_node(prism_root) do |node|
29
+ next unless node.is_a?(Prism::AliasGlobalVariableNode)
30
+
31
+ @gvar_rename_mapping.exclude_name(node.new_name.slice.to_sym)
32
+ @gvar_rename_mapping.exclude_name(node.old_name.slice.to_sym)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ryac
4
+ class GvarRenameMapping
5
+ # One pairing of English name to punctuation spelling. SpellingShorten
6
+ # rewrites reads and plain writes to the short form; both spellings also
7
+ # join BUILT_IN_GLOBALS below, so the node kinds it does not rewrite
8
+ # (targets, operator/or/and writes) can never be renamed either.
9
+ PERL_SPELLINGS = {
10
+ '$PROGRAM_NAME': '$0',
11
+ '$LOAD_PATH': '$:',
12
+ '$LOADED_FEATURES': '$"',
13
+ }.freeze
14
+
15
+ # Built-in globals that must never be renamed: Ruby defines their
16
+ # storage, so a renamed spelling reads nil and writes into a dead cell.
17
+ BUILT_IN_GLOBALS = (Set.new(%i[
18
+ $stdout $stderr $stdin $DEBUG $VERBOSE $FILENAME
19
+ $ERROR_INFO $ERROR_POSITION $FIELD_SEPARATOR $OFS $OUTPUT_FIELD_SEPARATOR
20
+ $RS $INPUT_RECORD_SEPARATOR $ORS $OUTPUT_RECORD_SEPARATOR
21
+ $INPUT_LINE_NUMBER $NR $LAST_READ_LINE $DEFAULT_OUTPUT $DEFAULT_INPUT
22
+ $PID $PROCESS_ID $CHILD_STATUS $LAST_MATCH_INFO $IGNORECASE
23
+ $ARGV $MATCH $PREMATCH $POSTMATCH $LAST_PAREN_MATCH $FS
24
+ $0 $: $; $, $. $/ $\\ $_ $< $> $! $@ $~ $& $` $' $+ $*
25
+ $" $? $$ $-0 $-F $-I $-K $-W $-a $-d $-i $-l $-p $-v $-w
26
+ $1 $2 $3 $4 $5 $6 $7 $8 $9
27
+ ]) + PERL_SPELLINGS.keys + PERL_SPELLINGS.values.map(&:to_sym)).freeze
28
+
29
+ def initialize
30
+ @gvars = {}
31
+ @excluded_names = Set.new
32
+ @written_names = Set.new
33
+ @node_short_names = {}
34
+ end
35
+
36
+ def add_site(name, node, write: false)
37
+ return if BUILT_IN_GLOBALS.include?(name)
38
+ @written_names << name if write
39
+ @gvars[name] ||= []
40
+ @gvars[name] << node
41
+ end
42
+
43
+ def exclude_name(name)
44
+ @excluded_names << name
45
+ end
46
+
47
+ PREFIX = '$'
48
+ # Sigil + one character is already minimal — the same rule the
49
+ # SiteBucketMapping families derive from their prefix.
50
+ KEPT_NAME_MAX = PREFIX.size + 1
51
+
52
+ def assign_short_names
53
+ existing_names = Set.new
54
+ @gvars.each_key do |name|
55
+ existing_names << name.to_s if name.to_s.size <= KEPT_NAME_MAX
56
+ end
57
+
58
+ generator = NameGenerator.new(existing_names, prefix: PREFIX)
59
+ sorted_gvars = @gvars.sort_by do |name, nodes|
60
+ -(name.to_s.size * nodes.size)
61
+ end
62
+
63
+ sorted_gvars.each do |name, nodes|
64
+ next if @excluded_names.include?(name)
65
+ next if name.to_s.size <= KEPT_NAME_MAX
66
+ # A global the program only reads is either a built-in this list
67
+ # does not know or nil everywhere — renaming it can only break.
68
+ next unless @written_names.include?(name)
69
+
70
+ short_name = generator.next_name
71
+ savings = (name.to_s.size - short_name.size) * nodes.size
72
+ next unless savings > 0
73
+
74
+ nodes.each { |n| @node_short_names[AstUtils.location_key(n)] = short_name }
75
+ end
76
+ end
77
+
78
+ def node_mapping
79
+ @node_short_names.dup
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ryac
4
+ module AnalysisPhases
5
+ DYNAMIC_IVAR_METHODS = %i[
6
+ instance_variable_get instance_variable_set
7
+ instance_variable_defined? remove_instance_variable
8
+ instance_variables
9
+ ].freeze
10
+
11
+ ATTR_DECLARATION_METHODS = %i[attr attr_reader attr_writer attr_accessor].freeze
12
+
13
+ IVAR_WRITE_NODES = [
14
+ Prism::InstanceVariableWriteNode,
15
+ Prism::InstanceVariableTargetNode,
16
+ Prism::InstanceVariableOperatorWriteNode,
17
+ Prism::InstanceVariableOrWriteNode,
18
+ Prism::InstanceVariableAndWriteNode
19
+ ].freeze
20
+
21
+ def collect_ivar_definitions(prism_root, attr_backed)
22
+ Nesting.each(prism_root) do |node, cpath, _singleton, _in_def|
23
+ case node
24
+ when Prism::InstanceVariableReadNode, *IVAR_WRITE_NODES
25
+ # @type var node: Prism::InstanceVariableReadNode | ivar_write_node
26
+ next if attr_backed[cpath]&.include?(node.name)
27
+ @ivar_rename_mapping.add_site(cpath, node.name, node)
28
+ end
29
+ end
30
+ end
31
+
32
+ def collect_attr_backed_ivars(prism_root)
33
+ result = Hash.new { |h, k| h[k] = Set.new } #: Hash[Array[Symbol], Set[Symbol]]
34
+ each_attr_declaration(prism_root, ATTR_DECLARATION_METHODS, require_class_body: false) do |_node, cpath, _singleton, sym|
35
+ result[cpath] << :"@#{sym}"
36
+ end
37
+ result
38
+ end
39
+
40
+ def scan_dynamic_ivar_access(prism_root)
41
+ scan_dynamic_sigil_access(prism_root, DYNAMIC_IVAR_METHODS, @ivar_rename_mapping)
42
+ end
43
+
44
+ def merge_inherited_ivars
45
+ merge_inherited_sites(@ivar_rename_mapping)
46
+ end
47
+
48
+ def reserve_attr_ivar_names(prism_root)
49
+ each_attr_declaration(prism_root, %i[attr_reader attr_accessor]) do |_node, cpath, singleton, sym|
50
+ getter_short = @method_rename_mapping.short_name_for_key([cpath, singleton, sym].freeze)
51
+ next unless getter_short
52
+ @ivar_rename_mapping.reserve_name(cpath, "@#{getter_short}")
53
+ end
54
+ end
55
+
56
+ # attr declarations rename along two paths. Path A (method-driven): the
57
+ # getter already got a short name from method renaming, and the backing
58
+ # ivar follows it. Path B (ivar-driven): the getter wasn't renamed, so a
59
+ # name is chosen from the ivar side and the getter (and setter) follow.
60
+ # Phase 1 decides every name, phase 2 applies them — declaration, ivar
61
+ # sites, and accessor call sites — only once all decisions are in.
62
+ def coordinate_attr_renames(prism_root, rename_map, attr_ivar_entries)
63
+ path_a_info, path_b_info = classify_attr_declarations(prism_root)
64
+ return {} unless path_a_info.any? || path_b_info.any?
65
+
66
+ path_a_mapping = path_a_info.to_h { |info| [info[:ivar_key], "@#{info[:getter_short]}"] }
67
+ ivar_nodes_by_key = collect_ivar_nodes_by_key(prism_root)
68
+ path_b_mapping, path_b_method_mapping =
69
+ assign_ivar_driven_names(path_b_info, ivar_nodes_by_key, path_a_mapping, rename_map)
70
+
71
+ apply_ivar_renames(ivar_nodes_by_key, path_a_mapping.merge(path_b_mapping), attr_ivar_entries)
72
+ apply_accessor_call_renames(path_a_info, path_b_info, path_b_method_mapping, rename_map)
73
+ build_attr_rename_map(path_a_info, path_b_info, path_b_method_mapping)
74
+ end
75
+
76
+ private
77
+
78
+ # A receiverless (or explicit-self) call to a dynamic sigil-variable API
79
+ # can reach any name in its class — nothing in that cpath is safe to
80
+ # rename. Shared by the ivar and cvar scans, which differ only in table
81
+ # and mapping.
82
+ def scan_dynamic_sigil_access(prism_root, methods, mapping)
83
+ Nesting.each(prism_root) do |node, cpath, _singleton, _in_def|
84
+ next unless node.is_a?(Prism::CallNode)
85
+ next unless methods.include?(node.name)
86
+
87
+ recv = node.receiver
88
+ mapping.exclude_cpath(cpath) if recv.nil? || recv.is_a?(Prism::SelfNode)
89
+ end
90
+ end
91
+
92
+ def merge_inherited_sites(mapping)
93
+ cpaths = [] #: Array[Array[Symbol]]
94
+ mapping.each_canonical_cpath { |c| cpaths << c }
95
+ cpaths.each do |cpath|
96
+ @oracle.each_ancestor_cpath(cpath, false) do |ancestor_cpath|
97
+ next if ancestor_cpath == cpath
98
+ mapping.merge_with_ancestor(cpath, ancestor_cpath)
99
+ end
100
+ end
101
+ end
102
+
103
+ def classify_attr_declarations(prism_root)
104
+ path_a_info = [] #: Array[attr_info]
105
+ path_b_info = [] #: Array[attr_info]
106
+ each_attr_declaration(prism_root, %i[attr_reader attr_accessor]) do |node, cpath, singleton, sym|
107
+ method_key = [cpath, singleton, sym].freeze
108
+ getter_short = @method_rename_mapping.short_name_for_key(method_key)
109
+ info = {
110
+ cpath: cpath, singleton: singleton, mid: sym,
111
+ accessor: node.name == :attr_accessor,
112
+ loc_key: AstUtils.location_key(node),
113
+ ivar_key: [cpath, :"@#{sym}"],
114
+ method_key: method_key
115
+ }
116
+ if getter_short
117
+ info[:getter_short] = getter_short
118
+ path_a_info << info
119
+ elsif @method_rename_mapping.has_method?(method_key) &&
120
+ @method_rename_mapping.group_keys(method_key) == [method_key]
121
+ # An excluded mid (dynamic dispatch, unresolved call, hidden attr
122
+ # write, ...) was left alone for a reason — the ivar side must not
123
+ # rename it either. A group spanning other keys means the mid's
124
+ # sites are shared with defs this declaration does not cover.
125
+ path_b_info << info
126
+ end
127
+ end
128
+ [path_a_info, path_b_info]
129
+ end
130
+
131
+ def collect_ivar_nodes_by_key(prism_root)
132
+ ivar_nodes_by_key = Hash.new { |h, k| h[k] = [] } #: Hash[[Array[Symbol], Symbol], Array[Prism::Node]]
133
+ Nesting.each(prism_root) do |node, cpath, _singleton, _in_def|
134
+ case node
135
+ when Prism::InstanceVariableReadNode, *IVAR_WRITE_NODES
136
+ # @type var node: Prism::InstanceVariableReadNode | ivar_write_node
137
+ ivar_nodes_by_key[[cpath, node.name]] << node
138
+ end
139
+ end
140
+ ivar_nodes_by_key
141
+ end
142
+
143
+ # Path B: pick a fresh @name whose bare form is free as a method name too,
144
+ # then keep it only when renaming ivar sites plus accessor calls saves
145
+ # more than it costs.
146
+ def assign_ivar_driven_names(path_b_info, ivar_nodes_by_key, path_a_mapping, rename_map)
147
+ path_b_mapping = {} #: Hash[[Array[Symbol], Symbol], String]
148
+ path_b_method_mapping = {} #: Hash[[Array[Symbol], Symbol], Symbol]
149
+ return [path_b_mapping, path_b_method_mapping] if path_b_info.none?
150
+
151
+ used_ivar_names = @ivar_rename_mapping.node_mapping.values.to_set
152
+ used_ivar_names.merge(path_a_mapping.values)
153
+ used_method_names = rename_map.values.to_set
154
+ scope_vars = MethodRenameMapping.build_scope_vars(@scope_mappings)
155
+ generator = NameGenerator.new(prefix: "@")
156
+
157
+ path_b_info
158
+ .sort_by do |info|
159
+ ivar_name = info[:ivar_key][1]
160
+ count = ivar_nodes_by_key[info[:ivar_key]].size
161
+ -(ivar_name.to_s.size * count)
162
+ end
163
+ .each do |info|
164
+ ivar_key = info[:ivar_key]
165
+ ivar_name = ivar_key[1]
166
+ ivar_count = ivar_nodes_by_key[ivar_key].size
167
+ next if ivar_count == 0
168
+ next if ivar_name.to_s.size <= '@'.size + 1 # sigil + one character is already minimal
169
+
170
+ getter_calls = 0
171
+ implicit_scope_ids = [] #: Array[scope_id]
172
+ @method_rename_mapping.each_group_call_site(info[:method_key]) do |_site, scope_id|
173
+ getter_calls += 1
174
+ implicit_scope_ids << scope_id if scope_id
175
+ end
176
+
177
+ short_name, method_short = loop do #: [String, Symbol]
178
+ candidate = generator.next_name
179
+ method_candidate = candidate.delete_prefix("@").to_sym
180
+
181
+ next if used_ivar_names.include?(candidate)
182
+ next if used_method_names.include?(method_candidate.to_s)
183
+ next if @oracle.method_defined?(info[:cpath], info[:singleton], method_candidate)
184
+ # A bare renamed getter call would parse as the local if a visible
185
+ # local already owns the candidate at an implicit-receiver site.
186
+ next if implicit_scope_ids.any? { |sid| scope_vars[sid].include?(method_candidate.to_s) }
187
+
188
+ break [candidate, method_candidate]
189
+ end
190
+
191
+ setter_calls = 0
192
+ if info[:accessor]
193
+ setter_calls = @oracle.method_call_count(info[:cpath], info[:singleton], :"#{info[:mid]}=")
194
+ end
195
+
196
+ ivar_savings = (ivar_name.to_s.size - short_name.size) * ivar_count
197
+ method_savings = (info[:mid].to_s.size - method_short.to_s.size) * (getter_calls + setter_calls + 1)
198
+ next unless ivar_savings + method_savings > 0
199
+
200
+ used_ivar_names << short_name
201
+ used_method_names << method_short.to_s
202
+
203
+ path_b_mapping[ivar_key] = short_name
204
+ path_b_method_mapping[ivar_key] = method_short
205
+ end
206
+
207
+ [path_b_mapping, path_b_method_mapping]
208
+ end
209
+
210
+
211
+ # Ivar read/write sites take their attr's name; a subclass's sites follow
212
+ # the ancestor that declared the attr.
213
+ def apply_ivar_renames(ivar_nodes_by_key, combined_mapping, attr_ivar_entries)
214
+ ivar_nodes_by_key.each do |(cpath, ivar_name), nodes_list|
215
+ short = combined_mapping[[cpath, ivar_name]]
216
+ unless short
217
+ @oracle.each_ancestor_cpath(cpath, false) do |ancestor_cpath|
218
+ next if ancestor_cpath == cpath
219
+ short = combined_mapping[[ancestor_cpath, ivar_name]]
220
+ break if short
221
+ end
222
+ end
223
+ next unless short
224
+ nodes_list.each { |n| attr_ivar_entries[AstUtils.location_key(n)] = short }
225
+ end
226
+ end
227
+
228
+ def apply_accessor_call_renames(path_a_info, path_b_info, path_b_method_mapping, rename_map)
229
+ path_a_info.each do |info|
230
+ next unless info[:accessor]
231
+ @oracle.each_call_site_key(info[:cpath], info[:singleton], :"#{info[:mid]}=") do |key|
232
+ rename_map[key] = "#{info[:getter_short]}="
233
+ end
234
+ end
235
+
236
+ path_b_info.each do |info|
237
+ method_short = path_b_method_mapping[info[:ivar_key]]
238
+ next unless method_short
239
+
240
+ # The mapping's group sites, not the oracle's: sites attached by the
241
+ # unresolved-call pass exist only in the mapping, and missing one here
242
+ # would leave it calling the old name.
243
+ @method_rename_mapping.each_group_call_site(info[:method_key]) do |site, _scope_id|
244
+ rename_map[AstUtils.location_key(site)] = method_short.to_s
245
+ end
246
+
247
+ next unless info[:accessor]
248
+ @oracle.each_call_site_key(info[:cpath], info[:singleton], :"#{info[:mid]}=") do |key|
249
+ rename_map[key] = "#{method_short}="
250
+ end
251
+ end
252
+ end
253
+
254
+ def build_attr_rename_map(path_a_info, path_b_info, path_b_method_mapping)
255
+ # Values are the getter shorts: path A stores Strings, path B Symbols;
256
+ # consumers interpolate, so both work.
257
+ attr_rename_map = {} #: Hash[location_key, Hash[Symbol, untyped]]
258
+ path_a_info.each do |info|
259
+ (attr_rename_map[info[:loc_key]] ||= {})[info[:mid]] = info[:getter_short]
260
+ end
261
+ path_b_info.each do |info|
262
+ method_short = path_b_method_mapping[info[:ivar_key]]
263
+ next unless method_short
264
+ (attr_rename_map[info[:loc_key]] ||= {})[info[:mid]] = method_short
265
+ end
266
+ attr_rename_map
267
+ end
268
+
269
+ # attr declarations with meta semantics, one yield per symbol argument.
270
+ # With require_class_body: false, bare attr_* calls anywhere count — the
271
+ # attr-backed scan wants those too, since the methods they define still
272
+ # read the ivar.
273
+ def each_attr_declaration(prism_root, methods, require_class_body: true)
274
+ Nesting.each_meta_call(prism_root, methods, loose: !require_class_body) do |node, cpath, singleton|
275
+ AstUtils.symbol_arguments(node).each do |sym|
276
+ yield node, cpath, singleton, sym
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ryac
4
+ module AnalysisPhases
5
+ def collect_keyword_info(prism_root)
6
+ @keyword_def_node_registry = {}
7
+ @keyword_forwarding_super_keys = Set.new
8
+
9
+ Nesting.each_method_definition(prism_root) do |node, method_key|
10
+ keywords = def_keyword_names(node)
11
+ next if keywords.empty?
12
+
13
+ keywords.each { |sym| @keyword_rename_mapping.add_keyword_def(method_key, sym) }
14
+
15
+ @keyword_def_node_registry[method_key] ||= []
16
+ @keyword_def_node_registry[method_key] << node
17
+
18
+ @keyword_rename_mapping.exclude_method(method_key) if def_keyword_rest?(node)
19
+
20
+ @keyword_forwarding_super_keys << method_key if forwards_parameters_to_super?(node)
21
+ end
22
+
23
+ collect_keyword_call_sites(prism_root)
24
+ end
25
+
26
+ private
27
+
28
+ def def_keyword_names(def_node)
29
+ params = def_node.parameters
30
+ return [] unless params
31
+
32
+ names = [] #: Array[Symbol]
33
+ params.keywords&.each do |kw|
34
+ names << kw.name if kw.respond_to?(:name)
35
+ end
36
+ names
37
+ end
38
+
39
+ def def_keyword_rest?(def_node)
40
+ def_node.parameters&.keyword_rest.is_a?(Prism::KeywordRestParameterNode)
41
+ end
42
+
43
+ # A bare `super` forwards this method's parameters to the parent by name, so
44
+ # the keyword names have to keep matching the parent's signature. When the
45
+ # parent is a def we also collected, collect_keyword_call_sites merges the
46
+ # two groups and both sides are renamed together; this only reports the
47
+ # forwarding, the decision is made once we know whether that merge happened.
48
+ def forwards_parameters_to_super?(def_node)
49
+ found = false
50
+ walk = lambda do |n|
51
+ next if found || !n.is_a?(Prism::Node)
52
+ # A nested def has its own parameters; its `super` is not about ours.
53
+ next if n.is_a?(Prism::DefNode)
54
+ if n.is_a?(Prism::ForwardingSuperNode)
55
+ found = true
56
+ next
57
+ end
58
+ n.compact_child_nodes.each { |child| walk.call(child) }
59
+ end
60
+ walk.call(def_node.body)
61
+ found
62
+ end
63
+
64
+ # Everything call sites decide about keyword renames, in order: literal
65
+ # keywords register with their method, super-forwarding merges child and
66
+ # parent groups (or excludes the child when the parent is outside our
67
+ # control), polymorphic call sites merge every method they can reach,
68
+ # methods no resolved call reaches are excluded, and unresolved
69
+ # keyword-writing calls disqualify their method name entirely.
70
+ def collect_keyword_call_sites(prism_root)
71
+ sites = register_keyword_calls
72
+ merge_super_forwarding(sites[:super_merges])
73
+ merge_polymorphic_keyword_groups(sites[:call_node_to_keys])
74
+ exclude_unreachable_keyword_methods(sites[:zero_call_keys], sites[:super_merges])
75
+ exclude_unresolved_keyword_calls(prism_root, sites[:resolved_site_keys])
76
+ exclude_method_valued_shorthand_calls(prism_root)
77
+ end
78
+
79
+ def register_keyword_calls
80
+ call_node_to_keys = Hash.new { |h, k| h[k] = [] } #: Hash[location_key, Array[method_key]]
81
+ super_merges = [] #: Array[[method_key, method_key]]
82
+ zero_call_keys = [] #: Array[method_key]
83
+ resolved_site_keys = Set.new
84
+
85
+ @keyword_rename_mapping.each_method_key do |key|
86
+ call_count = 0
87
+ splat_seen = false
88
+
89
+ @oracle.each_caller(key[0], key[1], key[2]) do |info|
90
+ resolved_site_keys << AstUtils.location_key(info.prism_node) if info.prism_node
91
+ next if splat_seen
92
+
93
+ if info.super
94
+ # caller_cpath is always present on super records
95
+ entry = [info.caller_cpath, key[1], key[2]].freeze #: method_key
96
+ super_merges << [entry, key]
97
+ next
98
+ end
99
+
100
+ if info.prism_node.nil?
101
+ # An unrewritable dispatch with no source site: the method is
102
+ # reached in a way we cannot see, which is exactly what the
103
+ # zero-call rule below covers — it must not count as a seen call.
104
+ next
105
+ end
106
+
107
+ call_count += 1
108
+
109
+ next unless info.keyword_entries
110
+
111
+ if info.keyword_splat
112
+ @keyword_rename_mapping.exclude_method(key)
113
+ splat_seen = true
114
+ next
115
+ end
116
+
117
+ info.keyword_entries.each do |sym_node, val_node|
118
+ @keyword_rename_mapping.add_keyword_call(key, sym_node.unescaped.to_sym, sym_node, val_node)
119
+ end
120
+
121
+ call_node_to_keys[AstUtils.location_key(info.prism_node)] << key
122
+ end
123
+
124
+ zero_call_keys << key if call_count == 0 && @oracle.method_known?(key[0], key[1], key[2])
125
+ end
126
+
127
+ { call_node_to_keys: call_node_to_keys, super_merges: super_merges,
128
+ zero_call_keys: zero_call_keys, resolved_site_keys: resolved_site_keys }
129
+ end
130
+
131
+ # Supers are discovered from the parent's call boxes, so a `super` whose
132
+ # parent we never collected produces no merge at all. That parent's
133
+ # signature is outside our control — Data.define and Struct.new generate
134
+ # theirs from the member list — and renaming only the child raises
135
+ # "unknown keywords" at runtime, so leave those keywords alone.
136
+ def merge_super_forwarding(super_merges)
137
+ super_merges.each do |child_key, parent_key|
138
+ @keyword_rename_mapping.merge_groups(child_key, parent_key)
139
+ end
140
+
141
+ merged_children = Set.new(super_merges.map { |child_key, _| child_key })
142
+ @keyword_forwarding_super_keys.each do |key|
143
+ next if merged_children.include?(key)
144
+ @keyword_rename_mapping.exclude_method(key)
145
+ end
146
+ end
147
+
148
+ def merge_polymorphic_keyword_groups(call_node_to_keys)
149
+ call_node_to_keys.each_value do |keys|
150
+ next if keys.size < 2
151
+ (1...keys.size).each { |i| @keyword_rename_mapping.merge_groups(keys[i - 1], keys[i]) }
152
+ end
153
+ end
154
+
155
+ # A keyword-taking method type analysis knows but connects no call to is
156
+ # reachable in ways we cannot see; renaming its keywords would strand the
157
+ # unseen callers. A method reached through super stays: the merge already
158
+ # ties it to callers we did see.
159
+ def exclude_unreachable_keyword_methods(zero_call_keys, super_merges)
160
+ super_targets = super_merges.map(&:last).to_set
161
+ zero_call_keys.each do |key|
162
+ next if super_targets.include?(key)
163
+ @keyword_rename_mapping.exclude_method(key)
164
+ end
165
+ end
166
+
167
+ # A call to a keyword-taking method that type inference never connected to
168
+ # its target would keep its written keywords while the def's got renamed —
169
+ # so any such call disqualifies the whole method name. Only calls that
170
+ # actually write keywords (literals or a **splat) can go stale, though: a
171
+ # keyword-less call is untouched by the rename no matter which method it
172
+ # reaches, and unrelated methods sharing the name would otherwise poison
173
+ # each other through it.
174
+ def exclude_unresolved_keyword_calls(prism_root, resolved_site_keys)
175
+ keyword_mids = Set.new
176
+ @keyword_rename_mapping.each_method_key { |key| keyword_mids << key[2] }
177
+ return if keyword_mids.empty?
178
+
179
+ unresolved_mids = Set.new
180
+ AstUtils.each_node(prism_root) do |node|
181
+ next unless node.is_a?(Prism::CallNode)
182
+ if keyword_mids.include?(node.name) &&
183
+ carries_keyword_arguments?(node) &&
184
+ !resolved_site_keys.include?(AstUtils.location_key(node))
185
+ unresolved_mids << node.name
186
+ end
187
+ end
188
+
189
+ @keyword_rename_mapping.exclude_methods_by_mid(unresolved_mids)
190
+ end
191
+
192
+ # A keyword shorthand can pun on a METHOD, not a local: `n(label:)` calls
193
+ # `label` for its value. Renaming the keyword rewrites the pun and with it
194
+ # the method the value resolves to — methods called with such a pun keep
195
+ # their keywords.
196
+ def exclude_method_valued_shorthand_calls(prism_root)
197
+ punned_mids = Set.new
198
+ AstUtils.each_node(prism_root) do |node|
199
+ next unless node.is_a?(Prism::CallNode)
200
+ node.arguments&.arguments&.each do |arg|
201
+ punned_mids << node.name if AstUtils.shorthand_pun_values(arg).any?
202
+ end
203
+ end
204
+ @keyword_rename_mapping.exclude_methods_by_mid(punned_mids) if punned_mids.any?
205
+ end
206
+
207
+ def carries_keyword_arguments?(call_node)
208
+ call_node.arguments&.arguments&.any? { |arg| arg.is_a?(Prism::KeywordHashNode) } || false
209
+ end
210
+
211
+ public
212
+ end
213
+ end