unparser 0.6.7 → 0.9.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -10
  3. data/lib/unparser/adamantium.rb +3 -1
  4. data/lib/unparser/anima.rb +12 -3
  5. data/lib/unparser/ast/local_variable_scope.rb +29 -26
  6. data/lib/unparser/ast.rb +32 -49
  7. data/lib/unparser/buffer.rb +44 -2
  8. data/lib/unparser/cli.rb +48 -11
  9. data/lib/unparser/color.rb +9 -1
  10. data/lib/unparser/comments.rb +2 -2
  11. data/lib/unparser/diff.rb +8 -1
  12. data/lib/unparser/either.rb +13 -11
  13. data/lib/unparser/emitter/args.rb +5 -1
  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 +18 -7
  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 -8
  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 +4 -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 +11 -11
  46. data/lib/unparser/equalizer.rb +105 -71
  47. data/lib/unparser/generation.rb +33 -29
  48. data/lib/unparser/node_details/send.rb +1 -0
  49. data/lib/unparser/node_details.rb +8 -1
  50. data/lib/unparser/node_helpers.rb +19 -9
  51. data/lib/unparser/util.rb +24 -0
  52. data/lib/unparser/validation.rb +70 -28
  53. data/lib/unparser/writer/array.rb +51 -0
  54. data/lib/unparser/writer/binary.rb +8 -4
  55. data/lib/unparser/writer/dynamic_string.rb +130 -138
  56. data/lib/unparser/writer/regexp.rb +101 -0
  57. data/lib/unparser/writer/resbody.rb +37 -3
  58. data/lib/unparser/writer/rescue.rb +3 -7
  59. data/lib/unparser/writer/send/unary.rb +9 -4
  60. data/lib/unparser/writer/send.rb +8 -14
  61. data/lib/unparser/writer.rb +31 -1
  62. data/lib/unparser.rb +157 -41
  63. metadata +82 -33
  64. data/lib/unparser/concord.rb +0 -114
@@ -7,31 +7,38 @@ 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]
17
21
  #
18
22
  # @api private
19
23
  #
24
+ # rubocop:disable Style/OperatorMethodCall
25
+ # mutant:disable
20
26
  def success?
21
27
  [
22
28
  original_source,
23
- original_node,
29
+ original_ast,
24
30
  generated_source,
25
31
  generated_node
26
32
  ].all?(&:right?) && generated_node.from_right.==(original_node.from_right)
27
33
  end
34
+ # rubocop:enable Style/OperatorMethodCall
28
35
 
29
36
  # Return error report
30
37
  #
31
38
  # @return [String]
32
39
  #
33
40
  # @api private
34
- #
41
+ # mutant:disable
35
42
  def report
36
43
  message = [identification]
37
44
 
@@ -45,48 +52,57 @@ module Unparser
45
52
  end
46
53
  memoize :report
47
54
 
55
+ # mutant:disable
56
+ def original_node
57
+ original_ast.fmap(&:node)
58
+ end
59
+
48
60
  # Create validator from string
49
61
  #
50
62
  # @param [String] original_source
51
63
  #
52
64
  # @return [Validator]
65
+ # mutant:disable
53
66
  def self.from_string(original_source)
54
- original_node = Unparser
55
- .parse_either(original_source)
67
+ original_ast = parse_ast_either(original_source)
56
68
 
57
- generated_source = original_node
69
+ generated_source = original_ast
58
70
  .lmap(&method(:const_unit))
59
- .bind(&Unparser.method(:unparse_either))
71
+ .bind(&method(:unparse_ast_either))
60
72
 
61
73
  generated_node = generated_source
62
74
  .lmap(&method(:const_unit))
63
- .bind(&Unparser.method(:parse_either))
75
+ .bind(&method(:parse_ast_either))
76
+ .fmap(&:node)
64
77
 
65
78
  new(
66
- identification: '(string)',
67
- original_source: Either::Right.new(original_source),
68
- original_node: original_node,
79
+ generated_node: generated_node,
69
80
  generated_source: generated_source,
70
- generated_node: generated_node
81
+ identification: '(string)',
82
+ original_ast: original_ast,
83
+ original_source: Either::Right.new(original_source)
71
84
  )
72
85
  end
73
86
 
74
- # Create validator from node
87
+ # Create validator from ast
75
88
  #
76
- # @param [Parser::AST::Node] original_node
89
+ # @param [Unparser::AST] ast
77
90
  #
78
91
  # @return [Validator]
79
- def self.from_node(original_node)
80
- 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)
81
96
 
82
97
  generated_node = generated_source
83
98
  .lmap(&method(:const_unit))
84
- .bind(&Unparser.public_method(:parse_either))
99
+ .bind(&method(:parse_ast_either))
100
+ .fmap(&:node)
85
101
 
86
102
  new(
87
103
  identification: '(string)',
88
104
  original_source: generated_source,
89
- original_node: Either::Right.new(original_node),
105
+ original_ast: Either::Right.new(ast),
90
106
  generated_source: generated_source,
91
107
  generated_node: generated_node
92
108
  )
@@ -97,24 +113,45 @@ module Unparser
97
113
  # @param [Pathname] path
98
114
  #
99
115
  # @return [Validator]
116
+ #
117
+ # mutant:disable
100
118
  def self.from_path(path)
101
- from_string(path.read).with(identification: path.to_s)
119
+ from_string(path.read.freeze).with(identification: path.to_s)
102
120
  end
103
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
+
104
138
  private
105
139
 
140
+ # mutant:disable
106
141
  def make_report(label, attribute_name)
107
142
  ["#{label}:"].concat(public_send(attribute_name).either(method(:report_exception), ->(value) { [value] }))
108
143
  end
109
144
 
110
- def report_exception(exception)
111
- if exception
112
- [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) || [])
113
149
  else
114
- ['undefined']
150
+ %w[undefined]
115
151
  end
116
152
  end
117
153
 
154
+ # mutant:disable
118
155
  def node_diff_report
119
156
  diff = nil
120
157
 
@@ -130,14 +167,13 @@ module Unparser
130
167
  diff ? ['Node-Diff:', diff] : []
131
168
  end
132
169
 
133
- def self.const_unit(_value); end
134
- private_class_method :const_unit
135
-
136
170
  class Literal < self
171
+ # mutant:disable
137
172
  def success?
138
173
  original_source.eql?(generated_source)
139
174
  end
140
175
 
176
+ # mutant:disable
141
177
  def report
142
178
  message = [identification]
143
179
 
@@ -153,20 +189,26 @@ module Unparser
153
189
 
154
190
  private
155
191
 
192
+ # mutant:disable
156
193
  def source_diff_report
157
194
  diff = nil
158
195
 
159
196
  original_source.fmap do |original|
160
197
  generated_source.fmap do |generated|
161
198
  diff = Diff.new(
162
- original.split("\n", -1),
163
- generated.split("\n", -1)
199
+ encode(original).split("\n", -1),
200
+ encode(generated).split("\n", -1)
164
201
  ).colorized_diff
165
202
  end
166
203
  end
167
204
 
168
205
  diff ? ['Source-Diff:', diff] : []
169
206
  end
207
+
208
+ # mutant:disable
209
+ def encode(string)
210
+ string.encode('UTF-8', invalid: :replace, undef: :replace)
211
+ end
170
212
  end # Literal
171
213
  end # Validation
172
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,122 @@ 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
 
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
16
+
23
17
  private_constant(*constants(false))
24
18
 
25
- def emit_heredoc_reminder
26
- return unless heredoc?
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
28
+ def dispatch
29
+ # Try predictable patterns first before exhaustive search
27
30
 
28
- emit_heredoc_body
29
- emit_heredoc_footer
30
- end
31
+ # Pattern 1: HEREDOC for large dstr (>= 8 children, preserve readability)
32
+ return write_heredoc if children.length >= HEREDOC_THRESHOLD && round_trips_heredoc?
31
33
 
32
- def dispatch
33
- if heredoc?
34
- emit_heredoc_header
35
- else
36
- emit_dstr
37
- end
34
+ # Pattern 2: Limited search (prevent exponential blowup)
35
+ source = limited_search_segmented_source
36
+ return write(source) if source
37
+
38
+ # Pattern 3: HEREDOC fallback (last resort if segmentation fails)
39
+ return write_heredoc if round_trips_heredoc?
40
+
41
+ fail UnsupportedNodeError, "Unparser cannot round trip this node: #{node.inspect}"
38
42
  end
39
43
 
40
44
  private
41
45
 
42
- def heredoc_header
43
- '<<-HEREDOC'
46
+ def write_heredoc
47
+ write(HEREDOC_HEADER)
48
+ buffer.push_heredoc(heredoc_body)
44
49
  end
45
50
 
46
- def heredoc?
47
- !children.empty? && (nl_last_child? && heredoc_pattern?)
51
+ def round_trips_heredoc?
52
+ round_trips?(source: heredoc_source)
48
53
  end
54
+ memoize :round_trips_heredoc?
49
55
 
50
- def emit_heredoc_header
51
- write(heredoc_header)
52
- end
56
+ def limited_search_segmented_source
57
+ each_segments(children) do |segments|
58
+ source = segmented_source(segments: segments)
59
+ return source if round_trips?(source: source)
60
+ end
53
61
 
54
- def emit_heredoc_body
55
- nl
56
- emit_normal_heredoc_body
62
+ nil
57
63
  end
58
64
 
59
- def emit_heredoc_footer
60
- write('HEREDOC')
61
- end
65
+ def each_segments(array)
66
+ yield [array]
62
67
 
63
- def classify(node)
64
- if n_str?(node)
65
- classify_str(node)
66
- else
67
- node.type
68
+ 1.upto(array.length) do |take|
69
+ prefix = [array.take(take)]
70
+ suffix = array.drop(take)
71
+ each_segments(suffix) do |items|
72
+ yield(prefix + items)
73
+ end
68
74
  end
69
75
  end
70
76
 
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
80
- end
81
- end
77
+ def segmented_source(segments:)
78
+ buffer = Buffer.new
82
79
 
83
- def str_nl?(node)
84
- node.eql?(s(:str, "\n"))
85
- end
80
+ Segmented.new(
81
+ buffer:,
82
+ comments:,
83
+ explicit_encoding: nil,
84
+ local_variable_scope:,
85
+ node:,
86
+ segments:
87
+ ).dispatch
86
88
 
87
- def str_empty?(node)
88
- node.eql?(s(:str, ''))
89
+ buffer.content
89
90
  end
90
91
 
91
- def str_ws?(node)
92
- /\A( |\t)+\z/.match?(node.children.first)
93
- end
92
+ def heredoc_body
93
+ buffer = Buffer.new
94
+
95
+ writer = Heredoc.new(
96
+ buffer:,
97
+ comments:,
98
+ explicit_encoding: nil,
99
+ local_variable_scope:,
100
+ node:
101
+ )
94
102
 
95
- def heredoc_pattern?
96
- heredoc_pattern_2? || heredoc_pattern_3?
103
+ writer.emit
104
+ buffer.content
97
105
  end
106
+ memoize :heredoc_body
98
107
 
99
- def heredoc_pattern_3?
100
- children.each_cons(3).any? do |group|
101
- PATTERNS_3.include?(group.map(&method(:classify)))
102
- end
108
+ def heredoc_source
109
+ "#{HEREDOC_HEADER}\n#{heredoc_body}"
103
110
  end
111
+ memoize :heredoc_source
112
+
113
+ class Heredoc
114
+ include Writer, Adamantium
104
115
 
105
- def heredoc_pattern_2?
106
- children.each_cons(2).any? do |group|
107
- PATTERNS_2.include?(group.map(&method(:classify)))
116
+ def emit
117
+ emit_heredoc_body
118
+ write(HEREDOC_FOOTER)
108
119
  end
109
- end
110
120
 
111
- def nl_last_child?
112
- last = children.last
113
- n_str?(last) && last.children.first[-1].eql?("\n")
114
- end
121
+ private
115
122
 
116
- def emit_normal_heredoc_body
117
- buffer.root_indent do
123
+ def emit_heredoc_body
118
124
  children.each do |child|
119
125
  if n_str?(child)
120
126
  write(escape_dynamic(child.children.first))
@@ -123,88 +129,74 @@ module Unparser
123
129
  end
124
130
  end
125
131
  end
126
- end
127
132
 
128
- def escape_dynamic(string)
129
- string.gsub('#', '\#')
130
- end
133
+ def escape_dynamic(string)
134
+ string.gsub('#', '\#')
135
+ end
131
136
 
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
137
+ def emit_dynamic(child)
139
138
  write('#{')
140
139
  emit_dynamic_component(child.children.first)
141
140
  write('}')
142
141
  end
143
- end
144
142
 
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
143
+ def emit_dynamic_component(node)
144
+ visit(node) if node
156
145
  end
157
- end
158
-
159
- def breakpoint?(child, current)
160
- last_type = current.last&.type
146
+ end # Heredoc
161
147
 
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
148
+ class Segmented
149
+ include Writer, Adamantium
168
150
 
169
- def segments
170
- segments = []
151
+ include anima.add(:segments)
171
152
 
172
- segments << current = []
173
-
174
- children.each do |child|
175
- if breakpoint?(child, current)
176
- segments << current = []
153
+ def dispatch
154
+ if children.empty?
155
+ write('%()')
156
+ else
157
+ segments.each_with_index { |segment, index| emit_segment(segment, index) }
177
158
  end
178
-
179
- current << child
180
159
  end
181
160
 
182
- segments
183
- end
161
+ private
184
162
 
185
- def emit_segment(children, index)
186
- write(' ') unless index.zero?
163
+ def emit_segment(children, index)
164
+ write(' ') unless index.zero?
187
165
 
188
- write('"')
189
- emit_body(children)
190
- write('"')
191
- end
166
+ write('"')
167
+ emit_segment_body(children)
168
+ write('"')
169
+ end
192
170
 
193
- def emit_body(children)
194
- buffer.root_indent do
171
+ def emit_segment_body(children)
195
172
  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)
173
+ case child.type
174
+ when :begin
175
+ write('#{')
176
+ visit(child.children.first) if child.children.first
177
+ write('}')
178
+ when FLAT_INTERPOLATION
179
+ write('#')
180
+ visit(child)
181
+ when :str
182
+ visit_str(children, child, index)
183
+ when :dstr
184
+ emit_segment_body(child.children)
205
185
  end
206
186
  end
207
187
  end
188
+
189
+ def visit_str(children, child, index)
190
+ string = child.children.first
191
+
192
+ next_child = children.at(index.succ)
193
+
194
+ if next_child && next_child.type.equal?(:str)
195
+ write(string.gsub('"', '\\"'))
196
+ else
197
+ write(child.children.first.inspect[1..-2])
198
+ end
199
+ end
208
200
  end
209
201
  end # DynamicString
210
202
  end # Writer