Class: Rufo::Formatter

Inherits:
Object
  • Object
show all
Includes:
Settings
Defined in:
lib/rufo/formatter.rb

Constant Summary collapse

INDENT_SIZE =
2
EMPTY_STRING =
[:string_literal, [:string_content]]
EMPTY_HASH =
[:hash, nil]

Constants included from Settings

Settings::OPTIONS

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Settings

#init_settings

Constructor Details

#initialize(code, **options) ⇒ Formatter

Returns a new instance of Formatter.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/rufo/formatter.rb', line 16

def initialize(code, **options)
  @code = code

  @tokens = Rufo::Parser.lex(code).reverse!
  @sexp = Rufo::Parser.sexp(code)
  @sexp ||= Rufo::Parser.sexp_unparsable_code(code)

  # sexp being nil means that the code is not valid.
  # Parse the code so we get better error messages.
  if @sexp.nil?
    Rufo::Parser.parse(code)
    raise Rufo::UnknownSyntaxError # Sometimes parsing does not raise an error
  end

  @indent = 0
  @line = 0
  @column = 0
  @last_was_newline = true
  @output = +""

  # The column of a `obj.method` call, so we can align
  # calls to that dot
  @dot_column = nil

  # Same as above, but the column of the original dot, not
  # the one we finally wrote
  @original_dot_column = nil

  # Did this line already set the `@dot_column` variable?
  @line_has_dot_column = nil

  # The column of a `obj.method` call, but only the name part,
  # so we can also align arguments accordingly
  @name_dot_column = nil

  # Heredocs list, associated with calls ([heredoc, tilde])
  @heredocs = []

  # Current node, to be able to associate it to heredocs
  @current_node = nil

  # The current heredoc being printed
  @current_heredoc = nil

  # The current hash or call or method that has hash-like parameters
  @current_hash = nil

  @current_type = nil

  # Are we inside a type body?
  @inside_type_body = false

  # Map lines to commands that start at the begining of a line with the following info:
  # - line indent
  # - first param indent
  # - first line ends with '(', '[' or '{'?
  # - line of matching pair of the previous item
  # - last line of that call
  #
  # This is needed to dedent some calls that look like this:
  #
  # foo bar(
  #   2,
  # )
  #
  # Without the dedent it would normally look like this:
  #
  # foo bar(
  #       2,
  #     )
  #
  # Because the formatter aligns this to the first parameter in the call.
  # However, for these cases it's better to not align it like that.
  @line_to_call_info = {}

  # Lists [first_line, last_line, indent] of lines that need an indent because
  # of alignment of literals. For example this:#
  #
  #     foo [
  #           1,
  #         ]
  #
  # is normally formatted to:
  #
  #     foo [
  #       1,
  #     ]
  #
  # However, if it's already formatted like the above we preserve it.
  @literal_indents = []

  # First non-space token in this line
  @first_token_in_line = nil

  # Do we want to compute the above?
  @want_first_token_in_line = false

  # Each line that belongs to a string literal besides the first
  # go here, so we don't break them when indenting/dedenting stuff
  @unmodifiable_string_lines = {}

  # Position of comments that occur at the end of a line
  @comments_positions = []

  # Token for the last comment found
  @last_comment = nil

  # Actual column of the last comment written
  @last_comment_column = nil

  # Associate lines to alignments
  # Associate a line to an index inside @comments_position
  # becuase when aligning something to the left of a comment
  # we need to adjust the relative comment
  @line_to_alignments_positions = Hash.new { |h, k| h[k] = [] }

  # Position of assignments
  @assignments_positions = []

  # Range of assignment (line => end_line)
  #
  # We need this because when we have to format:
  #
  # ```
  # abc = 1
  # a = foo bar: 2
  #         baz: #
  # ```
  #
  # Because we'll insert two spaces after `a`, this will
  # result in a mis-alignment for baz (and possibly other lines
  # below it). So, we remember the line ranges of an assignment,
  # and once we align the first one we fix the other ones.
  @assignments_ranges = {}

  # Case when positions
  @case_when_positions = []

  # Declarations that are written in a single line, like:
  #
  #    def foo; 1; end
  #
  # We want to track these because we allow consecutive inline defs
  # to be together (without an empty line between them)
  #
  # This is [[line, original_line], ...]
  @inline_declarations = []

  # This is used to track how far deep we are in the AST.
  # This is useful as it allows you to check if you are inside an array
  # when dealing with heredocs.
  @node_level = 0

  # This represents the node level of the most recent literal elements list.
  # It is used to track if we are in a list of elements so that commas
  # can be added appropriately for heredocs for example.
  @literal_elements_level = nil

  init_settings(options)
end

Class Method Details

.format(code, **options) ⇒ Object



10
11
12
13
14
# File 'lib/rufo/formatter.rb', line 10

def self.format(code, **options)
  formatter = new(code, **options)
  formatter.format
  formatter.result
end

Instance Method Details

#adjust_other_alignments(scope, line, column, offset) ⇒ Object



4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
# File 'lib/rufo/formatter.rb', line 4136

def adjust_other_alignments(scope, line, column, offset)
  adjustments = @line_to_alignments_positions[line]
  return unless adjustments

  adjustments.each do |key, adjustment_column, target, index|
    next if adjustment_column <= column
    next if scope == key

    target[index][1] += offset if target[index]
  end
end

#block_arg_type(node) ⇒ Object



4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
# File 'lib/rufo/formatter.rb', line 4207

def block_arg_type(node)
  case node
  when nil
    :anonymous
  when false
    :no_arg
  else
    node
  end
end

#bug(msg) ⇒ Object

Raises:



3881
3882
3883
# File 'lib/rufo/formatter.rb', line 3881

def bug(msg)
  raise Rufo::Bug.new("#{msg} at #{current_token}")
end

#capture_outputObject



3783
3784
3785
3786
3787
3788
3789
3790
# File 'lib/rufo/formatter.rb', line 3783

def capture_output
  old_output = @output
  @output = +""
  yield
  result = @output
  @output = old_output
  result
end

#check(kind) ⇒ Object



3875
3876
3877
3878
3879
# File 'lib/rufo/formatter.rb', line 3875

def check(kind)
  if current_token_kind != kind
    bug "Expected token #{kind}, not #{current_token_kind}"
  end
end

#check_heredocs_in_literal_elements(is_last, wrote_comma) ⇒ Object



2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
# File 'lib/rufo/formatter.rb', line 2833

def check_heredocs_in_literal_elements(is_last, wrote_comma)
  if (newline? || comment?) && !@heredocs.empty?
    if is_last && trailing_commas
      write "," unless wrote_comma
      wrote_comma = true
    end

    flush_heredocs
  end
  wrote_comma
end

#comma?Boolean

Returns:

  • (Boolean)


3924
3925
3926
# File 'lib/rufo/formatter.rb', line 3924

def comma?
  current_token_kind == :on_comma
end

#comment?Boolean

Returns:

  • (Boolean)


3916
3917
3918
# File 'lib/rufo/formatter.rb', line 3916

def comment?
  current_token_kind == :on_comment
end

#consume_block_args(args) ⇒ Object



1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
# File 'lib/rufo/formatter.rb', line 1408

def consume_block_args(args)
  if args
    consume_space_or_newline
    # + 1 because of |...|
    #                ^
    indent(@column + 1) do
      visit args
    end
  end
end

#consume_call_dotObject



1025
1026
1027
1028
1029
1030
1031
# File 'lib/rufo/formatter.rb', line 1025

def consume_call_dot
  if current_token_kind == :on_op
    consume_token :on_op
  else
    consume_token :on_period
  end
end

#consume_embedded_commentObject



3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
# File 'lib/rufo/formatter.rb', line 3648

def consume_embedded_comment
  consume_token_value current_token_value
  next_token

  while current_token_kind != :on_embdoc_end
    consume_token_value current_token_value
    next_token
  end

  consume_token_value current_token_value.rstrip
  next_token
end

#consume_endObject



3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
# File 'lib/rufo/formatter.rb', line 3661

def consume_end
  return unless current_token_kind == :on___end__

  line = current_token_line

  write_line unless @output.empty?
  consume_token :on___end__

  lines = @code.lines[line..-1]
  lines.each do |current_line|
    write current_line.chomp
    write_line
  end
end

#consume_end_of_line(at_prefix: false, want_semicolon: false, want_multiline: true, needs_two_lines_on_comment: false, first_space: nil) ⇒ Object

Consume and print an end of line, handling semicolons and comments

  • at_prefix: are we at a point before an expression? (if so, we don’t need a space before the first comment)

  • want_semicolon: do we want do print a semicolon to separate expressions?

  • want_multiline: do we want multiple lines to appear, or at most one?



3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
# File 'lib/rufo/formatter.rb', line 3475

def consume_end_of_line(at_prefix: false, want_semicolon: false, want_multiline: true, needs_two_lines_on_comment: false, first_space: nil)
  found_newline = false               # Did we find any newline during this method?
  found_comment_after_newline = false # Did we find a comment after some newline?
  last = nil                          # Last token kind found
  multilple_lines = false             # Did we pass through more than one newline?
  last_comment_has_newline = false    # Does the last comment has a newline?
  newline_count = 0                   # Number of newlines we passed
  last_space = first_space            # Last found space

  loop do
    case current_token_kind
    when :on_sp
      # Ignore spaces
      last_space = current_token
      next_token
    when :on_nl, :on_ignored_nl
      if last == :newline
        # If we pass through consecutive newlines, don't print them
        # yet, but remember this fact
        multilple_lines = true unless last_comment_has_newline
      else
        # If we just printed a comment that had a newline,
        # we must print two newlines because we remove newlines from comments (rstrip call)
        write_line
        if last == :comment && last_comment_has_newline
          multilple_lines = true
        else
          multilple_lines = false
        end
      end
      found_newline = true
      next_token
      last = :newline
      newline_count += 1
    when :on_semicolon
      next_token
      # If we want to print semicolons and we didn't find a newline yet,
      # print it, but only if it's not followed by a newline
      if !found_newline && want_semicolon && last != :semicolon
        skip_space
        kind = current_token_kind
        unless [:on_ignored_nl, :on_eof].include?(kind)
          return if (kind == :on_kw) &&
                    (%w[class module def].include?(current_token_value))
          write "; "
          last = :semicolon
        end
      end
      multilple_lines = false
    when :on_comment
      if last == :comment
        # Since we remove newlines from comments, we must add the last
        # one if it was a comment
        write_line

        # If the last comment is in the previous line and it was already
        # aligned to this comment, keep it aligned. This is useful for
        # this:
        #
        # ```
        # a = 1 # some comment
        #       # that continues here
        # ```
        #
        # We want to preserve it like that and not change it to:
        #
        # ```
        # a = 1 # some comment
        # # that continues here
        # ```
        if current_comment_aligned_to_previous_one?
          write_indent(@last_comment_column)
          track_comment(match_previous_id: true)
        else
          write_indent
        end
      else
        if found_newline
          if newline_count == 1 && needs_two_lines_on_comment
            if multilple_lines
              write_line
              multilple_lines = false
            else
              multilple_lines = true
            end
            needs_two_lines_on_comment = false
          end

          # Write line or second line if needed
          write_line if last != :newline || multilple_lines
          write_indent
          track_comment(id: @last_was_newline ? true : nil)
        else
          # If we didn't find any newline yet, this is the first comment,
          # so append a space if needed (for example after an expression)
          unless at_prefix
            # Preserve whitespace before comment unless we need to align them
            if last_space
              write last_space[2]
            else
              write_space
            end
          end

          # First we check if the comment was aligned to the previous comment
          # in the previous line, in order to keep them like that.
          if current_comment_aligned_to_previous_one?
            track_comment(match_previous_id: true)
          else
            # We want to distinguish comments that appear at the beginning
            # of a line (which means the line has only a comment) and comments
            # that appear after some expression. We don't want to align these
            # and consider them separate entities. So, we use `@last_was_newline`
            # as an id to distinguish that.
            #
            # For example, this:
            #
            #     # comment 1
            #       # comment 2
            #     call # comment 3
            #
            # Should format to:
            #
            #     # comment 1
            #     # comment 2
            #     call # comment 3
            #
            # Instead of:
            #
            #          # comment 1
            #          # comment 2
            #     call # comment 3
            #
            # We still want to track the first two comments to align to the
            # beginning of the line according to indentation in case they
            # are not already there.
            track_comment(id: @last_was_newline ? true : nil)
          end
        end
      end
      @last_comment = current_token
      @last_comment_column = @column
      last_comment_has_newline = current_token_value.end_with?("\n")
      last = :comment
      found_comment_after_newline = found_newline
      multilple_lines = false

      write current_token_value.rstrip
      next_token
    when :on_embdoc_beg
      if multilple_lines || last == :comment
        write_line
      end

      consume_embedded_comment
      last = :comment
      last_comment_has_newline = true
    else
      break
    end
  end

  # Output a newline if we didn't do so yet:
  # either we didn't find a newline and we are at the end of a line (and we didn't just pass a semicolon),
  # or the last thing was a comment (from which we removed the newline)
  # or we just passed multiple lines (but printed only one)
  if (!found_newline && !at_prefix && !(want_semicolon && last == :semicolon)) ||
     last == :comment ||
     (multilple_lines && (want_multiline || found_comment_after_newline))
    write_line
  end
end

#consume_keyword(value) ⇒ Object



3452
3453
3454
3455
3456
3457
3458
3459
# File 'lib/rufo/formatter.rb', line 3452

def consume_keyword(value)
  check :on_kw
  if current_token_value != value
    bug "Expected keyword #{value}, not #{current_token_value}"
  end
  write value
  next_token
end

#consume_op(value) ⇒ Object



3461
3462
3463
3464
3465
3466
3467
3468
# File 'lib/rufo/formatter.rb', line 3461

def consume_op(value)
  check :on_op
  if current_token_value != value
    bug "Expected op #{value}, not #{current_token_value}"
  end
  write value
  next_token
end

#consume_op_or_keywordObject



1890
1891
1892
1893
1894
1895
1896
1897
1898
# File 'lib/rufo/formatter.rb', line 1890

def consume_op_or_keyword
  case current_token_kind
  when :on_op, :on_kw
    write current_token_value
    next_token
  else
    bug "Expected op or kw, not #{current_token_kind}"
  end
end

#consume_pin_opObject



3676
3677
3678
3679
3680
3681
# File 'lib/rufo/formatter.rb', line 3676

def consume_pin_op
  return unless op?("^")

  consume_op "^"
  skip_space
end

#consume_space(want_preserve_whitespace: false) ⇒ Object



3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
# File 'lib/rufo/formatter.rb', line 3325

def consume_space(want_preserve_whitespace: false)
  first_space = skip_space
  if want_preserve_whitespace && !newline? && !comment? && first_space
    write_space first_space[2] unless @output[-1] == " "
    skip_space_or_newline
  else
    skip_space_or_newline
    write_space unless @output[-1] == " "
  end
end

#consume_space_after_command_nameObject



1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
# File 'lib/rufo/formatter.rb', line 1237

def consume_space_after_command_name
  has_backslash, first_space = skip_space_backslash
  if has_backslash
    write " \\"
    write_line
    write_indent(next_indent)
  else
    write_space_using_setting(first_space, :one)
  end
end

#consume_space_or_newlineObject



3336
3337
3338
3339
3340
3341
3342
3343
3344
# File 'lib/rufo/formatter.rb', line 3336

def consume_space_or_newline
  skip_space
  if newline? || comment?
    consume_end_of_line
    write_indent(next_indent)
  else
    consume_space
  end
end

#consume_token(kind) ⇒ Object



3433
3434
3435
3436
3437
# File 'lib/rufo/formatter.rb', line 3433

def consume_token(kind)
  check kind
  consume_token_value(current_token_value)
  next_token
end

#consume_token_value(value) ⇒ Object



3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
# File 'lib/rufo/formatter.rb', line 3439

def consume_token_value(value)
  write value

  # If the value has newlines, we need to adjust line and column
  number_of_lines = value.count("\n")
  if number_of_lines > 0
    @line += number_of_lines
    last_line_index = value.rindex("\n")
    @column = value.size - (last_line_index + 1)
    @last_was_newline = @column == 0
  end
end

#current_comment_aligned_to_previous_one?Boolean

Returns:

  • (Boolean)


905
906
907
908
909
# File 'lib/rufo/formatter.rb', line 905

def current_comment_aligned_to_previous_one?
  @last_comment &&
    @last_comment[0][0] + 1 == current_token_line &&
    @last_comment[0][1] == current_token_column
end

#current_tokenObject

[1, 0], :on_int, “1”


3886
3887
3888
# File 'lib/rufo/formatter.rb', line 3886

def current_token
  @tokens.last
end

#current_token_columnObject



3904
3905
3906
# File 'lib/rufo/formatter.rb', line 3904

def current_token_column
  current_token[0][1]
end

#current_token_kindObject



3890
3891
3892
3893
# File 'lib/rufo/formatter.rb', line 3890

def current_token_kind
  tok = current_token
  tok ? tok[1] : :on_eof
end

#current_token_lineObject



3900
3901
3902
# File 'lib/rufo/formatter.rb', line 3900

def current_token_line
  current_token[0][0]
end

#current_token_valueObject



3895
3896
3897
3898
# File 'lib/rufo/formatter.rb', line 3895

def current_token_value
  tok = current_token
  tok ? tok[2] : ""
end

#declaration?(exp) ⇒ Boolean

Returns:

  • (Boolean)


571
572
573
574
575
576
577
578
# File 'lib/rufo/formatter.rb', line 571

def declaration?(exp)
  case exp[0]
  when :def, :class, :module
    true
  else
    false
  end
end

#dedent_callsObject



4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
# File 'lib/rufo/formatter.rb', line 4026

def dedent_calls
  return if @line_to_call_info.empty?

  lines = @output.lines

  while (line_to_call_info = @line_to_call_info.shift)
    first_line, call_info = line_to_call_info
    next unless call_info.size == 5

    indent, first_param_indent, needs_dedent, first_paren_end_line, last_line = call_info
    next unless needs_dedent
    next unless first_paren_end_line == last_line

    diff = first_param_indent - indent
    (first_line + 1..last_line).each do |line|
      @line_to_call_info.delete(line)

      next if @unmodifiable_string_lines[line]

      current_line = lines[line]
      current_line = current_line[diff..-1] if diff >= 0

      # It can happen that this line didn't need an indent because
      # it simply had a newline
      if current_line
        lines[line] = current_line
        adjust_other_alignments nil, line, 0, -diff
      end
    end
  end

  @output = lines.join
end

#do_align(components, scope) ⇒ Object



4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
# File 'lib/rufo/formatter.rb', line 4087

def do_align(components, scope)
  lines = @output.lines

  # Chunk components that are in consecutive lines
  chunks = components.chunk_while do |(l1, _c1, i1, id1), (l2, _c2, i2, id2)|
    l1 + 1 == l2 && i1 == i2 && id1 == id2
  end

  chunks.each do |elements|
    next if elements.size == 1

    max_column = elements.map { |_l, c| c }.max

    elements.each do |(line, column, _, _, offset)|
      next if column == max_column

      split_index = column
      split_index -= offset if offset

      target_line = lines[line]

      before = target_line[0...split_index]
      after = target_line[split_index..-1]

      filler_size = max_column - column
      filler = " " * filler_size

      # Move all lines affected by the assignment shift
      if scope == :assign && (range = @assignments_ranges[line])
        (line + 1..range).each do |line_number|
          lines[line_number] = "#{filler}#{lines[line_number]}"

          # And move other elements too if applicable
          adjust_other_alignments scope, line_number, column, filler_size
        end
      end

      # Move comments to the right if a change happened
      if scope != :comment
        adjust_other_alignments scope, line, column, filler_size
      end

      lines[line] = "#{before}#{filler}#{after}"
    end
  end

  @output = lines.join
end

#do_align_case_whenObject



4083
4084
4085
# File 'lib/rufo/formatter.rb', line 4083

def do_align_case_when
  do_align @case_when_positions, :case
end

#empty_body?(body) ⇒ Boolean

Returns:

  • (Boolean)


3427
3428
3429
3430
3431
# File 'lib/rufo/formatter.rb', line 3427

def empty_body?(body)
  body[0] == :bodystmt &&
    body[1].size == 1 &&
    body[1][0][0] == :void_stmt
end

#empty_params?(node) ⇒ Boolean

Returns:

  • (Boolean)


2065
2066
2067
2068
# File 'lib/rufo/formatter.rb', line 2065

def empty_params?(node)
  _, a, b, c, d, e, f, g = node
  !a && !b && !c && !d && !e && !f && !g
end

#find_closing_brace_tokenObject



3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
# File 'lib/rufo/formatter.rb', line 3940

def find_closing_brace_token
  count = 0
  i = @tokens.size - 1
  while i >= 0
    token = @tokens[i]
    _, kind = token
    case kind
    when :on_lbrace, :on_tlambeg
      count += 1
    when :on_rbrace
      count -= 1
      return [token, i] if count == 0
    end
    i -= 1
  end
  nil
end

#flush_heredocsObject



1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
# File 'lib/rufo/formatter.rb', line 1175

def flush_heredocs
  if comment?
    write_space unless @output[-1] == " "
    write current_token_value.rstrip
    next_token
    write_line
  end

  printed = false

  until @heredocs.empty?
    heredoc, tilde = @heredocs.first

    @heredocs.shift
    @current_heredoc = [heredoc, tilde]
    visit_string_literal_end(heredoc)
    @current_heredoc = nil
    printed = true
  end

  @last_was_heredoc = true if printed
end

#formatObject



177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/rufo/formatter.rb', line 177

def format
  visit @sexp
  consume_end
  write_line if !@last_was_newline || @output == ""
  @output.chomp! if @output.end_with?("\n\n")

  dedent_calls
  indent_literals
  do_align_case_when if align_case_when
  remove_lines_before_inline_declarations
  @output.lstrip!
  @output = "\n" if @output.empty?
end

#format_endless_methodObject



2058
2059
2060
2061
2062
2063
# File 'lib/rufo/formatter.rb', line 2058

def format_endless_method
  consume_space
  consume_op "="
  consume_space
  skip_space
end

#format_simple_string(node) ⇒ Object



647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
# File 'lib/rufo/formatter.rb', line 647

def format_simple_string(node)
  # is it a simple string node?
  string = simple_string(node)
  return if !string

  # is it eligible for formatting?
  return if !should_format_string?(string)

  # success!
  write quote_char
  next_token
  with_unmodifiable_string_lines do
    inner = node[1][1..-1]
    visit_exps(inner, with_lines: false)
  end
  write quote_char
  next_token

  true
end

#indent(value = nil) ⇒ Object



3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
# File 'lib/rufo/formatter.rb', line 3683

def indent(value = nil)
  if value
    old_indent = @indent
    @indent = value
    yield
    @indent = old_indent
  else
    @indent += INDENT_SIZE
    yield
    @indent -= INDENT_SIZE
  end
end

#indent_after_space(node, sticky: false, want_space: true, needed_indent: next_indent, token_column: nil, base_column: nil) ⇒ Object



3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
# File 'lib/rufo/formatter.rb', line 3836

def indent_after_space(node, sticky: false, want_space: true, needed_indent: next_indent, token_column: nil, base_column: nil)
  skip_space

  case current_token_kind
  when :on_ignored_nl, :on_comment
    indent(needed_indent) do
      consume_end_of_line
    end

    if token_column && base_column && token_column == current_token_column
      # If the expression is aligned with the one above, keep it like that
      indent(base_column) do
        write_indent
        visit node
      end
    else
      indent(needed_indent) do
        write_indent
        visit node
      end
    end
  else
    if want_space
      write_space
    end
    if sticky
      indent(@column) do
        visit node
      end
    else
      visit node
    end
  end
end

#indent_body(exps, force_multiline: false) ⇒ Object



3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
# File 'lib/rufo/formatter.rb', line 3696

def indent_body(exps, force_multiline: false)
  first_space = skip_space

  has_semicolon = semicolon?

  if has_semicolon
    next_token
    skip_semicolons
    first_space = nil
  end

  # If an end follows there's nothing to do
  if keyword?("end")
    if has_semicolon
      write "; "
    else
      write_space_using_setting(first_space, :one)
    end
    return
  end

  # A then keyword can appear after a newline after an `if`, `unless`, etc.
  # Since that's a super weird formatting for if, probably way too obsolete
  # by now, we just remove it.
  has_then = keyword?("then")
  if has_then
    next_token
    second_space = skip_space
  end

  has_do = keyword?("do")
  if has_do
    next_token
    second_space = skip_space
  end

  # If no newline or comment follows, we format it inline.
  if !force_multiline && !(newline? || comment?)
    if has_then
      write " then "
    elsif has_do
      write_space_using_setting(first_space, :one, at_least_one: true)
      write "do"
      write_space_using_setting(second_space, :one, at_least_one: true)
    elsif has_semicolon
      write "; "
    else
      write_space_using_setting(first_space, :one, at_least_one: true)
    end
    visit_exps exps, with_indent: false, with_lines: false

    consume_space

    return
  end

  indent do
    consume_end_of_line(want_multiline: false)
  end

  if keyword?("then")
    next_token
    skip_space_or_newline
  end

  # If the body is [[:void_stmt]] it's an empty body
  # so there's nothing to write
  if exps.size == 1 && exps[0][0] == :void_stmt
    skip_space_or_newline
  else
    indent do
      visit_exps exps, with_indent: true
    end
    write_line unless @last_was_newline
  end
end

#indent_literalsObject



4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
# File 'lib/rufo/formatter.rb', line 4060

def indent_literals
  return if @literal_indents.empty?

  lines = @output.lines

  modified_lines = []
  @literal_indents.each do |first_line, last_line, indent|
    (first_line + 1..last_line).each do |line|
      next if @unmodifiable_string_lines[line]

      current_line = lines[line]
      current_line = "#{" " * indent}#{current_line}"
      unless modified_lines[line]
        modified_lines[line] = current_line
        lines[line] = current_line
        adjust_other_alignments nil, line, 0, indent
      end
    end
  end

  @output = lines.join
end

#keyword?(keyword) ⇒ Boolean

Returns:

  • (Boolean)


3908
3909
3910
# File 'lib/rufo/formatter.rb', line 3908

def keyword?(keyword)
  current_token_kind == :on_kw && current_token_value == keyword
end

#last?(index, array) ⇒ Boolean

Returns:

  • (Boolean)


3985
3986
3987
# File 'lib/rufo/formatter.rb', line 3985

def last?(index, array)
  index == array.size - 1
end

#maybe_indent(toggle, indent_size) ⇒ Object



3773
3774
3775
3776
3777
3778
3779
3780
3781
# File 'lib/rufo/formatter.rb', line 3773

def maybe_indent(toggle, indent_size)
  if toggle
    indent(indent_size) do
      yield
    end
  else
    yield
  end
end

#need_space_for_hash?(node, elements, closing_brace_token) ⇒ Boolean

Check to see if need to add space inside hash literal braces.

Returns:

  • (Boolean)


4167
4168
4169
4170
4171
4172
4173
4174
# File 'lib/rufo/formatter.rb', line 4167

def need_space_for_hash?(node, elements, closing_brace_token)
  return false if elements.nil? || elements.empty?

  left_need_space = current_token_line == node_line(node, beginning: true)
  right_need_space = closing_brace_token[0][0] == node_line(node, beginning: false)

  left_need_space && right_need_space
end

#needs_two_lines?(exp) ⇒ Boolean

Returns:

  • (Boolean)


552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
# File 'lib/rufo/formatter.rb', line 552

def needs_two_lines?(exp)
  kind = exp[0]
  case kind
  when :def, :class, :module
    return true
  when :vcall
    # Check if it's private/protected/public
    nested = exp[1]
    if nested[0] == :@ident
      case nested[1]
      when "private", "protected", "public"
        return true
      end
    end
  end

  false
end

#newline?Boolean

Returns:

  • (Boolean)


3912
3913
3914
# File 'lib/rufo/formatter.rb', line 3912

def newline?
  current_token_kind == :on_nl || current_token_kind == :on_ignored_nl
end

#next_indentObject



3871
3872
3873
# File 'lib/rufo/formatter.rb', line 3871

def next_indent
  @indent + INDENT_SIZE
end

#next_tokenObject



3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
# File 'lib/rufo/formatter.rb', line 3958

def next_token
  prev_token = self.current_token

  @tokens.pop

  if (newline? || comment?) && !@heredocs.empty?
    flush_heredocs
  end

  # First first token in newline if requested
  if @want_first_token_in_line && prev_token && (prev_token[1] == :on_nl || prev_token[1] == :on_ignored_nl)
    @tokens.reverse_each do |token|
      case token[1]
      when :on_sp
        next
      else
        @first_token_in_line = token
        break
      end
    end
  end
end

#next_token_no_heredoc_checkObject



3981
3982
3983
# File 'lib/rufo/formatter.rb', line 3981

def next_token_no_heredoc_check
  @tokens.pop
end

#node_line(node, beginning: true) ⇒ Object



4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
# File 'lib/rufo/formatter.rb', line 4176

def node_line(node, beginning: true)
  return if node.nil?
  # get line of node, it is only used in visit_hash right now,
  # so handling the following node types is enough.
  case node.first
  when :hash, :string_literal, :symbol_literal, :symbol, :vcall, :string_content, :assoc_splat, :var_ref, :dyna_symbol, :var_field
    node_line(node[1], beginning: beginning)
  when :hshptn
    _, _, elements, rest = node
    elem = if beginning
        (elements[0] && elements[0][0]) || rest
      else
        rest || elements.last[0]
      end
    node_line(elem, beginning: beginning)
  when :assoc_new
    # There's no line number info for empty strings or hashes.
    if node[1] != EMPTY_STRING && node[1] != EMPTY_HASH
      node_line(node[1], beginning: beginning)
    elsif node.last != EMPTY_STRING && node.last != EMPTY_HASH
      node_line(node.last, beginning: beginning)
    else
      return
    end
  when :assoclist_from_args
    node_line(beginning ? node[1][0] : node[1].last, beginning: beginning)
  when :@label, :@int, :@ident, :@tstring_content, :@kw
    node[2][0]
  end
end

#op?(value) ⇒ Boolean

Returns:

  • (Boolean)


3932
3933
3934
# File 'lib/rufo/formatter.rb', line 3932

def op?(value)
  current_token_kind == :on_op && current_token_value == value
end

#push_call(node) ⇒ Object



3989
3990
3991
3992
3993
3994
3995
3996
3997
# File 'lib/rufo/formatter.rb', line 3989

def push_call(node)
  push_node(node) do
    # A call can specify hash arguments so it acts as a
    # hash for key alignment purposes
    push_hash(node) do
      yield
    end
  end
end

#push_hash(node) ⇒ Object



4008
4009
4010
4011
4012
4013
# File 'lib/rufo/formatter.rb', line 4008

def push_hash(node)
  old_hash = @current_hash
  @current_hash = node
  yield
  @current_hash = old_hash
end

#push_node(node) ⇒ Object



3999
4000
4001
4002
4003
4004
4005
4006
# File 'lib/rufo/formatter.rb', line 3999

def push_node(node)
  old_node = @current_node
  @current_node = node

  yield

  @current_node = old_node
end

#push_type(node) ⇒ Object



4015
4016
4017
4018
4019
4020
# File 'lib/rufo/formatter.rb', line 4015

def push_type(node)
  old_type = @current_type
  @current_type = node
  yield
  @current_type = old_type
end

#quote_charObject

Which quote character are we using?



629
630
631
# File 'lib/rufo/formatter.rb', line 629

def quote_char
  (quote_style == :double) ? '"' : "'"
end

#remove_lines_before_inline_declarationsObject



4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
# File 'lib/rufo/formatter.rb', line 4148

def remove_lines_before_inline_declarations
  return if @inline_declarations.empty?

  lines = @output.lines

  @inline_declarations.reverse.each_cons(2) do |(after, after_original), (before, before_original)|
    if before + 2 == after && before_original + 1 == after_original && lines[before + 1].strip.empty?
      lines.delete_at(before + 1)
    end
  end

  @output = lines.join
end

#resultObject



4162
4163
4164
# File 'lib/rufo/formatter.rb', line 4162

def result
  @output
end

#semicolon?Boolean

Returns:

  • (Boolean)


3920
3921
3922
# File 'lib/rufo/formatter.rb', line 3920

def semicolon?
  current_token_kind == :on_semicolon
end

#should_format_string?(string) ⇒ Boolean

should we format this string according to :quote_style?

Returns:

  • (Boolean)


634
635
636
637
638
639
640
641
642
643
644
645
# File 'lib/rufo/formatter.rb', line 634

def should_format_string?(string)
  return if quote_style == :mixed
  # don't format %q or %Q
  return unless current_token_value == "'" || current_token_value == '"'
  # don't format strings containing slashes
  return if string.include?("\\")
  # don't format strings that contain our quote character
  return if string.include?(quote_char)
  return if string.include?('#{')
  return if string.include?('#$')
  true
end

#simple_string(node) ⇒ Object

For simple string formatting, look for nodes like:

[:string_literal, [:string_content, [:@tstring_content, "abc", [...]]]]

and return the simple string inside.



618
619
620
621
622
623
624
625
626
# File 'lib/rufo/formatter.rb', line 618

def simple_string(node)
  inner = node[1][1..-1]
  return if inner.length > 1
  inner = inner[0]
  return "" if !inner
  return if inner[0] != :@tstring_content
  string = inner[1]
  string
end

#skip_ignored_spaceObject



3352
3353
3354
# File 'lib/rufo/formatter.rb', line 3352

def skip_ignored_space
  next_token while current_token_kind == :on_ignored_sp
end

#skip_semicolonsObject



3421
3422
3423
3424
3425
# File 'lib/rufo/formatter.rb', line 3421

def skip_semicolons
  while semicolon? || space?
    next_token
  end
end

#skip_spaceObject



3346
3347
3348
3349
3350
# File 'lib/rufo/formatter.rb', line 3346

def skip_space
  first_space = space? ? current_token : nil
  next_token while space?
  first_space
end

#skip_space_backslashObject



3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
# File 'lib/rufo/formatter.rb', line 3364

def skip_space_backslash
  return [false, false] unless space?

  first_space = current_token
  has_slash_newline = false
  while space?
    has_slash_newline ||= current_token_value == "\\\n"
    next_token
  end
  [has_slash_newline, first_space]
end

#skip_space_no_heredoc_checkObject



3356
3357
3358
3359
3360
3361
3362
# File 'lib/rufo/formatter.rb', line 3356

def skip_space_no_heredoc_check
  first_space = space? ? current_token : nil
  while space?
    next_token_no_heredoc_check
  end
  first_space
end

#skip_space_or_newline(_want_semicolon: false, write_first_semicolon: false) ⇒ Object



3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
# File 'lib/rufo/formatter.rb', line 3376

def skip_space_or_newline(_want_semicolon: false, write_first_semicolon: false)
  found_newline = false
  found_comment = false
  found_semicolon = false
  last = nil

  loop do
    case current_token_kind
    when :on_sp
      next_token
    when :on_nl, :on_ignored_nl
      next_token
      last = :newline
      found_newline = true
    when :on_semicolon
      if (!found_newline && !found_comment) || (!found_semicolon && write_first_semicolon)
        write "; "
      end
      next_token
      last = :semicolon
      found_semicolon = true
    when :on_comment
      write_line if last == :newline

      write_indent if found_comment
      if current_token_value.end_with?("\n")
        write_space
        write current_token_value.rstrip
        write "\n"
        write_indent(next_indent)
        @column = next_indent
      else
        write current_token_value
      end
      next_token
      found_comment = true
      last = :comment
    else
      break
    end
  end

  found_semicolon
end

#skip_space_or_newline_using_setting(setting, indent_size = @indent) ⇒ Object



3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
# File 'lib/rufo/formatter.rb', line 3812

def skip_space_or_newline_using_setting(setting, indent_size = @indent)
  indent(indent_size) do
    first_space = skip_space
    if newline? || comment?
      consume_end_of_line(want_multiline: false, first_space: first_space)
      write_indent
    else
      write_space_using_setting(first_space, setting)
    end
  end
end

#space?Boolean

Returns:

  • (Boolean)


3928
3929
3930
# File 'lib/rufo/formatter.rb', line 3928

def space?
  current_token_kind == :on_sp
end

#to_ary(node) ⇒ Object



4022
4023
4024
# File 'lib/rufo/formatter.rb', line 4022

def to_ary(node)
  node[0].is_a?(Symbol) ? [node] : node
end

#track_alignment(key, target, offset = 0, id = nil) ⇒ Object



928
929
930
931
932
933
934
935
936
937
938
939
# File 'lib/rufo/formatter.rb', line 928

def track_alignment(key, target, offset = 0, id = nil)
  last = target.last
  if last && last[0] == @line
    # Track only the first alignment in a line
    return
  end

  @line_to_alignments_positions[@line] << [key, @column, target, target.size]
  info = [@line, @column, @indent, id, offset]
  target << info
  info
end

#track_assignment(offset = 0) ⇒ Object



920
921
922
# File 'lib/rufo/formatter.rb', line 920

def track_assignment(offset = 0)
  track_alignment :assign, @assignments_positions, offset
end

#track_case_whenObject



924
925
926
# File 'lib/rufo/formatter.rb', line 924

def track_case_when
  track_alignment :case_whem, @case_when_positions
end

#track_comment(id: nil, match_previous_id: false) ⇒ Object



911
912
913
914
915
916
917
918
# File 'lib/rufo/formatter.rb', line 911

def track_comment(id: nil, match_previous_id: false)
  if match_previous_id && !@comments_positions.empty?
    id = @comments_positions.last[3]
  end

  @line_to_alignments_positions[@line] << [:comment, @column, @comments_positions, @comments_positions.size]
  @comments_positions << [@line, @column, 0, id, 0]
end

#visit(node) ⇒ Object



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# File 'lib/rufo/formatter.rb', line 191

def visit(node)
  @node_level += 1
  unless node.is_a?(Array)
    bug "unexpected node: #{node} at #{current_token}"
  end

  case node.first
  when :program
    # Topmost node
    #
    # [:program, exps]
    visit_exps node[1], with_indent: true
  when :void_stmt
    # Empty statement
    #
    # [:void_stmt]
    skip_space_or_newline
  when :@int
    # Integer literal
    #
    # [:@int, "123", [1, 0]]
    consume_token :on_int
  when :@float
    # Float literal
    #
    # [:@float, "123.45", [1, 0]]
    consume_token :on_float
  when :@rational
    # Rational literal
    #
    # [:@rational, "123r", [1, 0]]
    consume_token :on_rational
  when :@imaginary
    # Imaginary literal
    #
    # [:@imaginary, "123i", [1, 0]]
    consume_token :on_imaginary
  when :@CHAR
    # [:@CHAR, "?a", [1, 0]]
    consume_token :on_CHAR
  when :@backref, :@label, :@op
    # [:@gvar, "$abc", [1, 0]]
    # [:@backref, "$1", [1, 0]]
    # [:@label, "foo:", [1, 3]]
    # [:@op, "*", [1, 1]]
    write node[1]
    next_token
  when :@backtick
    # [:@backtick, "`", [1, 4]]
    consume_token :on_backtick
  when :string_literal, :xstring_literal
    visit_string_literal node
  when :string_concat
    visit_string_concat node
  when :@tstring_content
    # [:@tstring_content, "hello ", [1, 1]]
    heredoc, tilde = @current_heredoc
    looking_at_newline = current_token_kind == :on_tstring_content && current_token_value == "\n"
    if heredoc && tilde && !@last_was_newline && looking_at_newline
      check :on_tstring_content
      consume_token_value(current_token_value)
      next_token
    else
      # For heredocs with tilde we sometimes need to align the contents
      if heredoc && tilde
        while (current_token_kind == :on_ignored_sp) ||
              (current_token_kind == :on_tstring_content)
          if @last_was_newline && current_token_value != "\n"
            write_indent(next_indent)
            @last_was_newline = false
          end
          if current_token_kind == :on_ignored_sp
            skip_ignored_space
          else
            consume_token current_token_kind
          end
        end
      else
        while (current_token_kind == :on_ignored_sp) ||
              (current_token_kind == :on_tstring_content)
          consume_token current_token_kind
        end
      end
    end
  when :string_content
    # [:string_content, exp]
    visit_exps node[1..-1], with_lines: false
  when :string_embexpr
    # String interpolation piece ( #{exp} )
    visit_string_interpolation node
  when :string_dvar
    visit_string_dvar(node)
  when :symbol_literal
    visit_symbol_literal(node)
  when :symbol
    visit_symbol(node)
  when :dyna_symbol
    visit_quoted_symbol_literal(node)
  when :@ident
    consume_pin_op
    consume_token :on_ident
  when :var_ref, :var_field, :const_ref, :vcall, :fcall
    # [:var_ref, exp]
    # [:var_field, exp]
    # [:const_ref, [:@const, "Foo", [1, 8]]]
    # [:vcall, exp]
    # [:fcall, [:@ident, "foo", [1, 0]]]
    visit node[1]
  when :@kw
    # [:@kw, "nil", [1, 0]]
    consume_token :on_kw
  when :@ivar
    # [:@ivar, "@foo", [1, 0]]
    consume_pin_op
    consume_token :on_ivar
  when :@cvar
    # [:@cvar, "@@foo", [1, 0]]
    consume_pin_op
    consume_token :on_cvar
  when :@gvar
    # [:@gvar, "$foo", [1, 0]]
    consume_pin_op
    consume_token :on_gvar
  when :@const
    # [:@const, "FOO", [1, 0]]
    consume_token :on_const
  when :top_const_ref
    # [:top_const_ref, [:@const, "Foo", [1, 2]]]
    consume_op "::"
    skip_space_or_newline
    visit node[1]
  when :top_const_field
    # [:top_const_field, [:@const, "Foo", [1, 2]]]
    consume_op "::"
    visit node[1]
  when :const_path_ref, :const_path_field
    visit_path(node)
  when :assign
    visit_assign(node)
  when :opassign
    visit_op_assign(node)
  when :massign
    visit_multiple_assign(node)
  when :ifop
    visit_ternary_if(node)
  when :if_mod
    visit_suffix(node, "if")
  when :unless_mod
    visit_suffix(node, "unless")
  when :while_mod
    visit_suffix(node, "while")
  when :until_mod
    visit_suffix(node, "until")
  when :rescue_mod
    visit_suffix(node, "rescue")
  when :command
    visit_command(node)
  when :command_call
    visit_command_call(node)
  when :args_add_block
    visit_call_args(node)
  when :args_add_star
    visit_args_add_star(node)
  when :bare_assoc_hash
    # [:bare_assoc_hash, exps]

    # Align hash elements to the first key
    indent(@column) do
      visit_comma_separated_list node[1]
    end
  when :method_add_arg
    visit_call_without_receiver(node)
  when :method_add_block
    visit_call_with_block(node)
  when :call
    visit_call_with_receiver(node)
  when :brace_block
    visit_brace_block(node)
  when :do_block
    visit_do_block(node)
  when :block_var
    visit_block_arguments(node)
  when :begin
    visit_begin(node)
  when :bodystmt
    visit_bodystmt(node)
  when :if
    visit_if(node)
  when :unless
    visit_unless(node)
  when :while
    visit_while(node)
  when :until
    visit_until(node)
  when :case
    visit_case(node)
  when :when, :in
    visit_when(node)
  when :unary
    visit_unary(node)
  when :binary
    visit_binary(node)
  when :class
    visit_class(node)
  when :module
    visit_module(node)
  when :mrhs_new_from_args
    visit_mrhs_new_from_args(node)
  when :mlhs
    visit_mlhs(node)
  when :mrhs_add_star
    visit_mrhs_add_star(node)
  when :def
    visit_def(node)
  when :defs
    visit_def_with_receiver(node)
  when :paren
    visit_paren(node)
  when :params
    visit_params(node)
  when :array
    visit_array(node)
  when :hash
    visit_hash(node)
  when :assoc_new
    visit_hash_key_value(node)
  when :assoc_splat
    visit_splat_inside_hash(node)
  when :dot2
    visit_range(node, true)
  when :dot3
    visit_range(node, false)
  when :regexp_literal
    visit_regexp_literal(node)
  when :aref
    visit_array_access(node)
  when :aref_field
    visit_array_setter(node)
  when :sclass
    visit_sclass(node)
  when :field
    visit_setter(node)
  when :return0
    consume_keyword "return"
  when :return
    visit_return(node)
  when :break
    visit_break(node)
  when :next
    visit_next(node)
  when :yield0
    consume_keyword "yield"
  when :yield
    visit_yield(node)
  when :lambda
    visit_lambda(node)
  when :zsuper
    # [:zsuper]
    consume_keyword "super"
  when :super
    visit_super(node)
  when :defined
    visit_defined(node)
  when :alias, :var_alias
    visit_alias(node)
  when :undef
    visit_undef(node)
  when :rest_param
    visit_rest_param(node)
  when :kwrest_param
    visit_kwrest_param(node)
  when :retry
    # [:retry]
    consume_keyword "retry"
  when :redo
    # [:redo]
    consume_keyword "redo"
  when :for
    visit_for(node)
  when :BEGIN
    visit_begin_node(node)
  when :END
    visit_end_node(node)
  when :args_forward
    consume_op("...")
  when :aryptn
    visit_array_pattern(node)
  when :fndptn
    visit_find_pattern(node)
  when :hshptn
    visit_hash_pattern(node)
  else
    bug "Unhandled node: #{node.first}"
  end
ensure
  @node_level -= 1
end

#visit_alias(node) ⇒ Object



2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
# File 'lib/rufo/formatter.rb', line 2687

def visit_alias(node)
  # [:alias, from, to]
  _, from, to = node

  consume_keyword "alias"
  consume_space
  visit from
  consume_space
  visit to
end

#visit_args_add_star(node) ⇒ Object



1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
# File 'lib/rufo/formatter.rb', line 1495

def visit_args_add_star(node)
  # [:args_add_star, args, star, post_args]
  _, args, star, *post_args = node

  if newline? || comment?
    needs_indent = true
    base_column = next_indent
  else
    base_column = @column
  end
  if !args.empty? && args[0] == :args_add_star
    # arg1, ..., *star
    visit args
  elsif !args.empty?
    visit_comma_separated_list args
  else
    consume_end_of_line if needs_indent
  end

  skip_space

  # Disable indentation in write_params_comma to avoid double indentation with write_indent
  write_params_comma(with_indent: !needs_indent) if comma?
  write_indent(base_column) if needs_indent
  consume_op "*"
  skip_space_or_newline
  # The name of rest arguments might be omitted.
  if star
    visit star
  end

  if post_args && !post_args.empty?
    # Disable indentation in write_params_comma to avoid double indentation with visit_comma_separated_list
    write_params_comma(with_indent: !needs_indent)
    visit_comma_separated_list post_args, needs_indent: needs_indent, base_column: base_column
  end
end

#visit_array(node) ⇒ Object



2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
# File 'lib/rufo/formatter.rb', line 2195

def visit_array(node)
  # [:array, elements]

  # Check if it's `%w(...)` or `%i(...)`
  case current_token_kind
  when :on_qwords_beg, :on_qsymbols_beg, :on_words_beg, :on_symbols_beg
    visit_q_or_i_array(node)
    return
  end

  _, elements = node

  base_column = @column

  check :on_lbracket
  write "["
  next_token

  if elements
    visit_literal_elements to_ary(elements), inside_array: true, token_column: base_column
  else
    skip_space_or_newline
  end

  check :on_rbracket
  write "]"
  next_token
end

#visit_array_access(node) ⇒ Object



2426
2427
2428
2429
2430
2431
2432
2433
# File 'lib/rufo/formatter.rb', line 2426

def visit_array_access(node)
  # exp[arg1, ..., argN]
  #
  # [:aref, name, args]
  _, name, args = node

  visit_array_getter_or_setter name, args
end

#visit_array_getter_or_setter(name, args) ⇒ Object



2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
# File 'lib/rufo/formatter.rb', line 2445

def visit_array_getter_or_setter(name, args)
  visit name

  token_column = current_token_column

  skip_space
  check :on_lbracket
  write "["
  next_token

  column = @column

  first_space = skip_space

  # Sometimes args comes with an array...
  if args && args[0].is_a?(Array)
    visit_literal_elements args, token_column: token_column
  else
    if newline? || comment?
      needed_indent = next_indent
      if args
        consume_end_of_line
        write_indent(needed_indent)
      else
        skip_space_or_newline
      end
    else
      write_space_using_setting(first_space, :never)
      needed_indent = column
    end

    if args
      indent(needed_indent) do
        visit args
      end
    end
  end

  skip_space_or_newline_using_setting(:never)

  check :on_rbracket
  write "]"
  next_token
end

#visit_array_pattern(node) ⇒ Object



3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
# File 'lib/rufo/formatter.rb', line 3085

def visit_array_pattern(node)
  # [:aryptn, const_ref, pre_rest, rest, post_rest]
  _, const_ref, pre_rest, rest, post_rest = node

  if const_ref
    visit const_ref
  end

  left_paren_token, right_paren_token = if current_token_kind == :on_lparen
      i[on_lparen on_rparen]
    elsif current_token_kind == :on_lbracket
      i[on_lbracket on_rbracket]
    else
      []
    end

  # pattern is [*]
  if !pre_rest && !post_rest && rest == [:var_field, nil]
    if left_paren_token
      consume_token left_paren_token
    end

    skip_space_or_newline
    consume_op "*"
    skip_space_or_newline

    if right_paren_token
      consume_token right_paren_token
    end
    return
  end

  token_column = current_token_column

  if left_paren_token
    consume_token left_paren_token
    skip_space
  end

  newline = newline?
  wrote_comma = false
  if pre_rest
    visit_comma_separated_list pre_rest
    skip_space

    if comma?
      check :on_comma
      write ","
      next_token
      skip_space
      wrote_comma = true
    end
    if newline
      skip_space_or_newline
      write_line
      write_indent
    end
  end

  if rest
    if wrote_comma
      consume_space
    end

    # pattern like `[a,]` will make `rest` as `[:var_field, nil]`
    if (var_name_node = rest[1]) || current_token_value == "*"
      consume_op "*"
      if var_name_node
        visit rest
      end
    end
  elsif wrote_comma && !newline?
    # In Ruby 3.3, rest is nil when the code is like `[a,]`. Insert a space after comma at here.
    consume_space
  end

  if post_rest
    skip_space
    check :on_comma
    write ","
    consume_space
    next_token

    visit_literal_elements post_rest, inside_array: true, token_column: token_column, keep_final_newline: !left_paren_token
  end

  skip_space

  if right_paren_token
    consume_token right_paren_token
  end
end

#visit_array_setter(node) ⇒ Object



2435
2436
2437
2438
2439
2440
2441
2442
2443
# File 'lib/rufo/formatter.rb', line 2435

def visit_array_setter(node)
  # exp[arg1, ..., argN]
  # (followed by `=`, though not included in this node)
  #
  # [:aref_field, name, args]
  _, name, args = node

  visit_array_getter_or_setter name, args
end

#visit_assign(node) ⇒ Object



815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
# File 'lib/rufo/formatter.rb', line 815

def visit_assign(node)
  # target = value
  #
  # [:assign, target, value]
  _, target, value = node

  line = @line

  visit target
  consume_space

  track_assignment
  consume_op "="
  visit_assign_value value

  @assignments_ranges[line] = @line if @line != line
end

#visit_assign_value(value) ⇒ Object



881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
# File 'lib/rufo/formatter.rb', line 881

def visit_assign_value(value)
  has_slash_newline, _first_space = skip_space_backslash

  # Remove backslash after equal + newline (it's useless)
  if has_slash_newline
    skip_space_or_newline
    write_space
    indent(next_indent) do
      visit(value)
    end
  else
    if [:begin, :case, :if, :unless].include?(value.first)
      skip_space_or_newline
      write_space
      indent(next_indent) do
        visit value
      end
    else
      indent_after_space value, sticky: false,
                                want_space: true
    end
  end
end

#visit_begin(node) ⇒ Object



1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
# File 'lib/rufo/formatter.rb', line 1533

def visit_begin(node)
  if op?("^")
    # ^(expression)
    #
    # [:begin, expression_node]
    consume_op "^"
    skip_space
    consume_token :on_lparen
    skip_space
    visit node[1]
    skip_space
    consume_token :on_rparen
  else
    # begin
    #   body
    # end
    #
    # [:begin, [:bodystmt, body, rescue_body, else_body, ensure_body]]
    consume_keyword "begin"
    visit node[1]
  end
end

#visit_begin_node(node) ⇒ Object



1718
1719
1720
# File 'lib/rufo/formatter.rb', line 1718

def visit_begin_node(node)
  visit_begin_or_end node, "BEGIN"
end

#visit_begin_or_end(node, keyword) ⇒ Object



1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
# File 'lib/rufo/formatter.rb', line 1726

def visit_begin_or_end(node, keyword)
  # [:BEGIN, body]
  _, body = node

  consume_keyword(keyword)
  consume_space

  closing_brace_token, _index = find_closing_brace_token

  # If the whole block fits into a single line, format
  # in a single line
  if current_token_line == closing_brace_token[0][0]
    consume_token :on_lbrace
    consume_space
    visit_exps body, with_lines: false
    consume_space
    consume_token :on_rbrace
  else
    consume_token :on_lbrace
    indent_body body
    write_indent
    consume_token :on_rbrace
  end
end

#visit_binary(node) ⇒ Object



1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
# File 'lib/rufo/formatter.rb', line 1840

def visit_binary(node)
  # [:binary, left, op, right]
  _, left, _, right = node

  # If this binary is not at the beginning of a line, if there's
  # a newline following the op we want to align it with the left
  # value. So for example:
  #
  # var = left_exp ||
  #       right_exp
  #
  # But:
  #
  # def foo
  #   left_exp ||
  #     right_exp
  # end
  needed_indent = @column == @indent ? next_indent : @column
  base_column = @column
  token_column = current_token_column

  visit left
  needs_space = space?

  has_backslash, _ = skip_space_backslash
  if has_backslash
    needs_space = true
    write " \\"
    write_line
    write_indent(next_indent)
  else
    write_space
  end

  consume_op_or_keyword

  skip_space

  if newline? || comment?
    indent_after_space right,
                       want_space: needs_space,
                       needed_indent: needed_indent,
                       token_column: token_column,
                       base_column: base_column
  else
    write_space
    visit right
  end
end

#visit_block_arguments(node) ⇒ Object



1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
# File 'lib/rufo/formatter.rb', line 1419

def visit_block_arguments(node)
  # [:block_var, params, local_params]
  _, params, local_params = node

  empty_params = empty_params?(params)

  check :on_op

  # check for ||
  if empty_params && !local_params
    # Don't write || as it's meaningless
    next_token
    skip_space_or_newline
    check :on_op
    next_token
    return
  end

  consume_token :on_op
  found_semicolon = skip_space_or_newline(_want_semicolon: true, write_first_semicolon: true)

  if found_semicolon
    # Nothing
  elsif empty_params && local_params
    consume_token :on_semicolon
  end

  skip_space_or_newline

  unless empty_params
    visit params
    skip_space
  end

  if local_params
    if semicolon?
      consume_token :on_semicolon
      consume_space
    end

    visit_comma_separated_list local_params
  else
    skip_space_or_newline
  end

  consume_op "|"
end

#visit_bodystmt(node) ⇒ Object



1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
# File 'lib/rufo/formatter.rb', line 1556

def visit_bodystmt(node)
  # [:bodystmt, body, rescue_body, else_body, ensure_body]
  # [:bodystmt, [[:@int, "1", [2, 1]]], nil, [[:@int, "2", [4, 1]]], nil] (2.6.0)
  _, body, rescue_body, else_body, ensure_body = node

  @inside_type_body = false

  line = @line

  endless = body[0].is_a?(Symbol)
  if endless
    visit body
  else
    indent_body body
  end

  while rescue_body
    # [:rescue, type, name, body, more_rescue]
    _, type, name, body, more_rescue = rescue_body
    write_indent
    consume_keyword "rescue"
    if type
      skip_space
      write_space
      indent(@column) do
        visit_rescue_types(type)
      end
    end

    if name
      skip_space
      write_space
      consume_op "=>"
      skip_space
      write_space
      visit name
    end

    indent_body body
    rescue_body = more_rescue
  end

  if else_body
    # [:else, body]
    # [[:@int, "2", [4, 1]]] (2.6.0)
    write_indent
    consume_keyword "else"
    else_body = else_body[1] if else_body[0] == :else
    indent_body else_body
  end

  if ensure_body
    # [:ensure, body]
    write_indent
    consume_keyword "ensure"
    indent_body ensure_body[1]
  end

  write_indent if @line != line
  consume_keyword "end" unless endless
end

#visit_brace_block(node) ⇒ Object



1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
# File 'lib/rufo/formatter.rb', line 1337

def visit_brace_block(node)
  # [:brace_block, args, body]
  _, args, body = node

  # This is for the empty `{ }` block
  if void_exps?(body)
    consume_token :on_lbrace
    consume_block_args args
    consume_space
    consume_token :on_rbrace
    return
  end

  closing_brace_token, _ = find_closing_brace_token

  # If the whole block fits into a single line, use braces
  if current_token_line == closing_brace_token[0][0]
    consume_token :on_lbrace
    consume_block_args args
    consume_space
    visit_exps body, with_lines: false

    while semicolon?
      next_token
    end

    consume_space

    consume_token :on_rbrace
    return
  end

  # Otherwise it's multiline
  consume_token :on_lbrace
  consume_block_args args

  if (call_info = @line_to_call_info[@line])
    call_info << true
  end

  indent_body body, force_multiline: true
  write_indent

  # If the closing bracket matches the indent of the first parameter,
  # keep it like that. Otherwise dedent.
  if call_info && call_info[1] != current_token_column
    call_info << @line
  end

  consume_token :on_rbrace
end

#visit_break(node) ⇒ Object



2543
2544
2545
2546
# File 'lib/rufo/formatter.rb', line 2543

def visit_break(node)
  # [:break, exp]
  visit_control_keyword node, "break"
end

#visit_call_args(node) ⇒ Object



1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
# File 'lib/rufo/formatter.rb', line 1467

def visit_call_args(node)
  # [:args_add_block, args, block]
  _, args, block_arg = node

  if !args.empty? && args[0] == :args_add_star
    # arg1, ..., *star
    visit args
  else
    visit_comma_separated_list args
  end

  if block_arg_type(block_arg) != :no_arg
    skip_space_or_newline

    if comma?
      indent(next_indent) do
        write_params_comma
      end
    end

    consume_op "&"
    skip_space_or_newline
    if block_arg
      visit block_arg
    end
  end
end

#visit_call_at_paren(node, args) ⇒ Object



1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
# File 'lib/rufo/formatter.rb', line 1062

def visit_call_at_paren(node, args)
  consume_token :on_lparen

  # If there's a trailing comma then comes [:arg_paren, args],
  # which is a bit unexpected, so we fix it
  if args[1].is_a?(Array) && args[1][0].is_a?(Array)
    args_node = [:args_add_block, args[1], false]
  else
    args_node = args[1]
  end

  if args_node
    skip_space

    needs_trailing_newline = newline? || comment?
    if needs_trailing_newline && (call_info = @line_to_call_info[@line])
      call_info << true
    end

    want_trailing_comma = true

    # Check if there's a block arg and if the call ends with hash key/values
    if args_node[0] == :args_add_block
      _, args, block_arg = args_node
      want_trailing_comma = block_arg_type(block_arg) == :no_arg
      if args.is_a?(Array) && (last_arg = args.last) && last_arg.is_a?(Array) &&
         last_arg[0].is_a?(Symbol) && last_arg[0] != :bare_assoc_hash
        want_trailing_comma = false
      end
    end

    push_call(node) do
      visit args_node
      skip_space
    end

    found_comma = comma?

    heredoc_needs_newline = true

    if found_comma
      if needs_trailing_newline
        write "," if trailing_commas && !block_arg

        next_token
        heredoc_needs_newline = !newline?
        indent(next_indent) do
          consume_end_of_line
        end
        write_indent
      else
        next_token
        skip_space
      end
    end

    if newline? || comment?
      if needs_trailing_newline && !@last_was_heredoc
        write "," if trailing_commas && want_trailing_comma

        indent(next_indent) do
          consume_end_of_line
        end
        write_indent
      else
        skip_space_or_newline
      end
    else
      if needs_trailing_newline && !found_comma
        write "," if trailing_commas && want_trailing_comma && !@last_was_heredoc
        consume_end_of_line
        write_indent
      end
    end
  else
    skip_space_or_newline
  end

  # If the closing parentheses matches the indent of the first parameter,
  # keep it like that. Otherwise dedent.
  if call_info && call_info[1] != current_token_column
    call_info << @line
  end

  if @last_was_heredoc && heredoc_needs_newline
    write_line
    write_indent
  end
  consume_token :on_rparen
end

#visit_call_with_block(node) ⇒ Object



1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
# File 'lib/rufo/formatter.rb', line 1320

def visit_call_with_block(node)
  # [:method_add_block, call, block]
  _, call, block = node

  visit call

  consume_space

  old_dot_column = @dot_column
  old_original_dot_column = @original_dot_column

  visit block

  @dot_column = old_dot_column
  @original_dot_column = old_original_dot_column
end

#visit_call_with_receiver(node) ⇒ Object



977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
# File 'lib/rufo/formatter.rb', line 977

def visit_call_with_receiver(node)
  # [:call, obj, :".", name]
  _, obj, _, name = node

  @dot_column = nil
  visit obj

  first_space = skip_space

  if newline? || comment?
    consume_end_of_line

    # If align_chained_calls is off, we still want to preserve alignment if it's already there
    if align_chained_calls || (@original_dot_column && @original_dot_column == current_token_column)
      @name_dot_column = @dot_column || next_indent
      write_indent(@dot_column || next_indent)
    else
      # Make sure to reset dot_column so next lines don't align to the first dot
      @dot_column = next_indent
      @name_dot_column = next_indent
      write_indent(next_indent)
    end
  else
    write_space_using_setting(first_space, :no)
  end

  # Remember dot column, but only if there isn't one already set
  unless @dot_column
    dot_column = @column
    original_dot_column = current_token_column
  end

  consume_call_dot

  skip_space_or_newline_using_setting(:no, next_indent)

  if name == :call
    # :call means it's .()
  else
    visit name
  end

  # Only set it after we visit the call after the dot,
  # so we remember the outmost dot position
  @dot_column = dot_column if dot_column
  @original_dot_column = original_dot_column if original_dot_column
end

#visit_call_without_receiver(node) ⇒ Object



1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
# File 'lib/rufo/formatter.rb', line 1033

def visit_call_without_receiver(node)
  # foo(arg1, ..., argN)
  #
  # [:method_add_arg,
  #   [:fcall, [:@ident, "foo", [1, 0]]],
  #   [:arg_paren, [:args_add_block, [[:@int, "1", [1, 6]]], false]]]
  _, name, args = node

  @name_dot_column = nil
  visit name

  # Some times a call comes without parens (should probably come as command, but well...)
  return if args.empty?

  # Remember dot column so it's not affected by args
  dot_column = @dot_column
  original_dot_column = @original_dot_column

  want_indent = @name_dot_column && @name_dot_column > @indent

  maybe_indent(want_indent, @name_dot_column) do
    visit_call_at_paren(node, args)
  end

  # Restore dot column so it's not affected by args
  @dot_column = dot_column
  @original_dot_column = original_dot_column
end

#visit_case(node) ⇒ Object



2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
# File 'lib/rufo/formatter.rb', line 2917

def visit_case(node)
  # [:case, cond, case_when]
  _, cond, case_when = node

  # If node is inline pattern matching, case_expression will be false
  case_expression = keyword?("case")
  already_visit_cond = false
  if case_expression
    if cond && cond[0] == :case
      visit cond
      # In this case, the `case` token is for the `cond`, but not for `node`
      case_expression = false
      already_visit_cond = true
    else
      consume_keyword "case"
    end
  end

  if cond && !already_visit_cond
    consume_space
    visit cond
  end

  if case_expression
    consume_end_of_line
    write_indent
  else
    consume_space
  end
  visit case_when

  if case_expression
    write_indent
    consume_keyword "end"
  end
end

#visit_class(node) ⇒ Object



1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
# File 'lib/rufo/formatter.rb', line 1900

def visit_class(node)
  # [:class,
  #   name
  #   superclass
  #   [:bodystmt, body, nil, nil, nil]]
  _, name, superclass, body = node

  push_type(node) do
    consume_keyword "class"
    skip_space_or_newline
    write_space
    visit name

    if superclass
      skip_space_or_newline
      write_space
      consume_op "<"
      skip_space_or_newline
      write_space
      visit superclass
    end

    @inside_type_body = true
    visit body
  end
end

#visit_comma_separated_list(nodes, needs_indent: false, base_column: nil) ⇒ Object



1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
# File 'lib/rufo/formatter.rb', line 1751

def visit_comma_separated_list(nodes, needs_indent: false, base_column: nil)
  if newline? || comment?
    indent { consume_end_of_line }
    needs_indent = true
    base_column = next_indent
    write_indent(base_column)
  elsif needs_indent
    write_indent(base_column)
  else
    base_column ||= @column
  end

  nodes = to_ary(nodes)
  nodes.each_with_index do |exp, i|
    maybe_indent(needs_indent, base_column) do
      if block_given?
        yield exp
      else
        visit exp
      end
    end

    next if last?(i, nodes)

    skip_space
    check :on_comma
    write ","
    next_token
    skip_space_or_newline_using_setting(:one, base_column)
  end
end

#visit_command(node) ⇒ Object



1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
# File 'lib/rufo/formatter.rb', line 1153

def visit_command(node)
  # foo arg1, ..., argN
  #
  # [:command, name, args]
  _, name, args = node

  base_column = current_token_column

  push_call(node) do
    visit name
    consume_space_after_command_name
  end

  visit_command_end(node, args, base_column)
end

#visit_command_args(args, base_column) ⇒ Object



1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
# File 'lib/rufo/formatter.rb', line 1248

def visit_command_args(args, base_column)
  needed_indent = @column
  args_is_def_class_or_module = false
  param_column = current_token_column

  # Check if there's a single argument and it's
  # a def, class or module. In that case we don't
  # want to align the content to the position of
  # that keyword.
  if args[0] == :args_add_block
    nested_args = args[1]
    if nested_args.is_a?(Array) && nested_args.size == 1
      first = nested_args[0]
      if first.is_a?(Array)
        case first[0]
        when :def, :class, :module
          needed_indent = @indent
          args_is_def_class_or_module = true
        end
      end
    end
  end

  base_line = @line
  call_info = @line_to_call_info[@line]
  if call_info
    call_info = nil
  else
    call_info = [@indent, @column]
    @line_to_call_info[@line] = call_info
  end

  old_want_first_token_in_line = @want_first_token_in_line
  @want_first_token_in_line = true

  # We align call parameters to the first paramter
  indent(needed_indent) do
    visit_exps to_ary(args), with_lines: false
  end

  if call_info && call_info.size > 2
    # A call like:
    #
    #     foo, 1, [
    #       2,
    #     ]
    #
    # would normally be aligned like this (with the first parameter):
    #
    #     foo, 1, [
    #            2,
    #          ]
    #
    # However, the first style is valid too and we preserve it if it's
    # already formatted like that.
    call_info << @line
  elsif !args_is_def_class_or_module && @first_token_in_line && param_column == @first_token_in_line[0][1]
    # If the last line of the call is aligned with the first parameter, leave it like that:
    #
    #     foo 1,
    #         2
  elsif !args_is_def_class_or_module && @first_token_in_line && base_column + INDENT_SIZE == @first_token_in_line[0][1]
    # Otherwise, align it just by two spaces (so we need to dedent, we fake a dedent here)
    #
    #     foo 1,
    #       2
    @line_to_call_info[base_line] = [0, needed_indent - next_indent, true, @line, @line]
  end

  @want_first_token_in_line = old_want_first_token_in_line
end

#visit_command_call(node) ⇒ Object



1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
# File 'lib/rufo/formatter.rb', line 1198

def visit_command_call(node)
  # [:command_call,
  #   receiver
  #   :".",
  #   name
  #   [:args_add_block, [[:@int, "1", [1, 8]]], block]]
  _, receiver, _, name, args = node

  base_column = current_token_column

  visit receiver

  skip_space_or_newline

  # Remember dot column
  dot_column = @column
  original_dot_column = @original_dot_column

  consume_call_dot

  skip_space

  if newline? || comment?
    consume_end_of_line
    write_indent(next_indent)
  else
    skip_space_or_newline
  end

  visit name
  consume_space_after_command_name
  visit_command_args(args, base_column)

  # Only set it after we visit the call after the dot,
  # so we remember the outmost dot position
  @dot_column = dot_column
  @original_dot_column = original_dot_column
end

#visit_command_end(node, args, base_column) ⇒ Object



1169
1170
1171
1172
1173
# File 'lib/rufo/formatter.rb', line 1169

def visit_command_end(node, args, base_column)
  push_call(node) do
    visit_command_args(args, base_column)
  end
end

#visit_control_keyword(node, keyword) ⇒ Object



2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
# File 'lib/rufo/formatter.rb', line 2558

def visit_control_keyword(node, keyword)
  _, exp = node

  consume_keyword keyword

  if exp && !exp.empty?
    consume_space if space?

    indent(@column) do
      visit_exps to_ary(node[1]), with_lines: false
    end
  end
end

#visit_def(node) ⇒ Object



1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
# File 'lib/rufo/formatter.rb', line 1944

def visit_def(node)
  # [:def,
  #   [:@ident, "foo", [1, 6]],
  #   [:params, nil, nil, nil, nil, nil, nil, nil],
  #   [:bodystmt, [[:void_stmt]], nil, nil, nil]]
  #
  # OR For endless methods (in 3.0)
  # [:def,
  #   [:@ident, "foo", [1, 6]],
  #   nil,
  #   [:string_literal, [:string_content, [:@tstring_content, "bar", [1, 11]
  # OR For endless methods (in 3.1)
  # [:def,
  #   [:@ident, "foo", [1, 6]],
  #   nil,
  #   [:bodystmt,
  #    [:string_literal, [:string_content, [:@tstring_content, "bar", [1, 11]

  _, name, params, body = node

  consume_keyword "def"
  consume_space

  push_hash(node) do
    visit_def_from_name name, params, body
  end
end

#visit_def_from_name(name, params, body) ⇒ Object



1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
# File 'lib/rufo/formatter.rb', line 1995

def visit_def_from_name(name, params, body)
  visit name

  params = [] if params.nil?
  params = params[1] if params[0] == :paren

  skip_space

  if current_token_kind == :on_lparen
    next_token
    skip_space
    skip_semicolons
    broken_across_line = false

    if empty_params?(params)
      skip_space_or_newline
      check :on_rparen
      next_token
      write "()"
    else
      write "("

      if newline? || comment?
        broken_across_line = true
        indent(next_indent) do
          consume_end_of_line
          write_indent
          visit params
        end
      else
        indent(@column) do
          visit params
        end
      end

      skip_space_or_newline
      consume_keyword("nil") if current_token[1] == :on_kw
      check :on_rparen
      if broken_across_line
        write_line
        write_indent
      end
      write ")"
      next_token
      skip_space
    end
  elsif !empty_params?(params)
    if parens_in_def == :yes
      write "("
    else
      write_space
    end

    visit params
    write ")" if parens_in_def == :yes
    skip_space
  end

  format_endless_method if current_token_kind == :on_op

  visit body
end

#visit_def_with_receiver(node) ⇒ Object



1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
# File 'lib/rufo/formatter.rb', line 1972

def visit_def_with_receiver(node)
  # [:defs,
  # [:vcall, [:@ident, "foo", [1, 5]]],
  # [:@period, ".", [1, 8]],
  # [:@ident, "bar", [1, 9]],
  # [:params, nil, nil, nil, nil, nil, nil, nil],
  # [:bodystmt, [[:void_stmt]], nil, nil, nil]]
  _, receiver, _, name, params, body = node

  consume_keyword "def"
  consume_space
  visit receiver
  skip_space_or_newline

  consume_call_dot

  skip_space_or_newline

  push_hash(node) do
    visit_def_from_name name, params, body
  end
end

#visit_defined(node) ⇒ Object



2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
# File 'lib/rufo/formatter.rb', line 2656

def visit_defined(node)
  # [:defined, exp]
  _, exp = node

  consume_keyword "defined?"
  has_space = space?

  if has_space
    consume_space
  else
    skip_space_or_newline
  end

  has_paren = current_token_kind == :on_lparen

  if has_paren && !has_space
    write "("
    next_token
    skip_space_or_newline
  end

  visit exp

  if has_paren && !has_space
    skip_space_or_newline
    check :on_rparen
    write ")"
    next_token
  end
end

#visit_do_block(node) ⇒ Object



1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
# File 'lib/rufo/formatter.rb', line 1389

def visit_do_block(node)
  # [:brace_block, args, body]
  _, args, body = node

  line = @line

  consume_keyword "do"

  consume_block_args args

  if body.first == :bodystmt
    visit_bodystmt body
  else
    indent_body body
    write_indent unless @line == line
    consume_keyword "end"
  end
end

#visit_end_node(node) ⇒ Object



1722
1723
1724
# File 'lib/rufo/formatter.rb', line 1722

def visit_end_node(node)
  visit_begin_or_end node, "END"
end

#visit_exps(exps, with_indent: false, with_lines: true, want_trailing_multiline: false) ⇒ Object



489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
# File 'lib/rufo/formatter.rb', line 489

def visit_exps(exps, with_indent: false, with_lines: true, want_trailing_multiline: false)
  consume_end_of_line(at_prefix: true)

  line_before_endline = nil

  exps.each_with_index do |exp, i|
    next if exp == :string_content

    exp_kind = exp[0]

    # Skip voids to avoid extra indentation
    if exp_kind == :void_stmt
      next
    end

    if with_indent
      # Don't indent if this exp is in the same line as the previous
      # one (this happens when there's a semicolon between the exps)
      unless line_before_endline && line_before_endline == @line
        write_indent
      end
    end

    line_before_exp = @line
    original_line = current_token_line

    push_node(exp) do
      visit exp
    end

    if declaration?(exp) && @line == line_before_exp
      @inline_declarations << [@line, original_line]
    end

    is_last = last?(i, exps)

    line_before_endline = @line

    if with_lines
      exp_needs_two_lines = needs_two_lines?(exp)

      consume_end_of_line(want_semicolon: !is_last, want_multiline: !is_last || want_trailing_multiline, needs_two_lines_on_comment: exp_needs_two_lines)

      # Make sure to put two lines before defs, class and others
      if !is_last && (exp_needs_two_lines || needs_two_lines?(exps[i + 1])) && @line <= line_before_endline + 1
        write_line
      end
    elsif !is_last
      skip_space

      has_semicolon = semicolon?
      skip_semicolons
      if newline?
        write_line
        write_indent(next_indent)
      elsif has_semicolon
        write "; "
      end
      skip_space_or_newline
    end
  end
end

#visit_find_pattern(node) ⇒ Object



3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
# File 'lib/rufo/formatter.rb', line 3178

def visit_find_pattern(node)
  # [:fndptn, const_ref, pre, patterns, post]
  _, const_ref, pre, patterns, post = node

  parens = if const_ref
      visit const_ref
      current_token_kind == :on_lparen
    else
      false
    end

  if parens
    consume_token :on_lparen
  else
    consume_token :on_lbracket
  end

  skip_space
  consume_op "*"
  if pre[1] # check pre has name or not
    visit pre
  end

  patterns.each do |pattern|
    skip_space
    consume_token :on_comma
    consume_space
    visit pattern
  end

  skip_space
  consume_token :on_comma
  consume_space
  consume_op "*"
  if post[1] # check post has name or not
    visit post
  end

  skip_space
  if parens
    consume_token :on_rparen
  else
    consume_token :on_rbracket
  end
end

#visit_for(node) ⇒ Object



1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
# File 'lib/rufo/formatter.rb', line 1688

def visit_for(node)
  #[:for, var, collection, body]
  _, var, collection, body = node

  line = @line

  consume_keyword "for"
  consume_space

  visit_comma_separated_list to_ary(var)
  skip_space
  if comma?
    check :on_comma
    write ","
    next_token
    skip_space_or_newline
  end

  consume_space
  consume_keyword "in"
  consume_space
  visit collection
  skip_space

  indent_body body

  write_indent if @line != line
  consume_keyword "end"
end

#visit_hash(node) ⇒ Object



2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
# File 'lib/rufo/formatter.rb', line 2321

def visit_hash(node)
  # [:hash, elements]
  _, elements = node
  base_column = @column

  closing_brace_token, _ = find_closing_brace_token
  need_space = need_space_for_hash?(node, node[1], closing_brace_token)

  check :on_lbrace
  write "{"
  brace_position = @output.length - 1
  write " " if need_space
  next_token

  if elements
    # [:assoclist_from_args, elements]
    push_hash(node) do
      visit_literal_elements(elements[1], inside_hash: true, token_column: base_column)
    end
    char_after_brace = @output[brace_position + 1]
    # Check that need_space is set correctly.
    if !need_space && !["\n", " "].include?(char_after_brace)
      need_space = true
      # Add a space in the missing position.
      @output.insert(brace_position + 1, " ")
    end
  else
    skip_space_or_newline
  end

  check :on_rbrace
  write " " if need_space
  write "}"
  next_token
end

#visit_hash_key_value(node) ⇒ Object



2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
# File 'lib/rufo/formatter.rb', line 2357

def visit_hash_key_value(node)
  # key => value
  #
  # [:assoc_new, key, value]
  _, key, value = node

  # If a symbol comes it means it's something like
  # `:foo => 1` or `:"foo" => 1` and a `=>`
  # always follows
  symbol = current_token_kind == :on_symbeg
  arrow = symbol || !(key[0] == :@label || key[0] == :dyna_symbol)

  visit key

  # Don't output `=>` for keys that are `label: value`
  # or `"label": value`
  if arrow
    consume_space
    consume_op "=>"
  end

  if value.nil?
    # The value for the key is omitted.
    skip_space
  else
    consume_space
    visit value
  end
end

#visit_hash_pattern(node) ⇒ Object



3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
# File 'lib/rufo/formatter.rb', line 3224

def visit_hash_pattern(node)
  _, const_ref, elements, rest = node

  if const_ref
    visit const_ref
  end

  token_column = current_token_column

  need_space = false
  expected_right_token = nil
  if const_ref
    if current_token_kind == :on_lparen
      consume_token :on_lparen
      expected_right_token = :on_rparen
    else
      consume_token :on_lbracket
      expected_right_token = :on_rbracket
    end
  elsif current_token_kind == :on_lbrace
    expected_right_token = :on_rbrace
    closing_brace_token, _ = find_closing_brace_token
    need_space = need_space_for_hash?(node, [*elements, rest].compact, closing_brace_token)

    consume_token :on_lbrace
    brace_position = @output.length - 1
    consume_space if need_space
  end

  # pattern is {}
  empty = !const_ref && !elements && !rest
  if empty
    consume_token :on_rbrace
    return
  end

  # pattern is {**}
  empty = !const_ref && elements.empty? && !rest
  if empty
    consume_space
    consume_op "**"
    consume_space
    consume_token :on_rbrace
    return
  end

  visit_literal_elements elements, inside_hash: true, token_column: token_column, keep_final_newline: expected_right_token.nil? do |element|
    key, value = element

    if current_token_kind == :on_tstring_beg
      consume_token :on_tstring_beg
      visit key
      consume_token :on_label_end
    else
      visit key
    end

    if value
      consume_space
      visit value
    end
  end

  if rest || op?("**") || comma?
    unless elements.empty?
      write ","
    end

    skip_space_or_newline
    if rest || op?("**")
      consume_space
      consume_op "**"
      if rest
        visit rest
      end
    end
  end

  unless expected_right_token.nil?
    skip_space

    if expected_right_token == :on_rbrace
      # in some case, need_space_for_hash? might be unexpected behaviour for some patterns, example:
      #   { a: 1,
      #     ** }
      # so re-check need_space? at here, and insert a space in the missing position if needed.
      char_after_brace = @output[brace_position + 1]
      if !need_space && !["\n", " "].include?(char_after_brace)
        need_space = true
        @output.insert(brace_position + 1, " ")
      end
    end

    check expected_right_token
    right = current_token_value
    write " " if need_space
    write right
    next_token
  end
end

#visit_if(node) ⇒ Object



2845
2846
2847
# File 'lib/rufo/formatter.rb', line 2845

def visit_if(node)
  visit_if_or_unless node, "if"
end

#visit_if_or_unless(node, keyword, check_end: true) ⇒ Object



2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
# File 'lib/rufo/formatter.rb', line 2853

def visit_if_or_unless(node, keyword, check_end: true)
  # if cond
  #   then_body
  # else
  #   else_body
  # end
  #
  # [:if, cond, then, else]
  line = @line

  consume_keyword(keyword)
  consume_space
  visit node[1]
  skip_space

  indent_body node[2]
  if (else_body = node[3])
    # [:else, else_contents]
    # [:elsif, cond, then, else]
    write_indent if @line != line

    case else_body[0]
    when :else
      consume_keyword "else"
      indent_body else_body[1]
    when :elsif
      visit_if_or_unless else_body, "elsif", check_end: false
    else
      bug "expected else or elsif, not #{else_body[0]}"
    end
  end

  if check_end
    write_indent if @line != line
    consume_keyword "end"
  end
end

#visit_kwrest_param(node) ⇒ Object



1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
# File 'lib/rufo/formatter.rb', line 1796

def visit_kwrest_param(node)
  # [:kwrest_param, name]

  _, name = node

  if name
    skip_space_or_newline
    visit name
  end
end

#visit_lambda(node) ⇒ Object



2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
# File 'lib/rufo/formatter.rb', line 2572

def visit_lambda(node)
  # [:lambda, [:params, nil, nil, nil, nil, nil, nil, nil], [[:void_stmt]]]
  # [:lambda, [:params, nil, nil, nil, nil, nil, nil, nil], [[:@int, "1", [2, 2]], [:@int, "2", [3, 2]]]]
  # [:lambda, [:params, nil, nil, nil, nil, nil, nil, nil], [:bodystmt, [[:@int, "1", [2, 2]], [:@int, "2", [3, 2]]], nil, nil, nil]] (on 2.6.0)
  _, params, body = node

  body = body[1] if body[0] == :bodystmt
  check :on_tlambda
  write "->"
  next_token

  skip_space

  if empty_params?(params)
    if current_token_kind == :on_lparen
      next_token
      skip_space_or_newline
      check :on_rparen
      next_token
      skip_space_or_newline
    end
  else
    visit params
  end

  if void_exps?(body)
    consume_space
    consume_token :on_tlambeg
    consume_space
    consume_token :on_rbrace
    return
  end

  consume_space

  brace = current_token_value == "{"

  if brace
    closing_brace_token, _ = find_closing_brace_token

    # Check if the whole block fits into a single line
    if current_token_line == closing_brace_token[0][0]
      consume_token :on_tlambeg

      consume_space
      visit_exps body, with_lines: false
      consume_space

      consume_token :on_rbrace
      return
    end

    consume_token :on_tlambeg
  else
    consume_keyword "do"
  end

  indent_body body, force_multiline: true

  write_indent

  if brace
    consume_token :on_rbrace
  else
    consume_keyword "end"
  end
end

#visit_literal_elements(elements, inside_hash: false, inside_array: false, token_column:, keep_final_newline: false, &block) ⇒ Object



2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
# File 'lib/rufo/formatter.rb', line 2707

def visit_literal_elements(elements, inside_hash: false, inside_array: false, token_column:, keep_final_newline: false, &block)
  base_column = @column
  base_line = @line
  needs_final_space = (inside_hash || inside_array) && space?
  skip_space

  if inside_hash
    needs_final_space = false
  end

  if inside_array
    needs_final_space = false
  end

  if newline? || comment?
    needs_final_space = false
  end

  # If there's a newline right at the beginning,
  # write it, and we'll indent element and always
  # add a trailing comma to the last element
  needs_trailing_comma = newline? || comment?
  if needs_trailing_comma
    if (call_info = @line_to_call_info[@line])
      call_info << true
    end

    needed_indent = next_indent
    indent { consume_end_of_line }
    write_indent(needed_indent)
  else
    needed_indent = base_column
  end

  wrote_comma = false
  first_space = nil

  visitor = block_given? ? block : ->(elem) { visit elem }
  elements.each_with_index do |elem, i|
    @literal_elements_level = @node_level

    is_last = last?(i, elements)
    wrote_comma = false

    if needs_trailing_comma
      indent(needed_indent) { visitor.call(elem) }
    else
      visitor.call(elem)
    end

    # We have to be careful not to aumatically write a heredoc on next_token,
    # because we miss the chance to write a comma to separate elements
    first_space = skip_space_no_heredoc_check
    indent(needed_indent) do
      wrote_comma = check_heredocs_in_literal_elements(is_last, wrote_comma)
    end
    next unless comma?

    unless is_last
      write ","
      wrote_comma = true
    end

    # We have to be careful not to aumatically write a heredoc on next_token,
    # because we miss the chance to write a comma to separate elements
    next_token_no_heredoc_check

    first_space = skip_space_no_heredoc_check
    indent(needed_indent) do
      wrote_comma = check_heredocs_in_literal_elements(is_last, wrote_comma)
    end

    if newline? || comment?
      if is_last
        # Nothing
      else
        indent(needed_indent) do
          consume_end_of_line(first_space: first_space)
          write_indent
        end
      end
    else
      write_space unless is_last
    end
  end
  @literal_elements_level = nil

  if needs_trailing_comma
    if !wrote_comma && trailing_commas
      if @last_was_heredoc
        @output.insert(@end_of_heredoc_position, ",")
      else
        write ","
      end
    end

    consume_end_of_line(first_space: first_space)
    write_indent
  elsif comment?
    consume_end_of_line(first_space: first_space)
  else
    if needs_final_space
      consume_space
    elsif keep_final_newline
      skip_space
    else
      skip_space_or_newline
    end
  end

  if current_token_column == token_column && needed_indent < token_column
    # If the closing token is aligned with the opening token, we want to
    # keep it like that, for example in:
    #
    # foo([
    #       2,
    #     ])
    @literal_indents << [base_line, @line, token_column + INDENT_SIZE - needed_indent]
  elsif call_info && call_info[0] == current_token_column
    # If the closing literal position matches the column where
    # the call started, we want to preserve it like that
    # (otherwise we align it to the first parameter)
    call_info << @line
  end
end

#visit_mlhs(node) ⇒ Object



1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
# File 'lib/rufo/formatter.rb', line 1636

def visit_mlhs(node)
  # [:mlsh, *args]
  _, *args = node

  # Sometimes a paren comes, some times not, so act accordingly.
  has_paren = current_token_kind == :on_lparen
  if has_paren
    consume_token :on_lparen
    skip_space_or_newline
  end

  # For some reason there's nested :mlhs_paren for
  # a single parentheses. It seems when there's
  # a nested array we need parens, otherwise we
  # just output whatever's inside `args`.
  if args.is_a?(Array) && args[0].is_a?(Array)
    indent(@column) do
      visit_comma_separated_list args
      skip_space_or_newline
    end
  else
    visit args
  end

  if has_paren
    # Ripper has a bug where parsing `|(w, *x, y), z|`,
    # the "y" isn't returned. In this case we just consume
    # all tokens until we find a `)`.
    while current_token_kind != :on_rparen
      consume_token current_token_kind
    end

    consume_token :on_rparen
  end
end

#visit_module(node) ⇒ Object



1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
# File 'lib/rufo/formatter.rb', line 1927

def visit_module(node)
  # [:module,
  #   name
  #   [:bodystmt, body, nil, nil, nil]]
  _, name, body = node

  push_type(node) do
    consume_keyword "module"
    skip_space_or_newline
    write_space
    visit name

    @inside_type_body = true
    visit body
  end
end

#visit_mrhs_add_star(node) ⇒ Object



1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
# File 'lib/rufo/formatter.rb', line 1672

def visit_mrhs_add_star(node)
  # [:mrhs_add_star, [], [:vcall, [:@ident, "x", [3, 8]]]]
  _, x, y = node

  if x.empty?
    consume_op "*"
    visit y
  else
    visit x
    write_params_comma
    consume_space
    consume_op "*"
    visit y
  end
end

#visit_mrhs_new_from_args(node) ⇒ Object



1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
# File 'lib/rufo/formatter.rb', line 1622

def visit_mrhs_new_from_args(node)
  # Multiple exception types
  # [:mrhs_new_from_args, exps, final_exp]
  _, exps, final_exp = node

  if final_exp
    visit_comma_separated_list exps
    write_params_comma
    visit final_exp
  else
    visit_comma_separated_list to_ary(exps)
  end
end

#visit_multiple_assign(node) ⇒ Object



860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
# File 'lib/rufo/formatter.rb', line 860

def visit_multiple_assign(node)
  # [:massign, lefts, right]
  _, lefts, right = node

  visit_comma_separated_list lefts

  first_space = skip_space

  # A trailing comma can come after the left hand side
  if comma?
    consume_token :on_comma
    first_space = skip_space
  end

  write_space_using_setting(first_space, :one)

  track_assignment
  consume_op "="
  visit_assign_value right
end

#visit_next(node) ⇒ Object



2548
2549
2550
2551
# File 'lib/rufo/formatter.rb', line 2548

def visit_next(node)
  # [:next, exp]
  visit_control_keyword node, "next"
end

#visit_op_assign(node) ⇒ Object



833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
# File 'lib/rufo/formatter.rb', line 833

def visit_op_assign(node)
  # target += value
  #
  # [:opassign, target, op, value]
  _, target, op, value = node

  line = @line

  visit target
  consume_space

  # [:@op, "+=", [1, 2]],
  check :on_op

  before = op[1][0...-1]
  after = op[1][-1]

  write before
  track_assignment before.size
  write after
  next_token

  visit_assign_value value

  @assignments_ranges[line] = @line if @line != line
end

#visit_params(node) ⇒ Object



2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
# File 'lib/rufo/formatter.rb', line 2089

def visit_params(node)
  # (def params)
  #
  # [:params, pre_rest_params, args_with_default, rest_param, post_rest_params, label_params, double_star_param, blockarg]
  _, pre_rest_params, args_with_default, rest_param, post_rest_params, label_params, double_star_param, blockarg = node

  needs_comma = false

  if pre_rest_params
    visit_comma_separated_list pre_rest_params
    needs_comma = true
  end

  if args_with_default
    write_params_comma if needs_comma
    visit_comma_separated_list(args_with_default) do |arg, default|
      visit arg
      consume_space
      consume_op "="
      consume_space
      visit default
    end
    needs_comma = true
  end

  if rest_param
    # check for trailing , |x, | (may be [:excessed_comma] in 2.6.0)
    case rest_param
    when 0, [:excessed_comma]
      write_params_comma
    when [:args_forward]
      write_params_comma if needs_comma
      consume_op "..."
    else
      # [:rest_param, [:@ident, "x", [1, 15]]]
      _, rest = rest_param
      write_params_comma if needs_comma
      consume_op "*"
      skip_space_or_newline
      visit rest if rest
      needs_comma = true
    end
  end

  if post_rest_params
    write_params_comma if needs_comma
    visit_comma_separated_list post_rest_params
    needs_comma = true
  end

  if label_params
    # [[label, value], ...]
    write_params_comma if needs_comma
    visit_comma_separated_list(label_params) do |label, value|
      # [:@label, "b:", [1, 20]]
      write label[1]
      next_token
      skip_space_or_newline
      if value
        consume_space
        visit value
      end
    end
    needs_comma = true
  end

  if double_star_param
    write_params_comma if needs_comma
    case double_star_param
    when [:args_forward] # may be [:args_forward] in 3.1.0
      consume_op "..."
    else
      consume_op "**" # here
      skip_space_or_newline

      # A nameless double star comes as an... Integer? :-S
      visit double_star_param if double_star_param.is_a?(Array)
      skip_space_or_newline
      needs_comma = true
    end
  end

  # In 3.1.0 blockarg may be just a symbol `:&`
  if blockarg && blockarg.is_a?(Array)
    # [:blockarg, [:@ident, "block", [1, 16]]]
    write_params_comma if needs_comma
    skip_space_or_newline
    consume_op "&"
    skip_space_or_newline
    # Ruby 3.1 or above, block arg might be anonymous
    if blockarg[1]
      visit blockarg[1]
    end
  end
end

#visit_paren(node) ⇒ Object



2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
# File 'lib/rufo/formatter.rb', line 2070

def visit_paren(node)
  # ( exps )
  #
  # [:paren, exps]
  _, exps = node

  consume_token :on_lparen
  skip_space_or_newline

  heredoc = current_token_kind == :on_heredoc_beg
  if exps
    visit_exps to_ary(exps), with_lines: false
  end

  skip_space_or_newline
  write "\n" if heredoc
  consume_token :on_rparen
end

#visit_path(node) ⇒ Object



799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
# File 'lib/rufo/formatter.rb', line 799

def visit_path(node)
  # Foo::Bar
  #
  # [:const_path_ref,
  #   [:var_ref, [:@const, "Foo", [1, 0]]],
  #   [:@const, "Bar", [1, 5]]]
  pieces = node[1..-1]
  pieces.each_with_index do |piece, i|
    visit piece
    unless last?(i, pieces)
      consume_op "::"
      skip_space_or_newline
    end
  end
end

#visit_q_or_i_array(node) ⇒ Object



2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
# File 'lib/rufo/formatter.rb', line 2224

def visit_q_or_i_array(node)
  _, elements = node

  # For %W it seems elements appear inside other arrays
  # for some reason, so we flatten them
  if elements[0].is_a?(Array) && elements[0][0].is_a?(Array)
    elements = elements.flat_map { |x| x }
  end

  has_space = current_token_value.end_with?(" ")
  write current_token_value.strip

  # (pre 2.5.0) If there's a newline after `%w(`, write line and indent
  if current_token_value.include?("\n") && elements # "%w[\n"
    write_line
    write_indent next_indent
  end

  next_token

  # fix for 2.5.0 ripper change
  if current_token_kind == :on_words_sep && elements && !elements.empty?
    value = current_token_value
    has_space = value.start_with?(" ")
    if value.include?("\n") && elements # "\n "
      write_line
      write_indent next_indent
    end
    next_token

    # fix for 3.3 ripper change. two :on_words_sep are generated for "#\n "
    while current_token_kind == :on_words_sep
      next_token
    end

    has_space = true if current_token_value.start_with?(" ")
  end

  if elements && !elements.empty?
    write_space if has_space
    column = @column

    elements.each_with_index do |elem, i|
      if elem[0] == :@tstring_content
        # elem is [:@tstring_content, string, [1, 5]
        write elem[1].strip
        next_token
      else
        visit elem
      end

      if !last?(i, elements) && current_token_kind == :on_words_sep
        # On a newline, write line and indent
        if current_token_value.include?("\n")
          next_token
          write_line
          write_indent(column)
          # two :on_words_sep are generated for "#\n " on ruby 3.3
          while current_token_kind == :on_words_sep
            next_token
          end
        else
          next_token
          write_space
        end
      end
    end
  end

  has_newline = false
  last_token = nil

  while current_token_kind == :on_words_sep
    has_newline ||= current_token_value.include?("\n")

    unless current_token[2].strip.empty?
      last_token = current_token
    end

    next_token
  end

  if has_newline
    write_line
    write_indent
  elsif has_space && elements && !elements.empty?
    write_space
  end

  if last_token
    write last_token[2].strip
  else
    write current_token_value.strip
    next_token
  end
end

#visit_quoted_symbol_literal(node) ⇒ Object



781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
# File 'lib/rufo/formatter.rb', line 781

def visit_quoted_symbol_literal(node)
  # :"foo"
  #
  # [:dyna_symbol, exps]
  _, exps = node

  # This is `"...":` as a hash key
  if current_token_kind == :on_tstring_beg
    consume_token :on_tstring_beg
    visit exps
    consume_token :on_label_end
  else
    consume_token :on_symbeg
    visit_exps exps, with_lines: false
    consume_token :on_tstring_end
  end
end

#visit_range(node, inclusive) ⇒ Object



2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
# File 'lib/rufo/formatter.rb', line 2400

def visit_range(node, inclusive)
  # [:dot2, left, right]
  _, left, right = node

  visit left unless left.nil?
  skip_space_or_newline
  consume_op(inclusive ? ".." : "...")
  skip_space_or_newline
  visit right unless right.nil?
end

#visit_regexp_literal(node) ⇒ Object



2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
# File 'lib/rufo/formatter.rb', line 2411

def visit_regexp_literal(node)
  # [:regexp_literal, pieces, [:@regexp_end, "/", [1, 1]]]
  _, pieces = node

  check :on_regexp_beg
  write current_token_value
  next_token

  visit_exps pieces, with_lines: false

  check :on_regexp_end
  write current_token_value
  next_token
end

#visit_rescue_types(node) ⇒ Object



1618
1619
1620
# File 'lib/rufo/formatter.rb', line 1618

def visit_rescue_types(node)
  visit_exps to_ary(node), with_lines: false
end

#visit_rest_param(node) ⇒ Object



1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
# File 'lib/rufo/formatter.rb', line 1783

def visit_rest_param(node)
  # [:rest_param, name]

  _, name = node

  consume_op "*"

  if name
    skip_space_or_newline
    visit name
  end
end

#visit_return(node) ⇒ Object



2538
2539
2540
2541
# File 'lib/rufo/formatter.rb', line 2538

def visit_return(node)
  # [:return, exp]
  visit_control_keyword node, "return"
end

#visit_sclass(node) ⇒ Object



2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
# File 'lib/rufo/formatter.rb', line 2490

def visit_sclass(node)
  # class << self
  #
  # [:sclass, target, body]
  _, target, body = node

  push_type(node) do
    consume_keyword "class"
    consume_space
    consume_op "<<"
    consume_space
    visit target

    @inside_type_body = true
    visit body
  end
end

#visit_setter(node) ⇒ Object



2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
# File 'lib/rufo/formatter.rb', line 2508

def visit_setter(node)
  # foo.bar
  # (followed by `=`, though not included in this node)
  #
  # [:field, receiver, :".", name]
  _, receiver, _, name = node

  @dot_column = nil
  @original_dot_column = nil

  visit receiver

  skip_space_or_newline_using_setting(:no, @dot_column || next_indent)

  # Remember dot column
  dot_column = @column
  original_dot_column = current_token_column

  consume_call_dot

  skip_space_or_newline_using_setting(:no, next_indent)

  visit name

  # Only set it after we visit the call after the dot,
  # so we remember the outmost dot position
  @dot_column = dot_column
  @original_dot_column = original_dot_column
end

#visit_splat_inside_hash(node) ⇒ Object



2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
# File 'lib/rufo/formatter.rb', line 2387

def visit_splat_inside_hash(node)
  # **exp
  #
  # [:assoc_splat, exp]
  consume_op "**"
  skip_space_or_newline
  exp = node[1]
  # The name of rest kwargs might be omitted.
  if exp
    visit exp
  end
end

#visit_string_concat(node) ⇒ Object



711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
# File 'lib/rufo/formatter.rb', line 711

def visit_string_concat(node)
  # string1 string2
  # [:string_concat, string1, string2]
  _, string1, string2 = node

  token_column = current_token_column
  base_column = @column

  visit string1

  has_backslash, _ = skip_space_backslash
  if has_backslash
    write " \\"
    write_line

    # If the strings are aligned, like in:
    #
    # foo bar, "hello"      #          "world"
    #
    # then keep it aligned.
    if token_column == current_token_column
      write_indent(base_column)
    else
      write_indent
    end
  else
    consume_space
  end

  visit string2
end

#visit_string_dvar(node) ⇒ Object



756
757
758
759
760
# File 'lib/rufo/formatter.rb', line 756

def visit_string_dvar(node)
  # [:string_dvar, [:var_ref, [:@ivar, "@foo", [1, 2]]]]
  consume_token :on_embvar
  visit node[1]
end

#visit_string_interpolation(node) ⇒ Object



744
745
746
747
748
749
750
751
752
753
754
# File 'lib/rufo/formatter.rb', line 744

def visit_string_interpolation(node)
  # [:string_embexpr, exps]
  consume_token :on_embexpr_beg
  skip_space_or_newline
  if current_token_kind == :on_tstring_content
    next_token
  end
  visit_exps(node[1], with_lines: false)
  skip_space_or_newline
  consume_token :on_embexpr_end
end

#visit_string_literal(node) ⇒ Object



580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
# File 'lib/rufo/formatter.rb', line 580

def visit_string_literal(node)
  # [:string_literal, [:string_content, exps]]
  heredoc = current_token_kind == :on_heredoc_beg
  tilde = current_token_value.include?("~")

  if heredoc
    write current_token_value.rstrip
    # Accumulate heredoc: we'll write it once
    # we find a newline.
    @heredocs << [node, tilde]
    # Get the next_token while capturing any output.
    # This is needed so that we can add a comma if one is not already present.
    captured_output = capture_output { next_token }

    inside_literal_elements_list = !@literal_elements_level.nil? &&
                                   (@node_level - @literal_elements_level) == 2
    needs_comma = !comma? && trailing_commas

    if inside_literal_elements_list && needs_comma
      @last_was_heredoc = true
      @end_of_heredoc_position = @output.length
    end

    @output << captured_output
    return
  elsif current_token_kind == :on_backtick
    consume_token :on_backtick
  else
    return if format_simple_string(node)
    consume_token :on_tstring_beg
  end

  visit_string_literal_end(node)
end

#visit_string_literal_end(node) ⇒ Object



682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
# File 'lib/rufo/formatter.rb', line 682

def visit_string_literal_end(node)
  inner = node[1]
  inner = inner[1..-1] unless node[0] == :xstring_literal

  with_unmodifiable_string_lines do
    visit_exps(inner, with_lines: false)
  end

  case current_token_kind
  when :on_heredoc_end
    heredoc, tilde = @current_heredoc
    if heredoc && tilde
      write_indent
      write current_token_value.strip
    else
      write current_token_value.rstrip
    end
    next_token
    skip_space

    # Simulate a newline after the heredoc
    @tokens << [[0, 0], :on_ignored_nl, "\n"]
  when :on_backtick
    consume_token :on_backtick
  else
    consume_token :on_tstring_end
  end
end

#visit_suffix(node, suffix) ⇒ Object



958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
# File 'lib/rufo/formatter.rb', line 958

def visit_suffix(node, suffix)
  # then if cond
  # then unless cond
  # exp rescue handler
  #
  # [:if_mod, cond, body]
  _, body, cond = node

  if suffix != "rescue"
    body, cond = cond, body
  end

  visit body
  consume_space
  consume_keyword(suffix)
  consume_space_or_newline
  visit cond
end

#visit_super(node) ⇒ Object



2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
# File 'lib/rufo/formatter.rb', line 2640

def visit_super(node)
  # [:super, args]
  _, args = node

  base_column = current_token_column

  consume_keyword "super"

  if space?
    consume_space
    visit_command_end node, args, base_column
  else
    visit_call_at_paren node, args
  end
end

#visit_symbol(node) ⇒ Object



773
774
775
776
777
778
779
# File 'lib/rufo/formatter.rb', line 773

def visit_symbol(node)
  # :foo
  #
  # [:symbol, [:@ident, "foo", [1, 1]]]
  consume_token :on_symbeg
  visit_exps node[1..-1], with_lines: false
end

#visit_symbol_literal(node) ⇒ Object



762
763
764
765
766
767
768
769
770
771
# File 'lib/rufo/formatter.rb', line 762

def visit_symbol_literal(node)
  # :foo
  #
  # [:symbol_literal, [:symbol, [:@ident, "foo", [1, 1]]]]
  #
  # A symbol literal not necessarily begins with `:`.
  # For example, an `alias foo bar` will treat `foo`
  # a as symbol_literal but without a `:symbol` child.
  visit node[1]
end

#visit_ternary_if(node) ⇒ Object



941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
# File 'lib/rufo/formatter.rb', line 941

def visit_ternary_if(node)
  # cond ? then : else
  #
  # [:ifop, cond, then_body, else_body]
  _, cond, then_body, else_body = node

  visit cond
  consume_space
  consume_op "?"
  consume_space_or_newline
  visit then_body
  consume_space
  consume_op ":"
  consume_space_or_newline
  visit else_body
end

#visit_unary(node) ⇒ Object



1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
# File 'lib/rufo/formatter.rb', line 1807

def visit_unary(node)
  # [:unary, :-@, [:vcall, [:@ident, "x", [1, 2]]]]
  _, op, exp = node

  consume_op_or_keyword

  first_space = space?
  skip_space_or_newline

  if op == :not
    has_paren = current_token_kind == :on_lparen

    if has_paren && !first_space
      write "("
      next_token
      skip_space_or_newline
    elsif !has_paren && !consume_space
      write_space
    end

    visit exp

    if has_paren && !first_space
      skip_space_or_newline
      check :on_rparen
      write ")"
      next_token
    end
  else
    visit exp
  end
end

#visit_undef(node) ⇒ Object



2698
2699
2700
2701
2702
2703
2704
2705
# File 'lib/rufo/formatter.rb', line 2698

def visit_undef(node)
  # [:undef, exps]
  _, exps = node

  consume_keyword "undef"
  consume_space
  visit_comma_separated_list exps
end

#visit_unless(node) ⇒ Object



2849
2850
2851
# File 'lib/rufo/formatter.rb', line 2849

def visit_unless(node)
  visit_if_or_unless node, "unless"
end

#visit_until(node) ⇒ Object



2896
2897
2898
2899
# File 'lib/rufo/formatter.rb', line 2896

def visit_until(node)
  # [:until, cond, body]
  visit_while_or_until node, "until"
end

#visit_when(node) ⇒ Object



2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
# File 'lib/rufo/formatter.rb', line 2954

def visit_when(node)
  # [:when, conds, body, next_exp]
  # [:in, pattern, body, next_exp]
  kw, conds_or_pattern, body, next_exp = node

  case kw
  when :when
    consume_keyword "when"
  when :in
    if current_token_kind == :on_op
      consume_op "=>"
    else
      consume_keyword "in"
    end
  end
  consume_space

  indent(@column) do
    case kw
    when :when
      visit_comma_separated_list conds_or_pattern
    when :in
      parens = current_token_kind == :on_lparen
      if parens
        consume_token :on_lparen
        skip_space_or_newline
      end

      visit conds_or_pattern

      if parens
        skip_space_or_newline
        consume_token :on_rparen
      end
    end
    skip_space
  end
  written_space = false
  if semicolon?
    inline = true
    skip_semicolons

    if newline? || comment?
      inline = false
    else
      write ";"
      track_case_when
      write " "
      written_space = true
    end
  end

  if keyword?("then")
    inline = true
    next_token

    skip_space

    info = track_case_when
    skip_semicolons

    if newline?
      inline = false

      # Cancel tracking of `case when ... then` on a nelwine.
      @case_when_positions.pop
    else
      write_space unless written_space

      write "then"

      # We adjust the column and offset from:
      #
      #     when 1 then 2
      #           ^ (with offset 0)
      #
      # to:
      #
      #     when 1 then 2
      #                ^ (with offset 5)
      #
      # In that way we can align this with an `else` clause.
      if info
        offset = @column - info[1]
        info[1] = @column
        info[-1] = offset
      end

      write_space
    end
  end

  # If node is inline pattern matching, body will be nil
  if body
    if inline
      indent do
        visit_exps body
      end
    else
      indent_body body
    end
  end

  if next_exp
    write_indent

    if next_exp[0] == :else
      # [:else, body]
      consume_keyword "else"
      track_case_when
      first_space = skip_space

      if newline? || semicolon? || comment?
        # Cancel tracking of `else` on a nelwine.
        @case_when_positions.pop

        indent_body next_exp[1]
      else
        if align_case_when
          write_space
        else
          write_space_using_setting(first_space, :one)
        end
        visit_exps next_exp[1]
      end
    else
      visit next_exp
    end
  end
end

#visit_while(node) ⇒ Object



2891
2892
2893
2894
# File 'lib/rufo/formatter.rb', line 2891

def visit_while(node)
  # [:while, cond, body]
  visit_while_or_until node, "while"
end

#visit_while_or_until(node, keyword) ⇒ Object



2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
# File 'lib/rufo/formatter.rb', line 2901

def visit_while_or_until(node, keyword)
  _, cond, body = node

  line = @line

  consume_keyword keyword
  consume_space

  visit cond

  indent_body body

  write_indent if @line != line
  consume_keyword "end"
end

#visit_yield(node) ⇒ Object



2553
2554
2555
2556
# File 'lib/rufo/formatter.rb', line 2553

def visit_yield(node)
  # [:yield, exp]
  visit_control_keyword node, "yield"
end

#void_exps?(node) ⇒ Boolean

Returns:

  • (Boolean)


3936
3937
3938
# File 'lib/rufo/formatter.rb', line 3936

def void_exps?(node)
  node.size == 1 && node[0].size == 1 && node[0][0] == :void_stmt
end

#with_unmodifiable_string_linesObject

Every line between the first line and end line of this string (excluding the first line) must remain like it is now (we don’t want to mess with that when indenting/dedenting)

This can happen with heredocs, but also with string literals spanning multiple lines.



674
675
676
677
678
679
680
# File 'lib/rufo/formatter.rb', line 674

def with_unmodifiable_string_lines
  line = @line
  yield
  (line + 1..@line).each do |i|
    @unmodifiable_string_lines[i] = true
  end
end

#write(value) ⇒ Object



3792
3793
3794
3795
3796
3797
# File 'lib/rufo/formatter.rb', line 3792

def write(value)
  @output << value
  @last_was_newline = false
  @last_was_heredoc = false
  @column += value.size
end

#write_indent(indent = @indent) ⇒ Object



3831
3832
3833
3834
# File 'lib/rufo/formatter.rb', line 3831

def write_indent(indent = @indent)
  @output << (" " * indent)
  @column += indent
end

#write_lineObject



3824
3825
3826
3827
3828
3829
# File 'lib/rufo/formatter.rb', line 3824

def write_line
  @output << "\n"
  @last_was_newline = true
  @column = 0
  @line += 1
end

#write_params_comma(with_indent: true) ⇒ Object



2185
2186
2187
2188
2189
2190
2191
2192
2193
# File 'lib/rufo/formatter.rb', line 2185

def write_params_comma(with_indent: true)
  skip_space
  check :on_comma
  write ","
  next_token

  indent_size = with_indent ? @indent : 0
  skip_space_or_newline_using_setting(:one, indent_size)
end

#write_space(value = " ") ⇒ Object



3799
3800
3801
3802
# File 'lib/rufo/formatter.rb', line 3799

def write_space(value = " ")
  @output << value
  @column += value.size
end

#write_space_using_setting(first_space, setting, at_least_one: false) ⇒ Object



3804
3805
3806
3807
3808
3809
3810
# File 'lib/rufo/formatter.rb', line 3804

def write_space_using_setting(first_space, setting, at_least_one: false)
  if first_space && setting == :dynamic
    write_space first_space[2]
  elsif setting == :one || at_least_one
    write_space
  end
end