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,1202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
module Pipeline
|
|
5
|
+
# Level 0: Compaction — full AST rebuild with no syntax optimizations.
|
|
6
|
+
# Removes comments and whitespace, joins with ";". Preserves original paren form.
|
|
7
|
+
# No renaming, no TypeProf needed. Self-contained — no shared mixins.
|
|
8
|
+
class Compactor
|
|
9
|
+
BINARY_OPERATORS = %i[== != < > <= >= + - * / % & | ^ << >> === =~ !~ <=>].freeze
|
|
10
|
+
NON_ASSOCIATIVE_OPS = %i[== === != =~ !~ <=>].to_set.freeze
|
|
11
|
+
|
|
12
|
+
OPERATOR_PRECEDENCE = {
|
|
13
|
+
:"|" => 1, :'^' => 1, :'&' => 2,
|
|
14
|
+
:'<=>' => 3, :== => 3, :=== => 3, :!= => 3, :=~ => 3, :'!~' => 3,
|
|
15
|
+
:> => 4, :>= => 4, :< => 4, :<= => 4,
|
|
16
|
+
:<< => 5, :>> => 5,
|
|
17
|
+
:+ => 6, :- => 6,
|
|
18
|
+
:* => 7, :/ => 7, :% => 7,
|
|
19
|
+
:** => 8
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
COMPOUND_WRITE_NODES = [
|
|
23
|
+
Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
|
|
24
|
+
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
25
|
+
Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
|
|
26
|
+
Prism::ClassVariableOperatorWriteNode, Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
|
|
27
|
+
Prism::ConstantOperatorWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode,
|
|
28
|
+
Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode,
|
|
29
|
+
Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode,
|
|
30
|
+
Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode,
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
def call(input_string)
|
|
34
|
+
@prism_ast = Prism.parse(input_string).value
|
|
35
|
+
@standard_error_redefined = nil
|
|
36
|
+
rebuild.join(";")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# --- Dispatch ---
|
|
42
|
+
|
|
43
|
+
def rebuild
|
|
44
|
+
render_statement_list(@prism_ast.statements.body)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def r(node)
|
|
48
|
+
case node
|
|
49
|
+
when Prism::CallNode then r_call(node)
|
|
50
|
+
when Prism::DefNode then r_def(node)
|
|
51
|
+
when Prism::YieldNode then r_yield(node)
|
|
52
|
+
when Prism::IfNode then r_if(node)
|
|
53
|
+
when Prism::UnlessNode then r_unless(node)
|
|
54
|
+
when Prism::WhileNode then r_while(node)
|
|
55
|
+
when Prism::CaseNode then r_case(node)
|
|
56
|
+
when Prism::ReturnNode then r_return(node)
|
|
57
|
+
when Prism::ClassNode then r_class(node)
|
|
58
|
+
when Prism::ModuleNode then r_module(node)
|
|
59
|
+
when Prism::AndNode then r_and(node)
|
|
60
|
+
when Prism::FlipFlopNode then r_flipflop(node)
|
|
61
|
+
when Prism::OrNode then r_or(node)
|
|
62
|
+
when Prism::LocalVariableWriteNode then "#{node.name}=#{r(node.value)}"
|
|
63
|
+
when Prism::InstanceVariableWriteNode, Prism::GlobalVariableWriteNode,
|
|
64
|
+
Prism::ClassVariableWriteNode then "#{node.name}=#{r(node.value)}"
|
|
65
|
+
when Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
|
|
66
|
+
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
67
|
+
Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
|
|
68
|
+
Prism::ClassVariableOperatorWriteNode, Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
|
|
69
|
+
Prism::ConstantOperatorWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode,
|
|
70
|
+
Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode,
|
|
71
|
+
Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode,
|
|
72
|
+
Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode
|
|
73
|
+
r_compound_write(node)
|
|
74
|
+
when Prism::ConstantPathNode
|
|
75
|
+
node.parent ? "#{r(node.parent)}::#{node.name}" : "::#{node.name}"
|
|
76
|
+
when Prism::ConstantWriteNode then "#{node.name}=#{r(node.value)}"
|
|
77
|
+
when Prism::ConstantPathWriteNode then "#{r(node.target)}=#{r(node.value)}"
|
|
78
|
+
when Prism::StringNode then r_string(node)
|
|
79
|
+
when Prism::IntegerNode then node.value.to_s
|
|
80
|
+
when Prism::FloatNode then r_float(node)
|
|
81
|
+
when Prism::ArrayNode then r_array(node)
|
|
82
|
+
when Prism::RangeNode then r_range(node)
|
|
83
|
+
when Prism::SymbolNode then r_symbol(node)
|
|
84
|
+
when Prism::HashNode then r_hash(node)
|
|
85
|
+
when Prism::InterpolatedStringNode then r_interp_string(node)
|
|
86
|
+
when Prism::InterpolatedSymbolNode then r_interp_symbol(node)
|
|
87
|
+
when Prism::InterpolatedRegularExpressionNode then r_interp_regexp(node)
|
|
88
|
+
when Prism::RegularExpressionNode then r_regexp(node)
|
|
89
|
+
when Prism::StatementsNode then r_stmt(node)
|
|
90
|
+
when Prism::ParenthesesNode then r_parens(node)
|
|
91
|
+
when Prism::SuperNode then r_super(node)
|
|
92
|
+
when Prism::BeginNode then r_begin(node)
|
|
93
|
+
when Prism::RescueModifierNode then "(#{r(node.expression)} rescue #{r(node.rescue_expression)})"
|
|
94
|
+
when Prism::BreakNode then r_break(node)
|
|
95
|
+
when Prism::NextNode then r_next(node)
|
|
96
|
+
when Prism::SplatNode then node.expression ? "*#{r_delimited(node.expression)}" : '*'
|
|
97
|
+
when Prism::UntilNode then r_until(node)
|
|
98
|
+
when Prism::AliasMethodNode then "alias #{r_alias_name(node.new_name)} #{r_alias_name(node.old_name)}"
|
|
99
|
+
when Prism::UndefNode then "undef #{node.names.map { |n| r_alias_name(n) }.join(',')}"
|
|
100
|
+
when Prism::LambdaNode then r_lambda(node)
|
|
101
|
+
when Prism::PostExecutionNode then "END{#{r_stmt(node.statements)}}"
|
|
102
|
+
when Prism::MultiWriteNode then r_multi_write(node)
|
|
103
|
+
when Prism::MatchWriteNode then r(node.call)
|
|
104
|
+
when Prism::DefinedNode then r_defined(node)
|
|
105
|
+
when Prism::ForNode then r_for(node)
|
|
106
|
+
when Prism::MatchRequiredNode then "#{r(node.value)}=>#{r_pattern(node.pattern)}"
|
|
107
|
+
when Prism::MatchPredicateNode then "#{r(node.value)} in #{r_pattern(node.pattern)}"
|
|
108
|
+
when Prism::SingletonClassNode then r_singleton_class(node)
|
|
109
|
+
when Prism::CaseMatchNode then r_case_match(node)
|
|
110
|
+
when Prism::ArrayPatternNode then r_array_pattern(node)
|
|
111
|
+
when Prism::HashPatternNode then r_hash_pattern(node)
|
|
112
|
+
when Prism::FindPatternNode then r_find_pattern(node)
|
|
113
|
+
when Prism::AlternationPatternNode, Prism::CapturePatternNode then r_pattern(node)
|
|
114
|
+
when Prism::PinnedVariableNode then "^#{r(node.variable)}"
|
|
115
|
+
when Prism::PinnedExpressionNode then "^(#{r_delimited(node.expression)})"
|
|
116
|
+
when Prism::ImplicitNode then r(node.value)
|
|
117
|
+
when Prism::ShareableConstantNode then r(node.write)
|
|
118
|
+
when Prism::CallTargetNode then "#{r(node.receiver)}.#{node.name.to_s.chomp('=')}"
|
|
119
|
+
when Prism::KeywordHashNode then r_keyword_hash(node)
|
|
120
|
+
# nil spelled as the empty grouped expression: same value, one byte
|
|
121
|
+
# shorter. Pattern positions reject it and go through r_pattern.
|
|
122
|
+
when Prism::NilNode then '()'
|
|
123
|
+
when Prism::TrueNode, Prism::FalseNode, Prism::SelfNode,
|
|
124
|
+
Prism::RedoNode, Prism::RetryNode, Prism::ForwardingSuperNode,
|
|
125
|
+
Prism::ItLocalVariableReadNode, Prism::NumberedReferenceReadNode,
|
|
126
|
+
Prism::SourceEncodingNode, Prism::SourceFileNode, Prism::SourceLineNode,
|
|
127
|
+
Prism::LocalVariableReadNode, Prism::LocalVariableTargetNode,
|
|
128
|
+
Prism::InstanceVariableReadNode, Prism::GlobalVariableReadNode,
|
|
129
|
+
Prism::ClassVariableReadNode, Prism::ConstantReadNode,
|
|
130
|
+
Prism::RationalNode, Prism::ImaginaryNode, Prism::MatchLastLineNode,
|
|
131
|
+
Prism::InterpolatedMatchLastLineNode,
|
|
132
|
+
Prism::BackReferenceReadNode, Prism::AliasGlobalVariableNode
|
|
133
|
+
node.slice
|
|
134
|
+
else
|
|
135
|
+
raise Ryac::MinifyError, "Unknown node: #{node.class}"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def r_stmt(nodes)
|
|
140
|
+
return '' if nodes.nil?
|
|
141
|
+
|
|
142
|
+
if nodes.is_a?(Prism::StatementsNode)
|
|
143
|
+
render_statement_list(nodes.body).join(';')
|
|
144
|
+
else
|
|
145
|
+
result = r(nodes)
|
|
146
|
+
result.nil? || result.empty? ? '' : result
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# `def self.x` runs of GROUP_MIN or more become one `class<<self`
|
|
151
|
+
# block. Only CONSECUTIVE defs form a run — grouping across other
|
|
152
|
+
# statements would reorder their side effects.
|
|
153
|
+
GROUP_MIN = 4
|
|
154
|
+
|
|
155
|
+
def singleton_self_def?(node)
|
|
156
|
+
node.is_a?(Prism::DefNode) && node.receiver.is_a?(Prism::SelfNode)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def render_statement_list(stmts)
|
|
160
|
+
results = [] #: Array[String]
|
|
161
|
+
run = [] #: Array[Prism::DefNode]
|
|
162
|
+
stmts.each do |subnode|
|
|
163
|
+
if singleton_self_def?(subnode)
|
|
164
|
+
# @type var subnode: Prism::DefNode
|
|
165
|
+
run << subnode
|
|
166
|
+
next
|
|
167
|
+
end
|
|
168
|
+
results.concat(flush_singleton_run(run))
|
|
169
|
+
result = r(subnode)
|
|
170
|
+
results << result unless result.nil? || result.empty?
|
|
171
|
+
break if node_returns_bot?(subnode)
|
|
172
|
+
end
|
|
173
|
+
results.concat(flush_singleton_run(run))
|
|
174
|
+
results
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Renders and clears the pending run of `def self.` statements: one
|
|
178
|
+
# class<<self group when the run is long enough to pay for the
|
|
179
|
+
# wrapper, the plain renders otherwise.
|
|
180
|
+
def flush_singleton_run(run)
|
|
181
|
+
rendered = if run.size >= GROUP_MIN
|
|
182
|
+
inner = run.map { |n| r_def(n, drop_self_receiver: true) }
|
|
183
|
+
inner.reject! { |s| s.nil? || s.empty? }
|
|
184
|
+
["class<<self;#{inner.join(';')};end"]
|
|
185
|
+
else
|
|
186
|
+
run.map { |n| r(n) }.reject { |s| s.nil? || s.empty? }
|
|
187
|
+
end
|
|
188
|
+
run.clear
|
|
189
|
+
rendered
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# --- Method calls ---
|
|
193
|
+
|
|
194
|
+
def r_call(node)
|
|
195
|
+
return r_binary_op(node) if AstUtils.middle_method?(node.name)
|
|
196
|
+
# has_block? guarantees a BlockNode/BlockArgumentNode, so r_block_call
|
|
197
|
+
# cannot actually return nil here.
|
|
198
|
+
return r_block_call(node) if AstUtils.has_block?(node) # steep:ignore ReturnTypeMismatch
|
|
199
|
+
return r_index_access(node) if node.name == :'[]'
|
|
200
|
+
return r_index_assign(node) if node.name == :'[]='
|
|
201
|
+
return r_unary(node, '!') if node.name == :'!'
|
|
202
|
+
return r_unary(node, node.name == :'-@' ? '-' : '+') if node.name == :'-@' || node.name == :'+@'
|
|
203
|
+
call_op = node.safe_navigation? ? '&.' : '.'
|
|
204
|
+
# `x.call(args)` has proc-call sugar: `x.(args)` dispatches to the
|
|
205
|
+
# exact same method on any receiver. Erasing the message this early
|
|
206
|
+
# is sound only because :call sits in EXCLUDED_METHODS — no renamer
|
|
207
|
+
# ever needs to find these sites by name.
|
|
208
|
+
if node.name == :call && node.receiver
|
|
209
|
+
raw_args = node.arguments&.arguments || []
|
|
210
|
+
return "#{recv_with_op(node.receiver, call_op)}(#{raw_args.map { |a| r_delimited(a) }.join(',')})"
|
|
211
|
+
end
|
|
212
|
+
method_name = node.name
|
|
213
|
+
if setter_call?(node)
|
|
214
|
+
# setter_call? implies exactly one argument is present
|
|
215
|
+
"#{recv_with_op(node.receiver, call_op)}#{method_name}#{r(node.arguments.arguments.first)}" # steep:ignore NoMethod
|
|
216
|
+
else
|
|
217
|
+
"#{recv_with_op(node.receiver, call_op)}#{method_name}#{build_call_args(node)}"
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def r_binary_op(node)
|
|
222
|
+
recv_str, recv_wrapped = wrap_operand(node.receiver, node.name, :left)
|
|
223
|
+
# binary operator calls always carry exactly one argument
|
|
224
|
+
arg_str, = wrap_operand(node.arguments.arguments.first, node.name, :right) # steep:ignore NoMethod
|
|
225
|
+
op = node.name
|
|
226
|
+
sep = binary_op_separator(node.receiver, recv_wrapped, op)
|
|
227
|
+
"#{recv_str}#{sep}#{op}#{arg_str}"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def r_block_call(node)
|
|
231
|
+
method_name = node.name
|
|
232
|
+
call_op = node.safe_navigation? ? '&.' : '.'
|
|
233
|
+
block = node.block
|
|
234
|
+
if block.is_a?(Prism::BlockNode)
|
|
235
|
+
block_params = extract_block_params(block)
|
|
236
|
+
uses_numbered = block_params.any? { |p| p.match?(/^_\d+$/) }
|
|
237
|
+
block_params_str = if block_params.empty? || uses_numbered || block_uses_it?(block.body)
|
|
238
|
+
""
|
|
239
|
+
else
|
|
240
|
+
"|#{block_params.join(',')}|"
|
|
241
|
+
end
|
|
242
|
+
"#{recv_with_op(node.receiver, call_op)}#{method_name}#{build_call_args(node)}{#{block_params_str}#{r_stmt(block.body)}}"
|
|
243
|
+
elsif block.is_a?(Prism::BlockArgumentNode)
|
|
244
|
+
bp = block.expression ? "&#{r(block.expression)}" : "&"
|
|
245
|
+
"#{recv_with_op(node.receiver, call_op)}#{method_name}#{build_call_args(node, bp)}"
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def r_index_access(node)
|
|
250
|
+
raw_args = node.arguments&.arguments || []
|
|
251
|
+
args_str = raw_args.map { |a| r_delimited(a) }.join(',')
|
|
252
|
+
if node.safe_navigation?
|
|
253
|
+
"#{r(node.receiver)}&.[](#{args_str})"
|
|
254
|
+
else
|
|
255
|
+
"#{r(node.receiver)}[#{args_str}]"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def r_index_assign(node)
|
|
260
|
+
raw_args = node.arguments&.arguments || []
|
|
261
|
+
if node.safe_navigation?
|
|
262
|
+
"#{r(node.receiver)}&.[]=(#{raw_args.map { |a| r_delimited(a) }.join(',')})"
|
|
263
|
+
else
|
|
264
|
+
keys = raw_args[0..-2].map { |a| r_delimited(a) }.join(',') # steep:ignore NoMethod
|
|
265
|
+
"#{r(node.receiver)}[#{keys}]=#{r(raw_args.last)}"
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def r_unary(node, op)
|
|
270
|
+
recv_str = r(node.receiver)
|
|
271
|
+
inner = AstUtils.unwrap_statements(node.receiver)
|
|
272
|
+
needs_wrap = binary_operator_call?(inner) || AstUtils.logical_op?(inner)
|
|
273
|
+
needs_wrap ? "#{op}(#{recv_str})" : "#{op}#{recv_str}"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# --- Definitions ---
|
|
277
|
+
|
|
278
|
+
# drop_self_receiver: the def sits directly inside `class<<self`, so
|
|
279
|
+
# its written `self.` disappears; anything nested in its body renders
|
|
280
|
+
# through the plain path and keeps its own receiver.
|
|
281
|
+
def r_def(node, drop_self_receiver: false)
|
|
282
|
+
method_name = node.name
|
|
283
|
+
receiver_prefix = if node.receiver
|
|
284
|
+
if drop_self_receiver && node.receiver.is_a?(Prism::SelfNode)
|
|
285
|
+
""
|
|
286
|
+
else
|
|
287
|
+
"#{r(node.receiver)}."
|
|
288
|
+
end
|
|
289
|
+
else
|
|
290
|
+
""
|
|
291
|
+
end
|
|
292
|
+
all_params = build_def_params(node.parameters)
|
|
293
|
+
body_node = node.body
|
|
294
|
+
if body_node.is_a?(Prism::StatementsNode) && body_node.body.size == 1 &&
|
|
295
|
+
body_node.body.first.is_a?(Prism::ParenthesesNode)
|
|
296
|
+
body_node = body_node.body.first.body # steep:ignore NoMethod
|
|
297
|
+
end
|
|
298
|
+
# A sole-nil body and an empty body are the same method, and the
|
|
299
|
+
# empty spelling is what `def f =()` parses back into — emitting it
|
|
300
|
+
# directly keeps re-minification at a fixed point.
|
|
301
|
+
if body_node.is_a?(Prism::StatementsNode) && body_node.body.size == 1 &&
|
|
302
|
+
body_node.body.first.is_a?(Prism::NilNode)
|
|
303
|
+
body_node = nil
|
|
304
|
+
end
|
|
305
|
+
body = r_stmt(body_node)
|
|
306
|
+
params_str = all_params.empty? ? "" : "(#{all_params.join(',')})"
|
|
307
|
+
"def #{receiver_prefix}#{method_name}#{params_str}#{fmt_body(body)};end"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def r_lambda(node)
|
|
311
|
+
params_str = build_lambda_params(node.parameters)
|
|
312
|
+
body = r_stmt(node.body)
|
|
313
|
+
"->#{params_str}{#{body}}"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# --- Control flow ---
|
|
317
|
+
|
|
318
|
+
def r_yield(node)
|
|
319
|
+
args = node.arguments&.arguments || []
|
|
320
|
+
return 'yield' if args.empty?
|
|
321
|
+
"yield(#{args.map { |a| r_delimited(a) }.join(',')})"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def r_if(node)
|
|
325
|
+
then_body = r_stmt(node.statements)
|
|
326
|
+
else_node = node.subsequent
|
|
327
|
+
else_body = case else_node
|
|
328
|
+
when nil then nil
|
|
329
|
+
when Prism::IfNode then r(else_node)
|
|
330
|
+
when Prism::ElseNode
|
|
331
|
+
inner = else_node.statements ? AstUtils.unwrap_statements(else_node.statements) : nil
|
|
332
|
+
(inner.nil? || inner.is_a?(Prism::NilNode)) ? nil : r_stmt(else_node.statements)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
if else_body.nil?
|
|
336
|
+
"if #{r(node.predicate)};#{then_body || ''};end"
|
|
337
|
+
elsif else_node.is_a?(Prism::IfNode)
|
|
338
|
+
build_if_chain(node)
|
|
339
|
+
else
|
|
340
|
+
"if #{r(node.predicate)};#{then_body || ''};else;#{else_body};end"
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def r_unless(node)
|
|
345
|
+
then_body = r_stmt(node.statements)
|
|
346
|
+
"unless #{r(node.predicate)};#{then_body || ''};end"
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def r_while(node)
|
|
350
|
+
cond = r(node.predicate)
|
|
351
|
+
body = r_stmt(node.statements)
|
|
352
|
+
if node.begin_modifier?
|
|
353
|
+
"#{body} while #{cond}"
|
|
354
|
+
else
|
|
355
|
+
"while #{cond}#{fmt_body(body)};end"
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def r_until(node)
|
|
360
|
+
cond = r(node.predicate)
|
|
361
|
+
body = r_stmt(node.statements)
|
|
362
|
+
if node.begin_modifier?
|
|
363
|
+
"#{body} until #{cond}"
|
|
364
|
+
else
|
|
365
|
+
"until #{cond}#{fmt_body(body)};end"
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def r_case(node)
|
|
370
|
+
predicate = node.predicate ? " #{r(node.predicate)}" : ''
|
|
371
|
+
body = "case#{predicate};" + node.conditions.map { |wn|
|
|
372
|
+
conditions = wn.conditions.map { |c| r_delimited(c) }.join(',')
|
|
373
|
+
"when #{conditions}#{fmt_body(r_stmt(wn.statements))}"
|
|
374
|
+
}.join(";")
|
|
375
|
+
if node.else_clause
|
|
376
|
+
eb = node.else_clause.statements ? r_stmt(node.else_clause.statements) : nil
|
|
377
|
+
body += ";else#{fmt_body(eb)}"
|
|
378
|
+
end
|
|
379
|
+
body + ";end"
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def r_case_match(node)
|
|
383
|
+
body = "case #{r(node.predicate)};"
|
|
384
|
+
body += node.conditions.map { |in_node|
|
|
385
|
+
"in #{r_in_pattern(in_node.pattern)}#{fmt_body(r_stmt(in_node.statements))}"
|
|
386
|
+
}.join(";")
|
|
387
|
+
if node.else_clause
|
|
388
|
+
body += ";else#{fmt_body(r_stmt(node.else_clause.statements))}"
|
|
389
|
+
end
|
|
390
|
+
body + ";end"
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# `in PATTERN if GUARD` parses with the guard as an If/Unless node
|
|
394
|
+
# wrapping the pattern — rendering that as a statement-if corrupts the
|
|
395
|
+
# clause into `in if guard;pattern;end`. Guards cannot nest, so the
|
|
396
|
+
# wrapped node is always a plain pattern.
|
|
397
|
+
def r_in_pattern(pattern)
|
|
398
|
+
case pattern
|
|
399
|
+
when Prism::IfNode, Prism::UnlessNode
|
|
400
|
+
keyword = pattern.is_a?(Prism::IfNode) ? 'if' : 'unless'
|
|
401
|
+
# a guard's IfNode always wraps a non-empty statements body
|
|
402
|
+
"#{r_pattern(pattern.statements.body[0])} #{keyword} #{r(pattern.predicate)}" # steep:ignore NoMethod
|
|
403
|
+
else
|
|
404
|
+
r_pattern(pattern)
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Patterns are their own little language: `nil` there is the nil
|
|
409
|
+
# pattern and `()` is a syntax error, so pattern positions dispatch
|
|
410
|
+
# through here. Anything that reads the same in both languages —
|
|
411
|
+
# other literals, constants, pins, capture targets — falls through
|
|
412
|
+
# to r.
|
|
413
|
+
def r_pattern(node)
|
|
414
|
+
case node
|
|
415
|
+
when Prism::NilNode then 'nil'
|
|
416
|
+
when Prism::ParenthesesNode
|
|
417
|
+
# pattern parens always wrap exactly one pattern
|
|
418
|
+
inner = AstUtils.unwrap_statements(node) #: Prism::Node
|
|
419
|
+
"(#{r_pattern(inner)})"
|
|
420
|
+
when Prism::AlternationPatternNode then "#{r_pattern(node.left)} | #{r_pattern(node.right)}"
|
|
421
|
+
when Prism::CapturePatternNode then "#{r_pattern(node.value)}=>#{r_multi_target(node.target)}"
|
|
422
|
+
else r(node)
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def r_for(node)
|
|
427
|
+
idx = node.index
|
|
428
|
+
target = idx.is_a?(Prism::LocalVariableTargetNode) ? idx.name.to_s : idx.slice
|
|
429
|
+
"for #{target} in #{r(node.collection)}#{fmt_body(r_stmt(node.statements))};end"
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# --- Return / Break / Next ---
|
|
433
|
+
|
|
434
|
+
MODIFIER_KEYWORD_NODES = [Prism::IfNode, Prism::UnlessNode, Prism::WhileNode, Prism::UntilNode].freeze
|
|
435
|
+
|
|
436
|
+
def r_return(node) = r_jump('return', node.arguments)
|
|
437
|
+
def r_break(node) = r_jump('break', node.arguments)
|
|
438
|
+
|
|
439
|
+
# A plain symbol drops its colon (`alias a b`); anything else — an
|
|
440
|
+
# interpolated symbol — must keep its own syntax.
|
|
441
|
+
def r_alias_name(node) = node.is_a?(Prism::SymbolNode) ? node.value.to_s : r(node)
|
|
442
|
+
def r_next(node) = r_jump('next', node.arguments)
|
|
443
|
+
|
|
444
|
+
def r_jump(keyword, arguments)
|
|
445
|
+
args = arguments&.arguments
|
|
446
|
+
return keyword if args.nil? || args.empty?
|
|
447
|
+
result = args.map { |a| r_delimited(a) }.join(',')
|
|
448
|
+
first = args.first
|
|
449
|
+
unwrapped = first.is_a?(Prism::ParenthesesNode) ? AstUtils.unwrap_statements(first) : first
|
|
450
|
+
MODIFIER_KEYWORD_NODES.any? { |t| unwrapped.is_a?(t) } ? "#{keyword}(#{result})" : "#{keyword} #{result}"
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# --- Compound assignments ---
|
|
454
|
+
|
|
455
|
+
def r_compound_write(node)
|
|
456
|
+
target = case node
|
|
457
|
+
when Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode
|
|
458
|
+
r(node.target)
|
|
459
|
+
when Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode
|
|
460
|
+
call_target(node)
|
|
461
|
+
when Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode
|
|
462
|
+
idx_target(node)
|
|
463
|
+
else
|
|
464
|
+
node.name
|
|
465
|
+
end
|
|
466
|
+
# binary_operator exists exactly on the *Operator* variants selected here
|
|
467
|
+
op = node.class.name.include?('Operator') ? node.binary_operator : (node.class.name.include?('Or') ? '||' : '&&') # steep:ignore NoMethod
|
|
468
|
+
compound(target, op, node)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def compound(target, op, node) = "#{target}#{op}=#{r(node.value)}"
|
|
472
|
+
|
|
473
|
+
def call_target(node)
|
|
474
|
+
recv_str = r(node.receiver)
|
|
475
|
+
base = node.read_name.to_s.delete_suffix('=')
|
|
476
|
+
call_op = node.safe_navigation? ? '&.' : '.'
|
|
477
|
+
"#{recv_str}#{call_op}#{base}"
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def idx_target(node)
|
|
481
|
+
keys = (node.arguments&.arguments || []).map { |a| r_delimited(a) }.join(',')
|
|
482
|
+
"#{r(node.receiver)}[#{keys}]"
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# --- Literals ---
|
|
486
|
+
|
|
487
|
+
def r_symbol(node)
|
|
488
|
+
sym = node.value.to_sym # steep:ignore NoMethod
|
|
489
|
+
name = sym.to_s
|
|
490
|
+
if name.match?(/\A[a-zA-Z_]\w*[?!=]?\z/) || BINARY_OPERATORS.include?(sym) || %i[[] []=].include?(sym)
|
|
491
|
+
":#{name}"
|
|
492
|
+
elsif name.start_with?('$') && !name.include?('\\')
|
|
493
|
+
# A backslash never survives the bare form — `:$\` does not parse —
|
|
494
|
+
# so those fall through to the quoted form, where the raw content
|
|
495
|
+
# (Prism's value keeps escapes as written) reproduces itself.
|
|
496
|
+
":#{name}"
|
|
497
|
+
else
|
|
498
|
+
":\"#{name.gsub('"', '\\"')}\""
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def r_regexp(node)
|
|
503
|
+
content = node.content
|
|
504
|
+
content = escape_regexp_slash(content) unless node.opening == '/'
|
|
505
|
+
flags = node.closing[1..]
|
|
506
|
+
"/#{content}/#{flags}"
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def escape_regexp_slash(content)
|
|
510
|
+
result = +""
|
|
511
|
+
i = 0
|
|
512
|
+
while i < content.size
|
|
513
|
+
if content[i] == '\\'
|
|
514
|
+
result << content[i] # steep:ignore ArgumentTypeMismatch
|
|
515
|
+
i += 1
|
|
516
|
+
result << content[i] if i < content.size # steep:ignore ArgumentTypeMismatch
|
|
517
|
+
i += 1
|
|
518
|
+
elsif content[i] == '/'
|
|
519
|
+
result << '\\/'
|
|
520
|
+
i += 1
|
|
521
|
+
else
|
|
522
|
+
result << content[i] # steep:ignore ArgumentTypeMismatch
|
|
523
|
+
i += 1
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
result
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# --- Collections ---
|
|
530
|
+
|
|
531
|
+
# A word must survive %w/%i verbatim: no whitespace, brackets, or
|
|
532
|
+
# backslash (those are the notation's escapes).
|
|
533
|
+
PERCENT_SAFE_WORD = /\A[^\s\[\]\\]+\z/
|
|
534
|
+
|
|
535
|
+
def r_array(node)
|
|
536
|
+
bracket = "[#{node.elements.map { r_delimited(_1) }.join(',')}]"
|
|
537
|
+
percent = percent_array_form(node)
|
|
538
|
+
percent && percent.bytesize < bracket.bytesize ? percent : bracket
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# The %-spelling of an all-static word array, whatever the source
|
|
542
|
+
# wrote — bracket, %w/%i, or a %W/%I that never interpolates. nil
|
|
543
|
+
# when the elements aren't uniformly plain words; the caller keeps
|
|
544
|
+
# whichever spelling is shorter (bracket on ties, so single-char
|
|
545
|
+
# strings stay available to CharShorten).
|
|
546
|
+
def percent_array_form(node)
|
|
547
|
+
elements = node.elements
|
|
548
|
+
words, kind = if elements.all? { |e| e.is_a?(Prism::SymbolNode) }
|
|
549
|
+
[elements.map { |e| e.value.to_s }, 'i'] # steep:ignore NoMethod
|
|
550
|
+
elsif elements.all? { |e| e.is_a?(Prism::StringNode) }
|
|
551
|
+
[elements.map(&:unescaped), 'w'] # steep:ignore BlockTypeMismatch
|
|
552
|
+
end
|
|
553
|
+
return nil unless words&.any?
|
|
554
|
+
return nil unless words.all? { |w| w.match?(PERCENT_SAFE_WORD) }
|
|
555
|
+
|
|
556
|
+
"%#{kind}[#{words.join(' ')}]"
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# The shortest spelling that denotes the same decimal value: such a
|
|
560
|
+
# respelling parses to the identical double, so only the bytes change.
|
|
561
|
+
# One shape covers plain and exponent forms — pull the digits
|
|
562
|
+
# together, fold leading/trailing zeros into the exponent — and the
|
|
563
|
+
# final to_f comparison is a belt-and-suspenders check.
|
|
564
|
+
def r_float(node)
|
|
565
|
+
s = node.value.to_s
|
|
566
|
+
m = s.match(/\A(-?)(\d+)\.(\d+)(?:e([+-]?\d+))?\z/)
|
|
567
|
+
return s unless m
|
|
568
|
+
|
|
569
|
+
frac = m[3].to_s
|
|
570
|
+
exp = m[4].to_i - frac.size
|
|
571
|
+
digits = "#{m[2]}#{frac}".sub(/\A0+/, '')
|
|
572
|
+
digits = digits.sub(/0+\z/) { |z| exp += z.size; '' }
|
|
573
|
+
return s if digits.empty? || exp == 0
|
|
574
|
+
|
|
575
|
+
cand = "#{m[1]}#{digits}e#{exp}"
|
|
576
|
+
cand.size < s.size && cand.to_f == node.value ? cand : s
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# Parenthesized by default: `..` binds below almost everything, so in
|
|
580
|
+
# an unknown position the parens are what keep the parse. A position
|
|
581
|
+
# with its own delimiters (argument lists, subscripts, array and hash
|
|
582
|
+
# literals, when conditions, splats, pins) takes the bare form.
|
|
583
|
+
def r_range(node, bare: false)
|
|
584
|
+
op = node.exclude_end? ? '...' : '..'
|
|
585
|
+
left = node.left ? r(node.left) : ''
|
|
586
|
+
right = node.right ? r(node.right) : ''
|
|
587
|
+
bare ? "#{left}#{op}#{right}" : "(#{left}#{op}#{right})"
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def r_delimited(node)
|
|
591
|
+
inner = AstUtils.unwrap_statements(node)
|
|
592
|
+
inner.is_a?(Prism::RangeNode) ? r_range(inner, bare: true) : r(node)
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# A flip-flop keeps its operands parenthesized — their own rendering,
|
|
596
|
+
# not a slice of the source, so interior spacing compacts like
|
|
597
|
+
# everything else.
|
|
598
|
+
def r_flipflop(node)
|
|
599
|
+
op = node.exclude_end? ? '...' : '..'
|
|
600
|
+
left = node.left ? "(#{r(node.left)})" : ''
|
|
601
|
+
right = node.right ? "(#{r(node.right)})" : ''
|
|
602
|
+
"#{left}#{op}#{right}"
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def r_hash(node)
|
|
606
|
+
"{" + node.elements.map { |e|
|
|
607
|
+
e.is_a?(Prism::AssocSplatNode) ? "**#{r(e.value)}" : r_assoc(e)
|
|
608
|
+
}.join(',') + "}"
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def r_keyword_hash(node)
|
|
612
|
+
node.elements.map { |e|
|
|
613
|
+
e.is_a?(Prism::AssocSplatNode) ? "**#{r(e.value)}" : r_assoc(e)
|
|
614
|
+
}.join(',')
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def r_assoc(element)
|
|
618
|
+
key = element.key
|
|
619
|
+
val = element.value
|
|
620
|
+
val_str = r_delimited(val)
|
|
621
|
+
if key.is_a?(Prism::SymbolNode) && key.value.to_s.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
|
|
622
|
+
key_name = key.value.to_s
|
|
623
|
+
return "#{key_name}:" if val.is_a?(Prism::ImplicitNode)
|
|
624
|
+
sep = val_str.start_with?(':') ? ': ' : ':'
|
|
625
|
+
"#{key_name}#{sep}#{val_str}"
|
|
626
|
+
else
|
|
627
|
+
key_str = r(key)
|
|
628
|
+
separator = if key.is_a?(Prism::SymbolNode) && !key.value.to_s.match?(/[a-zA-Z0-9_]\z/)
|
|
629
|
+
' =>'
|
|
630
|
+
else
|
|
631
|
+
'=>'
|
|
632
|
+
end
|
|
633
|
+
"#{key_str}#{separator}#{val_str}"
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# --- Strings ---
|
|
638
|
+
|
|
639
|
+
def r_string(node)
|
|
640
|
+
if node.opening == '"' || node.opening.nil?
|
|
641
|
+
"\"#{node.content}\""
|
|
642
|
+
else
|
|
643
|
+
node.unescaped.inspect
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# --- Interpolation ---
|
|
648
|
+
|
|
649
|
+
def r_interp_string(node)
|
|
650
|
+
needs_escape = node.opening != '"'
|
|
651
|
+
"\"" + node.parts.map { |p| interp_part(p, escape_quotes: needs_escape) }.join + "\""
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def r_interp_symbol(node)
|
|
655
|
+
":\"" + node.parts.map { |p| interp_part(p) }.join + "\""
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
def r_interp_regexp(node)
|
|
659
|
+
needs_slash_escape = node.opening != '/'
|
|
660
|
+
parts = node.parts.map { |p|
|
|
661
|
+
if needs_slash_escape && p.is_a?(Prism::StringNode)
|
|
662
|
+
escape_regexp_slash(p.content)
|
|
663
|
+
else
|
|
664
|
+
interp_part(p)
|
|
665
|
+
end
|
|
666
|
+
}
|
|
667
|
+
"/" + parts.join + "/#{node.closing[1..]}"
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def interp_part(part, escape_quotes: false)
|
|
671
|
+
case part
|
|
672
|
+
when Prism::StringNode
|
|
673
|
+
if escape_quotes
|
|
674
|
+
escape_for_dquote(part.unescaped)
|
|
675
|
+
else
|
|
676
|
+
part.content
|
|
677
|
+
end
|
|
678
|
+
when Prism::EmbeddedStatementsNode then "\#{#{r_stmt(part.statements)}}"
|
|
679
|
+
when Prism::EmbeddedVariableNode then "\#{#{r(part.variable)}}"
|
|
680
|
+
else "\#{#{r(part)}}"
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def escape_for_dquote(str)
|
|
685
|
+
result = +""
|
|
686
|
+
i = 0
|
|
687
|
+
while i < str.size
|
|
688
|
+
c = str[i]
|
|
689
|
+
case c
|
|
690
|
+
when '\\' then result << '\\\\'
|
|
691
|
+
when '"' then result << '\\"'
|
|
692
|
+
when "\n" then result << '\\n'
|
|
693
|
+
when "\t" then result << '\\t'
|
|
694
|
+
when "\r" then result << '\\r'
|
|
695
|
+
when "\0" then result << '\\0'
|
|
696
|
+
when '#'
|
|
697
|
+
if i + 1 < str.size && '{@$'.include?(str[i + 1]) # steep:ignore ArgumentTypeMismatch
|
|
698
|
+
result << '\\#'
|
|
699
|
+
else
|
|
700
|
+
result << '#'
|
|
701
|
+
end
|
|
702
|
+
else
|
|
703
|
+
result << c # steep:ignore ArgumentTypeMismatch
|
|
704
|
+
end
|
|
705
|
+
i += 1
|
|
706
|
+
end
|
|
707
|
+
result
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
# --- Classes / Modules ---
|
|
711
|
+
|
|
712
|
+
def r_class(node)
|
|
713
|
+
class_name = node.constant_path.slice
|
|
714
|
+
superclass_str = node.superclass ? "<#{r(node.superclass)}" : ''
|
|
715
|
+
"class #{class_name}#{superclass_str}#{fmt_body(r_stmt(node.body))};end"
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
def r_module(node)
|
|
719
|
+
"module #{node.constant_path.slice}#{fmt_body(r_stmt(node.body))};end"
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
def r_singleton_class(node)
|
|
723
|
+
"class<<#{r(node.expression)}#{fmt_body(r_stmt(node.body))};end"
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
# --- Logic ---
|
|
727
|
+
|
|
728
|
+
# Always the operator spelling. The keyword forms bind below
|
|
729
|
+
# assignment, so `and`/`or` accept operands `&&`/`||` cannot take
|
|
730
|
+
# bare — assignments, jumps with a value, control expressions — and
|
|
731
|
+
# those operands are parenthesized here. Rendering the keyword would
|
|
732
|
+
# need the surrounding position to be known (`z=(a and b)` unwrapped
|
|
733
|
+
# is a different program), and the renderer is position-free.
|
|
734
|
+
def r_and(node)
|
|
735
|
+
"#{logic_operand(node.left, tight: true)}&&#{logic_operand(node.right, tight: true)}"
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def r_or(node)
|
|
739
|
+
"#{logic_operand(node.left, tight: false)}||#{logic_operand(node.right, tight: false)}"
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def logic_operand(operand, tight:)
|
|
743
|
+
# Classified through source parens: `x&&(y||z)` arrives as a
|
|
744
|
+
# ParenthesesNode operand whose parens must survive.
|
|
745
|
+
inner = AstUtils.unwrap_statements(operand)
|
|
746
|
+
Compactor.loose_logic_operand?(inner, tight: tight) ? "(#{r(inner)})" : r(operand)
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
# --- Error handling ---
|
|
750
|
+
|
|
751
|
+
def r_begin(node)
|
|
752
|
+
parts = ["begin#{fmt_body(r_stmt(node.statements))}"]
|
|
753
|
+
rescue_node = node.rescue_clause
|
|
754
|
+
while rescue_node
|
|
755
|
+
rp = ';rescue'
|
|
756
|
+
exceptions = rescue_node.exceptions || []
|
|
757
|
+
# `rescue StandardError` is what a bare rescue already means —
|
|
758
|
+
# unless the program redefines StandardError, when the bare form
|
|
759
|
+
# would resolve differently.
|
|
760
|
+
unless exceptions.empty? || (bare_standard_error?(exceptions) && !standard_error_redefined?)
|
|
761
|
+
rp += " #{exceptions.map { |e| r(e) }.join(',')}"
|
|
762
|
+
end
|
|
763
|
+
if rescue_node.reference && rescue_var_used?(rescue_node)
|
|
764
|
+
# the reference is a name-bearing target node in valid rescue clauses
|
|
765
|
+
rp += "=>#{rescue_node.reference.name}" # steep:ignore NoMethod
|
|
766
|
+
end
|
|
767
|
+
parts << "#{rp}#{fmt_body(r_stmt(rescue_node.statements))}"
|
|
768
|
+
rescue_node = rescue_node.subsequent
|
|
769
|
+
end
|
|
770
|
+
parts << ";else#{fmt_body(r_stmt(node.else_clause.statements))}" if node.else_clause
|
|
771
|
+
parts << ";ensure#{fmt_body(r_stmt(node.ensure_clause.statements))}" if node.ensure_clause
|
|
772
|
+
parts << ';end'
|
|
773
|
+
parts.join
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
def bare_standard_error?(exceptions)
|
|
777
|
+
exceptions.size == 1 &&
|
|
778
|
+
exceptions[0].is_a?(Prism::ConstantReadNode) &&
|
|
779
|
+
exceptions[0].name == :StandardError
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
# Lazy: only a literal `rescue StandardError` clause ever needs the
|
|
783
|
+
# answer, so most programs never pay for the walk. Any definition-ish
|
|
784
|
+
# node whose written name is StandardError counts — over-matching a
|
|
785
|
+
# nested Foo::StandardError only forgoes a few bytes.
|
|
786
|
+
def standard_error_redefined?
|
|
787
|
+
# a non-nil memo is always one of the two bools
|
|
788
|
+
return @standard_error_redefined unless @standard_error_redefined.nil? # steep:ignore ReturnTypeMismatch
|
|
789
|
+
|
|
790
|
+
found = false
|
|
791
|
+
AstUtils.each_node(@prism_ast) do |node|
|
|
792
|
+
case node
|
|
793
|
+
when Prism::ClassNode
|
|
794
|
+
path = node.constant_path
|
|
795
|
+
name = path.is_a?(Prism::ConstantReadNode) || path.is_a?(Prism::ConstantPathNode) ? path.name : nil
|
|
796
|
+
found = true if name == :StandardError
|
|
797
|
+
when Prism::ConstantWriteNode, Prism::ConstantTargetNode
|
|
798
|
+
found = true if node.name == :StandardError
|
|
799
|
+
when Prism::ConstantPathWriteNode
|
|
800
|
+
found = true if node.target.name == :StandardError
|
|
801
|
+
when Prism::ConstantPathTargetNode
|
|
802
|
+
found = true if node.name == :StandardError
|
|
803
|
+
end
|
|
804
|
+
end
|
|
805
|
+
@standard_error_redefined = found
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
# --- Other ---
|
|
809
|
+
|
|
810
|
+
def r_super(node)
|
|
811
|
+
args = node.arguments&.arguments&.map { |a| r_delimited(a) } || []
|
|
812
|
+
args_str = args.join(',')
|
|
813
|
+
args_str.empty? ? 'super()' : "super(#{args_str})"
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
def r_defined(node)
|
|
817
|
+
"defined?(#{r(node.value)})"
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
# --- Multi-write ---
|
|
821
|
+
|
|
822
|
+
def r_multi_write(node)
|
|
823
|
+
targets = node.lefts.map { |t| r_multi_target(t) }
|
|
824
|
+
if node.rest
|
|
825
|
+
case node.rest
|
|
826
|
+
when Prism::SplatNode
|
|
827
|
+
targets << (node.rest.expression ? "*#{r_multi_target(node.rest.expression)}" : '*')
|
|
828
|
+
when Prism::ImplicitRestNode
|
|
829
|
+
targets << '*'
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
targets.concat(node.rights.map { |t| r_multi_target(t) })
|
|
833
|
+
value = node.value
|
|
834
|
+
sep = targets.last&.end_with?('*') ? ' ' : ''
|
|
835
|
+
if value.is_a?(Prism::ArrayNode) && (value.opening_loc.nil? || value.elements.size >= 2)
|
|
836
|
+
elements = value.elements.map { |e| r(e) }
|
|
837
|
+
"#{targets.join(',')}#{sep}=#{elements.join(',')}"
|
|
838
|
+
else
|
|
839
|
+
"#{targets.join(',')}#{sep}=#{r(value)}"
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def r_multi_target(target)
|
|
844
|
+
case target
|
|
845
|
+
when Prism::LocalVariableTargetNode, Prism::InstanceVariableTargetNode,
|
|
846
|
+
Prism::ClassVariableTargetNode, Prism::GlobalVariableTargetNode
|
|
847
|
+
target.name.to_s
|
|
848
|
+
when Prism::ConstantTargetNode
|
|
849
|
+
target.name.to_s
|
|
850
|
+
when Prism::ConstantPathTargetNode
|
|
851
|
+
target.parent ? "#{r(target.parent)}::#{target.name}" : "::#{target.name}"
|
|
852
|
+
when Prism::MultiTargetNode
|
|
853
|
+
parts = target.lefts.map { |t| r_multi_target(t) }
|
|
854
|
+
if target.rest
|
|
855
|
+
case target.rest
|
|
856
|
+
when Prism::SplatNode
|
|
857
|
+
parts << (target.rest.expression ? "*#{r_multi_target(target.rest.expression)}" : '*')
|
|
858
|
+
when Prism::ImplicitRestNode
|
|
859
|
+
parts << '*'
|
|
860
|
+
end
|
|
861
|
+
end
|
|
862
|
+
parts.concat(target.rights.map { |t| r_multi_target(t) })
|
|
863
|
+
parts.join(',')
|
|
864
|
+
when Prism::IndexTargetNode
|
|
865
|
+
args = target.arguments&.arguments || []
|
|
866
|
+
"#{r(target.receiver)}[#{args.map { |a| r(a) }.join(',')}]"
|
|
867
|
+
when Prism::SplatNode
|
|
868
|
+
target.expression ? "*#{r_multi_target(target.expression)}" : '*'
|
|
869
|
+
else
|
|
870
|
+
r(target)
|
|
871
|
+
end
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
# --- Pattern matching ---
|
|
875
|
+
|
|
876
|
+
def r_array_pattern(node)
|
|
877
|
+
parts = node.requireds.map { |n| r_pattern(n) }
|
|
878
|
+
if node.rest
|
|
879
|
+
parts << if node.rest.is_a?(Prism::SplatNode) && node.rest.expression
|
|
880
|
+
"*#{r(node.rest.expression)}"
|
|
881
|
+
else
|
|
882
|
+
'*'
|
|
883
|
+
end
|
|
884
|
+
end
|
|
885
|
+
parts.concat(node.posts.map { |p| r_pattern(p) })
|
|
886
|
+
"[#{parts.join(',')}]"
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
def r_hash_pattern(node)
|
|
890
|
+
# hash-pattern keys are always plain SymbolNodes
|
|
891
|
+
parts = node.elements.map { |a| "#{a.key.value}: #{r_pattern(a.value)}" } # steep:ignore NoMethod
|
|
892
|
+
if node.rest
|
|
893
|
+
case node.rest
|
|
894
|
+
when Prism::AssocSplatNode
|
|
895
|
+
parts << (node.rest.value ? "**#{r(node.rest.value)}" : '**')
|
|
896
|
+
when Prism::NoKeywordsParameterNode
|
|
897
|
+
parts << '**nil'
|
|
898
|
+
end
|
|
899
|
+
end
|
|
900
|
+
"{#{parts.join(',')}}"
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
def r_find_pattern(node)
|
|
904
|
+
parts = [] #: Array[String]
|
|
905
|
+
if node.left
|
|
906
|
+
parts << if node.left.is_a?(Prism::SplatNode) && node.left.expression
|
|
907
|
+
"*#{r(node.left.expression)}"
|
|
908
|
+
else
|
|
909
|
+
'*'
|
|
910
|
+
end
|
|
911
|
+
end
|
|
912
|
+
parts.concat(node.requireds.map { |n| r_pattern(n) })
|
|
913
|
+
if node.right
|
|
914
|
+
parts << if node.right.is_a?(Prism::SplatNode) && node.right.expression
|
|
915
|
+
"*#{r(node.right.expression)}"
|
|
916
|
+
else
|
|
917
|
+
'*'
|
|
918
|
+
end
|
|
919
|
+
end
|
|
920
|
+
"[#{parts.join(',')}]"
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
# --- Helpers ---
|
|
924
|
+
|
|
925
|
+
def build_call_args(node, block_pass = nil)
|
|
926
|
+
raw_args = node.arguments&.arguments || []
|
|
927
|
+
args = [] #: Array[String]
|
|
928
|
+
raw_args.each do |arg|
|
|
929
|
+
case arg
|
|
930
|
+
when Prism::SplatNode
|
|
931
|
+
args << (arg.expression ? "*#{r_delimited(arg.expression)}" : '*')
|
|
932
|
+
when Prism::KeywordHashNode
|
|
933
|
+
arg.elements.each do |el|
|
|
934
|
+
args << (el.is_a?(Prism::AssocSplatNode) ? "**#{r(el.value)}" : r_assoc(el))
|
|
935
|
+
end
|
|
936
|
+
when Prism::ForwardingArgumentsNode
|
|
937
|
+
args << '...'
|
|
938
|
+
else
|
|
939
|
+
args << r_delimited(arg)
|
|
940
|
+
end
|
|
941
|
+
end
|
|
942
|
+
args << block_pass if block_pass
|
|
943
|
+
return '' if args.empty?
|
|
944
|
+
"(#{args.join(',')})"
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
def build_def_params(params)
|
|
948
|
+
return [] unless params
|
|
949
|
+
|
|
950
|
+
req = (params.requireds || []).map { |p|
|
|
951
|
+
case p
|
|
952
|
+
when Prism::RequiredParameterNode then p.name.to_s
|
|
953
|
+
when Prism::MultiTargetNode then r_multi_target_param(p)
|
|
954
|
+
else p.slice
|
|
955
|
+
end
|
|
956
|
+
}
|
|
957
|
+
opt = (params.optionals || []).map { |p| "#{p.name}=#{r(p.value)}" }
|
|
958
|
+
rest = if params.rest.is_a?(Prism::RestParameterNode)
|
|
959
|
+
[params.rest.name ? "*#{params.rest.name}" : '*']
|
|
960
|
+
else
|
|
961
|
+
[] #: Array[String]
|
|
962
|
+
end
|
|
963
|
+
post = (params.posts || []).map { |p|
|
|
964
|
+
p.is_a?(Prism::RequiredParameterNode) ? p.name.to_s : p.slice
|
|
965
|
+
}
|
|
966
|
+
req_kw = [] #: Array[String]
|
|
967
|
+
opt_kw = [] #: Array[String]
|
|
968
|
+
(params.keywords || []).each do |p|
|
|
969
|
+
case p
|
|
970
|
+
when Prism::RequiredKeywordParameterNode then req_kw << "#{p.name}:"
|
|
971
|
+
when Prism::OptionalKeywordParameterNode
|
|
972
|
+
val = r(p.value)
|
|
973
|
+
sep = val.start_with?(':') ? ' ' : ''
|
|
974
|
+
opt_kw << "#{p.name}:#{sep}#{val}"
|
|
975
|
+
end
|
|
976
|
+
end
|
|
977
|
+
rest_kw = if params.keyword_rest.is_a?(Prism::KeywordRestParameterNode)
|
|
978
|
+
[params.keyword_rest.name ? "**#{params.keyword_rest.name}" : '**']
|
|
979
|
+
elsif params.keyword_rest.is_a?(Prism::NoKeywordsParameterNode)
|
|
980
|
+
['**nil']
|
|
981
|
+
elsif params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
982
|
+
['...']
|
|
983
|
+
else
|
|
984
|
+
[] #: Array[String]
|
|
985
|
+
end
|
|
986
|
+
block_param = if params.block.is_a?(Prism::BlockParameterNode)
|
|
987
|
+
[params.block.name ? "&#{params.block.name}" : '&']
|
|
988
|
+
else
|
|
989
|
+
[] #: Array[String]
|
|
990
|
+
end
|
|
991
|
+
req + opt + rest + post + req_kw + opt_kw + rest_kw + block_param
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
def r_multi_target_param(target)
|
|
995
|
+
parts = target.lefts.map { |t|
|
|
996
|
+
case t
|
|
997
|
+
when Prism::RequiredParameterNode then t.name.to_s
|
|
998
|
+
when Prism::MultiTargetNode then r_multi_target_param(t)
|
|
999
|
+
else t.slice
|
|
1000
|
+
end
|
|
1001
|
+
}
|
|
1002
|
+
if target.rest
|
|
1003
|
+
parts << (target.rest.is_a?(Prism::SplatNode) && target.rest.expression ? "*#{target.rest.expression.slice}" : '*')
|
|
1004
|
+
end
|
|
1005
|
+
parts.concat(target.rights.map { |t| t.respond_to?(:name) ? t.name.to_s : t.slice }) # steep:ignore NoMethod
|
|
1006
|
+
"(#{parts.join(',')})"
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
def build_lambda_params(block_params)
|
|
1010
|
+
return "" unless block_params.is_a?(Prism::BlockParametersNode)
|
|
1011
|
+
inner = block_params.parameters
|
|
1012
|
+
return "()" unless inner
|
|
1013
|
+
all = build_def_params(inner)
|
|
1014
|
+
all.empty? ? "" : "(#{all.join(',')})"
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
def extract_block_params(block)
|
|
1018
|
+
return [] unless block.parameters.is_a?(Prism::BlockParametersNode) && block.parameters.parameters
|
|
1019
|
+
build_def_params(block.parameters.parameters)
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
def recv_with_op(recv_node, call_op)
|
|
1023
|
+
return '' if recv_node.nil?
|
|
1024
|
+
recv_str = r(recv_node)
|
|
1025
|
+
inner = AstUtils.unwrap_statements(recv_node)
|
|
1026
|
+
# Unary calls must keep their parens under a chain: `(-vec).to_a`
|
|
1027
|
+
# stripped to `-vec.to_a` rebinds as `-(vec.to_a)` — String#-@ even
|
|
1028
|
+
# makes that run silently instead of failing.
|
|
1029
|
+
needs_wrap = binary_operator_call?(inner) || AstUtils.logical_op?(inner) ||
|
|
1030
|
+
inner.is_a?(Prism::TrueNode) || inner.is_a?(Prism::FalseNode) ||
|
|
1031
|
+
(inner.is_a?(Prism::CallNode) && %i[! -@ +@].include?(inner.name))
|
|
1032
|
+
needs_wrap ? "(#{recv_str})#{call_op}" : "#{recv_str}#{call_op}"
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
def wrap_operand(node, parent_op, side)
|
|
1036
|
+
return ['', true] if node.nil?
|
|
1037
|
+
str = r(node)
|
|
1038
|
+
inner = AstUtils.unwrap_statements(node)
|
|
1039
|
+
return ["(#{str})", true] if AstUtils.logical_op?(inner)
|
|
1040
|
+
# ** binds tighter than unary minus: `(-a)**b` must not become -a**b.
|
|
1041
|
+
if parent_op == :** && inner.is_a?(Prism::CallNode) && %i[-@ +@].include?(inner.name)
|
|
1042
|
+
return ["(#{str})", true]
|
|
1043
|
+
end
|
|
1044
|
+
if binary_operator_call?(inner)
|
|
1045
|
+
pp = OPERATOR_PRECEDENCE[parent_op]
|
|
1046
|
+
# binary_operator_call? has established inner is a CallNode
|
|
1047
|
+
cp = OPERATOR_PRECEDENCE[inner.name] # steep:ignore NoMethod
|
|
1048
|
+
if pp && cp
|
|
1049
|
+
return ["(#{str})", true] if cp < pp
|
|
1050
|
+
return ["(#{str})", true] if cp == pp && side == :right
|
|
1051
|
+
return ["(#{str})", true] if cp == pp && NON_ASSOCIATIVE_OPS.include?(parent_op)
|
|
1052
|
+
end
|
|
1053
|
+
end
|
|
1054
|
+
[str, false]
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
def binary_op_separator(recv_node, recv_wrapped, op)
|
|
1058
|
+
return '' if recv_wrapped
|
|
1059
|
+
inner = AstUtils.unwrap_statements(recv_node)
|
|
1060
|
+
if op == :'!~'
|
|
1061
|
+
AstUtils.ends_with_name_char?(inner) ? ' ' : ''
|
|
1062
|
+
elsif op.to_s.start_with?('=') && inner.is_a?(Prism::CallNode) && inner.name.to_s.end_with?('?', '!')
|
|
1063
|
+
' '
|
|
1064
|
+
else
|
|
1065
|
+
''
|
|
1066
|
+
end
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
def build_if_chain(node)
|
|
1070
|
+
parts = [] #: Array[String]
|
|
1071
|
+
current = node
|
|
1072
|
+
keyword = "if"
|
|
1073
|
+
loop do
|
|
1074
|
+
inner = current.statements ? AstUtils.unwrap_statements(current.statements) : nil
|
|
1075
|
+
then_body = (inner.nil? || inner.is_a?(Prism::NilNode)) ? nil : r_stmt(current.statements)
|
|
1076
|
+
parts << "#{keyword} #{r(current.predicate)}#{fmt_body(then_body)}"
|
|
1077
|
+
subsequent = current.subsequent
|
|
1078
|
+
if subsequent.is_a?(Prism::IfNode)
|
|
1079
|
+
current = subsequent
|
|
1080
|
+
keyword = "elsif"
|
|
1081
|
+
else
|
|
1082
|
+
if subsequent.is_a?(Prism::ElseNode)
|
|
1083
|
+
inner = subsequent.statements ? AstUtils.unwrap_statements(subsequent.statements) : nil
|
|
1084
|
+
unless inner.nil? || inner.is_a?(Prism::NilNode)
|
|
1085
|
+
eb = r_stmt(subsequent.statements)
|
|
1086
|
+
parts << "else#{fmt_body(eb)}" if eb && !eb.empty?
|
|
1087
|
+
end
|
|
1088
|
+
end
|
|
1089
|
+
break
|
|
1090
|
+
end
|
|
1091
|
+
end
|
|
1092
|
+
parts.join(";") + ";end"
|
|
1093
|
+
end
|
|
1094
|
+
|
|
1095
|
+
ASSIGNMENT_NODES = [
|
|
1096
|
+
Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
|
|
1097
|
+
Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode,
|
|
1098
|
+
Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::MultiWriteNode,
|
|
1099
|
+
*COMPOUND_WRITE_NODES,
|
|
1100
|
+
].freeze
|
|
1101
|
+
|
|
1102
|
+
# Match nodes keep parens like assignments do: an endless def body of
|
|
1103
|
+
# `(value in pattern)` stripped bare rebinds the `in` to the def
|
|
1104
|
+
# statement itself and no longer parses.
|
|
1105
|
+
PARENS_KEPT_NODES = [*ASSIGNMENT_NODES, *AstUtils::MATCH_REBIND_NODES].freeze
|
|
1106
|
+
|
|
1107
|
+
PARENS_KEPT_CLASSES = PARENS_KEPT_NODES.to_set.freeze
|
|
1108
|
+
|
|
1109
|
+
JUMP_NODES = [Prism::ReturnNode, Prism::BreakNode, Prism::NextNode].freeze
|
|
1110
|
+
|
|
1111
|
+
# Operand classes that bind at or below the keyword logic forms —
|
|
1112
|
+
# bare under && or || they would re-associate or fail to parse.
|
|
1113
|
+
# Derived from the paren-keeping and modifier-keyword families, so a
|
|
1114
|
+
# class added there joins this rule with it.
|
|
1115
|
+
LOGIC_LOOSE_OPERANDS = [
|
|
1116
|
+
*PARENS_KEPT_NODES, *MODIFIER_KEYWORD_NODES,
|
|
1117
|
+
Prism::CaseNode, Prism::CaseMatchNode, Prism::ForNode, Prism::BeginNode,
|
|
1118
|
+
Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode,
|
|
1119
|
+
Prism::FlipFlopNode, Prism::LambdaNode
|
|
1120
|
+
].freeze
|
|
1121
|
+
LOGIC_LOOSE_OPERAND_CLASSES = LOGIC_LOOSE_OPERANDS.to_set.freeze
|
|
1122
|
+
|
|
1123
|
+
# A jump is loose only when it carries a value: `x&&return` parses,
|
|
1124
|
+
# `x&&return 5` does not.
|
|
1125
|
+
def self.jump_with_value?(node)
|
|
1126
|
+
JUMP_NODES.include?(node.class) && !node.arguments.nil? # steep:ignore NoMethod
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
# The one owner of "does this operand need parens under && / ||".
|
|
1130
|
+
# ControlFlowSimplify asks the same question when it folds nested
|
|
1131
|
+
# conditionals into the compacted dialect. An OrNode operand needs
|
|
1132
|
+
# parens under && but not under ||. Prism node classes are concrete,
|
|
1133
|
+
# so a class-keyed Set replaces an is_a? scan.
|
|
1134
|
+
def self.loose_logic_operand?(node, tight:)
|
|
1135
|
+
LOGIC_LOOSE_OPERAND_CLASSES.include?(node.class) ||
|
|
1136
|
+
(tight && node.is_a?(Prism::OrNode)) ||
|
|
1137
|
+
jump_with_value?(node)
|
|
1138
|
+
end
|
|
1139
|
+
|
|
1140
|
+
def r_parens(node)
|
|
1141
|
+
body = r_stmt(node.body)
|
|
1142
|
+
# Empty parens ARE the nil literal this renderer emits — keeping
|
|
1143
|
+
# them (instead of collapsing to '') is what makes a second pass
|
|
1144
|
+
# over already-minified source round-trip.
|
|
1145
|
+
return '()' if body.empty?
|
|
1146
|
+
inner = AstUtils.unwrap_statements(node)
|
|
1147
|
+
keep = (node.body.is_a?(Prism::StatementsNode) && node.body.body.size > 1) ||
|
|
1148
|
+
PARENS_KEPT_CLASSES.include?(inner.class) ||
|
|
1149
|
+
# `(return 5)` bare after `&&` (or another operator) fails
|
|
1150
|
+
# to parse; a valueless jump is fine bare.
|
|
1151
|
+
Compactor.jump_with_value?(inner)
|
|
1152
|
+
keep ? "(#{body})" : body
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
def fmt_body(body) = body.nil? || body.empty? ? "" : ";#{body}"
|
|
1156
|
+
|
|
1157
|
+
def binary_operator_call?(node)
|
|
1158
|
+
node.is_a?(Prism::CallNode) && node.receiver &&
|
|
1159
|
+
node.arguments&.arguments&.size == 1 && BINARY_OPERATORS.include?(node.name)
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
def setter_call?(node)
|
|
1163
|
+
name = node.name.to_s
|
|
1164
|
+
node.receiver && name.end_with?('=') && name.size > 1 &&
|
|
1165
|
+
!%w[== != <= >= ===].include?(name) && name != '[]='
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1168
|
+
def node_returns_bot?(node)
|
|
1169
|
+
case node
|
|
1170
|
+
when Prism::ReturnNode, Prism::BreakNode, Prism::NextNode then true
|
|
1171
|
+
when Prism::CallNode
|
|
1172
|
+
# Only the Kernel spellings end the flow; `logger.raise(1)` is an
|
|
1173
|
+
# ordinary method call.
|
|
1174
|
+
node.receiver.nil? && (node.name == :raise || node.name == :fail)
|
|
1175
|
+
else false
|
|
1176
|
+
end
|
|
1177
|
+
end
|
|
1178
|
+
|
|
1179
|
+
def rescue_var_used?(rescue_node)
|
|
1180
|
+
# only called when the (name-bearing) reference is present
|
|
1181
|
+
var = rescue_node.reference.name # steep:ignore NoMethod
|
|
1182
|
+
return false unless rescue_node.statements
|
|
1183
|
+
prism_traverse(rescue_node.statements) { |n|
|
|
1184
|
+
return true if n.is_a?(Prism::LocalVariableReadNode) && n.name == var
|
|
1185
|
+
}
|
|
1186
|
+
false
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1189
|
+
def block_uses_it?(body)
|
|
1190
|
+
return false unless body
|
|
1191
|
+
prism_traverse(body) { |n| return true if n.is_a?(Prism::ItLocalVariableReadNode) }
|
|
1192
|
+
false
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
def prism_traverse(node, &block)
|
|
1196
|
+
return unless node
|
|
1197
|
+
yield node
|
|
1198
|
+
node.compact_child_nodes.each { |child| prism_traverse(child, &block) }
|
|
1199
|
+
end
|
|
1200
|
+
end
|
|
1201
|
+
end
|
|
1202
|
+
end
|