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