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
@@ -2,22 +2,15 @@
2
2
 
3
3
  module Unparser
4
4
  class Emitter
5
- # Root emitter a special case
6
5
  class Root < self
7
- include Concord::Public.new(:buffer, :node, :comments)
8
- include LocalVariableRoot
9
-
10
6
  END_NL = %i[class sclass module begin].freeze
11
7
 
12
8
  private_constant(*constants(false))
13
9
 
14
10
  def dispatch
15
- if children.any?
16
- emit_body(node, indent: false)
17
- else
18
- visit_deep(node)
19
- end
11
+ emit_body(node, indent: false)
20
12
 
13
+ buffer.nl_flush_heredocs
21
14
  emit_eof_comments
22
15
 
23
16
  nl if END_NL.include?(node.type) && !buffer.fresh_line?
@@ -10,10 +10,6 @@ module Unparser
10
10
  writer.emit_mlhs
11
11
  end
12
12
 
13
- def emit_heredoc_reminders
14
- writer.emit_heredoc_reminders
15
- end
16
-
17
13
  private
18
14
 
19
15
  def dispatch
@@ -21,7 +17,7 @@ module Unparser
21
17
  end
22
18
 
23
19
  def writer
24
- writer_with(Writer::Send, node)
20
+ writer_with(Writer::Send, node:)
25
21
  end
26
22
  memoize :writer
27
23
  end # Send
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unparser
4
+ class Emitter
5
+ # Base class for primitive emitters
6
+ class String < self
7
+ children :value
8
+
9
+ handle :str
10
+
11
+ private
12
+
13
+ def dispatch
14
+ if explicit_encoding && !value.encoding.equal?(explicit_encoding)
15
+ write_utf8_escaped
16
+ else
17
+ write(value.inspect)
18
+ end
19
+ end
20
+
21
+ def write_utf8_escaped
22
+ write('"')
23
+ value.each_codepoint do |codepoint|
24
+ write("\\u{#{codepoint.to_s(16)}}")
25
+ end
26
+ write('"')
27
+ end
28
+
29
+ end # String
30
+ end # Emitter
31
+ end # Unparser
@@ -41,6 +41,8 @@ module Unparser
41
41
  children.each do |child|
42
42
  if n_begin?(child)
43
43
  emit_begin(child)
44
+ elsif n_gvar?(child)
45
+ emit_gvar(child)
44
46
  else
45
47
  emit_string(child)
46
48
  end
@@ -64,9 +66,14 @@ module Unparser
64
66
 
65
67
  def emit_begin(component)
66
68
  write('#{')
67
- visit(unwrap_single_begin(component))
69
+ visit(Util.one(component.children)) if component.children.any?
68
70
  write('}')
69
71
  end
72
+
73
+ def emit_gvar(component)
74
+ write('#')
75
+ write(Util.one(component.children).to_s)
76
+ end
70
77
  end # XStr
71
78
  end # Emitter
72
79
  end # Unparser
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Unparser
4
- UnknownNodeError = Class.new(ArgumentError)
4
+ class UnknownNodeError < ArgumentError
5
+ end
5
6
 
6
7
  # Emitter base class
7
8
  class Emitter
8
9
  include Adamantium, AbstractType, Constants, Generation, NodeHelpers
9
- include Anima.new(:buffer, :comments, :node, :local_variable_scope)
10
+ include Anima.new(:buffer, :comments, :explicit_encoding, :local_variable_scope, :node)
10
11
 
11
12
  public :node
12
13
 
@@ -22,10 +23,9 @@ module Unparser
22
23
  #
23
24
  # @return [Parser::AST::Node]
24
25
  #
25
- # @api private
26
- #
26
+ # mutant:disable
27
27
  def local_variable_scope
28
- AST::LocalVariableScope.new(node)
28
+ AST::LocalVariableScope.new(node: node, static_local_variables: Set.new)
29
29
  end
30
30
 
31
31
  def self.included(descendant)
@@ -67,7 +67,7 @@ module Unparser
67
67
  # @api private
68
68
  #
69
69
  # rubocop:disable Metrics/ParameterLists
70
- def self.emitter(buffer:, comments:, node:, local_variable_scope:)
70
+ def self.emitter(buffer:, explicit_encoding:, comments:, node:, local_variable_scope:)
71
71
  type = node.type
72
72
 
73
73
  klass = REGISTRY.fetch(type) do
@@ -75,10 +75,11 @@ module Unparser
75
75
  end
76
76
 
77
77
  klass.new(
78
- buffer: buffer,
79
- comments: comments,
80
- local_variable_scope: local_variable_scope,
81
- node: node
78
+ buffer:,
79
+ comments:,
80
+ explicit_encoding:,
81
+ local_variable_scope:,
82
+ node:
82
83
  )
83
84
  end
84
85
  # rubocop:enable Metrics/ParameterLists
@@ -90,6 +91,5 @@ module Unparser
90
91
  # @api private
91
92
  #
92
93
  abstract_method :dispatch
93
-
94
94
  end # Emitter
95
95
  end # Unparser
@@ -4,95 +4,129 @@ module Unparser
4
4
  # Define equality, equivalence and inspection methods
5
5
  #
6
6
  # Original code before vendoring and reduction from: https://github.com/dkubb/equalizer.
7
- class Equalizer < Module
8
- # Initialize an Equalizer with the given keys
7
+ module Equalizer
8
+ # Creates a module providing equality methods based on the given attributes
9
9
  #
10
- # Will use the keys with which it is initialized to define #cmp?,
11
- # #hash, and #inspect
10
+ # @param keys [Array<Symbol>] attribute names to use for equality
11
+ # @param inspect [Boolean] whether to override #inspect and #pretty_print
12
+ # @return [Module] a module to include in your class
13
+ # @raise [ArgumentError] if keys is empty or contains non-Symbols
12
14
  #
13
- # @param [Array<Symbol>] keys
14
- #
15
- # @return [undefined]
16
- #
17
- # @api private
18
- #
19
- # rubocop:disable Lint/MissingSuper
20
- def initialize(*keys)
21
- @keys = keys
22
- define_methods
23
- freeze
24
- end
25
- # rubocop:enable Lint/MissingSuper
26
-
27
- private
28
-
29
- def included(descendant)
30
- descendant.include(Methods)
31
- end
32
-
33
- def define_methods
34
- define_cmp_method
35
- define_hash_method
36
- define_inspect_method
15
+ # @api public
16
+ def self.new(*keys, inspect: true)
17
+ build_module(keys.freeze, inspect:)
37
18
  end
38
19
 
39
- def define_cmp_method
40
- keys = @keys
41
- define_method(:cmp?) do |comparator, other|
42
- keys.all? do |key|
43
- __send__(key).public_send(comparator, other.__send__(key))
44
- end
20
+ # Instance methods mixed into classes that include an Equalizer module
21
+ #
22
+ # @api private
23
+ module InstanceMethods
24
+ # Equality comparison allowing subclasses
25
+ #
26
+ # @param other [Object] object to compare
27
+ # @return [Boolean] true if other is_a? same class with equal attributes
28
+ def ==(other)
29
+ other.is_a?(self.class) &&
30
+ cmp?(:==, other)
45
31
  end
46
- private :cmp?
47
- end
48
32
 
49
- def define_hash_method
50
- keys = @keys
51
- define_method(:hash) do
52
- keys.map(&public_method(:__send__)).push(self.class).hash
33
+ # Strict equality requiring exact class match
34
+ #
35
+ # @param other [Object] object to compare
36
+ # @return [Boolean] true if other is exact same class with eql? attributes
37
+ def eql?(other)
38
+ other.instance_of?(self.class) &&
39
+ cmp?(:eql?, other)
53
40
  end
54
- end
55
41
 
56
- def define_inspect_method
57
- keys = @keys
58
- define_method(:inspect) do
59
- klass = self.class
60
- name = klass.name || klass.inspect
61
- "#<#{name}#{keys.map { |key| " #{key}=#{__send__(key).inspect}" }.join}>"
42
+ # Hash code based on class and attribute values
43
+ #
44
+ # @return [Integer] hash code
45
+ def hash
46
+ [self.class, *deconstruct].hash
62
47
  end
63
- end
64
48
 
65
- # The comparison methods
66
- module Methods
67
- # Compare the object with other object for equality
49
+ # Array deconstruction for pattern matching
68
50
  #
69
- # @example
70
- # object.eql?(other) # => true or false
51
+ # @return [Array] attribute values in order
52
+ def deconstruct
53
+ equalizer_keys.map { |key| public_send(key) }
54
+ end
55
+
56
+ # Hash deconstruction for pattern matching
71
57
  #
72
- # @param [Object] other
73
- # the other object to compare with
58
+ # @param requested [Array<Symbol>, nil] keys to include, or nil for all
59
+ # @return [Hash{Symbol => Object}] requested attribute key-value pairs
60
+ def deconstruct_keys(requested)
61
+ subset = requested.nil? ? equalizer_keys : equalizer_keys & requested
62
+ subset.to_h { |key| [key, public_send(key)] }
63
+ end
64
+
65
+ private
66
+
67
+ # Compare all attributes using the given comparator
74
68
  #
75
- # @return [Boolean]
69
+ # @param comparator [Symbol] method to use for comparison
70
+ # @param other [Object] object to compare against
71
+ # @return [Boolean] true if all attributes match
76
72
  #
77
- # @api public
78
- def eql?(other)
79
- instance_of?(other.class) && cmp?(__method__, other)
73
+ # @api private
74
+ def cmp?(comparator, other)
75
+ equalizer_keys.all? do |key|
76
+ public_send(key)
77
+ .public_send(comparator, other.public_send(key))
78
+ end
80
79
  end
80
+ end
81
81
 
82
- # Compare the object with other object for equivalency
83
- #
84
- # @example
85
- # object == other # => true or false
82
+ # Instance methods for inspect and pretty print output
83
+ #
84
+ # @api private
85
+ module InspectMethods
86
+ # String representation showing only equalizer attributes
86
87
  #
87
- # @param [Object] other
88
- # the other object to compare with
88
+ # @return [String] inspect output
89
+ def inspect
90
+ attrs = equalizer_keys
91
+ .map { |key| "@#{key}=#{public_send(key).inspect}" }
92
+ .join(', ')
93
+ Object.instance_method(:to_s).bind_call(self).sub(/>\z/, " #{attrs}>")
94
+ end
95
+
96
+ # Pretty print output using PP's object formatting
89
97
  #
90
- # @return [Boolean]
98
+ # @param q [PP] pretty printer
99
+ # @return [void]
100
+ def pretty_print(pretty_printer)
101
+ pretty_printer.pp_object(self)
102
+ end
103
+
104
+ # Instance variables to display in pretty print output
91
105
  #
92
- # @api public
93
- def ==(other)
94
- instance_of?(other.class) && cmp?(__method__, other)
106
+ # @return [Array<Symbol>] instance variable names
107
+ def pretty_print_instance_variables
108
+ equalizer_keys.map { |key| :"@#{key}" }
95
109
  end
96
- end # module Methods
97
- end # class Equalizer
110
+ end
111
+
112
+ # Builds the module with equality methods for the given keys
113
+ #
114
+ # @param keys [Array<Symbol>] attribute names (frozen)
115
+ # @param inspect [Boolean] whether to include inspect methods
116
+ # @return [Module] the configured module
117
+ #
118
+ # @api private
119
+ def self.build_module(keys, inspect:)
120
+ Module.new do
121
+ include InstanceMethods
122
+ include InspectMethods if inspect
123
+
124
+ set_temporary_name("Equalizer(#{keys.join(', ')})")
125
+
126
+ define_method(:equalizer_keys) { keys }
127
+ private :equalizer_keys
128
+ end
129
+ end
130
+ private_class_method :build_module
131
+ end # module Equalizer
98
132
  end # Unparser
@@ -7,8 +7,6 @@ module Unparser
7
7
 
8
8
  private_constant(*constants(false))
9
9
 
10
- def emit_heredoc_reminders; end
11
-
12
10
  def symbol_name; end
13
11
 
14
12
  def write_to_buffer
@@ -121,40 +119,50 @@ module Unparser
121
119
  end
122
120
 
123
121
  def emit_body(node, indent: true)
124
- if indent
125
- buffer.indent
126
- nl
127
- end
128
-
129
- if n_begin?(node)
130
- if node.children.one?
131
- visit_deep(node)
122
+ with_indent(indent: indent) do
123
+ if n_begin?(node)
124
+ if node.children.empty?
125
+ write('()')
126
+ elsif node.children.one?
127
+ visit_deep(node)
128
+ else
129
+ emit_body_inner(node)
130
+ end
132
131
  else
133
- emit_body_inner(node)
132
+ visit_deep(node)
134
133
  end
135
- else
136
- visit_deep(node)
137
134
  end
135
+ end
138
136
 
139
- if indent
140
- buffer.unindent
141
- nl
142
- end
137
+ def with_indent(indent:)
138
+ return yield unless indent
139
+
140
+ buffer.indent
141
+ nl
142
+ yield
143
+ buffer.unindent
144
+ nl
143
145
  end
144
146
 
145
147
  def emit_body_inner(node)
146
148
  head, *tail = node.children
147
149
  emit_body_member(head)
150
+ write(';') if requires_explicit_statement_terminator?(head, tail)
148
151
 
149
152
  tail.each do |child|
150
- nl
153
+ buffer.ensure_nl
151
154
 
152
155
  nl if EXTRA_NL.include?(child.type)
153
156
 
154
157
  emit_body_member(child)
158
+ write(';') if requires_explicit_statement_terminator?(child, tail)
155
159
  end
156
160
  end
157
161
 
162
+ def requires_explicit_statement_terminator?(node, nodes_group)
163
+ n_range?(node) && node.children.fetch(1).nil? && !node.eql?(nodes_group.fetch(-1))
164
+ end
165
+
158
166
  def emit_body_member(node)
159
167
  if n_rescue?(node)
160
168
  emit_rescue_postcontrol(node)
@@ -204,21 +212,20 @@ module Unparser
204
212
  end
205
213
 
206
214
  def emit_rescue_postcontrol(node)
207
- writer = writer_with(Writer::Rescue, node)
215
+ writer = writer_with(Writer::Rescue, node:)
208
216
  writer.emit_postcontrol
209
- writer.emit_heredoc_reminders
210
217
  end
211
218
 
212
219
  def emit_rescue_regular(node)
213
- writer_with(Writer::Rescue, node).emit_regular
220
+ writer_with(Writer::Rescue, node:).emit_regular
214
221
  end
215
222
 
216
- def writer_with(klass, node)
217
- klass.new(to_h.merge(node: node))
223
+ def emitter(node)
224
+ Emitter.emitter(**to_h, node: node)
218
225
  end
219
226
 
220
- def emitter(node)
221
- Emitter.emitter(**to_h.merge(node: node))
227
+ def writer_with(klass, node:, **attributes)
228
+ klass.new(to_h.merge(node: node, **attributes))
222
229
  end
223
230
 
224
231
  def visit(node)
@@ -226,10 +233,7 @@ module Unparser
226
233
  end
227
234
 
228
235
  def visit_deep(node)
229
- emitter(node).tap do |emitter|
230
- emitter.write_to_buffer
231
- emitter.emit_heredoc_reminders
232
- end
236
+ emitter(node).tap(&:write_to_buffer)
233
237
  end
234
238
 
235
239
  def first_child
@@ -20,6 +20,7 @@ module Unparser
20
20
 
21
21
  def binary_syntax_allowed?
22
22
  selector_binary_operator? \
23
+ && n_send?(node) \
23
24
  && arguments.one? \
24
25
  && !n_splat?(arguments.first) \
25
26
  && !n_kwargs?(arguments.first)
@@ -4,9 +4,16 @@ module Unparser
4
4
  module NodeDetails
5
5
  include Constants, NodeHelpers
6
6
 
7
+ # mutant:disable
7
8
  def self.included(descendant)
8
9
  descendant.class_eval do
9
- include Adamantium, Concord.new(:node)
10
+ include Adamantium, Equalizer.new(:node)
11
+
12
+ attr_reader :node
13
+
14
+ def initialize(node)
15
+ @node = node
16
+ end
10
17
 
11
18
  extend DSL
12
19
  end
@@ -31,7 +31,16 @@ module Unparser
31
31
  node.type.equal?(type)
32
32
  end
33
33
 
34
+ def n_flipflop?(node)
35
+ n_iflipflop?(node) || n_eflipflop?(node)
36
+ end
37
+
38
+ def n_range?(node)
39
+ n_irange?(node) || n_erange?(node)
40
+ end
41
+
34
42
  %i[
43
+ and
35
44
  arg
36
45
  args
37
46
  array
@@ -41,18 +50,26 @@ module Unparser
41
50
  cbase
42
51
  const
43
52
  dstr
53
+ eflipflop
44
54
  empty_else
55
+ erange
45
56
  ensure
57
+ gvar
46
58
  hash
47
59
  hash_pattern
48
60
  if
61
+ iflipflop
49
62
  in_pattern
50
63
  int
64
+ irange
51
65
  kwarg
52
66
  kwargs
53
67
  kwsplat
54
68
  lambda
69
+ lvar
55
70
  match_rest
71
+ mlhs
72
+ or
56
73
  pair
57
74
  rescue
58
75
  send
@@ -60,20 +77,13 @@ module Unparser
60
77
  splat
61
78
  str
62
79
  sym
63
- ].each do |type|
80
+ xstr
81
+ ].to_set.each do |type|
64
82
  name = "n_#{type}?"
65
83
  define_method(name) do |node|
66
84
  n?(type, node)
67
85
  end
68
86
  private(name)
69
87
  end
70
-
71
- def unwrap_single_begin(node)
72
- if n_begin?(node) && node.children.one?
73
- node.children.first
74
- else
75
- node
76
- end
77
- end
78
88
  end # NodeHelpers
79
89
  end # Unparser
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unparser
4
+ # Original code before vendoring and reduction from: https://github.com/mbj/mutant/blob/main/lib/mutant/util.rb
5
+ module Util
6
+ # Error raised by `Util.one` if size is not exactly one
7
+ class SizeError < IndexError
8
+ end
9
+
10
+ # Return only element in array if it contains exactly one member
11
+ #
12
+ # @param array [Array]
13
+ #
14
+ # @return [Object] first entry
15
+ def self.one(array)
16
+ case array
17
+ in [value]
18
+ value
19
+ else
20
+ fail SizeError, "expected size to be exactly 1 but size was #{array.size}"
21
+ end
22
+ end
23
+ end
24
+ end