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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/unparser/anima.rb +11 -0
  3. data/lib/unparser/ast/local_variable_scope.rb +28 -24
  4. data/lib/unparser/ast.rb +18 -22
  5. data/lib/unparser/buffer.rb +44 -2
  6. data/lib/unparser/cli.rb +26 -5
  7. data/lib/unparser/either.rb +6 -6
  8. data/lib/unparser/emitter/array.rb +0 -4
  9. data/lib/unparser/emitter/array_pattern.rb +1 -9
  10. data/lib/unparser/emitter/assignment.rb +7 -8
  11. data/lib/unparser/emitter/begin.rb +0 -6
  12. data/lib/unparser/emitter/binary.rb +1 -1
  13. data/lib/unparser/emitter/block.rb +3 -4
  14. data/lib/unparser/emitter/def.rb +1 -1
  15. data/lib/unparser/emitter/dstr.rb +6 -5
  16. data/lib/unparser/emitter/flow_modifier.rb +0 -6
  17. data/lib/unparser/emitter/for.rb +1 -1
  18. data/lib/unparser/emitter/hash.rb +0 -8
  19. data/lib/unparser/emitter/hash_pattern.rb +1 -1
  20. data/lib/unparser/emitter/index.rb +0 -4
  21. data/lib/unparser/emitter/op_assign.rb +0 -10
  22. data/lib/unparser/emitter/primitive.rb +0 -13
  23. data/lib/unparser/emitter/regexp.rb +5 -17
  24. data/lib/unparser/emitter/rescue.rb +7 -1
  25. data/lib/unparser/emitter/root.rb +2 -9
  26. data/lib/unparser/emitter/send.rb +1 -5
  27. data/lib/unparser/emitter/string.rb +31 -0
  28. data/lib/unparser/emitter.rb +9 -10
  29. data/lib/unparser/generation.rb +8 -14
  30. data/lib/unparser/node_details.rb +1 -0
  31. data/lib/unparser/validation.rb +68 -28
  32. data/lib/unparser/writer/dynamic_string.rb +128 -135
  33. data/lib/unparser/writer/regexp.rb +98 -0
  34. data/lib/unparser/writer/resbody.rb +30 -1
  35. data/lib/unparser/writer/rescue.rb +2 -6
  36. data/lib/unparser/writer/send.rb +8 -14
  37. data/lib/unparser/writer.rb +32 -1
  38. data/lib/unparser.rb +103 -30
  39. 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
- private_constant(*constants(false))
24
-
25
- def emit_heredoc_reminder
26
- return unless heredoc?
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
- emit_heredoc_body
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
- emit_heredoc_header
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
- emit_dstr
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
- !children.empty? && (nl_last_child? && heredoc_pattern?)
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 emit_heredoc_header
51
- write(heredoc_header)
50
+ def round_trips_heredoc?
51
+ round_trips?(source: heredoc_source)
52
52
  end
53
+ memoize :round_trips_heredoc?
53
54
 
54
- def emit_heredoc_body
55
- nl
56
- emit_normal_heredoc_body
57
- end
55
+ def round_tripping_segmented_source
56
+ each_segments(children) do |segments|
58
57
 
59
- def emit_heredoc_footer
60
- write('HEREDOC')
61
- end
58
+ source = segmented_source(segments: segments)
62
59
 
63
- def classify(node)
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 classify_str(node)
72
- if str_nl?(node)
73
- :str_nl
74
- elsif node.children.first.end_with?("\n")
75
- :str_nl_eol
76
- elsif str_ws?(node)
77
- :str_ws
78
- elsif str_empty?(node)
79
- :str_empty
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 str_nl?(node)
84
- node.eql?(s(:str, "\n"))
85
- end
78
+ def segmented_source(segments:)
79
+ buffer = Buffer.new
86
80
 
87
- def str_empty?(node)
88
- node.eql?(s(:str, ''))
89
- end
81
+ Segmented.new(
82
+ buffer:,
83
+ comments:,
84
+ explicit_encoding: nil,
85
+ local_variable_scope:,
86
+ node:,
87
+ segments:
88
+ ).dispatch
90
89
 
91
- def str_ws?(node)
92
- /\A( |\t)+\z/.match?(node.children.first)
90
+ buffer.content
93
91
  end
94
92
 
95
- def heredoc_pattern?
96
- heredoc_pattern_2? || heredoc_pattern_3?
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 heredoc_pattern_3?
100
- children.each_cons(3).any? do |group|
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
- def heredoc_pattern_2?
106
- children.each_cons(2).any? do |group|
107
- PATTERNS_2.include?(group.map(&method(:classify)))
117
+ def emit
118
+ emit_heredoc_body
119
+ write(HEREDOC_FOOTER)
108
120
  end
109
- end
110
121
 
111
- def nl_last_child?
112
- last = children.last
113
- n_str?(last) && last.children.first[-1].eql?("\n")
114
- end
122
+ private
115
123
 
116
- def emit_normal_heredoc_body
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
- def escape_dynamic(string)
129
- string.gsub('#', '\#')
130
- end
134
+ def escape_dynamic(string)
135
+ string.gsub('#', '\#')
136
+ end
131
137
 
132
- def emit_dynamic(child)
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
- def emit_dynamic_component(node)
146
- visit(node) if node
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
- n_str?(child) && last_type.equal?(:str) && current.none?(&method(:n_begin?)),
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 << current = []
152
+ include anima.add(:segments)
173
153
 
174
- children.each do |child|
175
- if breakpoint?(child, current)
176
- segments << current = []
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
- segments
183
- end
162
+ private
184
163
 
185
- def emit_segment(children, index)
186
- write(' ') unless index.zero?
164
+ def emit_segment(children, index)
165
+ write(' ') unless index.zero?
187
166
 
188
- write('"')
189
- emit_body(children)
190
- write('"')
191
- end
167
+ write('"')
168
+ emit_segment_body(children)
169
+ write('"')
170
+ end
192
171
 
193
- def emit_body(children)
194
- buffer.root_indent do
172
+ def emit_segment_body(children)
195
173
  children.each_with_index do |child, index|
196
- if n_str?(child)
197
- string = child.children.first
198
- if string.eql?("\n") && children.fetch(index.pred).type.equal?(:begin)
199
- write("\n")
200
- else
201
- write(string.inspect[1..-2])
202
- end
203
- else
204
- emit_dynamic(child)
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
- visit(assignment)
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
@@ -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.parse_either(selector.to_s).from_right do
95
- fail InvalidNodeError.new("Invalid selector for send node: #{selector.inspect}", node)
96
- end
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
@@ -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