unparser 0.6.5 → 0.8.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -6
  3. data/bin/unparser +1 -1
  4. data/lib/unparser/adamantium.rb +3 -1
  5. data/lib/unparser/anima.rb +11 -0
  6. data/lib/unparser/ast/local_variable_scope.rb +28 -25
  7. data/lib/unparser/ast.rb +18 -22
  8. data/lib/unparser/buffer.rb +43 -15
  9. data/lib/unparser/cli.rb +30 -7
  10. data/lib/unparser/color.rb +5 -0
  11. data/lib/unparser/either.rb +6 -6
  12. data/lib/unparser/emitter/args.rb +5 -1
  13. data/lib/unparser/emitter/argument.rb +6 -4
  14. data/lib/unparser/emitter/array.rb +0 -4
  15. data/lib/unparser/emitter/array_pattern.rb +1 -9
  16. data/lib/unparser/emitter/assignment.rb +17 -8
  17. data/lib/unparser/emitter/begin.rb +0 -6
  18. data/lib/unparser/emitter/binary.rb +1 -1
  19. data/lib/unparser/emitter/block.rb +13 -6
  20. data/lib/unparser/emitter/def.rb +1 -1
  21. data/lib/unparser/emitter/dstr.rb +6 -5
  22. data/lib/unparser/emitter/dsym.rb +1 -1
  23. data/lib/unparser/emitter/ensure.rb +16 -0
  24. data/lib/unparser/emitter/flipflop.rb +7 -2
  25. data/lib/unparser/emitter/flow_modifier.rb +1 -7
  26. data/lib/unparser/emitter/for.rb +1 -1
  27. data/lib/unparser/emitter/hash.rb +0 -16
  28. data/lib/unparser/emitter/hash_pattern.rb +1 -1
  29. data/lib/unparser/emitter/in_pattern.rb +9 -1
  30. data/lib/unparser/emitter/index.rb +0 -4
  31. data/lib/unparser/emitter/kwbegin.rb +1 -1
  32. data/lib/unparser/emitter/match_pattern.rb +7 -11
  33. data/lib/unparser/emitter/match_pattern_p.rb +6 -1
  34. data/lib/unparser/emitter/mlhs.rb +7 -1
  35. data/lib/unparser/emitter/op_assign.rb +0 -5
  36. data/lib/unparser/emitter/pair.rb +31 -5
  37. data/lib/unparser/emitter/primitive.rb +19 -6
  38. data/lib/unparser/emitter/range.rb +23 -2
  39. data/lib/unparser/emitter/regexp.rb +5 -17
  40. data/lib/unparser/emitter/rescue.rb +7 -1
  41. data/lib/unparser/emitter/root.rb +2 -9
  42. data/lib/unparser/emitter/send.rb +1 -5
  43. data/lib/unparser/emitter/string.rb +31 -0
  44. data/lib/unparser/emitter/xstr.rb +8 -1
  45. data/lib/unparser/emitter.rb +9 -10
  46. data/lib/unparser/generation.rb +33 -29
  47. data/lib/unparser/node_details/send.rb +4 -3
  48. data/lib/unparser/node_details.rb +1 -0
  49. data/lib/unparser/node_helpers.rb +19 -9
  50. data/lib/unparser/util.rb +23 -0
  51. data/lib/unparser/validation.rb +70 -28
  52. data/lib/unparser/writer/array.rb +51 -0
  53. data/lib/unparser/writer/binary.rb +8 -4
  54. data/lib/unparser/writer/dynamic_string.rb +127 -146
  55. data/lib/unparser/writer/regexp.rb +101 -0
  56. data/lib/unparser/writer/resbody.rb +37 -3
  57. data/lib/unparser/writer/rescue.rb +3 -7
  58. data/lib/unparser/writer/send/unary.rb +9 -4
  59. data/lib/unparser/writer/send.rb +8 -14
  60. data/lib/unparser/writer.rb +31 -1
  61. data/lib/unparser.rb +149 -38
  62. metadata +38 -20
@@ -5,128 +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
65
+
66
+ def each_segments(array)
67
+ yield [array]
70
68
 
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
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?
97
- end
93
+ def heredoc_body
94
+ buffer = Buffer.new
98
95
 
99
- def heredoc_pattern_3?
100
- children.each_cons(3).any? do |group|
101
- PATTERNS_3.include?(group.map(&method(:classify)))
102
- end
103
- end
96
+ writer = Heredoc.new(
97
+ buffer:,
98
+ comments:,
99
+ explicit_encoding: nil,
100
+ local_variable_scope:,
101
+ node:
102
+ )
104
103
 
105
- def heredoc_pattern_2?
106
- children.each_cons(2).any? do |group|
107
- PATTERNS_2.include?(group.map(&method(:classify)))
108
- end
104
+ writer.emit
105
+ buffer.content
109
106
  end
107
+ memoize :heredoc_body
110
108
 
111
- def nl_last_child?
112
- last = children.last
113
- n_str?(last) && last.children.first[-1].eql?("\n")
109
+ def heredoc_source
110
+ "#{HEREDOC_HEADER}\n#{heredoc_body}"
114
111
  end
112
+ memoize :heredoc_source
115
113
 
116
- def emit_squiggly_heredoc_body
117
- buffer.indent
118
- children.each do |child|
119
- if n_str?(child)
120
- write(escape_dynamic(child.children.first))
121
- else
122
- emit_dynamic(child)
123
- end
114
+ class Heredoc
115
+ include Writer, Adamantium
116
+
117
+ def emit
118
+ emit_heredoc_body
119
+ write(HEREDOC_FOOTER)
124
120
  end
125
- buffer.unindent
126
- end
127
121
 
128
- def emit_normal_heredoc_body
129
- buffer.root_indent do
122
+ private
123
+
124
+ def emit_heredoc_body
130
125
  children.each do |child|
131
126
  if n_str?(child)
132
127
  write(escape_dynamic(child.children.first))
@@ -135,88 +130,74 @@ module Unparser
135
130
  end
136
131
  end
137
132
  end
138
- end
139
133
 
140
- def escape_dynamic(string)
141
- string.gsub('#', '\#')
142
- end
134
+ def escape_dynamic(string)
135
+ string.gsub('#', '\#')
136
+ end
143
137
 
144
- def emit_dynamic(child)
145
- if FLAT_INTERPOLATION.include?(child.type)
146
- write('#')
147
- visit(child)
148
- elsif n_dstr?(child)
149
- emit_body(child.children)
150
- else
138
+ def emit_dynamic(child)
151
139
  write('#{')
152
140
  emit_dynamic_component(child.children.first)
153
141
  write('}')
154
142
  end
155
- end
156
143
 
157
- def emit_dynamic_component(node)
158
- visit(node) if node
159
- end
160
-
161
- def emit_dstr
162
- if children.empty?
163
- write('%()')
164
- else
165
- segments.each_with_index do |children, index|
166
- emit_segment(children, index)
167
- end
144
+ def emit_dynamic_component(node)
145
+ visit(node) if node
168
146
  end
169
- end
170
-
171
- def breakpoint?(child, current)
172
- last_type = current.last&.type
147
+ end # Heredoc
173
148
 
174
- [
175
- n_str?(child) && last_type.equal?(:str) && current.none?(&method(:n_begin?)),
176
- last_type.equal?(:dstr),
177
- n_dstr?(child) && last_type
178
- ].any?
179
- end
180
-
181
- def segments
182
- segments = []
149
+ class Segmented
150
+ include Writer, Adamantium
183
151
 
184
- segments << current = []
152
+ include anima.add(:segments)
185
153
 
186
- children.each do |child|
187
- if breakpoint?(child, current)
188
- segments << current = []
154
+ def dispatch
155
+ if children.empty?
156
+ write('%()')
157
+ else
158
+ segments.each_with_index { |segment, index| emit_segment(segment, index) }
189
159
  end
190
-
191
- current << child
192
160
  end
193
161
 
194
- segments
195
- end
162
+ private
196
163
 
197
- def emit_segment(children, index)
198
- write(' ') unless index.zero?
164
+ def emit_segment(children, index)
165
+ write(' ') unless index.zero?
199
166
 
200
- write('"')
201
- emit_body(children)
202
- write('"')
203
- end
167
+ write('"')
168
+ emit_segment_body(children)
169
+ write('"')
170
+ end
204
171
 
205
- def emit_body(children)
206
- buffer.root_indent do
172
+ def emit_segment_body(children)
207
173
  children.each_with_index do |child, index|
208
- if n_str?(child)
209
- string = child.children.first
210
- if string.eql?("\n") && children.fetch(index.pred).type.equal?(:begin)
211
- write("\n")
212
- else
213
- write(string.inspect[1..-2])
214
- end
215
- else
216
- 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)
217
186
  end
218
187
  end
219
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
220
201
  end
221
202
  end # DynamicString
222
203
  end # Writer
@@ -0,0 +1,101 @@
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
+ elsif n_gvar?(node)
59
+ write('#')
60
+ write_regular(node.children.first.to_s)
61
+ else
62
+ write_regular(node.children.first)
63
+ end
64
+ end
65
+
66
+ def write_regular(string)
67
+ if string.length > 1 && string.start_with?("\n")
68
+ string.each_char do |char|
69
+ buffer.append_without_prefix(char.eql?("\n") ? '\c*' : char)
70
+ end
71
+ else
72
+ buffer.append_without_prefix(string)
73
+ end
74
+ end
75
+
76
+ def emit_options
77
+ write(children.last.children.join)
78
+ end
79
+ end
80
+
81
+ # mutant:disable
82
+ def render_with_delimiter(token_close:, token_open:)
83
+ buffer = Buffer.new
84
+
85
+ writer = Effective.new(
86
+ buffer:,
87
+ comments:,
88
+ explicit_encoding:,
89
+ local_variable_scope:,
90
+ node:,
91
+ token_close:,
92
+ token_open:
93
+ )
94
+
95
+ writer.dispatch
96
+ buffer.nl_flush_heredocs
97
+ buffer.content
98
+ end
99
+ end # Regexp
100
+ end # Emitter
101
+ end # Unparser
@@ -6,11 +6,21 @@ 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
12
- write(' rescue ')
13
- visit(body)
17
+ if body
18
+ write(' rescue ')
19
+ visit(body)
20
+ else
21
+ nl
22
+ write('rescue')
23
+ end
14
24
  end
15
25
 
16
26
  def emit_regular
@@ -33,7 +43,31 @@ module Unparser
33
43
  return unless assignment
34
44
 
35
45
  write(' => ')
36
- visit(assignment)
46
+
47
+ case assignment.type
48
+ when :send, :csend
49
+ write_send_assignment
50
+ when :indexasgn
51
+ write_index_assignment
52
+ else
53
+ visit(assignment)
54
+ end
55
+ end
56
+
57
+ def write_send_assignment
58
+ details = NodeDetails::Send.new(assignment)
59
+
60
+ visit(details.receiver)
61
+ write(OPERATORS.fetch(assignment.type))
62
+ write(details.non_assignment_selector)
63
+ end
64
+
65
+ def write_index_assignment
66
+ receiver, *indexes = assignment.children
67
+ visit(receiver)
68
+ write('[')
69
+ delimited(indexes)
70
+ write(']')
37
71
  end
38
72
  end # Resbody
39
73
  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
- visit(body)
29
- writer_with(Resbody, rescue_body).emit_postcontrol
24
+ visit(body) if body
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
@@ -12,13 +12,18 @@ module Unparser
12
12
 
13
13
  private_constant(*constants(false))
14
14
 
15
- def dispatch
15
+ def dispatch # rubocop:disable Metrics/AbcSize
16
16
  name = selector
17
+ first_child = children.fetch(0)
17
18
 
18
- write(MAP.fetch(name, name).to_s)
19
+ if n_flipflop?(first_child) || n_and?(first_child) || n_or?(first_child)
20
+ write 'not '
21
+ else
22
+ write(MAP.fetch(name, name).to_s)
19
23
 
20
- if n_int?(receiver) && selector.equal?(:+@)
21
- write('+')
24
+ if n_int?(receiver) && selector.equal?(:+@)
25
+ write('+')
26
+ end
22
27
  end
23
28
 
24
29
  visit(receiver)
@@ -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,42 @@ 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
+ local_variable_scope.local_variables_for_node(node).each do |local_variable|
33
+ parser.declare_local_variable(local_variable)
34
+ end
35
+
36
+ buffer = Buffer.new
37
+ buffer.write_encoding(explicit_encoding) if explicit_encoding
38
+ buffer.write(source)
39
+
40
+ node.eql?(parser.parse(Unparser.buffer(buffer.content)))
41
+ rescue Parser::SyntaxError
42
+ false
43
+ end
14
44
  end # Writer
15
45
  end # Unparser