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
@@ -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
data/lib/unparser.rb CHANGED
@@ -3,37 +3,76 @@
3
3
  require 'diff/lcs'
4
4
  require 'diff/lcs/hunk'
5
5
  require 'optparse'
6
- require 'parser/current'
7
- require 'set'
8
6
 
9
7
  require 'unparser/equalizer'
10
8
  require 'unparser/adamantium'
11
9
  require 'unparser/adamantium/method_builder'
12
10
  require 'unparser/abstract_type'
13
11
 
14
- require 'unparser/concord'
15
12
  require 'unparser/either'
16
13
  require 'unparser/anima'
17
14
  require 'unparser/anima/attribute'
18
15
  require 'unparser/anima/error'
19
16
 
20
17
  # Library namespace
21
- module Unparser
18
+ module Unparser # rubocop:disable Metrics/ModuleLength
22
19
  # Unparser specific AST builder defaulting to modern AST format
23
- class Builder < Parser::Builders::Default
24
- modernize
20
+ if Gem::Version.new(RUBY_VERSION) <= '3.4'
21
+ require 'parser/current'
22
+ class Builder < Parser::Builders::Default
23
+ modernize
25
24
 
26
- def initialize
27
- super
25
+ # mutant:disable
26
+ def initialize
27
+ super
28
28
 
29
- self.emit_file_line_as_literals = false
29
+ self.emit_file_line_as_literals = false
30
+ end
31
+ end
32
+ else
33
+ require 'prism'
34
+ class Builder < Prism::Translation::Parser::Builder
35
+ modernize
36
+
37
+ # mutant:disable
38
+ def initialize
39
+ super
40
+
41
+ self.emit_file_line_as_literals = false
42
+ end
30
43
  end
31
44
  end
32
45
 
46
+ PARSER_CLASS =
47
+ if Gem::Version.new(RUBY_VERSION) <= '3.4'
48
+ Class.new(Parser::CurrentRuby) do
49
+ def declare_local_variable(local_variable)
50
+ static_env.declare(local_variable)
51
+ end
52
+ end
53
+ else
54
+ prism_translation_parser =
55
+ if Gem::Version.new(RUBY_VERSION) >= '4.0'
56
+ Prism::Translation::Parser40
57
+ else
58
+ Prism::Translation::Parser34
59
+ end
60
+
61
+ Class.new(prism_translation_parser) do
62
+ def declare_local_variable(local_variable)
63
+ (@local_variables ||= Set.new) << local_variable
64
+ end
65
+
66
+ def prism_options
67
+ super.merge(scopes: [@local_variables.to_a])
68
+ end
69
+ end
70
+ end
71
+
33
72
  EMPTY_STRING = ''.freeze
34
73
  EMPTY_ARRAY = [].freeze
35
74
 
36
- private_constant(*constants(false) - %i[Adamantium AbstractType Anima Concord Either Equalizer Memoizable])
75
+ private_constant(*constants(false) - %i[Adamantium AbstractType Anima Either Equalizer Memoizable])
37
76
 
38
77
  # Error raised when unparser encounters an invalid AST
39
78
  class InvalidNodeError < RuntimeError
@@ -44,12 +83,18 @@ module Unparser
44
83
  @node = node
45
84
  freeze
46
85
  end
47
- end
86
+ end # InvalidNodeError
87
+
88
+ # Error raised when unparser encounders AST it cannot generate source for that would parse to the same AST.
89
+ class UnsupportedNodeError < RuntimeError
90
+ end # UnsupportedNodeError
48
91
 
49
92
  # Unparse an AST (and, optionally, comments) into a string
50
93
  #
51
94
  # @param [Parser::AST::Node, nil] node
52
- # @param [Array] comment_array
95
+ # @param [Array] comments
96
+ # @param [Encoding, nil] explicit_encoding
97
+ # @param [Set<Symbol>] static_local_variables
53
98
  #
54
99
  # @return [String]
55
100
  #
@@ -57,44 +102,99 @@ module Unparser
57
102
  # if the node passed is invalid
58
103
  #
59
104
  # @api public
60
- def self.unparse(node, comment_array = [])
61
- return '' if node.nil?
105
+ #
106
+ # mutant:disable
107
+ # rubocop:disable Metrics/ParameterLists
108
+ def self.unparse(
109
+ node,
110
+ comments: EMPTY_ARRAY,
111
+ explicit_encoding: nil,
112
+ static_local_variables: Set.new
113
+ )
114
+ unparse_ast(
115
+ AST.new(
116
+ comments: comments,
117
+ explicit_encoding: explicit_encoding,
118
+ node: node,
119
+ static_local_variables: static_local_variables
120
+ )
121
+ )
122
+ end
123
+ # rubocop:enable Metrics/ParameterLists
124
+
125
+ # Unparse an AST
126
+ #
127
+ # @param [AST] ast
128
+ #
129
+ # @return [String]
130
+ #
131
+ # @raise InvalidNodeError
132
+ # if the node passed is invalid
133
+ #
134
+ # @raise UnsupportedNodeError
135
+ # if the node passed is valid but unparser cannot unparse it
136
+ #
137
+ # @api public
138
+ def self.unparse_ast(ast)
139
+ return EMPTY_STRING if ast.node.nil?
140
+
141
+ local_variable_scope = AST::LocalVariableScope.new(
142
+ node: ast.node,
143
+ static_local_variables: ast.static_local_variables
144
+ )
62
145
 
63
146
  Buffer.new.tap do |buffer|
64
147
  Emitter::Root.new(
65
- buffer,
66
- node,
67
- Comments.new(comment_array)
148
+ buffer: buffer,
149
+ comments: Comments.new(ast.comments),
150
+ explicit_encoding: ast.explicit_encoding,
151
+ local_variable_scope: local_variable_scope,
152
+ node: ast.node
68
153
  ).write_to_buffer
69
154
  end.content
70
155
  end
71
156
 
72
- # Unparse with validation
157
+ # Unparse AST either
73
158
  #
74
- # @param [Parser::AST::Node, nil] node
75
- # @param [Array] comment_array
159
+ # @param [AST] ast
76
160
  #
77
- # @return [Either<Validation,String>]
78
- def self.unparse_validate(node, comment_array = [])
79
- generated = unparse(node, comment_array)
80
- validation = Validation.from_string(generated)
161
+ # @return [Either<Exception,String>]
162
+ def self.unparse_ast_either(ast)
163
+ Either.wrap_error(Exception) { unparse_ast(ast) }
164
+ end
165
+
166
+ # Unparse AST either
167
+ #
168
+ # @param [AST] ast
169
+ #
170
+ # @return [Either<Exception,String>]
171
+ #
172
+ # mutant:disable
173
+ def self.unparse_validate_ast_either(ast:)
174
+ validation = Validation.from_ast(ast:)
81
175
 
82
176
  if validation.success?
83
- Either::Right.new(generated)
177
+ Either::Right.new(validation.generated_source.from_right)
84
178
  else
85
179
  Either::Left.new(validation)
86
180
  end
87
181
  end
88
182
 
89
- # Unparse capturing errors
90
- #
91
- # This is mostly useful for writing testing tools against unparser.
183
+ # Unparse with validation
92
184
  #
93
185
  # @param [Parser::AST::Node, nil] node
186
+ # @param [Array] comments
94
187
  #
95
- # @return [Either<Exception, String>]
96
- def self.unparse_either(node)
97
- Either.wrap_error(Exception) { unparse(node) }
188
+ # @return [Either<Validation,String>]
189
+ def self.unparse_validate(node, comments: EMPTY_ARRAY)
190
+ generated = unparse(node, comments:)
191
+ validation = Validation.from_string(generated)
192
+
193
+ if validation.success?
194
+ Either::Right.new(generated)
195
+ else
196
+ Either::Left.new(validation)
197
+ end
98
198
  end
99
199
 
100
200
  # Parse string into AST
@@ -103,27 +203,37 @@ module Unparser
103
203
  #
104
204
  # @return [Parser::AST::Node, nil]
105
205
  def self.parse(source)
106
- parser.parse(buffer(source))
206
+ parse_ast(source).node
107
207
  end
108
208
 
109
209
  # Parse string into either syntax error or AST
110
210
  #
111
211
  # @param [String] source
112
212
  #
113
- # @return [Either<Parser::SyntaxError, (Parser::ASTNode, nil)>]
114
- def self.parse_either(source)
115
- Either.wrap_error(Parser::SyntaxError) do
116
- parser.parse(buffer(source))
213
+ # @return [Either<Exception, (Parser::ASTNode, nil)>]
214
+ def self.parse_ast_either(source)
215
+ Either.wrap_error(Exception) do
216
+ parse_ast(source)
117
217
  end
118
218
  end
119
219
 
120
- # Parse string into AST, with comments
220
+ # Parse source with ast details
121
221
  #
122
222
  # @param [String] source
123
223
  #
124
- # @return [Parser::AST::Node]
125
- def self.parse_with_comments(source)
126
- parser.parse_with_comments(buffer(source))
224
+ # @return [AST]
225
+ #
226
+ # mutant:disable
227
+ def self.parse_ast(source, static_local_variables: Set.new)
228
+ explicit_encoding = Parser::Source::Buffer.recognize_encoding(source.dup.force_encoding(Encoding::BINARY))
229
+ node, comments = parser.parse_with_comments(buffer(source))
230
+
231
+ AST.new(
232
+ comments: comments,
233
+ explicit_encoding: explicit_encoding,
234
+ node: node,
235
+ static_local_variables: static_local_variables
236
+ )
127
237
  end
128
238
 
129
239
  # Parser instance that produces AST unparser understands
@@ -131,8 +241,9 @@ module Unparser
131
241
  # @return [Parser::Base]
132
242
  #
133
243
  # @api private
244
+ # mutant:disable
134
245
  def self.parser
135
- Parser::CurrentRuby.new(Builder.new).tap do |parser|
246
+ PARSER_CLASS.new(Builder.new).tap do |parser|
136
247
  parser.diagnostics.tap do |diagnostics|
137
248
  diagnostics.all_errors_are_fatal = true
138
249
  end
@@ -179,6 +290,7 @@ require 'unparser/emitter/def'
179
290
  require 'unparser/emitter/defined'
180
291
  require 'unparser/emitter/dstr'
181
292
  require 'unparser/emitter/dsym'
293
+ require 'unparser/emitter/ensure'
182
294
  require 'unparser/emitter/flipflop'
183
295
  require 'unparser/emitter/float'
184
296
  require 'unparser/emitter/flow_modifier'
@@ -210,6 +322,7 @@ require 'unparser/emitter/rescue'
210
322
  require 'unparser/emitter/root'
211
323
  require 'unparser/emitter/send'
212
324
  require 'unparser/emitter/simple'
325
+ require 'unparser/emitter/string'
213
326
  require 'unparser/emitter/splat'
214
327
  require 'unparser/emitter/super'
215
328
  require 'unparser/emitter/undef'
@@ -222,8 +335,10 @@ require 'unparser/emitter/find_pattern'
222
335
  require 'unparser/emitter/match_pattern'
223
336
  require 'unparser/emitter/match_pattern_p'
224
337
  require 'unparser/writer'
338
+ require 'unparser/writer/array'
225
339
  require 'unparser/writer/binary'
226
340
  require 'unparser/writer/dynamic_string'
341
+ require 'unparser/writer/regexp'
227
342
  require 'unparser/writer/resbody'
228
343
  require 'unparser/writer/rescue'
229
344
  require 'unparser/writer/send'
@@ -234,6 +349,7 @@ require 'unparser/writer/send/unary'
234
349
  require 'unparser/node_details'
235
350
  require 'unparser/node_details/send'
236
351
  require 'unparser/cli'
352
+ require 'unparser/util'
237
353
 
238
354
  require 'unparser/validation'
239
355
  # make it easy for zombie