unparser 0.6.15 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/unparser/anima.rb +11 -0
- data/lib/unparser/ast/local_variable_scope.rb +28 -24
- data/lib/unparser/ast.rb +18 -22
- data/lib/unparser/buffer.rb +44 -2
- data/lib/unparser/cli.rb +26 -5
- data/lib/unparser/either.rb +6 -6
- data/lib/unparser/emitter/array.rb +0 -4
- data/lib/unparser/emitter/array_pattern.rb +1 -9
- data/lib/unparser/emitter/assignment.rb +7 -8
- data/lib/unparser/emitter/begin.rb +0 -6
- data/lib/unparser/emitter/binary.rb +1 -1
- data/lib/unparser/emitter/block.rb +3 -4
- data/lib/unparser/emitter/def.rb +1 -1
- data/lib/unparser/emitter/dstr.rb +6 -5
- data/lib/unparser/emitter/flow_modifier.rb +0 -6
- data/lib/unparser/emitter/for.rb +1 -1
- data/lib/unparser/emitter/hash.rb +0 -8
- data/lib/unparser/emitter/hash_pattern.rb +1 -1
- data/lib/unparser/emitter/index.rb +0 -4
- data/lib/unparser/emitter/op_assign.rb +0 -10
- data/lib/unparser/emitter/primitive.rb +0 -13
- data/lib/unparser/emitter/regexp.rb +5 -17
- data/lib/unparser/emitter/rescue.rb +7 -1
- data/lib/unparser/emitter/root.rb +2 -9
- data/lib/unparser/emitter/send.rb +1 -5
- data/lib/unparser/emitter/string.rb +31 -0
- data/lib/unparser/emitter.rb +9 -10
- data/lib/unparser/generation.rb +8 -14
- data/lib/unparser/node_details.rb +1 -0
- data/lib/unparser/validation.rb +68 -28
- data/lib/unparser/writer/dynamic_string.rb +128 -135
- data/lib/unparser/writer/regexp.rb +98 -0
- data/lib/unparser/writer/resbody.rb +30 -1
- data/lib/unparser/writer/rescue.rb +2 -6
- data/lib/unparser/writer/send.rb +8 -14
- data/lib/unparser/writer.rb +32 -1
- data/lib/unparser.rb +103 -30
- metadata +15 -13
@@ -5,116 +5,123 @@ module Unparser
|
|
5
5
|
class DynamicString
|
6
6
|
include Writer, Adamantium
|
7
7
|
|
8
|
-
PATTERNS_2 =
|
9
|
-
[
|
10
|
-
%i[str_empty begin].freeze,
|
11
|
-
%i[begin str_nl].freeze
|
12
|
-
].freeze
|
13
|
-
|
14
|
-
PATTERNS_3 =
|
15
|
-
[
|
16
|
-
%i[begin str_nl_eol str_nl_eol].freeze,
|
17
|
-
%i[str_nl_eol begin str_nl_eol].freeze,
|
18
|
-
%i[str_ws begin str_nl_eol].freeze
|
19
|
-
].freeze
|
20
|
-
|
21
8
|
FLAT_INTERPOLATION = %i[ivar cvar gvar nth_ref].to_set.freeze
|
22
9
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
10
|
+
# Amount of dstr children at which heredoc emitting is
|
11
|
+
# preferred, but not guaranteed.
|
12
|
+
HEREDOC_THRESHOLD = 8
|
13
|
+
HEREDOC_DELIMITER = 'HEREDOC'
|
14
|
+
HEREDOC_HEADER = "<<-#{HEREDOC_DELIMITER}".freeze
|
15
|
+
HEREDOC_FOOTER = "#{HEREDOC_DELIMITER}\n".freeze
|
27
16
|
|
28
|
-
|
29
|
-
emit_heredoc_footer
|
30
|
-
end
|
17
|
+
private_constant(*constants(false))
|
31
18
|
|
19
|
+
# The raise below is not reachable if unparser is correctly implemented
|
20
|
+
# but has to exist as I have to assume unparser still has bugs.
|
21
|
+
#
|
22
|
+
# But unless I had such a bug in my test corpus: I cannot enable mutant, and if I
|
23
|
+
# knew about such a bug: I'd fix it so would be back at the start.
|
24
|
+
#
|
25
|
+
# TLDR: Good case for a mutant disable.
|
26
|
+
#
|
27
|
+
# mutant:disable
|
32
28
|
def dispatch
|
33
29
|
if heredoc?
|
34
|
-
|
30
|
+
write(HEREDOC_HEADER)
|
31
|
+
buffer.push_heredoc(heredoc_body)
|
32
|
+
elsif round_tripping_segmented_source
|
33
|
+
write(round_tripping_segmented_source)
|
35
34
|
else
|
36
|
-
|
35
|
+
fail UnsupportedNodeError, "Unparser cannot round trip this node: #{node.inspect}"
|
37
36
|
end
|
38
37
|
end
|
39
38
|
|
40
39
|
private
|
41
40
|
|
42
|
-
def heredoc_header
|
43
|
-
'<<-HEREDOC'
|
44
|
-
end
|
45
|
-
|
46
41
|
def heredoc?
|
47
|
-
|
42
|
+
if children.length >= HEREDOC_THRESHOLD
|
43
|
+
round_trips_heredoc?
|
44
|
+
else
|
45
|
+
round_tripping_segmented_source.nil? # && round_trips_heredoc?
|
46
|
+
end
|
48
47
|
end
|
48
|
+
memoize :heredoc?
|
49
49
|
|
50
|
-
def
|
51
|
-
|
50
|
+
def round_trips_heredoc?
|
51
|
+
round_trips?(source: heredoc_source)
|
52
52
|
end
|
53
|
+
memoize :round_trips_heredoc?
|
53
54
|
|
54
|
-
def
|
55
|
-
|
56
|
-
emit_normal_heredoc_body
|
57
|
-
end
|
55
|
+
def round_tripping_segmented_source
|
56
|
+
each_segments(children) do |segments|
|
58
57
|
|
59
|
-
|
60
|
-
write('HEREDOC')
|
61
|
-
end
|
58
|
+
source = segmented_source(segments: segments)
|
62
59
|
|
63
|
-
|
64
|
-
if n_str?(node)
|
65
|
-
classify_str(node)
|
66
|
-
else
|
67
|
-
node.type
|
60
|
+
return source if round_trips?(source: source)
|
68
61
|
end
|
62
|
+
nil
|
69
63
|
end
|
64
|
+
memoize :round_tripping_segmented_source
|
70
65
|
|
71
|
-
def
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
66
|
+
def each_segments(array)
|
67
|
+
yield [array]
|
68
|
+
|
69
|
+
1.upto(array.length) do |take|
|
70
|
+
prefix = [array.take(take)]
|
71
|
+
suffix = array.drop(take)
|
72
|
+
each_segments(suffix) do |items|
|
73
|
+
yield(prefix + items)
|
74
|
+
end
|
80
75
|
end
|
81
76
|
end
|
82
77
|
|
83
|
-
def
|
84
|
-
|
85
|
-
end
|
78
|
+
def segmented_source(segments:)
|
79
|
+
buffer = Buffer.new
|
86
80
|
|
87
|
-
|
88
|
-
|
89
|
-
|
81
|
+
Segmented.new(
|
82
|
+
buffer:,
|
83
|
+
comments:,
|
84
|
+
explicit_encoding: nil,
|
85
|
+
local_variable_scope:,
|
86
|
+
node:,
|
87
|
+
segments:
|
88
|
+
).dispatch
|
90
89
|
|
91
|
-
|
92
|
-
/\A( |\t)+\z/.match?(node.children.first)
|
90
|
+
buffer.content
|
93
91
|
end
|
94
92
|
|
95
|
-
def
|
96
|
-
|
93
|
+
def heredoc_body
|
94
|
+
buffer = Buffer.new
|
95
|
+
|
96
|
+
writer = Heredoc.new(
|
97
|
+
buffer:,
|
98
|
+
comments:,
|
99
|
+
explicit_encoding: nil,
|
100
|
+
local_variable_scope:,
|
101
|
+
node:
|
102
|
+
)
|
103
|
+
|
104
|
+
writer.emit
|
105
|
+
buffer.content
|
97
106
|
end
|
107
|
+
memoize :heredoc_body
|
98
108
|
|
99
|
-
def
|
100
|
-
|
101
|
-
PATTERNS_3.include?(group.map(&method(:classify)))
|
102
|
-
end
|
109
|
+
def heredoc_source
|
110
|
+
"#{HEREDOC_HEADER}\n#{heredoc_body}"
|
103
111
|
end
|
112
|
+
memoize :heredoc_source
|
113
|
+
|
114
|
+
class Heredoc
|
115
|
+
include Writer, Adamantium
|
104
116
|
|
105
|
-
|
106
|
-
|
107
|
-
|
117
|
+
def emit
|
118
|
+
emit_heredoc_body
|
119
|
+
write(HEREDOC_FOOTER)
|
108
120
|
end
|
109
|
-
end
|
110
121
|
|
111
|
-
|
112
|
-
last = children.last
|
113
|
-
n_str?(last) && last.children.first[-1].eql?("\n")
|
114
|
-
end
|
122
|
+
private
|
115
123
|
|
116
|
-
|
117
|
-
buffer.root_indent do
|
124
|
+
def emit_heredoc_body
|
118
125
|
children.each do |child|
|
119
126
|
if n_str?(child)
|
120
127
|
write(escape_dynamic(child.children.first))
|
@@ -123,88 +130,74 @@ module Unparser
|
|
123
130
|
end
|
124
131
|
end
|
125
132
|
end
|
126
|
-
end
|
127
133
|
|
128
|
-
|
129
|
-
|
130
|
-
|
134
|
+
def escape_dynamic(string)
|
135
|
+
string.gsub('#', '\#')
|
136
|
+
end
|
131
137
|
|
132
|
-
|
133
|
-
if FLAT_INTERPOLATION.include?(child.type)
|
134
|
-
write('#')
|
135
|
-
visit(child)
|
136
|
-
elsif n_dstr?(child)
|
137
|
-
emit_body(child.children)
|
138
|
-
else
|
138
|
+
def emit_dynamic(child)
|
139
139
|
write('#{')
|
140
140
|
emit_dynamic_component(child.children.first)
|
141
141
|
write('}')
|
142
142
|
end
|
143
|
-
end
|
144
143
|
|
145
|
-
|
146
|
-
|
147
|
-
end
|
148
|
-
|
149
|
-
def emit_dstr
|
150
|
-
if children.empty?
|
151
|
-
write('%()')
|
152
|
-
else
|
153
|
-
segments.each_with_index do |children, index|
|
154
|
-
emit_segment(children, index)
|
155
|
-
end
|
144
|
+
def emit_dynamic_component(node)
|
145
|
+
visit(node) if node
|
156
146
|
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def breakpoint?(child, current)
|
160
|
-
last_type = current.last&.type
|
147
|
+
end # Heredoc
|
161
148
|
|
162
|
-
|
163
|
-
|
164
|
-
last_type.equal?(:dstr),
|
165
|
-
n_dstr?(child) && last_type
|
166
|
-
].any?
|
167
|
-
end
|
168
|
-
|
169
|
-
def segments
|
170
|
-
segments = []
|
149
|
+
class Segmented
|
150
|
+
include Writer, Adamantium
|
171
151
|
|
172
|
-
segments
|
152
|
+
include anima.add(:segments)
|
173
153
|
|
174
|
-
|
175
|
-
if
|
176
|
-
|
154
|
+
def dispatch
|
155
|
+
if children.empty?
|
156
|
+
write('%()')
|
157
|
+
else
|
158
|
+
segments.each_with_index { |segment, index| emit_segment(segment, index) }
|
177
159
|
end
|
178
|
-
|
179
|
-
current << child
|
180
160
|
end
|
181
161
|
|
182
|
-
|
183
|
-
end
|
162
|
+
private
|
184
163
|
|
185
|
-
|
186
|
-
|
164
|
+
def emit_segment(children, index)
|
165
|
+
write(' ') unless index.zero?
|
187
166
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
167
|
+
write('"')
|
168
|
+
emit_segment_body(children)
|
169
|
+
write('"')
|
170
|
+
end
|
192
171
|
|
193
|
-
|
194
|
-
buffer.root_indent do
|
172
|
+
def emit_segment_body(children)
|
195
173
|
children.each_with_index do |child, index|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
174
|
+
case child.type
|
175
|
+
when :begin
|
176
|
+
write('#{')
|
177
|
+
visit(child.children.first) if child.children.first
|
178
|
+
write('}')
|
179
|
+
when FLAT_INTERPOLATION
|
180
|
+
write('#')
|
181
|
+
visit(child)
|
182
|
+
when :str
|
183
|
+
visit_str(children, child, index)
|
184
|
+
when :dstr
|
185
|
+
emit_segment_body(child.children)
|
205
186
|
end
|
206
187
|
end
|
207
188
|
end
|
189
|
+
|
190
|
+
def visit_str(children, child, index)
|
191
|
+
string = child.children.first
|
192
|
+
|
193
|
+
next_child = children.at(index.succ)
|
194
|
+
|
195
|
+
if next_child && next_child.type.equal?(:str)
|
196
|
+
write(string.gsub('"', '\\"'))
|
197
|
+
else
|
198
|
+
write(child.children.first.inspect[1..-2])
|
199
|
+
end
|
200
|
+
end
|
208
201
|
end
|
209
202
|
end # DynamicString
|
210
203
|
end # Writer
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Unparser
|
4
|
+
module Writer
|
5
|
+
# Writer for regexp literals
|
6
|
+
class Regexp
|
7
|
+
include Writer, Adamantium
|
8
|
+
|
9
|
+
CANDIDATES = [
|
10
|
+
['/', '/'].freeze,
|
11
|
+
['%r{', '}'].freeze
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
define_group(:body, 0..-2)
|
15
|
+
|
16
|
+
def dispatch
|
17
|
+
effective_writer.write_to_buffer
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# mutant:disable
|
23
|
+
def effective_writer
|
24
|
+
CANDIDATES.each do |token_open, token_close|
|
25
|
+
source = render_with_delimiter(token_close:, token_open:)
|
26
|
+
|
27
|
+
next unless round_trips?(source:)
|
28
|
+
|
29
|
+
return writer_with(Effective, node:, token_close:, token_open:)
|
30
|
+
end
|
31
|
+
|
32
|
+
fail 'Could not find a round tripping solution for regexp'
|
33
|
+
end
|
34
|
+
|
35
|
+
class Effective
|
36
|
+
include Writer, Adamantium
|
37
|
+
|
38
|
+
include anima.add(:token_close, :token_open)
|
39
|
+
|
40
|
+
define_group(:body, 0..-2)
|
41
|
+
|
42
|
+
def dispatch
|
43
|
+
buffer.root_indent do
|
44
|
+
write(token_open)
|
45
|
+
body.each(&method(:emit_body))
|
46
|
+
write(token_close)
|
47
|
+
emit_options
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def emit_body(node)
|
54
|
+
if n_begin?(node)
|
55
|
+
write('#{')
|
56
|
+
node.children.each(&method(:visit))
|
57
|
+
write('}')
|
58
|
+
else
|
59
|
+
write_regular(node.children.first)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def write_regular(string)
|
64
|
+
if string.length > 1 && string.start_with?("\n")
|
65
|
+
string.each_char do |char|
|
66
|
+
buffer.append_without_prefix(char.eql?("\n") ? '\c*' : char)
|
67
|
+
end
|
68
|
+
else
|
69
|
+
buffer.append_without_prefix(string)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def emit_options
|
74
|
+
write(children.last.children.join)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# mutant:disable
|
79
|
+
def render_with_delimiter(token_close:, token_open:)
|
80
|
+
buffer = Buffer.new
|
81
|
+
|
82
|
+
writer = Effective.new(
|
83
|
+
buffer:,
|
84
|
+
comments:,
|
85
|
+
explicit_encoding:,
|
86
|
+
local_variable_scope:,
|
87
|
+
node:,
|
88
|
+
token_close:,
|
89
|
+
token_open:
|
90
|
+
)
|
91
|
+
|
92
|
+
writer.dispatch
|
93
|
+
buffer.nl_flush_heredocs
|
94
|
+
buffer.content
|
95
|
+
end
|
96
|
+
end # Regexp
|
97
|
+
end # Emitter
|
98
|
+
end # Unparser
|
@@ -6,6 +6,11 @@ module Unparser
|
|
6
6
|
class Resbody
|
7
7
|
include Writer
|
8
8
|
|
9
|
+
OPERATORS = {
|
10
|
+
csend: '&.',
|
11
|
+
send: '.'
|
12
|
+
}.freeze
|
13
|
+
|
9
14
|
children :exception, :assignment, :body
|
10
15
|
|
11
16
|
def emit_postcontrol
|
@@ -33,7 +38,31 @@ module Unparser
|
|
33
38
|
return unless assignment
|
34
39
|
|
35
40
|
write(' => ')
|
36
|
-
|
41
|
+
|
42
|
+
case assignment.type
|
43
|
+
when :send, :csend
|
44
|
+
write_send_assignment
|
45
|
+
when :indexasgn
|
46
|
+
write_index_assignment
|
47
|
+
else
|
48
|
+
visit(assignment)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def write_send_assignment
|
53
|
+
details = NodeDetails::Send.new(assignment)
|
54
|
+
|
55
|
+
visit(details.receiver)
|
56
|
+
write(OPERATORS.fetch(assignment.type))
|
57
|
+
write(details.non_assignment_selector)
|
58
|
+
end
|
59
|
+
|
60
|
+
def write_index_assignment
|
61
|
+
receiver, index = assignment.children
|
62
|
+
visit(receiver)
|
63
|
+
write('[')
|
64
|
+
visit(index) if index
|
65
|
+
write(']')
|
37
66
|
end
|
38
67
|
end # Resbody
|
39
68
|
end # Writer
|
@@ -20,13 +20,9 @@ module Unparser
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
def emit_heredoc_reminders
|
24
|
-
emitter(body).emit_heredoc_reminders
|
25
|
-
end
|
26
|
-
|
27
23
|
def emit_postcontrol
|
28
24
|
visit(body)
|
29
|
-
writer_with(Resbody, rescue_body).emit_postcontrol
|
25
|
+
writer_with(Resbody, node: rescue_body).emit_postcontrol
|
30
26
|
end
|
31
27
|
|
32
28
|
private
|
@@ -36,7 +32,7 @@ module Unparser
|
|
36
32
|
end
|
37
33
|
|
38
34
|
def emit_rescue_body(node)
|
39
|
-
writer_with(Resbody, node).emit_regular
|
35
|
+
writer_with(Resbody, node:).emit_regular
|
40
36
|
end
|
41
37
|
end # Rescue
|
42
38
|
end # Writer
|
data/lib/unparser/writer/send.rb
CHANGED
@@ -30,15 +30,10 @@ module Unparser
|
|
30
30
|
write(details.string_selector)
|
31
31
|
end
|
32
32
|
|
33
|
-
def emit_heredoc_reminders
|
34
|
-
emitter(receiver).emit_heredoc_reminders if receiver
|
35
|
-
arguments.each(&method(:emit_heredoc_reminder))
|
36
|
-
end
|
37
|
-
|
38
33
|
private
|
39
34
|
|
40
35
|
def effective_writer
|
41
|
-
writer_with(effective_writer_class, node)
|
36
|
+
writer_with(effective_writer_class, node:)
|
42
37
|
end
|
43
38
|
memoize :effective_writer
|
44
39
|
|
@@ -78,10 +73,6 @@ module Unparser
|
|
78
73
|
parentheses { delimited(arguments) }
|
79
74
|
end
|
80
75
|
|
81
|
-
def emit_heredoc_reminder(argument)
|
82
|
-
emitter(argument).emit_heredoc_reminders
|
83
|
-
end
|
84
|
-
|
85
76
|
def avoid_clash?
|
86
77
|
local_variable_clash? || parses_as_constant?
|
87
78
|
end
|
@@ -91,9 +82,12 @@ module Unparser
|
|
91
82
|
end
|
92
83
|
|
93
84
|
def parses_as_constant?
|
94
|
-
test = Unparser
|
95
|
-
|
96
|
-
|
85
|
+
test = Unparser
|
86
|
+
.parse_ast_either(selector.to_s)
|
87
|
+
.fmap(&:node)
|
88
|
+
.from_right do
|
89
|
+
fail InvalidNodeError.new("Invalid selector for send node: #{selector.inspect}", node)
|
90
|
+
end
|
97
91
|
|
98
92
|
n_const?(test)
|
99
93
|
end
|
@@ -105,7 +99,7 @@ module Unparser
|
|
105
99
|
|
106
100
|
def emit_send_regular(node)
|
107
101
|
if n_send?(node)
|
108
|
-
writer_with(Regular, node).dispatch
|
102
|
+
writer_with(Regular, node:).dispatch
|
109
103
|
else
|
110
104
|
visit(node)
|
111
105
|
end
|
data/lib/unparser/writer.rb
CHANGED
@@ -4,12 +4,43 @@ module Unparser
|
|
4
4
|
module Writer
|
5
5
|
include Generation, NodeHelpers
|
6
6
|
|
7
|
+
# mutant:disable
|
7
8
|
def self.included(descendant)
|
8
9
|
descendant.class_eval do
|
9
|
-
include Anima.new(:buffer, :comments, :node, :local_variable_scope)
|
10
|
+
include Adamantium, Anima.new(:buffer, :comments, :explicit_encoding, :node, :local_variable_scope)
|
10
11
|
|
11
12
|
extend DSL
|
12
13
|
end
|
13
14
|
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# mutant:disable
|
19
|
+
def emitter(node)
|
20
|
+
Emitter.emitter(
|
21
|
+
buffer: buffer,
|
22
|
+
comments: comments,
|
23
|
+
explicit_encoding: explicit_encoding,
|
24
|
+
local_variable_scope: local_variable_scope,
|
25
|
+
node: node
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
# mutant:disable
|
30
|
+
def round_trips?(source:)
|
31
|
+
parser = Unparser.parser
|
32
|
+
|
33
|
+
local_variable_scope
|
34
|
+
.local_variables_for_node(node)
|
35
|
+
.each(&parser.static_env.public_method(:declare))
|
36
|
+
|
37
|
+
buffer = Buffer.new
|
38
|
+
buffer.write_encoding(explicit_encoding) if explicit_encoding
|
39
|
+
buffer.write(source)
|
40
|
+
|
41
|
+
node.eql?(parser.parse(Unparser.buffer(buffer.content)))
|
42
|
+
rescue Parser::SyntaxError
|
43
|
+
false
|
44
|
+
end
|
14
45
|
end # Writer
|
15
46
|
end # Unparser
|