syntax_tree 0.1.0 → 1.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.
data/lib/syntax_tree.rb CHANGED
@@ -7,7 +7,7 @@ require "stringio"
7
7
 
8
8
  require_relative "syntax_tree/version"
9
9
 
10
- # If PrettyPrint::Assign isn't defined, then we haven't gotten the updated
10
+ # If PrettyPrint::Align isn't defined, then we haven't gotten the updated
11
11
  # version of prettyprint. In that case we'll define our own. This is going to
12
12
  # overwrite a bunch of methods, so silencing them as well.
13
13
  unless PrettyPrint.const_defined?(:Align)
@@ -26,12 +26,14 @@ class SyntaxTree < Ripper
26
26
  # every character in the string is 1 byte in length, so we can just return the
27
27
  # start of the line + the index.
28
28
  class SingleByteString
29
+ attr_reader :start
30
+
29
31
  def initialize(start)
30
32
  @start = start
31
33
  end
32
34
 
33
35
  def [](byteindex)
34
- @start + byteindex
36
+ start + byteindex
35
37
  end
36
38
  end
37
39
 
@@ -40,7 +42,10 @@ class SyntaxTree < Ripper
40
42
  # an array of indices, such that array[byteindex] will be equal to the index
41
43
  # of the character within the string.
42
44
  class MultiByteString
45
+ attr_reader :start, :indices
46
+
43
47
  def initialize(start, line)
48
+ @start = start
44
49
  @indices = []
45
50
 
46
51
  line.each_char.with_index(start) do |char, index|
@@ -48,8 +53,11 @@ class SyntaxTree < Ripper
48
53
  end
49
54
  end
50
55
 
56
+ # Technically it's possible for the column index to be a negative value if
57
+ # there's a BOM at the beginning of the file, which is the reason we need to
58
+ # compare it to 0 here.
51
59
  def [](byteindex)
52
- @indices[byteindex]
60
+ indices[byteindex < 0 ? 0 : byteindex]
53
61
  end
54
62
  end
55
63
 
@@ -66,8 +74,7 @@ class SyntaxTree < Ripper
66
74
 
67
75
  def ==(other)
68
76
  other.is_a?(Location) && start_line == other.start_line &&
69
- start_char == other.start_char &&
70
- end_line == other.end_line &&
77
+ start_char == other.start_char && end_line == other.end_line &&
71
78
  end_char == other.end_char
72
79
  end
73
80
 
@@ -75,7 +82,7 @@ class SyntaxTree < Ripper
75
82
  Location.new(
76
83
  start_line: start_line,
77
84
  start_char: start_char,
78
- end_line: other.end_line,
85
+ end_line: [end_line, other.end_line].max,
79
86
  end_char: other.end_char
80
87
  )
81
88
  end
@@ -113,10 +120,15 @@ class SyntaxTree < Ripper
113
120
  # A slightly enhanced PP that knows how to format recursively including
114
121
  # comments.
115
122
  class Formatter < PP
116
- attr_reader :stack, :quote
123
+ COMMENT_PRIORITY = 1
124
+ HEREDOC_PRIORITY = 2
125
+
126
+ attr_reader :source, :stack, :quote
117
127
 
118
- def initialize(*)
119
- super
128
+ def initialize(source, ...)
129
+ super(...)
130
+
131
+ @source = source
120
132
  @stack = []
121
133
  @quote = "\""
122
134
  end
@@ -136,11 +148,17 @@ class SyntaxTree < Ripper
136
148
  breakable(force: true)
137
149
  end
138
150
 
139
- doc = node.format(self)
151
+ # If the node has a stree-ignore comment right before it, then we're
152
+ # going to just print out the node as it was seen in the source.
153
+ if leading.last&.ignore?
154
+ doc = text(source[node.location.start_char...node.location.end_char])
155
+ else
156
+ doc = node.format(self)
157
+ end
140
158
 
141
159
  # Print all comments that were found after the node.
142
160
  trailing.each do |comment|
143
- line_suffix do
161
+ line_suffix(priority: COMMENT_PRIORITY) do
144
162
  text(" ")
145
163
  comment.format(self)
146
164
  break_parent
@@ -173,6 +191,10 @@ class SyntaxTree < Ripper
173
191
  # [Array[ String ]] the list of lines in the source
174
192
  attr_reader :lines
175
193
 
194
+ # [Array[ SingleByteString | MultiByteString ]] the list of objects that
195
+ # represent the start of each line in character offsets
196
+ attr_reader :line_counts
197
+
176
198
  # [Array[ untyped ]] a running list of tokens that have been found in the
177
199
  # source. This list changes a lot as certain nodes will "consume" these tokens
178
200
  # to determine their bounds.
@@ -248,6 +270,12 @@ class SyntaxTree < Ripper
248
270
 
249
271
  last_index += line.size
250
272
  end
273
+
274
+ # Make sure line counts is filled out with the first and last line at
275
+ # minimum so that it has something to compare against if the parser is in a
276
+ # lineno=2 state for an empty file.
277
+ @line_counts << SingleByteString.new(0) if @line_counts.empty?
278
+ @line_counts << SingleByteString.new(last_index)
251
279
  end
252
280
 
253
281
  def self.parse(source)
@@ -259,7 +287,7 @@ class SyntaxTree < Ripper
259
287
  def self.format(source)
260
288
  output = []
261
289
 
262
- formatter = Formatter.new(output)
290
+ formatter = Formatter.new(source, output)
263
291
  parse(source).format(formatter)
264
292
 
265
293
  formatter.flush
@@ -280,7 +308,7 @@ class SyntaxTree < Ripper
280
308
  # this line, then we add the number of columns into this line that we've gone
281
309
  # through.
282
310
  def char_pos
283
- @line_counts[lineno - 1][column]
311
+ line_counts[lineno - 1][column]
284
312
  end
285
313
 
286
314
  # As we build up a list of tokens, we'll periodically need to go backwards and
@@ -294,7 +322,7 @@ class SyntaxTree < Ripper
294
322
  # (which would happen to be the innermost keyword). Then the outer one would
295
323
  # only be able to grab the first one. In this way all of the tokens act as
296
324
  # their own stack.
297
- def find_token(type, value = :any, consume: true)
325
+ def find_token(type, value = :any, consume: true, location: nil)
298
326
  index =
299
327
  tokens.rindex do |token|
300
328
  token.is_a?(type) && (value == :any || (token.value == value))
@@ -307,8 +335,16 @@ class SyntaxTree < Ripper
307
335
  # could also be caused by accidentally attempting to consume a token twice
308
336
  # by two different parser event handlers.
309
337
  unless index
310
- message = "Cannot find expected #{value == :any ? type : value}"
311
- raise ParseError.new(message, lineno, column)
338
+ token = value == :any ? type.name.split("::", 2).last : value
339
+ message = "Cannot find expected #{token}"
340
+
341
+ if location
342
+ lineno = location.start_line
343
+ column = location.start_char - line_counts[lineno - 1].start
344
+ raise ParseError.new(message, lineno, column)
345
+ else
346
+ raise ParseError.new(message, lineno, column)
347
+ end
312
348
  end
313
349
 
314
350
  tokens.delete_at(index)
@@ -476,7 +512,7 @@ class SyntaxTree < Ripper
476
512
  q.text(value)
477
513
  else
478
514
  q.text(q.quote)
479
- q.text(value[1])
515
+ q.text(value[1] == "\"" ? "\\\"" : value[1])
480
516
  q.text(q.quote)
481
517
  end
482
518
  end
@@ -628,7 +664,9 @@ class SyntaxTree < Ripper
628
664
  def format(q)
629
665
  q.text("__END__")
630
666
  q.breakable(force: true)
631
- q.text(value)
667
+
668
+ separator = -> { q.breakable(indent: false, force: true) }
669
+ q.seplist(value.split(/\r?\n/, -1), separator) { |line| q.text(line) }
632
670
  end
633
671
 
634
672
  def pretty_print(q)
@@ -654,7 +692,7 @@ class SyntaxTree < Ripper
654
692
  def on___end__(value)
655
693
  @__end__ =
656
694
  EndContent.new(
657
- value: lines[lineno..-1].join("\n"),
695
+ value: source[(char_pos + value.length)..-1],
658
696
  location: Location.token(line: lineno, char: char_pos, size: value.size)
659
697
  )
660
698
  end
@@ -1349,7 +1387,11 @@ class SyntaxTree < Ripper
1349
1387
  q.indent do
1350
1388
  q.breakable("")
1351
1389
  q.seplist(contents.parts, -> { q.breakable }) do |part|
1352
- q.format(part.parts.first)
1390
+ if part.is_a?(StringLiteral)
1391
+ q.format(part.parts.first)
1392
+ else
1393
+ q.text(part.value[1..-1])
1394
+ end
1353
1395
  end
1354
1396
  end
1355
1397
  q.breakable("")
@@ -1373,10 +1415,39 @@ class SyntaxTree < Ripper
1373
1415
  q.format(part.value)
1374
1416
  end
1375
1417
  end
1418
+ q.breakable("")
1419
+ end
1420
+ end
1421
+ end
1422
+
1423
+ class VarRefsFormatter
1424
+ # [Args] the contents of the array
1425
+ attr_reader :contents
1426
+
1427
+ def initialize(contents)
1428
+ @contents = contents
1429
+ end
1430
+
1431
+ def format(q)
1432
+ q.group(0, "[", "]") do
1433
+ q.indent do
1434
+ q.breakable("")
1435
+
1436
+ separator = -> do
1437
+ q.text(",")
1438
+ q.fill_breakable
1439
+ end
1440
+
1441
+ q.seplist(contents.parts, separator) { |part| q.format(part) }
1442
+ end
1443
+ q.breakable("")
1376
1444
  end
1377
1445
  end
1378
1446
  end
1379
1447
 
1448
+ # [LBracket] the bracket that opens this array
1449
+ attr_reader :lbracket
1450
+
1380
1451
  # [nil | Args] the contents of the array
1381
1452
  attr_reader :contents
1382
1453
 
@@ -1386,22 +1457,18 @@ class SyntaxTree < Ripper
1386
1457
  # [Array[ Comment | EmbDoc ]] the comments attached to this node
1387
1458
  attr_reader :comments
1388
1459
 
1389
- def initialize(contents:, location:, comments: [])
1460
+ def initialize(lbracket:, contents:, location:, comments: [])
1461
+ @lbracket = lbracket
1390
1462
  @contents = contents
1391
1463
  @location = location
1392
1464
  @comments = comments
1393
1465
  end
1394
1466
 
1395
1467
  def child_nodes
1396
- [contents]
1468
+ [lbracket, contents]
1397
1469
  end
1398
1470
 
1399
1471
  def format(q)
1400
- unless contents
1401
- q.text("[]")
1402
- return
1403
- end
1404
-
1405
1472
  if qwords?
1406
1473
  QWordsFormatter.new(contents).format(q)
1407
1474
  return
@@ -1412,12 +1479,23 @@ class SyntaxTree < Ripper
1412
1479
  return
1413
1480
  end
1414
1481
 
1415
- q.group(0, "[", "]") do
1416
- q.indent do
1417
- q.breakable("")
1418
- q.format(contents)
1482
+ if var_refs?(q)
1483
+ VarRefsFormatter.new(contents).format(q)
1484
+ return
1485
+ end
1486
+
1487
+ q.group do
1488
+ q.format(lbracket)
1489
+
1490
+ if contents
1491
+ q.indent do
1492
+ q.breakable("")
1493
+ q.format(contents)
1494
+ end
1419
1495
  end
1496
+
1420
1497
  q.breakable("")
1498
+ q.text("]")
1421
1499
  end
1422
1500
  end
1423
1501
 
@@ -1441,21 +1519,40 @@ class SyntaxTree < Ripper
1441
1519
  private
1442
1520
 
1443
1521
  def qwords?
1444
- contents && contents.comments.empty? && contents.parts.length > 1 &&
1522
+ lbracket.comments.empty? && contents && contents.comments.empty? &&
1523
+ contents.parts.length > 1 &&
1445
1524
  contents.parts.all? do |part|
1446
- part.is_a?(StringLiteral) && part.comments.empty? &&
1447
- part.parts.length == 1 &&
1448
- part.parts.first.is_a?(TStringContent) &&
1449
- !part.parts.first.value.match?(/[\s\\\]]/)
1525
+ case part
1526
+ when StringLiteral
1527
+ part.comments.empty? && part.parts.length == 1 &&
1528
+ part.parts.first.is_a?(TStringContent) &&
1529
+ !part.parts.first.value.match?(/[\s\[\]\\]/)
1530
+ when CHAR
1531
+ !part.value.match?(/[\[\]\\]/)
1532
+ else
1533
+ false
1534
+ end
1450
1535
  end
1451
1536
  end
1452
1537
 
1453
1538
  def qsymbols?
1454
- contents && contents.comments.empty? && contents.parts.length > 1 &&
1539
+ lbracket.comments.empty? && contents && contents.comments.empty? &&
1540
+ contents.parts.length > 1 &&
1455
1541
  contents.parts.all? do |part|
1456
1542
  part.is_a?(SymbolLiteral) && part.comments.empty?
1457
1543
  end
1458
1544
  end
1545
+
1546
+ def var_refs?(q)
1547
+ lbracket.comments.empty? && contents && contents.comments.empty? &&
1548
+ contents.parts.all? do |part|
1549
+ part.is_a?(VarRef) && part.comments.empty?
1550
+ end &&
1551
+ (
1552
+ contents.parts.sum { |part| part.value.value.length + 2 } >
1553
+ q.maxwidth * 2
1554
+ )
1555
+ end
1459
1556
  end
1460
1557
 
1461
1558
  # :call-seq:
@@ -1467,13 +1564,16 @@ class SyntaxTree < Ripper
1467
1564
  rbracket = find_token(RBracket)
1468
1565
 
1469
1566
  ArrayLiteral.new(
1567
+ lbracket: lbracket,
1470
1568
  contents: contents,
1471
1569
  location: lbracket.location.to(rbracket.location)
1472
1570
  )
1473
1571
  else
1474
- tstring_end = find_token(TStringEnd)
1572
+ tstring_end =
1573
+ find_token(TStringEnd, location: contents.beginning.location)
1475
1574
 
1476
1575
  contents.class.new(
1576
+ beginning: contents.beginning,
1477
1577
  elements: contents.elements,
1478
1578
  location: contents.location.to(tstring_end.location)
1479
1579
  )
@@ -1571,7 +1671,7 @@ class SyntaxTree < Ripper
1571
1671
  end
1572
1672
 
1573
1673
  parent = q.parent
1574
- if parts.length == 1 || PATTERNS.any? { |pattern| parent.is_a?(pattern) }
1674
+ if parts.length == 1 || PATTERNS.include?(parent.class)
1575
1675
  q.text("[")
1576
1676
  q.seplist(parts) { |part| q.format(part) }
1577
1677
  q.text("]")
@@ -1678,7 +1778,7 @@ class SyntaxTree < Ripper
1678
1778
  q.format(target)
1679
1779
  q.text(" =")
1680
1780
 
1681
- if skip_indent?
1781
+ if target.comments.empty? && (skip_indent_target? || skip_indent_value?)
1682
1782
  q.text(" ")
1683
1783
  q.format(value)
1684
1784
  else
@@ -1716,11 +1816,21 @@ class SyntaxTree < Ripper
1716
1816
 
1717
1817
  private
1718
1818
 
1719
- def skip_indent?
1720
- target.is_a?(ARefField) || value.is_a?(ArrayLiteral) ||
1721
- value.is_a?(HashLiteral) ||
1722
- value.is_a?(Heredoc) ||
1723
- value.is_a?(Lambda)
1819
+ def skip_indent_target?
1820
+ target.is_a?(ARefField)
1821
+ end
1822
+
1823
+ def skip_indent_value?
1824
+ [
1825
+ ArrayLiteral,
1826
+ HashLiteral,
1827
+ Heredoc,
1828
+ Lambda,
1829
+ QSymbols,
1830
+ QWords,
1831
+ Symbols,
1832
+ Words
1833
+ ].any? { |type| value.is_a?(type) }
1724
1834
  end
1725
1835
  end
1726
1836
 
@@ -2252,11 +2362,14 @@ class SyntaxTree < Ripper
2252
2362
  q.group do
2253
2363
  q.group { q.format(left) }
2254
2364
  q.text(" ") unless power
2255
- q.text(operator)
2256
2365
 
2257
- q.indent do
2258
- q.breakable(power ? "" : " ")
2259
- q.format(right)
2366
+ q.group do
2367
+ q.text(operator)
2368
+
2369
+ q.indent do
2370
+ q.breakable(power ? "" : " ")
2371
+ q.format(right)
2372
+ end
2260
2373
  end
2261
2374
  end
2262
2375
  end
@@ -2685,54 +2798,132 @@ class SyntaxTree < Ripper
2685
2798
  # [LBrace | Keyword] the node that opens the block
2686
2799
  attr_reader :block_open
2687
2800
 
2801
+ # [String] the string that closes the block
2802
+ attr_reader :block_close
2803
+
2688
2804
  # [BodyStmt | Statements] the statements inside the block
2689
2805
  attr_reader :statements
2690
2806
 
2691
- def initialize(node, block_open, statements)
2807
+ def initialize(node, block_open, block_close, statements)
2692
2808
  @node = node
2693
2809
  @block_open = block_open
2810
+ @block_close = block_close
2694
2811
  @statements = statements
2695
2812
  end
2696
2813
 
2697
2814
  def format(q)
2815
+ # If this is nested anywhere inside of a Command or CommandCall node, then
2816
+ # we can't change which operators we're using for the bounds of the block.
2817
+ break_opening, break_closing, flat_opening, flat_closing =
2818
+ if unchangeable_bounds?(q)
2819
+ [block_open.value, block_close, block_open.value, block_close]
2820
+ elsif forced_do_end_bounds?(q)
2821
+ %w[do end do end]
2822
+ elsif forced_brace_bounds?(q)
2823
+ %w[{ } { }]
2824
+ else
2825
+ %w[do end { }]
2826
+ end
2827
+
2828
+ # If the receiver of this block a Command or CommandCall node, then there
2829
+ # are no parentheses around the arguments to that command, so we need to
2830
+ # break the block.
2831
+ receiver = q.parent.call
2832
+ if receiver.is_a?(Command) || receiver.is_a?(CommandCall)
2833
+ q.break_parent
2834
+ format_break(q, break_opening, break_closing)
2835
+ return
2836
+ end
2837
+
2698
2838
  q.group do
2699
- q.text(" ")
2839
+ q.if_break { format_break(q, break_opening, break_closing) }.if_flat do
2840
+ format_flat(q, flat_opening, flat_closing)
2841
+ end
2842
+ end
2843
+ end
2700
2844
 
2701
- q.if_break do
2702
- q.format(BlockOpenFormatter.new("do", block_open))
2845
+ private
2703
2846
 
2704
- if node.block_var
2705
- q.text(" ")
2706
- q.format(node.block_var)
2707
- end
2847
+ # If this is nested anywhere inside certain nodes, then we can't change
2848
+ # which operators/keywords we're using for the bounds of the block.
2849
+ def unchangeable_bounds?(q)
2850
+ q.parents.any? do |parent|
2851
+ # If we hit a statements, then we're safe to use whatever since we
2852
+ # know for certain we're going to get split over multiple lines
2853
+ # anyway.
2854
+ break false if parent.is_a?(Statements)
2708
2855
 
2709
- unless statements.empty?
2710
- q.indent do
2711
- q.breakable
2712
- q.format(statements)
2713
- end
2714
- end
2856
+ [Command, CommandCall].include?(parent.class)
2857
+ end
2858
+ end
2715
2859
 
2716
- q.breakable
2717
- q.text("end")
2718
- end.if_flat do
2719
- q.format(BlockOpenFormatter.new("{", block_open))
2860
+ # If we're a sibling of a control-flow keyword, then we're going to have to
2861
+ # use the do..end bounds.
2862
+ def forced_do_end_bounds?(q)
2863
+ [Break, Next, Return, Super].include?(q.parent.call.class)
2864
+ end
2720
2865
 
2721
- if node.block_var
2722
- q.breakable
2723
- q.format(node.block_var)
2724
- q.breakable
2725
- end
2866
+ # If we're the predicate of a loop or conditional, then we're going to have
2867
+ # to go with the {..} bounds.
2868
+ def forced_brace_bounds?(q)
2869
+ parents = q.parents.to_a
2870
+ parents.each_with_index.any? do |parent, index|
2871
+ # If we hit certain breakpoints then we know we're safe.
2872
+ break false if [Paren, Statements].include?(parent.class)
2726
2873
 
2727
- unless statements.empty?
2728
- q.breakable unless node.block_var
2729
- q.format(statements)
2730
- q.breakable
2731
- end
2874
+ [
2875
+ If,
2876
+ IfMod,
2877
+ IfOp,
2878
+ Unless,
2879
+ UnlessMod,
2880
+ While,
2881
+ WhileMod,
2882
+ Until,
2883
+ UntilMod
2884
+ ].include?(parent.class) && parent.predicate == parents[index - 1]
2885
+ end
2886
+ end
2732
2887
 
2733
- q.text("}")
2888
+ def format_break(q, opening, closing)
2889
+ q.text(" ")
2890
+ q.format(BlockOpenFormatter.new(opening, block_open))
2891
+
2892
+ if node.block_var
2893
+ q.text(" ")
2894
+ q.format(node.block_var)
2895
+ end
2896
+
2897
+ unless statements.empty?
2898
+ q.indent do
2899
+ q.breakable
2900
+ q.format(statements)
2734
2901
  end
2735
2902
  end
2903
+
2904
+ q.breakable
2905
+ q.text(closing)
2906
+ end
2907
+
2908
+ def format_flat(q, opening, closing)
2909
+ q.text(" ")
2910
+ q.format(BlockOpenFormatter.new(opening, block_open))
2911
+
2912
+ if node.block_var
2913
+ q.breakable
2914
+ q.format(node.block_var)
2915
+ q.breakable
2916
+ end
2917
+
2918
+ if statements.empty?
2919
+ q.text(" ") if opening == "do"
2920
+ else
2921
+ q.breakable unless node.block_var
2922
+ q.format(statements)
2923
+ q.breakable
2924
+ end
2925
+
2926
+ q.text(closing)
2736
2927
  end
2737
2928
  end
2738
2929
 
@@ -2770,7 +2961,7 @@ class SyntaxTree < Ripper
2770
2961
  end
2771
2962
 
2772
2963
  def format(q)
2773
- BlockFormatter.new(self, lbrace, statements).format(q)
2964
+ BlockFormatter.new(self, lbrace, "}", statements).format(q)
2774
2965
  end
2775
2966
 
2776
2967
  def pretty_print(q)
@@ -2851,15 +3042,35 @@ class SyntaxTree < Ripper
2851
3042
  q.text(keyword)
2852
3043
 
2853
3044
  if arguments.parts.any?
2854
- if arguments.parts.length == 1 && arguments.parts.first.is_a?(Paren)
2855
- q.format(arguments)
3045
+ if arguments.parts.length == 1
3046
+ part = arguments.parts.first
3047
+
3048
+ if part.is_a?(Paren)
3049
+ q.format(arguments)
3050
+ elsif part.is_a?(ArrayLiteral)
3051
+ q.text(" ")
3052
+ q.format(arguments)
3053
+ else
3054
+ format_arguments(q, "(", ")")
3055
+ end
2856
3056
  else
2857
- q.text(" ")
2858
- q.nest(keyword.length + 1) { q.format(arguments) }
3057
+ format_arguments(q, " [", "]")
2859
3058
  end
2860
3059
  end
2861
3060
  end
2862
3061
  end
3062
+
3063
+ private
3064
+
3065
+ def format_arguments(q, opening, closing)
3066
+ q.if_break { q.text(opening) }
3067
+ q.indent do
3068
+ q.breakable(" ")
3069
+ q.format(node.arguments)
3070
+ end
3071
+ q.breakable("")
3072
+ q.if_break { q.text(closing) }
3073
+ end
2863
3074
  end
2864
3075
 
2865
3076
  # Break represents using the +break+ keyword.
@@ -2946,9 +3157,7 @@ class SyntaxTree < Ripper
2946
3157
  end
2947
3158
  end
2948
3159
 
2949
- # Call represents a method call. This node doesn't contain the arguments being
2950
- # passed (if arguments are passed, this node will get nested under a
2951
- # MethodAddArg node).
3160
+ # Call represents a method call.
2952
3161
  #
2953
3162
  # receiver.message
2954
3163
  #
@@ -2962,32 +3171,60 @@ class SyntaxTree < Ripper
2962
3171
  # [:call | Backtick | Const | Ident | Op] the message being sent
2963
3172
  attr_reader :message
2964
3173
 
3174
+ # [nil | ArgParen | Args] the arguments to the method call
3175
+ attr_reader :arguments
3176
+
2965
3177
  # [Location] the location of this node
2966
3178
  attr_reader :location
2967
3179
 
2968
3180
  # [Array[ Comment | EmbDoc ]] the comments attached to this node
2969
3181
  attr_reader :comments
2970
3182
 
2971
- def initialize(receiver:, operator:, message:, location:, comments: [])
3183
+ def initialize(
3184
+ receiver:,
3185
+ operator:,
3186
+ message:,
3187
+ arguments:,
3188
+ location:,
3189
+ comments: []
3190
+ )
2972
3191
  @receiver = receiver
2973
3192
  @operator = operator
2974
3193
  @message = message
3194
+ @arguments = arguments
2975
3195
  @location = location
2976
3196
  @comments = comments
2977
3197
  end
2978
3198
 
2979
3199
  def child_nodes
2980
- [receiver, (operator if operator != :"::"), (message if message != :call)]
3200
+ [
3201
+ receiver,
3202
+ (operator if operator != :"::"),
3203
+ (message if message != :call),
3204
+ arguments
3205
+ ]
2981
3206
  end
2982
3207
 
2983
3208
  def format(q)
3209
+ call_operator = CallOperatorFormatter.new(operator)
3210
+
2984
3211
  q.group do
2985
3212
  q.format(receiver)
3213
+
3214
+ # If there are trailing comments on the call operator, then we need to
3215
+ # use the trailing form as opposed to the leading form.
3216
+ q.format(call_operator) if call_operator.comments.any?
3217
+
2986
3218
  q.group do
2987
3219
  q.indent do
2988
- q.format(CallOperatorFormatter.new(operator))
3220
+ if receiver.comments.any? || call_operator.comments.any?
3221
+ q.breakable(force: true)
3222
+ end
3223
+ q.format(call_operator) if call_operator.comments.empty?
2989
3224
  q.format(message) if message != :call
2990
3225
  end
3226
+
3227
+ q.format(arguments) if arguments
2991
3228
  end
2992
3229
  end
2993
3230
  end
@@ -3005,6 +3242,11 @@ class SyntaxTree < Ripper
3005
3242
  q.breakable
3006
3243
  q.pp(message)
3007
3244
 
3245
+ if arguments
3246
+ q.breakable
3247
+ q.pp(arguments)
3248
+ end
3249
+
3008
3250
  q.pp(Comment::List.new(comments))
3009
3251
  end
3010
3252
  end
@@ -3015,6 +3257,7 @@ class SyntaxTree < Ripper
3015
3257
  receiver: receiver,
3016
3258
  op: operator,
3017
3259
  message: message,
3260
+ args: arguments,
3018
3261
  loc: location,
3019
3262
  cmts: comments
3020
3263
  }.to_json(*opts)
@@ -3035,13 +3278,8 @@ class SyntaxTree < Ripper
3035
3278
  receiver: receiver,
3036
3279
  operator: operator,
3037
3280
  message: message,
3038
- location:
3039
- Location.new(
3040
- start_line: receiver.location.start_line,
3041
- start_char: receiver.location.start_char,
3042
- end_line: [ending.location.end_line, receiver.location.end_line].max,
3043
- end_char: ending.location.end_char
3044
- )
3281
+ arguments: nil,
3282
+ location: receiver.location.to(ending.location)
3045
3283
  )
3046
3284
  end
3047
3285
 
@@ -3057,6 +3295,9 @@ class SyntaxTree < Ripper
3057
3295
  # end
3058
3296
  #
3059
3297
  class Case
3298
+ # [Kw] the keyword that opens this expression
3299
+ attr_reader :keyword
3300
+
3060
3301
  # [nil | untyped] optional value being switched on
3061
3302
  attr_reader :value
3062
3303
 
@@ -3069,7 +3310,8 @@ class SyntaxTree < Ripper
3069
3310
  # [Array[ Comment | EmbDoc ]] the comments attached to this node
3070
3311
  attr_reader :comments
3071
3312
 
3072
- def initialize(value:, consequent:, location:, comments: [])
3313
+ def initialize(keyword:, value:, consequent:, location:, comments: [])
3314
+ @keyword = keyword
3073
3315
  @value = value
3074
3316
  @consequent = consequent
3075
3317
  @location = location
@@ -3077,11 +3319,13 @@ class SyntaxTree < Ripper
3077
3319
  end
3078
3320
 
3079
3321
  def child_nodes
3080
- [value, consequent]
3322
+ [keyword, value, consequent]
3081
3323
  end
3082
3324
 
3083
3325
  def format(q)
3084
- q.group(0, "case", "end") do
3326
+ q.group do
3327
+ q.format(keyword)
3328
+
3085
3329
  if value
3086
3330
  q.text(" ")
3087
3331
  q.format(value)
@@ -3090,6 +3334,8 @@ class SyntaxTree < Ripper
3090
3334
  q.breakable(force: true)
3091
3335
  q.format(consequent)
3092
3336
  q.breakable(force: true)
3337
+
3338
+ q.text("end")
3093
3339
  end
3094
3340
  end
3095
3341
 
@@ -3097,6 +3343,9 @@ class SyntaxTree < Ripper
3097
3343
  q.group(2, "(", ")") do
3098
3344
  q.text("case")
3099
3345
 
3346
+ q.breakable
3347
+ q.pp(keyword)
3348
+
3100
3349
  if value
3101
3350
  q.breakable
3102
3351
  q.pp(value)
@@ -3204,6 +3453,7 @@ class SyntaxTree < Ripper
3204
3453
  tokens.delete(keyword)
3205
3454
 
3206
3455
  Case.new(
3456
+ keyword: keyword,
3207
3457
  value: value,
3208
3458
  consequent: consequent,
3209
3459
  location: keyword.location.to(consequent.location)
@@ -3484,7 +3734,7 @@ class SyntaxTree < Ripper
3484
3734
  # [Const | Ident | Op] the message being send
3485
3735
  attr_reader :message
3486
3736
 
3487
- # [Args] the arguments going along with the message
3737
+ # [nil | Args] the arguments going along with the message
3488
3738
  attr_reader :arguments
3489
3739
 
3490
3740
  # [Location] the location of this node
@@ -3515,16 +3765,22 @@ class SyntaxTree < Ripper
3515
3765
 
3516
3766
  def format(q)
3517
3767
  q.group do
3518
- doc = q.format(receiver)
3519
- q.format(CallOperatorFormatter.new(operator))
3520
- q.format(message)
3521
- q.text(" ")
3768
+ doc =
3769
+ q.nest(0) do
3770
+ q.format(receiver)
3771
+ q.format(CallOperatorFormatter.new(operator))
3772
+ q.format(message)
3773
+ end
3522
3774
 
3523
- width = doc_width(doc)
3524
- if width > (q.maxwidth / 2) || width < 2
3525
- q.indent { q.format(arguments) }
3526
- else
3527
- q.nest(width) { q.format(arguments) }
3775
+ if arguments
3776
+ width = doc_width(doc) + 1
3777
+ q.text(" ")
3778
+
3779
+ if width > (q.maxwidth / 2)
3780
+ q.format(arguments)
3781
+ else
3782
+ q.nest(width) { q.format(arguments) }
3783
+ end
3528
3784
  end
3529
3785
  end
3530
3786
  end
@@ -3542,8 +3798,10 @@ class SyntaxTree < Ripper
3542
3798
  q.breakable
3543
3799
  q.pp(message)
3544
3800
 
3545
- q.breakable
3546
- q.pp(arguments)
3801
+ if arguments
3802
+ q.breakable
3803
+ q.pp(arguments)
3804
+ end
3547
3805
 
3548
3806
  q.pp(Comment::List.new(comments))
3549
3807
  end
@@ -3577,11 +3835,11 @@ class SyntaxTree < Ripper
3577
3835
  when PrettyPrint::Text
3578
3836
  width += doc.width
3579
3837
  when PrettyPrint::Indent, PrettyPrint::Align, PrettyPrint::Group
3580
- queue += doc.contents.reverse
3838
+ queue = doc.contents + queue
3581
3839
  when PrettyPrint::IfBreak
3582
- queue += doc.flat_contents.reverse
3840
+ queue = doc.break_contents + queue
3583
3841
  when PrettyPrint::Breakable
3584
- width = doc.force? ? 0 : width + doc.width
3842
+ width = 0
3585
3843
  end
3586
3844
  end
3587
3845
 
@@ -3594,7 +3852,7 @@ class SyntaxTree < Ripper
3594
3852
  # untyped receiver,
3595
3853
  # (:"::" | Op | Period) operator,
3596
3854
  # (Const | Ident | Op) message,
3597
- # Args arguments
3855
+ # (nil | Args) arguments
3598
3856
  # ) -> CommandCall
3599
3857
  def on_command_call(receiver, operator, message, arguments)
3600
3858
  ending = arguments || message
@@ -3665,6 +3923,10 @@ class SyntaxTree < Ripper
3665
3923
  @trailing
3666
3924
  end
3667
3925
 
3926
+ def ignore?
3927
+ value[1..-1].strip == "stree-ignore"
3928
+ end
3929
+
3668
3930
  def comments
3669
3931
  []
3670
3932
  end
@@ -4062,14 +4324,7 @@ class SyntaxTree < Ripper
4062
4324
  q.group do
4063
4325
  q.text("def ")
4064
4326
  q.format(name)
4065
-
4066
- if params.is_a?(Params) && !params.empty?
4067
- q.text("(")
4068
- q.format(params)
4069
- q.text(")")
4070
- else
4071
- q.format(params)
4072
- end
4327
+ q.format(params) if !params.is_a?(Params) || !params.empty?
4073
4328
  end
4074
4329
 
4075
4330
  unless bodystmt.empty?
@@ -4118,6 +4373,12 @@ class SyntaxTree < Ripper
4118
4373
  # def method = result
4119
4374
  #
4120
4375
  class DefEndless
4376
+ # [untyped] the target where the method is being defined
4377
+ attr_reader :target
4378
+
4379
+ # [Op | Period] the operator being used to declare the method
4380
+ attr_reader :operator
4381
+
4121
4382
  # [Backtick | Const | Ident | Kw | Op] the name of the method
4122
4383
  attr_reader :name
4123
4384
 
@@ -4133,7 +4394,17 @@ class SyntaxTree < Ripper
4133
4394
  # [Array[ Comment | EmbDoc ]] the comments attached to this node
4134
4395
  attr_reader :comments
4135
4396
 
4136
- def initialize(name:, paren:, statement:, location:, comments: [])
4397
+ def initialize(
4398
+ target:,
4399
+ operator:,
4400
+ name:,
4401
+ paren:,
4402
+ statement:,
4403
+ location:,
4404
+ comments: []
4405
+ )
4406
+ @target = target
4407
+ @operator = operator
4137
4408
  @name = name
4138
4409
  @paren = paren
4139
4410
  @statement = statement
@@ -4142,14 +4413,21 @@ class SyntaxTree < Ripper
4142
4413
  end
4143
4414
 
4144
4415
  def child_nodes
4145
- [name, paren, statement]
4416
+ [target, operator, name, paren, statement]
4146
4417
  end
4147
4418
 
4148
4419
  def format(q)
4149
4420
  q.group do
4150
4421
  q.text("def ")
4422
+
4423
+ if target
4424
+ q.format(target)
4425
+ q.format(CallOperatorFormatter.new(operator))
4426
+ end
4427
+
4151
4428
  q.format(name)
4152
4429
  q.format(paren) if paren && !paren.contents.empty?
4430
+
4153
4431
  q.text(" =")
4154
4432
  q.group do
4155
4433
  q.indent do
@@ -4164,6 +4442,14 @@ class SyntaxTree < Ripper
4164
4442
  q.group(2, "(", ")") do
4165
4443
  q.text("def_endless")
4166
4444
 
4445
+ if target
4446
+ q.breakable
4447
+ q.pp(target)
4448
+
4449
+ q.breakable
4450
+ q.pp(operator)
4451
+ end
4452
+
4167
4453
  q.breakable
4168
4454
  q.pp(name)
4169
4455
 
@@ -4210,6 +4496,8 @@ class SyntaxTree < Ripper
4210
4496
  unless bodystmt.is_a?(BodyStmt)
4211
4497
  node =
4212
4498
  DefEndless.new(
4499
+ target: nil,
4500
+ operator: nil,
4213
4501
  name: name,
4214
4502
  paren: params,
4215
4503
  statement: bodystmt,
@@ -4371,14 +4659,7 @@ class SyntaxTree < Ripper
4371
4659
  q.format(target)
4372
4660
  q.format(CallOperatorFormatter.new(operator))
4373
4661
  q.format(name)
4374
-
4375
- if params.is_a?(Params) && !params.empty?
4376
- q.text("(")
4377
- q.format(params)
4378
- q.text(")")
4379
- else
4380
- q.format(params)
4381
- end
4662
+ q.format(params) if !params.is_a?(Params) || !params.empty?
4382
4663
  end
4383
4664
 
4384
4665
  unless bodystmt.empty?
@@ -4460,6 +4741,22 @@ class SyntaxTree < Ripper
4460
4741
  end
4461
4742
 
4462
4743
  beginning = find_token(Kw, "def")
4744
+
4745
+ # If we don't have a bodystmt node, then we have a single-line method
4746
+ unless bodystmt.is_a?(BodyStmt)
4747
+ node =
4748
+ DefEndless.new(
4749
+ target: target,
4750
+ operator: operator,
4751
+ name: name,
4752
+ paren: params,
4753
+ statement: bodystmt,
4754
+ location: beginning.location.to(bodystmt.location)
4755
+ )
4756
+
4757
+ return node
4758
+ end
4759
+
4463
4760
  ending = find_token(Kw, "end")
4464
4761
 
4465
4762
  bodystmt.bind(
@@ -4512,7 +4809,7 @@ class SyntaxTree < Ripper
4512
4809
  end
4513
4810
 
4514
4811
  def format(q)
4515
- BlockFormatter.new(self, keyword, bodystmt).format(q)
4812
+ BlockFormatter.new(self, keyword, "end", bodystmt).format(q)
4516
4813
  end
4517
4814
 
4518
4815
  def pretty_print(q)
@@ -4576,8 +4873,7 @@ class SyntaxTree < Ripper
4576
4873
  end
4577
4874
 
4578
4875
  def format(q)
4579
- parent = q.parent
4580
- space = parent.is_a?(If) || parent.is_a?(Unless)
4876
+ space = [If, IfMod, Unless, UnlessMod].include?(q.parent.class)
4581
4877
 
4582
4878
  left = node.left
4583
4879
  right = node.right
@@ -4890,9 +5186,9 @@ class SyntaxTree < Ripper
4890
5186
  matching = Quotes.matching(quote[2])
4891
5187
  pattern = /[\n#{Regexp.escape(matching)}'"]/
4892
5188
 
4893
- if parts.any? do |part|
5189
+ if parts.any? { |part|
4894
5190
  part.is_a?(TStringContent) && part.value.match?(pattern)
4895
- end
5191
+ }
4896
5192
  [quote, matching]
4897
5193
  elsif Quotes.locked?(self)
4898
5194
  ["#{":" unless hash_key}'", "'"]
@@ -4917,7 +5213,7 @@ class SyntaxTree < Ripper
4917
5213
  if find_token(SymBeg, consume: false)
4918
5214
  # A normal dynamic symbol
4919
5215
  symbeg = find_token(SymBeg)
4920
- tstring_end = find_token(TStringEnd)
5216
+ tstring_end = find_token(TStringEnd, location: symbeg.location)
4921
5217
 
4922
5218
  DynaSymbol.new(
4923
5219
  quote: symbeg.value,
@@ -5009,6 +5305,7 @@ class SyntaxTree < Ripper
5009
5305
 
5010
5306
  node = tokens[index]
5011
5307
  ending = node.value == "end" ? tokens.delete_at(index) : node
5308
+ # ending = node
5012
5309
 
5013
5310
  statements.bind(beginning.location.end_char, ending.location.start_char)
5014
5311
 
@@ -5155,6 +5452,10 @@ class SyntaxTree < Ripper
5155
5452
  false
5156
5453
  end
5157
5454
 
5455
+ def ignore?
5456
+ false
5457
+ end
5458
+
5158
5459
  def comments
5159
5460
  []
5160
5461
  end
@@ -5480,24 +5781,29 @@ class SyntaxTree < Ripper
5480
5781
  # [Const | Ident] the name of the method
5481
5782
  attr_reader :value
5482
5783
 
5784
+ # [nil | ArgParen | Args] the arguments to the method call
5785
+ attr_reader :arguments
5786
+
5483
5787
  # [Location] the location of this node
5484
5788
  attr_reader :location
5485
5789
 
5486
5790
  # [Array[ Comment | EmbDoc ]] the comments attached to this node
5487
5791
  attr_reader :comments
5488
5792
 
5489
- def initialize(value:, location:, comments: [])
5793
+ def initialize(value:, arguments:, location:, comments: [])
5490
5794
  @value = value
5795
+ @arguments = arguments
5491
5796
  @location = location
5492
5797
  @comments = comments
5493
5798
  end
5494
5799
 
5495
5800
  def child_nodes
5496
- [value]
5801
+ [value, arguments]
5497
5802
  end
5498
5803
 
5499
5804
  def format(q)
5500
5805
  q.format(value)
5806
+ q.format(arguments)
5501
5807
  end
5502
5808
 
5503
5809
  def pretty_print(q)
@@ -5507,21 +5813,30 @@ class SyntaxTree < Ripper
5507
5813
  q.breakable
5508
5814
  q.pp(value)
5509
5815
 
5816
+ if arguments
5817
+ q.breakable
5818
+ q.pp(arguments)
5819
+ end
5820
+
5510
5821
  q.pp(Comment::List.new(comments))
5511
5822
  end
5512
5823
  end
5513
5824
 
5514
5825
  def to_json(*opts)
5515
- { type: :fcall, value: value, loc: location, cmts: comments }.to_json(
5516
- *opts
5517
- )
5826
+ {
5827
+ type: :fcall,
5828
+ value: value,
5829
+ args: arguments,
5830
+ loc: location,
5831
+ cmts: comments
5832
+ }.to_json(*opts)
5518
5833
  end
5519
5834
  end
5520
5835
 
5521
5836
  # :call-seq:
5522
5837
  # on_fcall: ((Const | Ident) value) -> FCall
5523
5838
  def on_fcall(value)
5524
- FCall.new(value: value, location: value.location)
5839
+ FCall.new(value: value, arguments: nil, location: value.location)
5525
5840
  end
5526
5841
 
5527
5842
  # Field is always the child of an assignment. It represents assigning to a
@@ -5864,6 +6179,7 @@ class SyntaxTree < Ripper
5864
6179
  # ) -> For
5865
6180
  def on_for(index, collection, statements)
5866
6181
  beginning = find_token(Kw, "for")
6182
+ in_keyword = find_token(Kw, "in")
5867
6183
  ending = find_token(Kw, "end")
5868
6184
 
5869
6185
  # Consume the do keyword if it exists so that it doesn't get confused for
@@ -5879,6 +6195,11 @@ class SyntaxTree < Ripper
5879
6195
  ending.location.start_char
5880
6196
  )
5881
6197
 
6198
+ if index.is_a?(MLHS)
6199
+ comma_range = index.location.end_char...in_keyword.location.start_char
6200
+ index.comma = true if source[comma_range].strip.start_with?(",")
6201
+ end
6202
+
5882
6203
  For.new(
5883
6204
  index: index,
5884
6205
  collection: collection,
@@ -5947,6 +6268,9 @@ class SyntaxTree < Ripper
5947
6268
  # { key => value }
5948
6269
  #
5949
6270
  class HashLiteral
6271
+ # [LBrace] the left brace that opens this hash
6272
+ attr_reader :lbrace
6273
+
5950
6274
  # [Array[ AssocNew | AssocSplat ]] the optional contents of the hash
5951
6275
  attr_reader :assocs
5952
6276
 
@@ -5956,24 +6280,31 @@ class SyntaxTree < Ripper
5956
6280
  # [Array[ Comment | EmbDoc ]] the comments attached to this node
5957
6281
  attr_reader :comments
5958
6282
 
5959
- def initialize(assocs:, location:, comments: [])
6283
+ def initialize(lbrace:, assocs:, location:, comments: [])
6284
+ @lbrace = lbrace
5960
6285
  @assocs = assocs
5961
6286
  @location = location
5962
6287
  @comments = comments
5963
6288
  end
5964
6289
 
5965
6290
  def child_nodes
5966
- assocs
6291
+ [lbrace] + assocs
5967
6292
  end
5968
6293
 
5969
6294
  def format(q)
5970
6295
  contents = -> do
5971
- q.text("{")
5972
- q.indent do
6296
+ q.format(lbrace)
6297
+
6298
+ if assocs.empty?
6299
+ q.breakable("")
6300
+ else
6301
+ q.indent do
6302
+ q.breakable
6303
+ q.format(HashFormatter.for(self))
6304
+ end
5973
6305
  q.breakable
5974
- q.format(HashFormatter.for(self))
5975
6306
  end
5976
- q.breakable
6307
+
5977
6308
  q.text("}")
5978
6309
  end
5979
6310
 
@@ -6007,6 +6338,7 @@ class SyntaxTree < Ripper
6007
6338
  rbrace = find_token(RBrace)
6008
6339
 
6009
6340
  HashLiteral.new(
6341
+ lbrace: lbrace,
6010
6342
  assocs: assocs || [],
6011
6343
  location: lbrace.location.to(rbrace.location)
6012
6344
  )
@@ -6059,7 +6391,7 @@ class SyntaxTree < Ripper
6059
6391
  q.group do
6060
6392
  q.format(beginning)
6061
6393
 
6062
- q.line_suffix do
6394
+ q.line_suffix(priority: Formatter::HEREDOC_PRIORITY) do
6063
6395
  q.group do
6064
6396
  breakable.call
6065
6397
 
@@ -6219,6 +6551,12 @@ class SyntaxTree < Ripper
6219
6551
  @value = value
6220
6552
  end
6221
6553
 
6554
+ # This is here so that when checking if its contained within a parent
6555
+ # pattern that it will return true.
6556
+ def class
6557
+ HshPtn
6558
+ end
6559
+
6222
6560
  def comments
6223
6561
  []
6224
6562
  end
@@ -6293,7 +6631,7 @@ class SyntaxTree < Ripper
6293
6631
  end
6294
6632
 
6295
6633
  parent = q.parent
6296
- if PATTERNS.any? { |pattern| parent.is_a?(pattern) }
6634
+ if PATTERNS.include?(parent.class)
6297
6635
  q.text("{ ")
6298
6636
  contents.call
6299
6637
  q.text(" }")
@@ -6442,38 +6780,50 @@ class SyntaxTree < Ripper
6442
6780
  end
6443
6781
 
6444
6782
  def format(q)
6445
- statements = node.statements
6446
- break_format = ->(force:) do
6447
- q.text("#{keyword} ")
6448
- q.nest(keyword.length + 1) { q.format(node.predicate) }
6783
+ # If the predicate of the conditional contains an assignment (in which
6784
+ # case we can't know for certain that that assignment doesn't impact the
6785
+ # statements inside the conditional) then we can't use the modifier form
6786
+ # and we must use the block form.
6787
+ if [Assign, MAssign, OpAssign].include?(node.predicate.class)
6788
+ format_break(q, force: true)
6789
+ return
6790
+ end
6449
6791
 
6450
- unless statements.empty?
6451
- q.indent do
6452
- q.breakable(force: force)
6453
- q.format(statements)
6792
+ if node.consequent || node.statements.empty?
6793
+ q.group { format_break(q, force: true) }
6794
+ else
6795
+ q.group do
6796
+ q.if_break { format_break(q, force: false) }.if_flat do
6797
+ Parentheses.flat(q) do
6798
+ q.format(node.statements)
6799
+ q.text(" #{keyword} ")
6800
+ q.format(node.predicate)
6801
+ end
6454
6802
  end
6455
6803
  end
6804
+ end
6805
+ end
6806
+
6807
+ private
6456
6808
 
6457
- if node.consequent
6809
+ def format_break(q, force:)
6810
+ q.text("#{keyword} ")
6811
+ q.nest(keyword.length + 1) { q.format(node.predicate) }
6812
+
6813
+ unless node.statements.empty?
6814
+ q.indent do
6458
6815
  q.breakable(force: force)
6459
- q.format(node.consequent)
6816
+ q.format(node.statements)
6460
6817
  end
6818
+ end
6461
6819
 
6820
+ if node.consequent
6462
6821
  q.breakable(force: force)
6463
- q.text("end")
6822
+ q.format(node.consequent)
6464
6823
  end
6465
6824
 
6466
- if node.consequent || statements.empty?
6467
- q.group { break_format.call(force: true) }
6468
- else
6469
- q.group do
6470
- q.if_break { break_format.call(force: false) }.if_flat do
6471
- q.format(node.statements)
6472
- q.text(" #{keyword} ")
6473
- q.format(node.predicate)
6474
- end
6475
- end
6476
- end
6825
+ q.breakable(force: force)
6826
+ q.text("end")
6477
6827
  end
6478
6828
  end
6479
6829
 
@@ -6604,38 +6954,19 @@ class SyntaxTree < Ripper
6604
6954
  end
6605
6955
 
6606
6956
  def format(q)
6607
- q.group do
6608
- q.if_break do
6609
- q.text("if ")
6610
- q.nest("if ".length) { q.format(predicate) }
6611
-
6612
- q.indent do
6613
- q.breakable
6614
- q.format(truthy)
6615
- end
6616
-
6617
- q.breakable
6618
- q.text("else")
6619
-
6620
- q.indent do
6621
- q.breakable
6622
- q.format(falsy)
6623
- end
6624
-
6625
- q.breakable
6626
- q.text("end")
6627
- end.if_flat do
6628
- q.format(predicate)
6629
- q.text(" ?")
6630
-
6631
- q.breakable
6632
- q.format(truthy)
6633
- q.text(" :")
6957
+ force_flat = [
6958
+ Alias, Assign, Break, Command, CommandCall, Heredoc, If, IfMod, IfOp,
6959
+ Lambda, MAssign, Next, OpAssign, RescueMod, Return, Return0, Super,
6960
+ Undef, Unless, UnlessMod, UntilMod, VarAlias, VoidStmt, WhileMod, Yield,
6961
+ Yield0, ZSuper
6962
+ ]
6634
6963
 
6635
- q.breakable
6636
- q.format(falsy)
6637
- end
6964
+ if force_flat.include?(truthy.class) || force_flat.include?(falsy.class)
6965
+ q.group { format_flat(q) }
6966
+ return
6638
6967
  end
6968
+
6969
+ q.group { q.if_break { format_break(q) }.if_flat { format_flat(q) } }
6639
6970
  end
6640
6971
 
6641
6972
  def pretty_print(q)
@@ -6665,6 +6996,43 @@ class SyntaxTree < Ripper
6665
6996
  cmts: comments
6666
6997
  }.to_json(*opts)
6667
6998
  end
6999
+
7000
+ private
7001
+
7002
+ def format_break(q)
7003
+ Parentheses.break(q) do
7004
+ q.text("if ")
7005
+ q.nest("if ".length) { q.format(predicate) }
7006
+
7007
+ q.indent do
7008
+ q.breakable
7009
+ q.format(truthy)
7010
+ end
7011
+
7012
+ q.breakable
7013
+ q.text("else")
7014
+
7015
+ q.indent do
7016
+ q.breakable
7017
+ q.format(falsy)
7018
+ end
7019
+
7020
+ q.breakable
7021
+ q.text("end")
7022
+ end
7023
+ end
7024
+
7025
+ def format_flat(q)
7026
+ q.format(predicate)
7027
+ q.text(" ?")
7028
+
7029
+ q.breakable
7030
+ q.format(truthy)
7031
+ q.text(" :")
7032
+
7033
+ q.breakable
7034
+ q.format(falsy)
7035
+ end
6668
7036
  end
6669
7037
 
6670
7038
  # :call-seq:
@@ -6703,9 +7071,11 @@ class SyntaxTree < Ripper
6703
7071
  q.breakable
6704
7072
  q.text("end")
6705
7073
  end.if_flat do
6706
- q.format(node.statement)
6707
- q.text(" #{keyword} ")
6708
- q.format(node.predicate)
7074
+ Parentheses.flat(q) do
7075
+ q.format(node.statement)
7076
+ q.text(" #{keyword} ")
7077
+ q.format(node.predicate)
7078
+ end
6709
7079
  end
6710
7080
  end
6711
7081
  end
@@ -6944,8 +7314,9 @@ class SyntaxTree < Ripper
6944
7314
  beginning = find_token(Kw, "in")
6945
7315
  ending = consequent || find_token(Kw, "end")
6946
7316
 
7317
+ statements_start = find_token(Kw, "then", consume: false) || pattern
6947
7318
  statements.bind(
6948
- find_next_statement_start(pattern.location.end_char),
7319
+ find_next_statement_start(statements_start.location.end_char),
6949
7320
  ending.location.start_char
6950
7321
  )
6951
7322
 
@@ -6982,7 +7353,7 @@ class SyntaxTree < Ripper
6982
7353
  end
6983
7354
 
6984
7355
  def format(q)
6985
- if !value.start_with?("0") && value.length >= 5 && !value.include?("_")
7356
+ if !value.start_with?(/\+?0/) && value.length >= 5 && !value.include?("_")
6986
7357
  # If it's a plain integer and it doesn't have any underscores separating
6987
7358
  # the values, then we're going to insert them every 3 characters
6988
7359
  # starting from the right.
@@ -7330,9 +7701,11 @@ class SyntaxTree < Ripper
7330
7701
  if params.is_a?(Paren)
7331
7702
  q.format(params) unless params.contents.empty?
7332
7703
  elsif !params.empty?
7333
- q.text("(")
7334
- q.format(params)
7335
- q.text(")")
7704
+ q.group do
7705
+ q.text("(")
7706
+ q.format(params)
7707
+ q.text(")")
7708
+ end
7336
7709
  end
7337
7710
 
7338
7711
  q.text(" ")
@@ -7391,8 +7764,11 @@ class SyntaxTree < Ripper
7391
7764
  def on_lambda(params, statements)
7392
7765
  beginning = find_token(TLambda)
7393
7766
 
7394
- if token = find_token(TLamBeg, consume: false)
7395
- opening = tokens.delete(token)
7767
+ if tokens.any? { |token|
7768
+ token.is_a?(TLamBeg) &&
7769
+ token.location.start_char > beginning.location.start_char
7770
+ }
7771
+ opening = find_token(TLamBeg)
7396
7772
  closing = find_token(RBrace)
7397
7773
  else
7398
7774
  opening = find_token(Kw, "do")
@@ -7472,9 +7848,34 @@ class SyntaxTree < Ripper
7472
7848
  # [Location] the location of this node
7473
7849
  attr_reader :location
7474
7850
 
7475
- def initialize(value:, location:)
7851
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
7852
+ attr_reader :comments
7853
+
7854
+ def initialize(value:, location:, comments: [])
7476
7855
  @value = value
7477
7856
  @location = location
7857
+ @comments = comments
7858
+ end
7859
+
7860
+ def format(q)
7861
+ q.text(value)
7862
+ end
7863
+
7864
+ def pretty_print(q)
7865
+ q.group(2, "(", ")") do
7866
+ q.text("lbracket")
7867
+
7868
+ q.breakable
7869
+ q.pp(value)
7870
+
7871
+ q.pp(Comment::List.new(comments))
7872
+ end
7873
+ end
7874
+
7875
+ def to_json(*opts)
7876
+ { type: :lbracket, value: value, loc: location, cmts: comments }.to_json(
7877
+ *opts
7878
+ )
7478
7879
  end
7479
7880
  end
7480
7881
 
@@ -7591,11 +7992,7 @@ class SyntaxTree < Ripper
7591
7992
 
7592
7993
  def format(q)
7593
7994
  q.group do
7594
- q.group do
7595
- q.format(target)
7596
- q.text(",") if target.is_a?(MLHS) && target.comma
7597
- end
7598
-
7995
+ q.group { q.format(target) }
7599
7996
  q.text(" =")
7600
7997
  q.indent do
7601
7998
  q.breakable
@@ -7642,86 +8039,26 @@ class SyntaxTree < Ripper
7642
8039
  )
7643
8040
  end
7644
8041
 
7645
- # MethodAddArg represents a method call with arguments and parentheses.
7646
- #
7647
- # method(argument)
7648
- #
7649
- # MethodAddArg can also represent with a method on an object, as in:
7650
- #
7651
- # object.method(argument)
7652
- #
7653
- # Finally, MethodAddArg can represent calling a method with no receiver that
7654
- # ends in a ?. In this case, the parser knows it's a method call and not a
7655
- # local variable, so it uses a MethodAddArg node as opposed to a VCall node,
7656
- # as in:
7657
- #
7658
- # method?
7659
- #
7660
- class MethodAddArg
7661
- # [Call | FCall] the method call
7662
- attr_reader :call
7663
-
7664
- # [ArgParen | Args] the arguments to the method call
7665
- attr_reader :arguments
7666
-
7667
- # [Location] the location of this node
7668
- attr_reader :location
7669
-
7670
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
7671
- attr_reader :comments
7672
-
7673
- def initialize(call:, arguments:, location:, comments: [])
7674
- @call = call
7675
- @arguments = arguments
7676
- @location = location
7677
- @comments = comments
7678
- end
7679
-
7680
- def child_nodes
7681
- [call, arguments]
7682
- end
7683
-
7684
- def format(q)
7685
- q.format(call)
7686
- q.text(" ") if !arguments.is_a?(ArgParen) && arguments.parts.any?
7687
- q.format(arguments)
7688
- end
7689
-
7690
- def pretty_print(q)
7691
- q.group(2, "(", ")") do
7692
- q.text("method_add_arg")
7693
-
7694
- q.breakable
7695
- q.pp(call)
7696
-
7697
- q.breakable
7698
- q.pp(arguments)
7699
-
7700
- q.pp(Comment::List.new(comments))
7701
- end
7702
- end
7703
-
7704
- def to_json(*opts)
7705
- {
7706
- type: :method_add_arg,
7707
- call: call,
7708
- args: arguments,
7709
- loc: location,
7710
- cmts: comments
7711
- }.to_json(*opts)
7712
- end
7713
- end
7714
-
7715
8042
  # :call-seq:
7716
8043
  # on_method_add_arg: (
7717
8044
  # (Call | FCall) call,
7718
8045
  # (ArgParen | Args) arguments
7719
- # ) -> MethodAddArg
8046
+ # ) -> Call | FCall
7720
8047
  def on_method_add_arg(call, arguments)
7721
8048
  location = call.location
7722
- location = location.to(arguments.location) unless arguments.is_a?(Args)
8049
+ location = location.to(arguments.location) if arguments.is_a?(ArgParen)
7723
8050
 
7724
- MethodAddArg.new(call: call, arguments: arguments, location: location)
8051
+ if call.is_a?(FCall)
8052
+ FCall.new(value: call.value, arguments: arguments, location: location)
8053
+ else
8054
+ Call.new(
8055
+ receiver: call.receiver,
8056
+ operator: call.operator,
8057
+ message: call.message,
8058
+ arguments: arguments,
8059
+ location: location
8060
+ )
8061
+ end
7725
8062
  end
7726
8063
 
7727
8064
  # MethodAddBlock represents a method call with a block argument.
@@ -7729,7 +8066,7 @@ class SyntaxTree < Ripper
7729
8066
  # method {}
7730
8067
  #
7731
8068
  class MethodAddBlock
7732
- # [Call | Command | CommandCall | FCall | MethodAddArg] the method call
8069
+ # [Call | Command | CommandCall | FCall] the method call
7733
8070
  attr_reader :call
7734
8071
 
7735
8072
  # [BraceBlock | DoBlock] the block being sent with the method call
@@ -7784,7 +8121,7 @@ class SyntaxTree < Ripper
7784
8121
 
7785
8122
  # :call-seq:
7786
8123
  # on_method_add_block: (
7787
- # (Call | Command | CommandCall | FCall | MethodAddArg) call,
8124
+ # (Call | Command | CommandCall | FCall) call,
7788
8125
  # (BraceBlock | DoBlock) block
7789
8126
  # ) -> MethodAddBlock
7790
8127
  def on_method_add_block(call, block)
@@ -7809,7 +8146,6 @@ class SyntaxTree < Ripper
7809
8146
  # list, which impacts destructuring. It's an attr_accessor so that while
7810
8147
  # the syntax tree is being built it can be set by its parent node
7811
8148
  attr_accessor :comma
7812
- alias comma? comma
7813
8149
 
7814
8150
  # [Location] the location of this node
7815
8151
  attr_reader :location
@@ -7830,6 +8166,7 @@ class SyntaxTree < Ripper
7830
8166
 
7831
8167
  def format(q)
7832
8168
  q.seplist(parts) { |part| q.format(part) }
8169
+ q.text(",") if comma
7833
8170
  end
7834
8171
 
7835
8172
  def pretty_print(q)
@@ -7932,7 +8269,6 @@ class SyntaxTree < Ripper
7932
8269
  q.indent do
7933
8270
  q.breakable("")
7934
8271
  q.format(contents)
7935
- q.text(",") if contents.is_a?(MLHS) && contents.comma?
7936
8272
  end
7937
8273
 
7938
8274
  q.breakable("")
@@ -8400,6 +8736,61 @@ class SyntaxTree < Ripper
8400
8736
  )
8401
8737
  end
8402
8738
 
8739
+ # If you have a modifier statement (for instance a modifier if statement or a
8740
+ # modifier while loop) there are times when you need to wrap the entire
8741
+ # statement in parentheses. This occurs when you have something like:
8742
+ #
8743
+ # foo[:foo] =
8744
+ # if bar?
8745
+ # baz
8746
+ # end
8747
+ #
8748
+ # Normally we would shorten this to an inline version, which would result in:
8749
+ #
8750
+ # foo[:foo] = baz if bar?
8751
+ #
8752
+ # but this actually has different semantic meaning. The first example will
8753
+ # result in a nil being inserted into the hash for the :foo key, whereas the
8754
+ # second example will result in an empty hash because the if statement applies
8755
+ # to the entire assignment.
8756
+ #
8757
+ # We can fix this in a couple of ways. We can use the then keyword, as in:
8758
+ #
8759
+ # foo[:foo] = if bar? then baz end
8760
+ #
8761
+ # But this isn't used very often. We can also just leave it as is with the
8762
+ # multi-line version, but for a short predicate and short value it looks
8763
+ # verbose. The last option and the one used here is to add parentheses on
8764
+ # both sides of the expression, as in:
8765
+ #
8766
+ # foo[:foo] = (baz if bar?)
8767
+ #
8768
+ # This approach maintains the nice conciseness of the inline version, while
8769
+ # keeping the correct semantic meaning.
8770
+ module Parentheses
8771
+ NODES = [Args, Assign, Assoc, Binary, Call, Defined, MAssign, OpAssign]
8772
+
8773
+ def self.flat(q)
8774
+ return yield unless NODES.include?(q.parent.class)
8775
+
8776
+ q.text("(")
8777
+ yield
8778
+ q.text(")")
8779
+ end
8780
+
8781
+ def self.break(q)
8782
+ return yield unless NODES.include?(q.parent.class)
8783
+
8784
+ q.text("(")
8785
+ q.indent do
8786
+ q.breakable("")
8787
+ yield
8788
+ end
8789
+ q.breakable("")
8790
+ q.text(")")
8791
+ end
8792
+ end
8793
+
8403
8794
  # def on_operator_ambiguous(value)
8404
8795
  # value
8405
8796
  # end
@@ -8538,9 +8929,7 @@ class SyntaxTree < Ripper
8538
8929
  # it's missing.
8539
8930
  def empty?
8540
8931
  requireds.empty? && optionals.empty? && !rest && posts.empty? &&
8541
- keywords.empty? &&
8542
- !keyword_rest &&
8543
- !block
8932
+ keywords.empty? && !keyword_rest && !block
8544
8933
  end
8545
8934
 
8546
8935
  def child_nodes
@@ -8571,10 +8960,22 @@ class SyntaxTree < Ripper
8571
8960
  parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest
8572
8961
  parts << block if block
8573
8962
 
8574
- q.nest(0) do
8963
+ contents = -> do
8575
8964
  q.seplist(parts) { |part| q.format(part) }
8576
8965
  q.format(rest) if rest && rest.is_a?(ExcessedComma)
8577
8966
  end
8967
+
8968
+ if [Def, Defs].include?(q.parent.class)
8969
+ q.group(0, "(", ")") do
8970
+ q.indent do
8971
+ q.breakable("")
8972
+ contents.call
8973
+ end
8974
+ q.breakable("")
8975
+ end
8976
+ else
8977
+ q.nest(0, &contents)
8978
+ end
8578
8979
  end
8579
8980
 
8580
8981
  def pretty_print(q)
@@ -8716,7 +9117,7 @@ class SyntaxTree < Ripper
8716
9117
  # [LParen] the left parenthesis that opened this statement
8717
9118
  attr_reader :lparen
8718
9119
 
8719
- # [untyped] the expression inside the parentheses
9120
+ # [nil | untyped] the expression inside the parentheses
8720
9121
  attr_reader :contents
8721
9122
 
8722
9123
  # [Location] the location of this node
@@ -8740,7 +9141,7 @@ class SyntaxTree < Ripper
8740
9141
  q.group do
8741
9142
  q.format(lparen)
8742
9143
 
8743
- if !contents.is_a?(Params) || !contents.empty?
9144
+ if contents && (!contents.is_a?(Params) || !contents.empty?)
8744
9145
  q.indent do
8745
9146
  q.breakable("")
8746
9147
  q.format(contents)
@@ -8805,7 +9206,7 @@ class SyntaxTree < Ripper
8805
9206
 
8806
9207
  Paren.new(
8807
9208
  lparen: lparen,
8808
- contents: contents,
9209
+ contents: contents || nil,
8809
9210
  location: lparen.location.to(rparen.location)
8810
9211
  )
8811
9212
  end
@@ -8896,7 +9297,11 @@ class SyntaxTree < Ripper
8896
9297
 
8897
9298
  def format(q)
8898
9299
  q.format(statements)
8899
- q.breakable(force: true)
9300
+
9301
+ # We're going to put a newline on the end so that it always has one unless
9302
+ # it ends with the special __END__ syntax. In that case we want to
9303
+ # replicate the text exactly so we will just let it be.
9304
+ q.breakable(force: true) unless statements.body.last.is_a?(EndContent)
8900
9305
  end
8901
9306
 
8902
9307
  def pretty_print(q)
@@ -9035,6 +9440,9 @@ class SyntaxTree < Ripper
9035
9440
  # %i[one two three]
9036
9441
  #
9037
9442
  class QSymbols
9443
+ # [QSymbolsBeg] the token that opens this array literal
9444
+ attr_reader :beginning
9445
+
9038
9446
  # [Array[ TStringContent ]] the elements of the array
9039
9447
  attr_reader :elements
9040
9448
 
@@ -9044,7 +9452,8 @@ class SyntaxTree < Ripper
9044
9452
  # [Array[ Comment | EmbDoc ]] the comments attached to this node
9045
9453
  attr_reader :comments
9046
9454
 
9047
- def initialize(elements:, location:, comments: [])
9455
+ def initialize(beginning:, elements:, location:, comments: [])
9456
+ @beginning = beginning
9048
9457
  @elements = elements
9049
9458
  @location = location
9050
9459
  @comments = comments
@@ -9055,7 +9464,14 @@ class SyntaxTree < Ripper
9055
9464
  end
9056
9465
 
9057
9466
  def format(q)
9058
- q.group(0, "%i[", "]") do
9467
+ opening, closing = "%i[", "]"
9468
+
9469
+ if elements.any? { |element| element.match?(/[\[\]]/) }
9470
+ opening = beginning.value
9471
+ closing = Quotes.matching(opening[2])
9472
+ end
9473
+
9474
+ q.group(0, opening, closing) do
9059
9475
  q.indent do
9060
9476
  q.breakable("")
9061
9477
  q.seplist(elements, -> { q.breakable }) do |element|
@@ -9091,6 +9507,7 @@ class SyntaxTree < Ripper
9091
9507
  # on_qsymbols_add: (QSymbols qsymbols, TStringContent element) -> QSymbols
9092
9508
  def on_qsymbols_add(qsymbols, element)
9093
9509
  QSymbols.new(
9510
+ beginning: qsymbols.beginning,
9094
9511
  elements: qsymbols.elements << element,
9095
9512
  location: qsymbols.location.to(element.location)
9096
9513
  )
@@ -9132,9 +9549,13 @@ class SyntaxTree < Ripper
9132
9549
  # :call-seq:
9133
9550
  # on_qsymbols_new: () -> QSymbols
9134
9551
  def on_qsymbols_new
9135
- qsymbols_beg = find_token(QSymbolsBeg)
9552
+ beginning = find_token(QSymbolsBeg)
9136
9553
 
9137
- QSymbols.new(elements: [], location: qsymbols_beg.location)
9554
+ QSymbols.new(
9555
+ beginning: beginning,
9556
+ elements: [],
9557
+ location: beginning.location
9558
+ )
9138
9559
  end
9139
9560
 
9140
9561
  # QWords represents a string literal array without interpolation.
@@ -9142,6 +9563,9 @@ class SyntaxTree < Ripper
9142
9563
  # %w[one two three]
9143
9564
  #
9144
9565
  class QWords
9566
+ # [QWordsBeg] the token that opens this array literal
9567
+ attr_reader :beginning
9568
+
9145
9569
  # [Array[ TStringContent ]] the elements of the array
9146
9570
  attr_reader :elements
9147
9571
 
@@ -9151,7 +9575,8 @@ class SyntaxTree < Ripper
9151
9575
  # [Array[ Comment | EmbDoc ]] the comments attached to this node
9152
9576
  attr_reader :comments
9153
9577
 
9154
- def initialize(elements:, location:, comments: [])
9578
+ def initialize(beginning:, elements:, location:, comments: [])
9579
+ @beginning = beginning
9155
9580
  @elements = elements
9156
9581
  @location = location
9157
9582
  @comments = comments
@@ -9162,7 +9587,14 @@ class SyntaxTree < Ripper
9162
9587
  end
9163
9588
 
9164
9589
  def format(q)
9165
- q.group(0, "%w[", "]") do
9590
+ opening, closing = "%w[", "]"
9591
+
9592
+ if elements.any? { |element| element.match?(/[\[\]]/) }
9593
+ opening = beginning.value
9594
+ closing = Quotes.matching(opening[2])
9595
+ end
9596
+
9597
+ q.group(0, opening, closing) do
9166
9598
  q.indent do
9167
9599
  q.breakable("")
9168
9600
  q.seplist(elements, -> { q.breakable }) do |element|
@@ -9195,6 +9627,7 @@ class SyntaxTree < Ripper
9195
9627
  # on_qwords_add: (QWords qwords, TStringContent element) -> QWords
9196
9628
  def on_qwords_add(qwords, element)
9197
9629
  QWords.new(
9630
+ beginning: qwords.beginning,
9198
9631
  elements: qwords.elements << element,
9199
9632
  location: qwords.location.to(element.location)
9200
9633
  )
@@ -9236,9 +9669,9 @@ class SyntaxTree < Ripper
9236
9669
  # :call-seq:
9237
9670
  # on_qwords_new: () -> QWords
9238
9671
  def on_qwords_new
9239
- qwords_beg = find_token(QWordsBeg)
9672
+ beginning = find_token(QWordsBeg)
9240
9673
 
9241
- QWords.new(elements: [], location: qwords_beg.location)
9674
+ QWords.new(beginning: beginning, elements: [], location: beginning.location)
9242
9675
  end
9243
9676
 
9244
9677
  # RationalLiteral represents the use of a rational number literal.
@@ -9550,11 +9983,32 @@ class SyntaxTree < Ripper
9550
9983
  q.format_each(parts)
9551
9984
  q.text(ending)
9552
9985
  end
9986
+ elsif braces
9987
+ q.group do
9988
+ q.text("%r{")
9989
+
9990
+ if beginning == "/"
9991
+ # If we're changing from a forward slash to a %r{, then we can
9992
+ # replace any escaped forward slashes with regular forward slashes.
9993
+ parts.each do |part|
9994
+ if part.is_a?(TStringContent)
9995
+ q.text(part.value.gsub("\\/", "/"))
9996
+ else
9997
+ q.format(part)
9998
+ end
9999
+ end
10000
+ else
10001
+ q.format_each(parts)
10002
+ end
10003
+
10004
+ q.text("}")
10005
+ q.text(ending[1..-1])
10006
+ end
9553
10007
  else
9554
10008
  q.group do
9555
- q.text(braces ? "%r{" : "/")
10009
+ q.text("/")
9556
10010
  q.format_each(parts)
9557
- q.text(braces ? "}" : "/")
10011
+ q.text("/")
9558
10012
  q.text(ending[1..-1])
9559
10013
  end
9560
10014
  end
@@ -10285,21 +10739,6 @@ class SyntaxTree < Ripper
10285
10739
  # value
10286
10740
  # end
10287
10741
 
10288
- # stmts_add is a parser event that represents a single statement inside a
10289
- # list of statements within any lexical block. It accepts as arguments the
10290
- # parent stmts node as well as an stmt which can be any expression in
10291
- # Ruby.
10292
- def on_stmts_add(statements, statement)
10293
- location =
10294
- if statements.body.empty?
10295
- statement.location
10296
- else
10297
- statements.location.to(statement.location)
10298
- end
10299
-
10300
- Statements.new(self, body: statements.body << statement, location: location)
10301
- end
10302
-
10303
10742
  # Everything that has a block of code inside of it has a list of statements.
10304
10743
  # Normally we would just track those as a node that has an array body, but we
10305
10744
  # have some special handling in order to handle empty statement lists. They
@@ -10379,12 +10818,22 @@ class SyntaxTree < Ripper
10379
10818
  # the only value is a comment. In that case a lot of nodes like
10380
10819
  # brace_block will attempt to format as a single line, but since that
10381
10820
  # wouldn't work with a comment, we intentionally break the parent group.
10382
- if body.length == 2 && body.first.is_a?(VoidStmt)
10383
- q.format(body.last)
10384
- q.break_parent
10385
- return
10821
+ if body.length == 2
10822
+ void_stmt, comment = body
10823
+
10824
+ if void_stmt.is_a?(VoidStmt) && comment.is_a?(Comment)
10825
+ q.format(comment)
10826
+ q.break_parent
10827
+ return
10828
+ end
10386
10829
  end
10387
10830
 
10831
+ access_controls =
10832
+ Hash.new do |hash, node|
10833
+ hash[node] = node.is_a?(VCall) &&
10834
+ %w[private protected public].include?(node.value.value)
10835
+ end
10836
+
10388
10837
  body.each_with_index do |statement, index|
10389
10838
  next if statement.is_a?(VoidStmt)
10390
10839
 
@@ -10394,7 +10843,7 @@ class SyntaxTree < Ripper
10394
10843
  q.breakable(force: true)
10395
10844
  q.breakable(force: true)
10396
10845
  q.format(statement)
10397
- elsif statement.is_a?(AccessCtrl) || body[index - 1].is_a?(AccessCtrl)
10846
+ elsif access_controls[statement] || access_controls[body[index - 1]]
10398
10847
  q.breakable(force: true)
10399
10848
  q.breakable(force: true)
10400
10849
  q.format(statement)
@@ -10446,7 +10895,7 @@ class SyntaxTree < Ripper
10446
10895
  location = comment.location
10447
10896
 
10448
10897
  if !comment.inline? && (start_char <= location.start_char) &&
10449
- (end_char >= location.end_char)
10898
+ (end_char >= location.end_char) && !comment.ignore?
10450
10899
  parser_comments.delete_at(comment_index)
10451
10900
 
10452
10901
  while (node = body[body_index]) &&
@@ -10465,6 +10914,21 @@ class SyntaxTree < Ripper
10465
10914
  end
10466
10915
  end
10467
10916
 
10917
+ # stmts_add is a parser event that represents a single statement inside a
10918
+ # list of statements within any lexical block. It accepts as arguments the
10919
+ # parent stmts node as well as an stmt which can be any expression in
10920
+ # Ruby.
10921
+ def on_stmts_add(statements, statement)
10922
+ location =
10923
+ if statements.body.empty?
10924
+ statement.location
10925
+ else
10926
+ statements.location.to(statement.location)
10927
+ end
10928
+
10929
+ Statements.new(self, body: statements.body << statement, location: location)
10930
+ end
10931
+
10468
10932
  # :call-seq:
10469
10933
  # on_stmts_new: () -> Statements
10470
10934
  def on_stmts_new
@@ -10736,10 +11200,16 @@ class SyntaxTree < Ripper
10736
11200
  embexpr_end.location.start_char
10737
11201
  )
10738
11202
 
10739
- StringEmbExpr.new(
10740
- statements: statements,
10741
- location: embexpr_beg.location.to(embexpr_end.location)
10742
- )
11203
+ location =
11204
+ Location.new(
11205
+ start_line: embexpr_beg.location.start_line,
11206
+ start_char: embexpr_beg.location.start_char,
11207
+ end_line:
11208
+ [embexpr_end.location.end_line, statements.location.end_line].max,
11209
+ end_char: embexpr_end.location.end_char
11210
+ )
11211
+
11212
+ StringEmbExpr.new(statements: statements, location: location)
10743
11213
  end
10744
11214
 
10745
11215
  # StringLiteral represents a string literal.
@@ -10839,12 +11309,21 @@ class SyntaxTree < Ripper
10839
11309
  )
10840
11310
  else
10841
11311
  tstring_beg = find_token(TStringBeg)
10842
- tstring_end = find_token(TStringEnd)
11312
+ tstring_end = find_token(TStringEnd, location: tstring_beg.location)
11313
+
11314
+ location =
11315
+ Location.new(
11316
+ start_line: tstring_beg.location.start_line,
11317
+ start_char: tstring_beg.location.start_char,
11318
+ end_line:
11319
+ [tstring_end.location.end_line, string.location.end_line].max,
11320
+ end_char: tstring_end.location.end_char
11321
+ )
10843
11322
 
10844
11323
  StringLiteral.new(
10845
11324
  parts: string.parts,
10846
11325
  quote: tstring_beg.value,
10847
- location: tstring_beg.location.to(tstring_end.location)
11326
+ location: location
10848
11327
  )
10849
11328
  end
10850
11329
  end
@@ -11066,6 +11545,9 @@ class SyntaxTree < Ripper
11066
11545
  # %I[one two three]
11067
11546
  #
11068
11547
  class Symbols
11548
+ # [SymbolsBeg] the token that opens this array literal
11549
+ attr_reader :beginning
11550
+
11069
11551
  # [Array[ Word ]] the words in the symbol array literal
11070
11552
  attr_reader :elements
11071
11553
 
@@ -11075,7 +11557,8 @@ class SyntaxTree < Ripper
11075
11557
  # [Array[ Comment | EmbDoc ]] the comments attached to this node
11076
11558
  attr_reader :comments
11077
11559
 
11078
- def initialize(elements:, location:, comments: [])
11560
+ def initialize(beginning:, elements:, location:, comments: [])
11561
+ @beginning = beginning
11079
11562
  @elements = elements
11080
11563
  @location = location
11081
11564
  @comments = comments
@@ -11086,7 +11569,14 @@ class SyntaxTree < Ripper
11086
11569
  end
11087
11570
 
11088
11571
  def format(q)
11089
- q.group(0, "%I[", "]") do
11572
+ opening, closing = "%I[", "]"
11573
+
11574
+ if elements.any? { |element| element.match?(/[\[\]]/) }
11575
+ opening = beginning.value
11576
+ closing = Quotes.matching(opening[2])
11577
+ end
11578
+
11579
+ q.group(0, opening, closing) do
11090
11580
  q.indent do
11091
11581
  q.breakable("")
11092
11582
  q.seplist(elements, -> { q.breakable }) do |element|
@@ -11122,6 +11612,7 @@ class SyntaxTree < Ripper
11122
11612
  # on_symbols_add: (Symbols symbols, Word word) -> Symbols
11123
11613
  def on_symbols_add(symbols, word)
11124
11614
  Symbols.new(
11615
+ beginning: symbols.beginning,
11125
11616
  elements: symbols.elements << word,
11126
11617
  location: symbols.location.to(word.location)
11127
11618
  )
@@ -11164,9 +11655,13 @@ class SyntaxTree < Ripper
11164
11655
  # :call-seq:
11165
11656
  # on_symbols_new: () -> Symbols
11166
11657
  def on_symbols_new
11167
- symbols_beg = find_token(SymbolsBeg)
11658
+ beginning = find_token(SymbolsBeg)
11168
11659
 
11169
- Symbols.new(elements: [], location: symbols_beg.location)
11660
+ Symbols.new(
11661
+ beginning: beginning,
11662
+ elements: [],
11663
+ location: beginning.location
11664
+ )
11170
11665
  end
11171
11666
 
11172
11667
  # TLambda represents the beginning of a lambda literal.
@@ -11258,6 +11753,11 @@ class SyntaxTree < Ripper
11258
11753
  [constant]
11259
11754
  end
11260
11755
 
11756
+ def format(q)
11757
+ q.text("::")
11758
+ q.format(constant)
11759
+ end
11760
+
11261
11761
  def pretty_print(q)
11262
11762
  q.group(2, "(", ")") do
11263
11763
  q.text("top_const_field")
@@ -11412,6 +11912,10 @@ class SyntaxTree < Ripper
11412
11912
  @comments = comments
11413
11913
  end
11414
11914
 
11915
+ def match?(pattern)
11916
+ value.match?(pattern)
11917
+ end
11918
+
11415
11919
  def child_nodes
11416
11920
  []
11417
11921
  end
@@ -11914,23 +12418,39 @@ class SyntaxTree < Ripper
11914
12418
  end
11915
12419
 
11916
12420
  def format(q)
12421
+ # If the predicate of the loop contains an assignment (in which case we
12422
+ # can't know for certain that that assignment doesn't impact the
12423
+ # statements inside the loop) then we can't use the modifier form and we
12424
+ # must use the block form.
12425
+ if [Assign, MAssign, OpAssign].include?(node.predicate.class)
12426
+ format_break(q)
12427
+ q.break_parent
12428
+ return
12429
+ end
12430
+
11917
12431
  q.group do
11918
- q.if_break do
11919
- q.text("#{keyword} ")
11920
- q.nest(keyword.length + 1) { q.format(node.predicate) }
11921
- q.indent do
11922
- q.breakable("")
12432
+ q.if_break { format_break(q) }.if_flat do
12433
+ Parentheses.flat(q) do
11923
12434
  q.format(statements)
12435
+ q.text(" #{keyword} ")
12436
+ q.format(node.predicate)
11924
12437
  end
11925
- q.breakable("")
11926
- q.text("end")
11927
- end.if_flat do
11928
- q.format(statements)
11929
- q.text(" #{keyword} ")
11930
- q.format(node.predicate)
11931
12438
  end
11932
12439
  end
11933
12440
  end
12441
+
12442
+ private
12443
+
12444
+ def format_break(q)
12445
+ q.text("#{keyword} ")
12446
+ q.nest(keyword.length + 1) { q.format(node.predicate) }
12447
+ q.indent do
12448
+ q.breakable("")
12449
+ q.format(statements)
12450
+ end
12451
+ q.breakable("")
12452
+ q.text("end")
12453
+ end
11934
12454
  end
11935
12455
 
11936
12456
  # Until represents an +until+ loop.
@@ -12055,7 +12575,22 @@ class SyntaxTree < Ripper
12055
12575
  end
12056
12576
 
12057
12577
  def format(q)
12058
- LoopFormatter.new("until", self, statement).format(q)
12578
+ # If we're in the modifier form and we're modifying a `begin`, then this
12579
+ # is a special case where we need to explicitly use the modifier form
12580
+ # because otherwise the semantic meaning changes. This looks like:
12581
+ #
12582
+ # begin
12583
+ # foo
12584
+ # end until bar
12585
+ #
12586
+ # The above is effectively a `do...until` loop.
12587
+ if statement.is_a?(Begin)
12588
+ q.format(statement)
12589
+ q.text(" until ")
12590
+ q.format(predicate)
12591
+ else
12592
+ LoopFormatter.new("until", self, statement).format(q)
12593
+ end
12059
12594
  end
12060
12595
 
12061
12596
  def pretty_print(q)
@@ -12291,56 +12826,6 @@ class SyntaxTree < Ripper
12291
12826
  VarRef.new(value: value, location: value.location)
12292
12827
  end
12293
12828
 
12294
- # AccessCtrl represents a call to a method visibility control, i.e., +public+,
12295
- # +protected+, or +private+.
12296
- #
12297
- # private
12298
- #
12299
- class AccessCtrl
12300
- # [Ident] the value of this expression
12301
- attr_reader :value
12302
-
12303
- # [Location] the location of this node
12304
- attr_reader :location
12305
-
12306
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
12307
- attr_reader :comments
12308
-
12309
- def initialize(value:, location:, comments: [])
12310
- @value = value
12311
- @location = location
12312
- @comments = comments
12313
- end
12314
-
12315
- def child_nodes
12316
- [value]
12317
- end
12318
-
12319
- def format(q)
12320
- q.format(value)
12321
- end
12322
-
12323
- def pretty_print(q)
12324
- q.group(2, "(", ")") do
12325
- q.text("access_ctrl")
12326
-
12327
- q.breakable
12328
- q.pp(value)
12329
-
12330
- q.pp(Comment::List.new(comments))
12331
- end
12332
- end
12333
-
12334
- def to_json(*opts)
12335
- {
12336
- type: :access_ctrl,
12337
- value: value,
12338
- loc: location,
12339
- cmts: comments
12340
- }.to_json(*opts)
12341
- end
12342
- end
12343
-
12344
12829
  # VCall represent any plain named object with Ruby that could be either a
12345
12830
  # local variable or a method call.
12346
12831
  #
@@ -12389,19 +12874,9 @@ class SyntaxTree < Ripper
12389
12874
  end
12390
12875
 
12391
12876
  # :call-seq:
12392
- # on_vcall: (Ident ident) -> AccessCtrl | VCall
12877
+ # on_vcall: (Ident ident) -> VCall
12393
12878
  def on_vcall(ident)
12394
- @controls ||= %w[private protected public].freeze
12395
-
12396
- if @controls.include?(ident.value) && ident.value == lines[lineno - 1].strip
12397
- # Access controls like private, protected, and public are reported as
12398
- # vcall nodes since they're technically method calls. We want to be able
12399
- # add new lines around them as necessary, so here we're going to
12400
- # explicitly track those as a different node type.
12401
- AccessCtrl.new(value: ident, location: ident.location)
12402
- else
12403
- VCall.new(value: ident, location: ident.location)
12404
- end
12879
+ VCall.new(value: ident, location: ident.location)
12405
12880
  end
12406
12881
 
12407
12882
  # VoidStmt represents an empty lexical block of code.
@@ -12552,7 +13027,11 @@ class SyntaxTree < Ripper
12552
13027
  beginning = find_token(Kw, "when")
12553
13028
  ending = consequent || find_token(Kw, "end")
12554
13029
 
12555
- statements.bind(arguments.location.end_char, ending.location.start_char)
13030
+ statements_start = find_token(Kw, "then", consume: false) || arguments
13031
+ statements.bind(
13032
+ find_next_statement_start(statements_start.location.end_char),
13033
+ ending.location.start_char
13034
+ )
12556
13035
 
12557
13036
  When.new(
12558
13037
  arguments: arguments,
@@ -12684,7 +13163,22 @@ class SyntaxTree < Ripper
12684
13163
  end
12685
13164
 
12686
13165
  def format(q)
12687
- LoopFormatter.new("while", self, statement).format(q)
13166
+ # If we're in the modifier form and we're modifying a `begin`, then this
13167
+ # is a special case where we need to explicitly use the modifier form
13168
+ # because otherwise the semantic meaning changes. This looks like:
13169
+ #
13170
+ # begin
13171
+ # foo
13172
+ # end while bar
13173
+ #
13174
+ # The above is effectively a `do...while` loop.
13175
+ if statement.is_a?(Begin)
13176
+ q.format(statement)
13177
+ q.text(" while ")
13178
+ q.format(predicate)
13179
+ else
13180
+ LoopFormatter.new("while", self, statement).format(q)
13181
+ end
12688
13182
  end
12689
13183
 
12690
13184
  def pretty_print(q)
@@ -12748,6 +13242,10 @@ class SyntaxTree < Ripper
12748
13242
  @comments = comments
12749
13243
  end
12750
13244
 
13245
+ def match?(pattern)
13246
+ parts.any? { |part| part.is_a?(TStringContent) && part.match?(pattern) }
13247
+ end
13248
+
12751
13249
  def child_nodes
12752
13250
  parts
12753
13251
  end
@@ -12797,6 +13295,9 @@ class SyntaxTree < Ripper
12797
13295
  # %W[one two three]
12798
13296
  #
12799
13297
  class Words
13298
+ # [WordsBeg] the token that opens this array literal
13299
+ attr_reader :beginning
13300
+
12800
13301
  # [Array[ Word ]] the elements of this array
12801
13302
  attr_reader :elements
12802
13303
 
@@ -12806,7 +13307,8 @@ class SyntaxTree < Ripper
12806
13307
  # [Array[ Comment | EmbDoc ]] the comments attached to this node
12807
13308
  attr_reader :comments
12808
13309
 
12809
- def initialize(elements:, location:, comments: [])
13310
+ def initialize(beginning:, elements:, location:, comments: [])
13311
+ @beginning = beginning
12810
13312
  @elements = elements
12811
13313
  @location = location
12812
13314
  @comments = comments
@@ -12817,7 +13319,14 @@ class SyntaxTree < Ripper
12817
13319
  end
12818
13320
 
12819
13321
  def format(q)
12820
- q.group(0, "%W[", "]") do
13322
+ opening, closing = "%W[", "]"
13323
+
13324
+ if elements.any? { |element| element.match?(/[\[\]]/) }
13325
+ opening = beginning.value
13326
+ closing = Quotes.matching(opening[2])
13327
+ end
13328
+
13329
+ q.group(0, opening, closing) do
12821
13330
  q.indent do
12822
13331
  q.breakable("")
12823
13332
  q.seplist(elements, -> { q.breakable }) do |element|
@@ -12850,6 +13359,7 @@ class SyntaxTree < Ripper
12850
13359
  # on_words_add: (Words words, Word word) -> Words
12851
13360
  def on_words_add(words, word)
12852
13361
  Words.new(
13362
+ beginning: words.beginning,
12853
13363
  elements: words.elements << word,
12854
13364
  location: words.location.to(word.location)
12855
13365
  )
@@ -12892,9 +13402,9 @@ class SyntaxTree < Ripper
12892
13402
  # :call-seq:
12893
13403
  # on_words_new: () -> Words
12894
13404
  def on_words_new
12895
- words_beg = find_token(WordsBeg)
13405
+ beginning = find_token(WordsBeg)
12896
13406
 
12897
- Words.new(elements: [], location: words_beg.location)
13407
+ Words.new(beginning: beginning, elements: [], location: beginning.location)
12898
13408
  end
12899
13409
 
12900
13410
  # def on_words_sep(value)
@@ -13011,7 +13521,7 @@ class SyntaxTree < Ripper
13011
13521
  location: heredoc.location
13012
13522
  )
13013
13523
  else
13014
- ending = find_token(TStringEnd)
13524
+ ending = find_token(TStringEnd, location: xstring.location)
13015
13525
 
13016
13526
  XStringLiteral.new(
13017
13527
  parts: xstring.parts,
@@ -13047,8 +13557,18 @@ class SyntaxTree < Ripper
13047
13557
  def format(q)
13048
13558
  q.group do
13049
13559
  q.text("yield")
13050
- q.text(" ") if arguments.is_a?(Args)
13051
- q.format(arguments)
13560
+
13561
+ if arguments.is_a?(Paren)
13562
+ q.format(arguments)
13563
+ else
13564
+ q.if_break { q.text("(") }.if_flat { q.text(" ") }
13565
+ q.indent do
13566
+ q.breakable("")
13567
+ q.format(arguments)
13568
+ end
13569
+ q.breakable("")
13570
+ q.if_break { q.text(")") }
13571
+ end
13052
13572
  end
13053
13573
  end
13054
13574