ryac 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +145 -0
- data/Rakefile +45 -0
- data/Steepfile +9 -0
- data/bin/console +11 -0
- data/bin/ryac +134 -0
- data/bin/setup +8 -0
- data/lib/ryac/analysis/constant/collection.rb +332 -0
- data/lib/ryac/analysis/constant/rename_mapping.rb +346 -0
- data/lib/ryac/analysis/cvar/collection.rb +37 -0
- data/lib/ryac/analysis/gvar/collection.rb +36 -0
- data/lib/ryac/analysis/gvar/rename_mapping.rb +82 -0
- data/lib/ryac/analysis/ivar/collection.rb +281 -0
- data/lib/ryac/analysis/keyword/collection.rb +213 -0
- data/lib/ryac/analysis/keyword/rename_mapping.rb +167 -0
- data/lib/ryac/analysis/local_scopes.rb +582 -0
- data/lib/ryac/analysis/method/collection.rb +298 -0
- data/lib/ryac/analysis/method/rename_mapping.rb +337 -0
- data/lib/ryac/analysis/method_aliases.rb +83 -0
- data/lib/ryac/analysis/method_aliasing.rb +65 -0
- data/lib/ryac/analysis/nesting.rb +123 -0
- data/lib/ryac/analysis/scope_management.rb +25 -0
- data/lib/ryac/analysis/site_bucket_mapping.rb +127 -0
- data/lib/ryac/analysis/type_oracle.rb +386 -0
- data/lib/ryac/ast_utils.rb +321 -0
- data/lib/ryac/errors.rb +32 -0
- data/lib/ryac/gem_resolver.rb +55 -0
- data/lib/ryac/minifier.rb +169 -0
- data/lib/ryac/name_generator.rb +68 -0
- data/lib/ryac/pipeline/analyzer.rb +264 -0
- data/lib/ryac/pipeline/attr_decl_shorten.rb +61 -0
- data/lib/ryac/pipeline/boolean_shorten.rb +26 -0
- data/lib/ryac/pipeline/char_shorten.rb +20 -0
- data/lib/ryac/pipeline/compactor.rb +1202 -0
- data/lib/ryac/pipeline/concatenator.rb +248 -0
- data/lib/ryac/pipeline/constant_aliaser.rb +240 -0
- data/lib/ryac/pipeline/constant_fold.rb +93 -0
- data/lib/ryac/pipeline/control_flow_simplify.rb +352 -0
- data/lib/ryac/pipeline/data_types.rb +179 -0
- data/lib/ryac/pipeline/endless_method.rb +60 -0
- data/lib/ryac/pipeline/errors.rb +94 -0
- data/lib/ryac/pipeline/file_collector.rb +360 -0
- data/lib/ryac/pipeline/method_renamer.rb +151 -0
- data/lib/ryac/pipeline/paren_optimizer.rb +161 -0
- data/lib/ryac/pipeline/rename_patcher.rb +274 -0
- data/lib/ryac/pipeline/source_patcher.rb +54 -0
- data/lib/ryac/pipeline/spelling_shorten.rb +60 -0
- data/lib/ryac/pipeline/stage.rb +72 -0
- data/lib/ryac/pipeline/stage_runner.rb +84 -0
- data/lib/ryac/pipeline/variable_renamer.rb +125 -0
- data/lib/ryac/union_find.rb +42 -0
- data/lib/ryac/version.rb +5 -0
- data/lib/ryac.rb +29 -0
- data/rbs_collection.lock.yaml +224 -0
- data/rbs_collection.yaml +25 -0
- data/sig/ryac/analysis/constant/collection.rbs +30 -0
- data/sig/ryac/analysis/constant/rename_mapping.rbs +60 -0
- data/sig/ryac/analysis/cvar/collection.rbs +11 -0
- data/sig/ryac/analysis/gvar/collection.rbs +9 -0
- data/sig/ryac/analysis/gvar/rename_mapping.rbs +23 -0
- data/sig/ryac/analysis/ivar/collection.rbs +26 -0
- data/sig/ryac/analysis/keyword/collection.rbs +27 -0
- data/sig/ryac/analysis/keyword/rename_mapping.rbs +31 -0
- data/sig/ryac/analysis/local_scopes.rbs +77 -0
- data/sig/ryac/analysis/method/collection.rbs +25 -0
- data/sig/ryac/analysis/method/rename_mapping.rbs +82 -0
- data/sig/ryac/analysis/method_aliases.rbs +10 -0
- data/sig/ryac/analysis/method_aliasing.rbs +9 -0
- data/sig/ryac/analysis/nesting.rbs +9 -0
- data/sig/ryac/analysis/scope_management.rbs +5 -0
- data/sig/ryac/analysis/site_bucket_mapping.rbs +26 -0
- data/sig/ryac/analysis/type_oracle.rbs +61 -0
- data/sig/ryac/ast_utils.rbs +34 -0
- data/sig/ryac/errors.rbs +21 -0
- data/sig/ryac/gem_resolver.rbs +28 -0
- data/sig/ryac/minifier.rbs +33 -0
- data/sig/ryac/name_generator.rbs +14 -0
- data/sig/ryac/pipeline/analyzer.rbs +34 -0
- data/sig/ryac/pipeline/attr_decl_shorten.rbs +17 -0
- data/sig/ryac/pipeline/boolean_shorten.rbs +11 -0
- data/sig/ryac/pipeline/char_shorten.rbs +11 -0
- data/sig/ryac/pipeline/compactor.rbs +125 -0
- data/sig/ryac/pipeline/concatenator.rbs +28 -0
- data/sig/ryac/pipeline/constant_aliaser.rbs +35 -0
- data/sig/ryac/pipeline/constant_fold.rbs +20 -0
- data/sig/ryac/pipeline/control_flow_simplify.rbs +34 -0
- data/sig/ryac/pipeline/data_types.rbs +151 -0
- data/sig/ryac/pipeline/endless_method.rbs +14 -0
- data/sig/ryac/pipeline/errors.rbs +46 -0
- data/sig/ryac/pipeline/file_collector.rbs +34 -0
- data/sig/ryac/pipeline/method_renamer.rbs +25 -0
- data/sig/ryac/pipeline/paren_optimizer.rbs +16 -0
- data/sig/ryac/pipeline/rename_patcher.rbs +38 -0
- data/sig/ryac/pipeline/source_patcher.rbs +12 -0
- data/sig/ryac/pipeline/spelling_shorten.rbs +14 -0
- data/sig/ryac/pipeline/stage.rbs +28 -0
- data/sig/ryac/pipeline/stage_runner.rbs +18 -0
- data/sig/ryac/pipeline/variable_renamer.rbs +21 -0
- data/sig/ryac/union_find.rbs +17 -0
- data/sig/ryac.rbs +13 -0
- data/sig/shims/env.rbs +19 -0
- data/sig/shims/typeprof/core/ast.rbs +299 -0
- data/sig/shims/typeprof/core/type.rbs +22 -0
- data/sig/shims/typeprof/core.rbs +73 -0
- data/tools/regen_pins.rb +45 -0
- metadata +201 -0
|
@@ -0,0 +1,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
|