unparser 0.6.15 → 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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/lib/unparser/anima.rb +11 -0
  4. data/lib/unparser/ast/local_variable_scope.rb +28 -24
  5. data/lib/unparser/ast.rb +18 -22
  6. data/lib/unparser/buffer.rb +44 -2
  7. data/lib/unparser/cli.rb +28 -5
  8. data/lib/unparser/color.rb +2 -2
  9. data/lib/unparser/either.rb +6 -6
  10. data/lib/unparser/emitter/args.rb +5 -1
  11. data/lib/unparser/emitter/array.rb +0 -4
  12. data/lib/unparser/emitter/array_pattern.rb +1 -9
  13. data/lib/unparser/emitter/assignment.rb +17 -8
  14. data/lib/unparser/emitter/begin.rb +0 -6
  15. data/lib/unparser/emitter/binary.rb +1 -1
  16. data/lib/unparser/emitter/block.rb +13 -6
  17. data/lib/unparser/emitter/def.rb +1 -1
  18. data/lib/unparser/emitter/dstr.rb +6 -5
  19. data/lib/unparser/emitter/dsym.rb +1 -1
  20. data/lib/unparser/emitter/ensure.rb +16 -0
  21. data/lib/unparser/emitter/flow_modifier.rb +1 -7
  22. data/lib/unparser/emitter/for.rb +1 -1
  23. data/lib/unparser/emitter/hash.rb +0 -8
  24. data/lib/unparser/emitter/hash_pattern.rb +1 -1
  25. data/lib/unparser/emitter/in_pattern.rb +9 -1
  26. data/lib/unparser/emitter/index.rb +0 -4
  27. data/lib/unparser/emitter/kwbegin.rb +1 -1
  28. data/lib/unparser/emitter/match_pattern.rb +6 -1
  29. data/lib/unparser/emitter/match_pattern_p.rb +6 -1
  30. data/lib/unparser/emitter/mlhs.rb +7 -1
  31. data/lib/unparser/emitter/op_assign.rb +0 -10
  32. data/lib/unparser/emitter/primitive.rb +0 -13
  33. data/lib/unparser/emitter/range.rb +23 -2
  34. data/lib/unparser/emitter/regexp.rb +5 -17
  35. data/lib/unparser/emitter/rescue.rb +7 -1
  36. data/lib/unparser/emitter/root.rb +2 -9
  37. data/lib/unparser/emitter/send.rb +1 -5
  38. data/lib/unparser/emitter/string.rb +31 -0
  39. data/lib/unparser/emitter/xstr.rb +8 -1
  40. data/lib/unparser/emitter.rb +9 -10
  41. data/lib/unparser/generation.rb +14 -14
  42. data/lib/unparser/node_details.rb +1 -0
  43. data/lib/unparser/node_helpers.rb +18 -9
  44. data/lib/unparser/util.rb +23 -0
  45. data/lib/unparser/validation.rb +68 -28
  46. data/lib/unparser/writer/array.rb +51 -0
  47. data/lib/unparser/writer/binary.rb +8 -4
  48. data/lib/unparser/writer/dynamic_string.rb +128 -135
  49. data/lib/unparser/writer/regexp.rb +101 -0
  50. data/lib/unparser/writer/resbody.rb +37 -3
  51. data/lib/unparser/writer/rescue.rb +3 -7
  52. data/lib/unparser/writer/send/unary.rb +9 -4
  53. data/lib/unparser/writer/send.rb +8 -14
  54. data/lib/unparser/writer.rb +31 -1
  55. data/lib/unparser.rb +149 -38
  56. metadata +33 -17
@@ -7,10 +7,14 @@ module Unparser
7
7
  :generated_node,
8
8
  :generated_source,
9
9
  :identification,
10
- :original_node,
10
+ :original_ast,
11
11
  :original_source
12
12
  )
13
13
 
14
+ class PhaseException
15
+ include Anima.new(:exception, :phase)
16
+ end
17
+
14
18
  # Test if source could be unparsed successfully
15
19
  #
16
20
  # @return [Boolean]
@@ -18,10 +22,11 @@ module Unparser
18
22
  # @api private
19
23
  #
20
24
  # rubocop:disable Style/OperatorMethodCall
25
+ # mutant:disable
21
26
  def success?
22
27
  [
23
28
  original_source,
24
- original_node,
29
+ original_ast,
25
30
  generated_source,
26
31
  generated_node
27
32
  ].all?(&:right?) && generated_node.from_right.==(original_node.from_right)
@@ -33,7 +38,7 @@ module Unparser
33
38
  # @return [String]
34
39
  #
35
40
  # @api private
36
- #
41
+ # mutant:disable
37
42
  def report
38
43
  message = [identification]
39
44
 
@@ -47,48 +52,57 @@ module Unparser
47
52
  end
48
53
  memoize :report
49
54
 
55
+ # mutant:disable
56
+ def original_node
57
+ original_ast.fmap(&:node)
58
+ end
59
+
50
60
  # Create validator from string
51
61
  #
52
62
  # @param [String] original_source
53
63
  #
54
64
  # @return [Validator]
65
+ # mutant:disable
55
66
  def self.from_string(original_source)
56
- original_node = Unparser
57
- .parse_either(original_source)
67
+ original_ast = parse_ast_either(original_source)
58
68
 
59
- generated_source = original_node
69
+ generated_source = original_ast
60
70
  .lmap(&method(:const_unit))
61
- .bind(&Unparser.method(:unparse_either))
71
+ .bind(&method(:unparse_ast_either))
62
72
 
63
73
  generated_node = generated_source
64
74
  .lmap(&method(:const_unit))
65
- .bind(&Unparser.method(:parse_either))
75
+ .bind(&method(:parse_ast_either))
76
+ .fmap(&:node)
66
77
 
67
78
  new(
68
- identification: '(string)',
69
- original_source: Either::Right.new(original_source),
70
- original_node: original_node,
79
+ generated_node: generated_node,
71
80
  generated_source: generated_source,
72
- generated_node: generated_node
81
+ identification: '(string)',
82
+ original_ast: original_ast,
83
+ original_source: Either::Right.new(original_source)
73
84
  )
74
85
  end
75
86
 
76
- # Create validator from node
87
+ # Create validator from ast
77
88
  #
78
- # @param [Parser::AST::Node] original_node
89
+ # @param [Unparser::AST] ast
79
90
  #
80
91
  # @return [Validator]
81
- def self.from_node(original_node)
82
- generated_source = Unparser.unparse_either(original_node)
92
+ #
93
+ # mutant:disable
94
+ def self.from_ast(ast:)
95
+ generated_source = Unparser.unparse_ast_either(ast)
83
96
 
84
97
  generated_node = generated_source
85
98
  .lmap(&method(:const_unit))
86
- .bind(&Unparser.public_method(:parse_either))
99
+ .bind(&method(:parse_ast_either))
100
+ .fmap(&:node)
87
101
 
88
102
  new(
89
103
  identification: '(string)',
90
104
  original_source: generated_source,
91
- original_node: Either::Right.new(original_node),
105
+ original_ast: Either::Right.new(ast),
92
106
  generated_source: generated_source,
93
107
  generated_node: generated_node
94
108
  )
@@ -99,24 +113,45 @@ module Unparser
99
113
  # @param [Pathname] path
100
114
  #
101
115
  # @return [Validator]
116
+ #
117
+ # mutant:disable
102
118
  def self.from_path(path)
103
- from_string(path.read).with(identification: path.to_s)
119
+ from_string(path.read.freeze).with(identification: path.to_s)
104
120
  end
105
121
 
122
+ # mutant:disable
123
+ def self.unparse_ast_either(ast)
124
+ Unparser.unparse_ast_either(ast)
125
+ end
126
+ private_class_method :unparse_ast_either
127
+
128
+ # mutant:disable
129
+ def self.parse_ast_either(source)
130
+ Unparser.parse_ast_either(source)
131
+ end
132
+ private_class_method :parse_ast_either
133
+
134
+ # mutant:disable
135
+ def self.const_unit(_); end
136
+ private_class_method :const_unit
137
+
106
138
  private
107
139
 
140
+ # mutant:disable
108
141
  def make_report(label, attribute_name)
109
142
  ["#{label}:"].concat(public_send(attribute_name).either(method(:report_exception), ->(value) { [value] }))
110
143
  end
111
144
 
112
- def report_exception(exception)
113
- if exception
114
- [exception.inspect].concat(exception.backtrace.take(20))
145
+ # mutant:disable
146
+ def report_exception(phase_exception)
147
+ if phase_exception
148
+ [phase_exception.inspect].concat(phase_exception.backtrace.take(20))
115
149
  else
116
- ['undefined']
150
+ %w[undefined]
117
151
  end
118
152
  end
119
153
 
154
+ # mutant:disable
120
155
  def node_diff_report
121
156
  diff = nil
122
157
 
@@ -132,14 +167,13 @@ module Unparser
132
167
  diff ? ['Node-Diff:', diff] : []
133
168
  end
134
169
 
135
- def self.const_unit(_value); end
136
- private_class_method :const_unit
137
-
138
170
  class Literal < self
171
+ # mutant:disable
139
172
  def success?
140
173
  original_source.eql?(generated_source)
141
174
  end
142
175
 
176
+ # mutant:disable
143
177
  def report
144
178
  message = [identification]
145
179
 
@@ -155,20 +189,26 @@ module Unparser
155
189
 
156
190
  private
157
191
 
192
+ # mutant:disable
158
193
  def source_diff_report
159
194
  diff = nil
160
195
 
161
196
  original_source.fmap do |original|
162
197
  generated_source.fmap do |generated|
163
198
  diff = Diff.new(
164
- original.split("\n", -1),
165
- generated.split("\n", -1)
199
+ encode(original).split("\n", -1),
200
+ encode(generated).split("\n", -1)
166
201
  ).colorized_diff
167
202
  end
168
203
  end
169
204
 
170
205
  diff ? ['Source-Diff:', diff] : []
171
206
  end
207
+
208
+ # mutant:disable
209
+ def encode(string)
210
+ string.encode('UTF-8', invalid: :replace, undef: :replace)
211
+ end
172
212
  end # Literal
173
213
  end # Validation
174
214
  end # Unparser
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unparser
4
+ module Writer
5
+ class Array
6
+ include Writer, Adamantium
7
+
8
+ MAP = {
9
+ dsym: '%I',
10
+ sym: '%i',
11
+ dstr: '%W',
12
+ str: '%w'
13
+ }.freeze
14
+ private_constant(*constants(false))
15
+
16
+ def emit_compact # rubocop:disable Metrics/AbcSize
17
+ children_generic_type = array_elements_generic_type
18
+
19
+ write(MAP.fetch(children_generic_type))
20
+
21
+ parentheses('[', ']') do
22
+ delimited(children, ' ') do |child|
23
+ if n_sym?(child) || n_str?(child)
24
+ write(Util.one(child.children).to_s)
25
+ else
26
+ write('#{')
27
+ emitter(Util.one(Util.one(child.children).children)).write_to_buffer
28
+ write('}')
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def array_elements_generic_type
37
+ children_types = children.to_set(&:type)
38
+
39
+ if children_types == Set[:sym, :dsym]
40
+ :dsym
41
+ elsif children_types == Set[:str, :dstr]
42
+ :dstr
43
+ elsif children_types == Set[]
44
+ :sym
45
+ else
46
+ Util.one(children_types.to_a)
47
+ end
48
+ end
49
+ end # Array
50
+ end # Writer
51
+ end # Unparser
@@ -39,7 +39,7 @@ module Unparser
39
39
  tANDOP: '&&'
40
40
  }.freeze
41
41
 
42
- NEED_KEYWORD = %i[return break next].freeze
42
+ NEED_KEYWORD = %i[return break next match_pattern_p].freeze
43
43
 
44
44
  private_constant(*constants(false))
45
45
 
@@ -52,9 +52,13 @@ module Unparser
52
52
  end
53
53
 
54
54
  def dispatch
55
- left_emitter.write_to_buffer
56
- write(' ', MAP.fetch(effective_symbol), ' ')
57
- visit(right)
55
+ if node.type.eql?(:and) && left.type.equal?(:or)
56
+ emit_with(KEYWORD_TOKENS)
57
+ else
58
+ left_emitter.write_to_buffer
59
+ write(' ', MAP.fetch(effective_symbol), ' ')
60
+ visit(right)
61
+ end
58
62
  end
59
63
 
60
64
  private
@@ -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