parser 2.4.0.2 → 2.5.0.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +5 -6
  3. data/CHANGELOG.md +35 -1
  4. data/Gemfile +2 -0
  5. data/README.md +1 -2
  6. data/Rakefile +2 -1
  7. data/bin/ruby-parse +2 -1
  8. data/bin/ruby-rewrite +2 -1
  9. data/lib/gauntlet_parser.rb +2 -0
  10. data/lib/parser.rb +16 -17
  11. data/lib/parser/all.rb +2 -0
  12. data/lib/parser/ast/node.rb +2 -0
  13. data/lib/parser/ast/processor.rb +2 -0
  14. data/lib/parser/base.rb +6 -12
  15. data/lib/parser/builders/default.rb +28 -47
  16. data/lib/parser/clobbering_error.rb +2 -0
  17. data/lib/parser/context.rb +42 -0
  18. data/lib/parser/current.rb +4 -20
  19. data/lib/parser/deprecation.rb +13 -0
  20. data/lib/parser/diagnostic.rb +3 -3
  21. data/lib/parser/diagnostic/engine.rb +2 -0
  22. data/lib/parser/lexer.rl +122 -60
  23. data/lib/parser/lexer/dedenter.rb +2 -0
  24. data/lib/parser/lexer/explanation.rb +2 -0
  25. data/lib/parser/lexer/literal.rb +4 -9
  26. data/lib/parser/lexer/stack_state.rb +4 -1
  27. data/lib/parser/macruby.y +32 -17
  28. data/lib/parser/messages.rb +14 -0
  29. data/lib/parser/meta.rb +2 -0
  30. data/lib/parser/rewriter.rb +30 -44
  31. data/lib/parser/ruby18.y +20 -13
  32. data/lib/parser/ruby19.y +32 -17
  33. data/lib/parser/ruby20.y +33 -18
  34. data/lib/parser/ruby21.y +32 -17
  35. data/lib/parser/ruby22.y +32 -17
  36. data/lib/parser/ruby23.y +32 -17
  37. data/lib/parser/ruby24.y +63 -46
  38. data/lib/parser/ruby25.y +72 -48
  39. data/lib/parser/rubymotion.y +33 -18
  40. data/lib/parser/runner.rb +4 -7
  41. data/lib/parser/runner/ruby_parse.rb +10 -0
  42. data/lib/parser/runner/ruby_rewrite.rb +2 -0
  43. data/lib/parser/source/buffer.rb +19 -24
  44. data/lib/parser/source/comment.rb +2 -0
  45. data/lib/parser/source/comment/associator.rb +2 -0
  46. data/lib/parser/source/map.rb +2 -0
  47. data/lib/parser/source/map/collection.rb +2 -0
  48. data/lib/parser/source/map/condition.rb +2 -0
  49. data/lib/parser/source/map/constant.rb +2 -0
  50. data/lib/parser/source/map/definition.rb +2 -0
  51. data/lib/parser/source/map/for.rb +2 -0
  52. data/lib/parser/source/map/heredoc.rb +2 -0
  53. data/lib/parser/source/map/keyword.rb +2 -0
  54. data/lib/parser/source/map/objc_kwarg.rb +2 -0
  55. data/lib/parser/source/map/operator.rb +2 -0
  56. data/lib/parser/source/map/rescue_body.rb +2 -0
  57. data/lib/parser/source/map/send.rb +2 -0
  58. data/lib/parser/source/map/ternary.rb +2 -0
  59. data/lib/parser/source/map/variable.rb +2 -0
  60. data/lib/parser/source/range.rb +81 -13
  61. data/lib/parser/source/rewriter.rb +48 -10
  62. data/lib/parser/source/rewriter/action.rb +2 -0
  63. data/lib/parser/source/tree_rewriter.rb +301 -0
  64. data/lib/parser/source/tree_rewriter/action.rb +133 -0
  65. data/lib/parser/static_environment.rb +2 -0
  66. data/lib/parser/syntax_error.rb +2 -0
  67. data/lib/parser/tree_rewriter.rb +133 -0
  68. data/lib/parser/version.rb +3 -1
  69. data/parser.gemspec +4 -1
  70. data/test/bug_163/fixtures/input.rb +2 -0
  71. data/test/bug_163/fixtures/output.rb +2 -0
  72. data/test/bug_163/rewriter.rb +2 -0
  73. data/test/helper.rb +7 -7
  74. data/test/parse_helper.rb +57 -10
  75. data/test/racc_coverage_helper.rb +2 -0
  76. data/test/test_base.rb +2 -0
  77. data/test/test_current.rb +2 -4
  78. data/test/test_diagnostic.rb +3 -1
  79. data/test/test_diagnostic_engine.rb +2 -0
  80. data/test/test_encoding.rb +61 -49
  81. data/test/test_lexer.rb +164 -77
  82. data/test/test_lexer_stack_state.rb +2 -0
  83. data/test/test_parse_helper.rb +8 -8
  84. data/test/test_parser.rb +613 -51
  85. data/test/test_runner_rewrite.rb +47 -0
  86. data/test/test_source_buffer.rb +22 -10
  87. data/test/test_source_comment.rb +2 -0
  88. data/test/test_source_comment_associator.rb +2 -0
  89. data/test/test_source_map.rb +2 -0
  90. data/test/test_source_range.rb +92 -45
  91. data/test/test_source_rewriter.rb +3 -1
  92. data/test/test_source_rewriter_action.rb +2 -0
  93. data/test/test_source_tree_rewriter.rb +177 -0
  94. data/test/test_static_environment.rb +2 -0
  95. data/test/using_tree_rewriter/fixtures/input.rb +3 -0
  96. data/test/using_tree_rewriter/fixtures/output.rb +3 -0
  97. data/test/using_tree_rewriter/using_tree_rewriter.rb +9 -0
  98. metadata +21 -10
  99. data/lib/parser/compatibility/ruby1_8.rb +0 -20
  100. data/lib/parser/compatibility/ruby1_9.rb +0 -32
  101. data/test/bug_163/test_runner_rewrite.rb +0 -35
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Parser
2
4
  module Source
3
5
 
@@ -0,0 +1,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parser
4
+ module Source
5
+
6
+ ##
7
+ # {TreeRewriter} performs the heavy lifting in the source rewriting process.
8
+ # It schedules code updates to be performed in the correct order.
9
+ #
10
+ # For simple cases, the resulting source will be obvious.
11
+ #
12
+ # Examples for more complex cases follow. Assume these examples are acting on
13
+ # the source `'puts(:hello, :world)`. The methods #wrap, #remove, etc.
14
+ # receive a Range as first argument; for clarity, examples below use english
15
+ # sentences and a string of raw code instead.
16
+ #
17
+ # ## Overlapping ranges:
18
+ #
19
+ # Any two rewriting actions on overlapping ranges will fail and raise
20
+ # a `ClobberingError`, unless they are both deletions (covered next).
21
+ #
22
+ # * wrap ':hello, ' with '(' and ')'
23
+ # * wrap ', :world' with '(' and ')'
24
+ # => CloberringError
25
+ #
26
+ # ## Overlapping deletions:
27
+ #
28
+ # * remove ':hello, '
29
+ # * remove ', :world'
30
+ #
31
+ # The overlapping ranges are merged and `':hello, :world'` will be removed.
32
+ # This policy can be changed. `:crossing_deletions` defaults to `:accept`
33
+ # but can be set to `:warn` or `:raise`.
34
+ #
35
+ # ## Multiple actions at the same end points:
36
+ #
37
+ # Results will always be independent on the order they were given.
38
+ # Exception: rewriting actions done on exactly the same range (covered next).
39
+ #
40
+ # Example:
41
+ # * replace ', ' by ' => '
42
+ # * wrap ':hello, :world' with '{' and '}'
43
+ # * replace ':world' with ':everybody'
44
+ # * wrap ':world' with '[', ']'
45
+ #
46
+ # The resulting string will be `'puts({:hello => [:everybody]})'`
47
+ # and this result is independent on the order the instructions were given in.
48
+ #
49
+ # Note that if the two "replace" were given as a single replacement of ', :world'
50
+ # for ' => :everybody', the result would be a `ClobberingError` because of the wrap
51
+ # in square brackets.
52
+ #
53
+ # ## Multiple wraps on same range:
54
+ # * wrap ':hello' with '(' and ')'
55
+ # * wrap ':hello' with '[' and ']'
56
+ #
57
+ # The wraps are combined in order given and results would be `'puts([(:hello)], :world)'`.
58
+ #
59
+ # ## Multiple replacements on same range:
60
+ # * replace ':hello' by ':hi', then
61
+ # * replace ':hello' by ':hey'
62
+ #
63
+ # The replacements are made in the order given, so the latter replacement
64
+ # supersedes the former and ':hello' will be replaced by ':hey'.
65
+ #
66
+ # This policy can be changed. `:different_replacements` defaults to `:accept`
67
+ # but can be set to `:warn` or `:raise`.
68
+ #
69
+ # ## Swallowed insertions:
70
+ # wrap 'world' by '__', '__'
71
+ # replace ':hello, :world' with ':hi'
72
+ #
73
+ # A containing replacement will swallow the contained rewriting actions
74
+ # and `':hello, :world'` will be replaced by `':hi'`.
75
+ #
76
+ # This policy can be changed for swallowed insertions. `:swallowed_insertions`
77
+ # defaults to `:accept` but can be set to `:warn` or `:raise`
78
+ #
79
+ # ## Implementation
80
+ # The updates are organized in a tree, according to the ranges they act on
81
+ # (where children are strictly contained by their parent), hence the name.
82
+ #
83
+ # @!attribute [r] source_buffer
84
+ # @return [Source::Buffer]
85
+ #
86
+ # @!attribute [r] diagnostics
87
+ # @return [Diagnostic::Engine]
88
+ #
89
+ # @api public
90
+ #
91
+ class TreeRewriter
92
+ attr_reader :source_buffer
93
+ attr_reader :diagnostics
94
+
95
+ ##
96
+ # @param [Source::Buffer] source_buffer
97
+ #
98
+ def initialize(source_buffer,
99
+ crossing_deletions: :accept,
100
+ different_replacements: :accept,
101
+ swallowed_insertions: :accept)
102
+ @diagnostics = Diagnostic::Engine.new
103
+ @diagnostics.consumer = -> diag { $stderr.puts diag.render }
104
+
105
+ @source_buffer = source_buffer
106
+ @in_transaction = false
107
+
108
+ @policy = {crossing_deletions: crossing_deletions,
109
+ different_replacements: different_replacements,
110
+ swallowed_insertions: swallowed_insertions}.freeze
111
+ check_policy_validity
112
+
113
+ @enforcer = method(:enforce_policy)
114
+ # We need a range that would be jugded as containing all other ranges,
115
+ # including 0...0 and size...size:
116
+ all_encompassing_range = @source_buffer.source_range.adjust(begin_pos: -1, end_pos: +1)
117
+ @action_root = TreeRewriter::Action.new(all_encompassing_range, @enforcer)
118
+ end
119
+
120
+ ##
121
+ # Replaces the code of the source range `range` with `content`.
122
+ #
123
+ # @param [Range] range
124
+ # @param [String] content
125
+ # @return [Rewriter] self
126
+ # @raise [ClobberingError] when clobbering is detected
127
+ #
128
+ def replace(range, content)
129
+ combine(range, replacement: content)
130
+ end
131
+
132
+ ##
133
+ # Inserts the given strings before and after the given range.
134
+ #
135
+ # @param [Range] range
136
+ # @param [String or nil] insert_before
137
+ # @param [String or nil] insert_after
138
+ # @return [Rewriter] self
139
+ # @raise [ClobberingError] when clobbering is detected
140
+ #
141
+ def wrap(range, insert_before, insert_after)
142
+ combine(range, insert_before: insert_before.to_s, insert_after: insert_after.to_s)
143
+ end
144
+
145
+ ##
146
+ # Shortcut for `replace(range, '')`
147
+ #
148
+ # @param [Range] range
149
+ # @return [Rewriter] self
150
+ # @raise [ClobberingError] when clobbering is detected
151
+ #
152
+ def remove(range)
153
+ replace(range, ''.freeze)
154
+ end
155
+
156
+
157
+ ##
158
+ # Shortcut for `wrap(range, content, nil)`
159
+ #
160
+ # @param [Range] range
161
+ # @param [String] content
162
+ # @return [Rewriter] self
163
+ # @raise [ClobberingError] when clobbering is detected
164
+ #
165
+ def insert_before(range, content)
166
+ wrap(range, content, nil)
167
+ end
168
+
169
+ ##
170
+ # Shortcut for `wrap(range, nil, content)`
171
+ #
172
+ # @param [Range] range
173
+ # @param [String] content
174
+ # @return [Rewriter] self
175
+ # @raise [ClobberingError] when clobbering is detected
176
+ #
177
+ def insert_after(range, content)
178
+ wrap(range, nil, content)
179
+ end
180
+
181
+ ##
182
+ # Applies all scheduled changes to the `source_buffer` and returns
183
+ # modified source as a new string.
184
+ #
185
+ # @return [String]
186
+ #
187
+ def process
188
+ source = @source_buffer.source.dup
189
+ adjustment = 0
190
+
191
+ @action_root.ordered_replacements.each do |range, replacement|
192
+ begin_pos = range.begin_pos + adjustment
193
+ end_pos = begin_pos + range.length
194
+
195
+ source[begin_pos...end_pos] = replacement
196
+
197
+ adjustment += replacement.length - range.length
198
+ end
199
+
200
+ source
201
+ end
202
+
203
+ ##
204
+ # Provides a protected block where a sequence of multiple rewrite actions
205
+ # are handled atomically. If any of the actions failed by clobbering,
206
+ # all the actions are rolled back.
207
+ #
208
+ # @raise [RuntimeError] when no block is passed
209
+ # @raise [RuntimeError] when already in a transaction
210
+ #
211
+ def transaction
212
+ unless block_given?
213
+ raise "#{self.class}##{__method__} requires block"
214
+ end
215
+
216
+ previous = @in_transaction
217
+ @in_transaction = true
218
+ restore_root = @action_root
219
+
220
+ yield
221
+
222
+ restore_root = nil
223
+
224
+ self
225
+ ensure
226
+ @action_root = restore_root if restore_root
227
+ @in_transaction = previous
228
+ end
229
+
230
+ def in_transaction?
231
+ @in_transaction
232
+ end
233
+
234
+ ##
235
+ # @api private
236
+ # @deprecated Use insert_after or wrap
237
+ #
238
+ def insert_before_multi(range, text)
239
+ self.class.warn_of_deprecation
240
+ insert_before(range, text)
241
+ end
242
+
243
+ ##
244
+ # @api private
245
+ # @deprecated Use insert_after or wrap
246
+ #
247
+ def insert_after_multi(range, text)
248
+ self.class.warn_of_deprecation
249
+ insert_after(range, text)
250
+ end
251
+
252
+ DEPRECATION_WARNING = [
253
+ 'TreeRewriter#insert_before_multi and insert_before_multi exist only for legacy compatibility.',
254
+ 'Please update your code to use `wrap`, `insert_before` or `insert_after` instead.'
255
+ ].join("\n").freeze
256
+
257
+ extend Deprecation
258
+
259
+ private
260
+
261
+ ACTIONS = %i[accept warn raise].freeze
262
+ def check_policy_validity
263
+ invalid = @policy.values - ACTIONS
264
+ raise ArgumentError, "Invalid policy: #{invalid.join(', ')}" unless invalid.empty?
265
+ end
266
+
267
+ def combine(range, attributes)
268
+ range = check_range_validity(range)
269
+ action = TreeRewriter::Action.new(range, @enforcer, attributes)
270
+ @action_root = @action_root.combine(action)
271
+ self
272
+ end
273
+
274
+ def check_range_validity(range)
275
+ if range.begin_pos < 0 || range.end_pos > @source_buffer.source.size
276
+ raise IndexError, "The range #{action.range} is outside the bounds of the source"
277
+ end
278
+ range
279
+ end
280
+
281
+ def enforce_policy(event)
282
+ return if @policy[event] == :accept
283
+ return unless (values = yield)
284
+ trigger_policy(event, values)
285
+ end
286
+
287
+ POLICY_TO_LEVEL = {warn: :warning, raise: :error}.freeze
288
+ def trigger_policy(event, range: raise, conflict: nil, **arguments)
289
+ action = @policy[event] || :raise
290
+ diag = Parser::Diagnostic.new(POLICY_TO_LEVEL[action], event, arguments, range)
291
+ @diagnostics.process(diag)
292
+ if conflict
293
+ range, *highlights = conflict
294
+ diag = Parser::Diagnostic.new(POLICY_TO_LEVEL[action], :"#{event}_conflict", arguments, range, highlights)
295
+ @diagnostics.process(diag)
296
+ end
297
+ raise Parser::ClobberingError, "Parser::Source::TreeRewriter detected clobbering" if action == :raise
298
+ end
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parser
4
+ module Source
5
+ ##
6
+ # @api private
7
+ #
8
+ # Actions are arranged in a tree and get combined so that:
9
+ # children are strictly contained by their parent
10
+ # sibblings all disjoint from one another
11
+ # only actions with replacement==nil may have children
12
+ #
13
+ class TreeRewriter::Action
14
+ attr_reader :range, :replacement, :insert_before, :insert_after
15
+
16
+ def initialize(range, enforcer,
17
+ insert_before: '',
18
+ replacement: nil,
19
+ insert_after: '',
20
+ children: []
21
+ )
22
+ @range, @enforcer, @children, @insert_before, @replacement, @insert_after =
23
+ range, enforcer, children.freeze, insert_before.freeze, replacement, insert_after.freeze
24
+
25
+ freeze
26
+ end
27
+
28
+ # Assumes action.children.empty?
29
+ def combine(action)
30
+ return self unless action.insertion? || action.replacement # Ignore empty action
31
+ do_combine(action)
32
+ end
33
+
34
+ def ordered_replacements
35
+ reps = []
36
+ reps << [@range.begin, @insert_before] unless @insert_before.empty?
37
+ reps << [@range, @replacement] if @replacement
38
+ reps.concat(@children.sort_by(&:range).flat_map(&:ordered_replacements))
39
+ reps << [@range.end, @insert_after] unless @insert_after.empty?
40
+ reps
41
+ end
42
+
43
+ def insertion?
44
+ !insert_before.empty? || !insert_after.empty? || (replacement && !replacement.empty?)
45
+ end
46
+
47
+ protected
48
+
49
+ def with(range: @range, children: @children, insert_before: @insert_before, replacement: @replacement, insert_after: @insert_after)
50
+ children = swallow(children) if replacement
51
+ self.class.new(range, @enforcer, children: children, insert_before: insert_before, replacement: replacement, insert_after: insert_after)
52
+ end
53
+
54
+ # Assumes range.contains?(action.range) && action.children.empty?
55
+ def do_combine(action)
56
+ if action.range == @range
57
+ merge(action)
58
+ else
59
+ place_in_hierachy(action)
60
+ end
61
+ end
62
+
63
+ def place_in_hierachy(action)
64
+ family = @children.group_by { |child| child.relationship_with(action) }
65
+
66
+ if family[:fusible]
67
+ fuse_deletions(action, family[:fusible], [*family[:sibbling], *family[:child]])
68
+ else
69
+ extra_sibbling = if family[:parent] # action should be a descendant of one of the children
70
+ family[:parent][0].do_combine(action)
71
+ elsif family[:child] # or it should become the parent of some of the children,
72
+ action.with(children: family[:child])
73
+ else # or else it should become an additional child
74
+ action
75
+ end
76
+ with(children: [*family[:sibbling], extra_sibbling])
77
+ end
78
+ end
79
+
80
+ def fuse_deletions(action, fusible, other_sibblings)
81
+ without_fusible = with(children: other_sibblings)
82
+ fused_range = [action, *fusible].map(&:range).inject(:join)
83
+ fused_deletion = action.with(range: fused_range)
84
+ without_fusible.do_combine(fused_deletion)
85
+ end
86
+
87
+ # Returns what relationship self should have with `action`; either of
88
+ # :sibbling, :parent, :child, :fusible or raises a CloberingError
89
+ # In case of equal range, returns :parent
90
+ def relationship_with(action)
91
+ if action.range == @range || @range.contains?(action.range)
92
+ :parent
93
+ elsif @range.contained?(action.range)
94
+ :child
95
+ elsif @range.disjoint?(action.range)
96
+ :sibbling
97
+ elsif !action.insertion? && !insertion?
98
+ @enforcer.call(:crossing_deletions) { {range: action.range, conflict: @range} }
99
+ :fusible
100
+ else
101
+ @enforcer.call(:crossing_insertions) { {range: action.range, conflict: @range} }
102
+ end
103
+ end
104
+
105
+ # Assumes action.range == range && action.children.empty?
106
+ def merge(action)
107
+ call_enforcer_for_merge(action)
108
+ with(
109
+ insert_before: "#{action.insert_before}#{insert_before}",
110
+ replacement: action.replacement || @replacement,
111
+ insert_after: "#{insert_after}#{action.insert_after}",
112
+ )
113
+ end
114
+
115
+ def call_enforcer_for_merge(action)
116
+ @enforcer.call(:different_replacements) do
117
+ if @replacement && action.replacement && @replacement != action.replacement
118
+ {range: @range, replacement: action.replacement, other_replacement: @replacement}
119
+ end
120
+ end
121
+ end
122
+
123
+ def swallow(children)
124
+ @enforcer.call(:swallowed_insertions) do
125
+ insertions = children.select(&:insertion?)
126
+
127
+ {range: @range, conflict: insertions.map(&:range)} unless insertions.empty?
128
+ end
129
+ []
130
+ end
131
+ end
132
+ end
133
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Parser
2
4
 
3
5
  class StaticEnvironment
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Parser
2
4
  ##
3
5
  # {Parser::SyntaxError} is raised whenever parser detects a syntax error,