parser 2.7.0.4 → 2.7.1.3

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +18 -29
  4. data/CHANGELOG.md +53 -1
  5. data/README.md +6 -6
  6. data/Rakefile +2 -1
  7. data/doc/AST_FORMAT.md +54 -5
  8. data/lib/parser.rb +1 -0
  9. data/lib/parser/all.rb +1 -0
  10. data/lib/parser/ast/processor.rb +7 -0
  11. data/lib/parser/builders/default.rb +63 -14
  12. data/lib/parser/current.rb +13 -4
  13. data/lib/parser/diagnostic.rb +1 -1
  14. data/lib/parser/diagnostic/engine.rb +1 -2
  15. data/lib/parser/lexer.rl +7 -0
  16. data/lib/parser/messages.rb +15 -0
  17. data/lib/parser/meta.rb +2 -2
  18. data/lib/parser/ruby27.y +16 -5
  19. data/lib/parser/ruby28.y +3016 -0
  20. data/lib/parser/runner.rb +26 -2
  21. data/lib/parser/runner/ruby_rewrite.rb +2 -2
  22. data/lib/parser/source/buffer.rb +3 -1
  23. data/lib/parser/source/comment/associator.rb +14 -4
  24. data/lib/parser/source/map/endless_definition.rb +23 -0
  25. data/lib/parser/source/range.rb +16 -0
  26. data/lib/parser/source/tree_rewriter.rb +49 -12
  27. data/lib/parser/source/tree_rewriter/action.rb +96 -26
  28. data/lib/parser/tree_rewriter.rb +1 -2
  29. data/lib/parser/version.rb +1 -1
  30. data/parser.gemspec +2 -1
  31. data/test/helper.rb +25 -6
  32. data/test/parse_helper.rb +11 -17
  33. data/test/test_ast_processor.rb +32 -0
  34. data/test/test_base.rb +1 -1
  35. data/test/test_current.rb +2 -0
  36. data/test/test_diagnostic.rb +6 -7
  37. data/test/test_diagnostic_engine.rb +5 -8
  38. data/test/test_lexer.rb +17 -8
  39. data/test/test_meta.rb +12 -0
  40. data/test/test_parser.rb +260 -21
  41. data/test/test_runner_parse.rb +22 -1
  42. data/test/test_runner_rewrite.rb +1 -1
  43. data/test/test_source_buffer.rb +4 -1
  44. data/test/test_source_comment.rb +2 -2
  45. data/test/test_source_comment_associator.rb +47 -15
  46. data/test/test_source_map.rb +1 -2
  47. data/test/test_source_range.rb +29 -9
  48. data/test/test_source_rewriter.rb +4 -4
  49. data/test/test_source_rewriter_action.rb +2 -2
  50. data/test/test_source_tree_rewriter.rb +96 -6
  51. metadata +14 -7
@@ -7,7 +7,7 @@ class TestRunnerParse < Minitest::Test
7
7
  PATH_TO_RUBY_PARSE = File.expand_path('../bin/ruby-parse', __dir__).freeze
8
8
 
9
9
  def assert_prints(argv, expected_output)
10
- stdout, stderr, status = Open3.capture3(PATH_TO_RUBY_PARSE, *argv)
10
+ stdout, _stderr, status = Open3.capture3(PATH_TO_RUBY_PARSE, *argv)
11
11
 
12
12
  assert_equal 0, status.to_i
13
13
  assert_includes(stdout, expected_output)
@@ -18,6 +18,27 @@ class TestRunnerParse < Minitest::Test
18
18
  's(:int, 123)'
19
19
  end
20
20
 
21
+ def test_emit_modern_ruby
22
+ assert_prints ['-e', '->{}'],
23
+ '(lambda)'
24
+ assert_prints ['-e', 'self[1] = 2'],
25
+ 'indexasgn'
26
+ end
27
+
28
+ def test_emit_legacy
29
+ assert_prints ['--legacy', '-e', '->{}'],
30
+ '(send nil :lambda)'
31
+ assert_prints ['--legacy', '-e', 'self[1] = 2'],
32
+ ':[]='
33
+ end
34
+
35
+ def test_emit_legacy_lambda
36
+ assert_prints ['--legacy-lambda', '-e', '->{}'],
37
+ '(send nil :lambda)'
38
+ assert_prints ['--legacy-lambda', '-e', 'self[1] = 2'],
39
+ 'indexasgn'
40
+ end
41
+
21
42
  def test_emit_json
22
43
  assert_prints ['--emit-json', '-e', '123'],
23
44
  '["int",123]'
@@ -21,7 +21,7 @@ class TestRunnerRewrite < Minitest::Test
21
21
  expected_file = @fixtures_dir + output
22
22
 
23
23
  FileUtils.cp(@fixtures_dir + input, sample_file_expanded)
24
- stdout, stderr, exit_code = Dir.chdir @test_dir do
24
+ stdout, stderr, _exit_code = Dir.chdir @test_dir do
25
25
  Open3.capture3 %Q{
26
26
  #{Shellwords.escape(@ruby_rewrite.to_s)} #{args} \
27
27
  #{Shellwords.escape(sample_file_expanded.to_s)}
@@ -20,6 +20,9 @@ class TestSourceBuffer < Minitest::Test
20
20
 
21
21
  buffer = Parser::Source::Buffer.new('(string)', 5)
22
22
  assert_equal 5, buffer.first_line
23
+
24
+ buffer = Parser::Source::Buffer.new('(string)', source: '2+2')
25
+ assert_equal '2+2', buffer.source
23
26
  end
24
27
 
25
28
  def test_source_setter
@@ -45,7 +48,7 @@ class TestSourceBuffer < Minitest::Test
45
48
  ].join("\n")
46
49
  end
47
50
 
48
- assert_match /invalid byte sequence in UTF\-8/, error.message
51
+ assert_match(/invalid byte sequence in UTF\-8/, error.message)
49
52
  end
50
53
 
51
54
  def test_read
@@ -4,8 +4,8 @@ require 'helper'
4
4
 
5
5
  class TestSourceComment < Minitest::Test
6
6
  def setup
7
- @buf = Parser::Source::Buffer.new('(string)')
8
- @buf.source = "# foo\n=begin foo\nbar\n=end baz\n"
7
+ @buf = Parser::Source::Buffer.new('(string)',
8
+ source: "# foo\n=begin foo\nbar\n=end baz\n")
9
9
  end
10
10
 
11
11
  def range(s, e)
@@ -95,7 +95,7 @@ class Foo
95
95
  end
96
96
  END
97
97
 
98
- klass_node = ast
98
+ _klass_node = ast
99
99
  method_node = ast.children[2]
100
100
  body = method_node.children[2]
101
101
  f1_1_node = body.children[0]
@@ -175,7 +175,7 @@ class Foo
175
175
  end
176
176
  END
177
177
 
178
- klass_node = ast
178
+ _klass_node = ast
179
179
  method_node = ast.children[2]
180
180
  body = method_node.children[2]
181
181
  f1_1_node = body.children[0]
@@ -201,12 +201,12 @@ end
201
201
  end
202
202
 
203
203
  def test_associate_empty_tree
204
- ast, associations = associate("")
204
+ _ast, associations = associate("")
205
205
  assert_equal 0, associations.size
206
206
  end
207
207
 
208
208
  def test_associate_shebang_only
209
- ast, associations = associate(<<-END)
209
+ _ast, associations = associate(<<-END)
210
210
  #!ruby
211
211
  class Foo
212
212
  end
@@ -216,7 +216,7 @@ end
216
216
  end
217
217
 
218
218
  def test_associate_frozen_string_literal
219
- ast, associations = associate(<<-END)
219
+ _ast, associations = associate(<<-END)
220
220
  # frozen_string_literal: true
221
221
  class Foo
222
222
  end
@@ -226,7 +226,7 @@ end
226
226
  end
227
227
 
228
228
  def test_associate_frozen_string_literal_dash_star_dash
229
- ast, associations = associate(<<-END)
229
+ _ast, associations = associate(<<-END)
230
230
  # -*- frozen_string_literal: true -*-
231
231
  class Foo
232
232
  end
@@ -236,7 +236,7 @@ end
236
236
  end
237
237
 
238
238
  def test_associate_frozen_string_literal_no_space_after_colon
239
- ast, associations = associate(<<-END)
239
+ _ast, associations = associate(<<-END)
240
240
  # frozen_string_literal:true
241
241
  class Foo
242
242
  end
@@ -246,7 +246,7 @@ end
246
246
  end
247
247
 
248
248
  def test_associate_warn_indent
249
- ast, associations = associate(<<-END)
249
+ _ast, associations = associate(<<-END)
250
250
  # warn_indent: true
251
251
  class Foo
252
252
  end
@@ -256,7 +256,7 @@ end
256
256
  end
257
257
 
258
258
  def test_associate_warn_indent_dash_star_dash
259
- ast, associations = associate(<<-END)
259
+ _ast, associations = associate(<<-END)
260
260
  # -*- warn_indent: true -*-
261
261
  class Foo
262
262
  end
@@ -266,7 +266,7 @@ end
266
266
  end
267
267
 
268
268
  def test_associate_warn_past_scope
269
- ast, associations = associate(<<-END)
269
+ _ast, associations = associate(<<-END)
270
270
  # warn_past_scope: true
271
271
  class Foo
272
272
  end
@@ -276,7 +276,7 @@ end
276
276
  end
277
277
 
278
278
  def test_associate_warn_past_scope_dash_star_dash
279
- ast, associations = associate(<<-END)
279
+ _ast, associations = associate(<<-END)
280
280
  # -*- warn_past_scope: true -*-
281
281
  class Foo
282
282
  end
@@ -286,7 +286,7 @@ end
286
286
  end
287
287
 
288
288
  def test_associate_multiple
289
- ast, associations = associate(<<-END)
289
+ _ast, associations = associate(<<-END)
290
290
  # frozen_string_literal: true; warn_indent: true
291
291
  class Foo
292
292
  end
@@ -296,7 +296,7 @@ end
296
296
  end
297
297
 
298
298
  def test_associate_multiple_dash_star_dash
299
- ast, associations = associate(<<-END)
299
+ _ast, associations = associate(<<-END)
300
300
  # -*- frozen_string_literal: true; warn_indent: true -*-
301
301
  class Foo
302
302
  end
@@ -306,7 +306,7 @@ end
306
306
  end
307
307
 
308
308
  def test_associate_no_comments
309
- ast, associations = associate(<<-END)
309
+ _ast, associations = associate(<<-END)
310
310
  class Foo
311
311
  end
312
312
  END
@@ -315,7 +315,7 @@ end
315
315
  end
316
316
 
317
317
  def test_associate_comments_after_root_node
318
- ast, associations = associate(<<-END)
318
+ _ast, associations = associate(<<-END)
319
319
  class Foo
320
320
  end
321
321
  # not associated
@@ -364,4 +364,36 @@ x
364
364
  assert_equal ['# bar'],
365
365
  associations[send_node].map(&:text)
366
366
  end
367
+
368
+ def test_associate_conditional_parent_class
369
+ ast, associations = associate(<<-END)
370
+ class Foo
371
+ # bar
372
+ class Bar
373
+ end
374
+ end if some_condition
375
+ END
376
+
377
+ if_node = ast
378
+ _condition, foo_class = if_node.children
379
+ _foo, _sub_class, bar_class = foo_class.children
380
+
381
+ assert_equal 1, associations.size
382
+ assert_equal ['# bar'],
383
+ associations[bar_class].map(&:text)
384
+ end
385
+
386
+ def test_children_in_source_order
387
+ obj = Parser::Source::Comment::Associator.new(nil, nil)
388
+ for_each_node do |node|
389
+ with_loc = node.children.select do |child|
390
+ child.is_a?(AST::Node) && child.loc && child.loc.expression
391
+ end
392
+ slow_sort = with_loc.sort_by.with_index do |child, index| # Index to ensure stable sort
393
+ [child.loc.expression.begin_pos, index]
394
+ end
395
+ optimized = obj.send(:children_in_source_order, node)
396
+ assert_equal slow_sort, optimized, "children_in_source_order incorrect for #{node}"
397
+ end
398
+ end
367
399
  end
@@ -7,8 +7,7 @@ class TestSourceMap < Minitest::Test
7
7
  include ParseHelper
8
8
 
9
9
  def test_to_hash
10
- buf = Parser::Source::Buffer.new("<input>")
11
- buf.source = "1"
10
+ buf = Parser::Source::Buffer.new("<input>", source: "1")
12
11
  ast = parser_for_ruby_version('1.8').parse(buf)
13
12
  assert_equal [:expression, :operator], ast.loc.to_hash.keys.sort_by(&:to_s)
14
13
  end
@@ -4,8 +4,8 @@ require 'helper'
4
4
 
5
5
  class TestSourceRange < Minitest::Test
6
6
  def setup
7
- @buf = Parser::Source::Buffer.new('(string)')
8
- @buf.source = "foobar\nbaz"
7
+ @buf = Parser::Source::Buffer.new('(string)',
8
+ source: "foobar\nbaz")
9
9
  @sr1_3 = Parser::Source::Range.new(@buf, 1, 3)
10
10
  @sr2_2 = Parser::Source::Range.new(@buf, 2, 2)
11
11
  @sr3_3 = Parser::Source::Range.new(@buf, 3, 3)
@@ -95,15 +95,15 @@ class TestSourceRange < Minitest::Test
95
95
 
96
96
  def test_order
97
97
  assert_equal 0, @sr1_3 <=> @sr1_3
98
- assert_equal -1, @sr1_3 <=> @sr5_8
99
- assert_equal -1, @sr2_2 <=> @sr2_6
100
- assert_equal +1, @sr2_6 <=> @sr2_2
98
+ assert_equal(-1, @sr1_3 <=> @sr5_8)
99
+ assert_equal(-1, @sr2_2 <=> @sr2_6)
100
+ assert_equal(+1, @sr2_6 <=> @sr2_2)
101
101
 
102
- assert_equal -1, @sr1_3 <=> @sr2_6
102
+ assert_equal(-1, @sr1_3 <=> @sr2_6)
103
103
 
104
- assert_equal +1, @sr2_2 <=> @sr1_3
105
- assert_equal -1, @sr1_3 <=> @sr2_2
106
- assert_equal -1, @sr5_7 <=> @sr5_8
104
+ assert_equal(+1, @sr2_2 <=> @sr1_3)
105
+ assert_equal(-1, @sr1_3 <=> @sr2_2)
106
+ assert_equal(-1, @sr5_7 <=> @sr5_8)
107
107
 
108
108
  assert_nil @sr1_3 <=> Parser::Source::Range.new(@buf.dup, 1, 3)
109
109
  assert_nil @sr1_3 <=> 4
@@ -155,6 +155,11 @@ class TestSourceRange < Minitest::Test
155
155
  refute sr.is?('bar')
156
156
  end
157
157
 
158
+ def test_to_range
159
+ sr = Parser::Source::Range.new(@buf, 10, 20)
160
+ assert_equal (10...20), sr.to_range
161
+ end
162
+
158
163
  def test_to_s
159
164
  sr = Parser::Source::Range.new(@buf, 8, 9)
160
165
  assert_equal '(string):2:2', sr.to_s
@@ -169,4 +174,19 @@ class TestSourceRange < Minitest::Test
169
174
  assert_equal 1, sr3.begin_pos
170
175
  assert_equal 4, sr3.end_pos
171
176
  end
177
+
178
+ def test_eql_and_hash
179
+ assert_equal false, @sr1_3.eql?(@sr3_3)
180
+ assert @sr1_3.hash != @sr3_3.hash
181
+
182
+ also_1_3 = @sr3_3.with(begin_pos: 1)
183
+ assert_equal true, @sr1_3.eql?(also_1_3)
184
+ assert_equal @sr1_3.hash, also_1_3.hash
185
+
186
+ buf2 = Parser::Source::Buffer.new('(string)',
187
+ source: "foobar\nbaz")
188
+ from_other_buf = Parser::Source::Range.new(buf2, 1, 3)
189
+ assert_equal false, @sr1_3.eql?(from_other_buf)
190
+ assert @sr1_3.hash != from_other_buf.hash
191
+ end
172
192
  end
@@ -4,8 +4,8 @@ require 'helper'
4
4
 
5
5
  class TestSourceRewriter < Minitest::Test
6
6
  def setup
7
- @buf = Parser::Source::Buffer.new('(rewriter)')
8
- @buf.source = 'foo bar baz'
7
+ @buf = Parser::Source::Buffer.new('(rewriter)',
8
+ source: 'foo bar baz')
9
9
  Parser::Source::Rewriter.warned_of_deprecation = true
10
10
  @rewriter = Parser::Source::Rewriter.new(@buf)
11
11
  end
@@ -522,7 +522,7 @@ class TestSourceRewriter < Minitest::Test
522
522
  end
523
523
  end
524
524
 
525
- assert_match /nested/i, error.message
525
+ assert_match(/nested/i, error.message)
526
526
  end
527
527
 
528
528
  def test_process_in_transaction_raises_error
@@ -532,7 +532,7 @@ class TestSourceRewriter < Minitest::Test
532
532
  end
533
533
  end
534
534
 
535
- assert_match /transaction/, error.message
535
+ assert_match(/transaction/, error.message)
536
536
  end
537
537
 
538
538
  def silence_diagnostics
@@ -4,8 +4,8 @@ require 'helper'
4
4
 
5
5
  class TestSourceRewriterAction < Minitest::Test
6
6
  def setup
7
- @buf = Parser::Source::Buffer.new('(rewriter_action)')
8
- @buf.source = 'foo bar baz'
7
+ @buf = Parser::Source::Buffer.new('(rewriter_action)',
8
+ source: 'foo bar baz')
9
9
  end
10
10
 
11
11
  def range(from, len)
@@ -4,12 +4,14 @@ require 'helper'
4
4
 
5
5
  class TestSourceTreeRewriter < Minitest::Test
6
6
  def setup
7
- @buf = Parser::Source::Buffer.new('(rewriter)')
8
- @buf.source = 'puts(:hello, :world)'
7
+ @buf = Parser::Source::Buffer.new('(rewriter)',
8
+ source: 'puts(:hello, :world)')
9
9
 
10
10
  @hello = range(5, 6)
11
+ @ll = range(7, 2)
11
12
  @comma_space = range(11,2)
12
13
  @world = range(13,6)
14
+ @whole = range(0, @buf.source.length)
13
15
  end
14
16
 
15
17
  def range(from, len)
@@ -17,11 +19,11 @@ class TestSourceTreeRewriter < Minitest::Test
17
19
  end
18
20
 
19
21
  # Returns either:
20
- # - String (Normal operation)
22
+ # - yield rewriter
21
23
  # - [diagnostic, ...] (Diagnostics)
22
24
  # - Parser::ClobberingError
23
25
  #
24
- def apply(actions, **policy)
26
+ def build(actions, **policy)
25
27
  diagnostics = []
26
28
  diags = -> { diagnostics.flatten.map(&:strip).join("\n") }
27
29
  rewriter = Parser::Source::TreeRewriter.new(@buf, **policy)
@@ -30,14 +32,23 @@ class TestSourceTreeRewriter < Minitest::Test
30
32
  rewriter.public_send(action, range, *args)
31
33
  end
32
34
  if diagnostics.empty?
33
- rewriter.process
35
+ yield rewriter
34
36
  else
35
37
  diags.call
36
38
  end
37
- rescue ::Parser::ClobberingError => e
39
+ rescue ::Parser::ClobberingError => _e
38
40
  [::Parser::ClobberingError, diags.call]
39
41
  end
40
42
 
43
+ # Returns either:
44
+ # - String (Normal operation)
45
+ # - [diagnostic, ...] (Diagnostics)
46
+ # - Parser::ClobberingError
47
+ #
48
+ def apply(actions, **policy)
49
+ build(actions, **policy) { |rewriter| rewriter.process }
50
+ end
51
+
41
52
  # Expects ordered actions to be grouped together
42
53
  def check_actions(expected, grouped_actions, **policy)
43
54
  grouped_actions.permutation do |sequence|
@@ -140,6 +151,16 @@ DIAGNOSTIC
140
151
  [:wrap, @hello, '[', ']']]
141
152
  end
142
153
 
154
+ def test_inserts_on_empty_ranges
155
+ assert_actions_result 'puts({x}:hello[y], :world)',
156
+ [:insert_before, @hello.begin, '{'],
157
+ [:replace, @hello.begin, 'x'],
158
+ [:insert_after, @hello.begin, '}'],
159
+ [:insert_before, @hello.end, '['],
160
+ [:replace, @hello.end, 'y'],
161
+ [:insert_after, @hello.end, ']']
162
+ end
163
+
143
164
  def test_replace_same_range
144
165
  assert_actions_result 'puts(:hey, :world)',
145
166
  [[:replace, @hello, ':hi'],
@@ -170,4 +191,73 @@ DIAGNOSTIC
170
191
  rewriter = Parser::Source::TreeRewriter.new(@buf)
171
192
  assert_raises(IndexError) { rewriter.insert_before(range(0, 100), 'hola') }
172
193
  end
194
+
195
+ def test_empty
196
+ rewriter = Parser::Source::TreeRewriter.new(@buf)
197
+ assert_equal true, rewriter.empty?
198
+
199
+ # This is a trivial wrap
200
+ rewriter.wrap(range(2,3), '', '')
201
+ assert_equal true, rewriter.empty?
202
+
203
+ # This is a trivial deletion
204
+ rewriter.remove(range(2,0))
205
+ assert_equal true, rewriter.empty?
206
+
207
+ rewriter.remove(range(2,3))
208
+ assert_equal false, rewriter.empty?
209
+ end
210
+
211
+ # splits array into two groups, yield all such possible pairs of groups
212
+ # each_split([1, 2, 3, 4]) yields [1, 2], [3, 4];
213
+ # then [1, 3], [2, 4]
214
+ # ...
215
+ # and finally [3, 4], [1, 2]
216
+ def each_split(array)
217
+ n = array.size
218
+ first_split_size = n.div(2)
219
+ splitting = (0...n).to_set
220
+ splitting.to_a.combination(first_split_size) do |indices|
221
+ yield array.values_at(*indices),
222
+ array.values_at(*(splitting - indices))
223
+ end
224
+ end
225
+
226
+ # Checks that `actions+extra` give the same result when
227
+ # made in order or from subgroups that are later merged.
228
+ # The `extra` actions are always added at the end of the second group.
229
+ #
230
+ def check_all_merge_possibilities(actions, extra, **policy)
231
+ expected = apply(actions + extra, **policy)
232
+
233
+ each_split(actions) do |actions_1, actions_2|
234
+ build(actions_1, **policy) do |rewriter_1|
235
+ build(actions_2 + extra, **policy) do |rewriter_2|
236
+ result = rewriter_1.merge(rewriter_2).process
237
+ assert_equal(expected, result,
238
+ "Group 1: #{actions_1.inspect}\n\n" +
239
+ "Group 2: #{(actions_2 + extra).inspect}"
240
+ )
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ def test_merge
247
+ check_all_merge_possibilities([
248
+ [:wrap, @whole, '<', '>'],
249
+ [:replace, @comma_space, ' => '],
250
+ [:wrap, @hello, '!', '!'],
251
+ # Following two wraps must have same value as they
252
+ # will be applied in different orders...
253
+ [:wrap, @hello.join(@world), '{', '}'],
254
+ [:wrap, @hello.join(@world), '{', '}'],
255
+ [:remove, @ll],
256
+ [:replace, @world, ':everybody'],
257
+ [:wrap, @world, '[', ']']
258
+ ],
259
+ [ # ... but this one is always going to be applied last (extra)
260
+ [:wrap, @hello.join(@world), '@', '@'],
261
+ ])
262
+ end
173
263
  end