Class: RubyCrystalCodemod::Formatter

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

Constant Summary collapse

INDENT_SIZE =
2

Constants included from Settings

Settings::OPTIONS

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Settings

#init_settings

Constructor Details

#initialize(code, filename, dir, **options) ⇒ Formatter

Returns a new instance of Formatter.



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
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/ruby_crystal_codemod/formatter.rb', line 20

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

  @code = code
  @code_lines = code.lines
  @prev_token = nil
  @tokens = Ripper.lex(code).reverse!
  @sexp = Ripper.sexp(code)

  # ap @tokens
  ap @sexp if ENV["SHOW_SEXP"]

  unless @sexp
    raise ::RubyCrystalCodemod::SyntaxError.new
  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

  @store_logs = false
  @logs = []

  init_settings(options)
end

Instance Attribute Details

#logsObject

Returns the value of attribute logs.



10
11
12
# File 'lib/ruby_crystal_codemod/formatter.rb', line 10

def logs
  @logs
end

Class Method Details

.format(code, filename, dir, **options) ⇒ Object



14
15
16
17
18
# File 'lib/ruby_crystal_codemod/formatter.rb', line 14

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

Instance Method Details

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



4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4170

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

#bug(msg) ⇒ Object



3919
3920
3921
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3919

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

#capture_outputObject



3821
3822
3823
3824
3825
3826
3827
3828
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3821

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

#check(kind) ⇒ Object



3913
3914
3915
3916
3917
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3913

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



3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3131

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)


3962
3963
3964
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3962

def comma?
  current_token_kind == :on_comma
end

#comment?Boolean

Returns:

  • (Boolean)


3954
3955
3956
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3954

def comment?
  current_token_kind == :on_comment
end

#consume_block_args(args) ⇒ Object



1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1743

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



1073
1074
1075
1076
1077
1078
1079
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1073

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

#consume_embedded_commentObject



3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3693

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



3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3706

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?



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
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3513

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
      # I don't know why but sometimes a on_ignored_nl
      # can appear with nil as the "text", and that's wrong
      if current_token[2].nil?
        next_token
        next
      end

      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



3490
3491
3492
3493
3494
3495
3496
3497
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3490

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



3499
3500
3501
3502
3503
3504
3505
3506
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3499

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



2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2253

def consume_op_or_keyword
  # Crystal doesn't have and / or
  # See: https://crystal-lang.org/reference/syntax_and_semantics/operators.html
  value = current_token_value
  case value
  when "and"
    value = "&&"
  when "or"
    value = "||"
  end

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

#consume_space(want_preserve_whitespace: false) ⇒ Object



3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3334

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



1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1572

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



3345
3346
3347
3348
3349
3350
3351
3352
3353
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3345

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



3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3442

def consume_token(kind)
  check kind

  value = current_token_value
  if kind == :on_ident
    # Some of these might be brittle and change too much, but this shouldn't be an issue,
    # because any mistakes will be caught by the Crystal type-checker.
    case value
    when "__dir__"
      value = "__DIR__"
    when "include?"
      value = "includes?"
    when "key?"
      value = "has_key?"
    when "detect"
      value = "find"
    when "collect"
      value = "map"
    when "respond_to?"
      value = "responds_to?"
    when "length", "count"
      value = "size"
    when "attr_accessor"
      value = "property"
    when "attr_reader"
      value = "getter"
    when "attr_writer"
      value = "setter"
    end
  end

  consume_token_value(value)
  next_token
end

#consume_token_value(value) ⇒ Object



3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3477

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)


953
954
955
956
957
# File 'lib/ruby_crystal_codemod/formatter.rb', line 953

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”


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

def current_token
  @tokens.last
end

#current_token_columnObject



3942
3943
3944
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3942

def current_token_column
  current_token[0][1]
end

#current_token_kindObject



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

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

#current_token_lineObject



3938
3939
3940
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3938

def current_token_line
  current_token[0][0]
end

#current_token_valueObject



3933
3934
3935
3936
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3933

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

#declaration?(exp) ⇒ Boolean

Returns:

  • (Boolean)


597
598
599
600
601
602
603
604
# File 'lib/ruby_crystal_codemod/formatter.rb', line 597

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

#dedent_callsObject



4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4060

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



4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
4162
4163
4164
4165
4166
4167
4168
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4121

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



4117
4118
4119
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4117

def do_align_case_when
  do_align @case_when_positions, :case
end

#empty_body?(body) ⇒ Boolean

Returns:

  • (Boolean)


3436
3437
3438
3439
3440
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3436

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

#empty_params?(node) ⇒ Boolean

Returns:

  • (Boolean)


2408
2409
2410
2411
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2408

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

#find_closing_brace_tokenObject



3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3974

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



1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1477

def flush_heredocs
  if comment?
    write_space unless @output[-1] == " "
    write current_token_value.rstrip
    next_token
    write_line
    if @heredocs.last[1]
      write_indent(next_indent)
    end
  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



196
197
198
199
200
201
202
203
204
205
206
# File 'lib/ruby_crystal_codemod/formatter.rb', line 196

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
end

#format_simple_string(node) ⇒ Object



672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
# File 'lib/ruby_crystal_codemod/formatter.rb', line 672

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



3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3721

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



3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3874

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



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
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3734

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



4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4094

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

#indentable_value?(value) ⇒ Boolean

Returns:

  • (Boolean)


933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
# File 'lib/ruby_crystal_codemod/formatter.rb', line 933

def indentable_value?(value)
  return unless current_token_kind == :on_kw

  case current_token_value
  when "if", "unless", "case"
    true
  when "begin"
    # Only indent if it's begin/rescue
    return false unless value[0] == :begin

    body = value[1]
    return false unless body[0] == :bodystmt

    _, _, rescue_body, else_body, ensure_body = body
    rescue_body || else_body || ensure_body
  else
    false
  end
end

#keyword?(keyword) ⇒ Boolean

Returns:

  • (Boolean)


3946
3947
3948
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3946

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

#last?(index, array) ⇒ Boolean

Returns:

  • (Boolean)


4019
4020
4021
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4019

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

#log(str = "") ⇒ Object



188
189
190
191
192
193
194
# File 'lib/ruby_crystal_codemod/formatter.rb', line 188

def log(str = "")
  if @store_logs
    @logs << str
    return
  end
  puts str
end

#maybe_indent(toggle, indent_size) ⇒ Object



3811
3812
3813
3814
3815
3816
3817
3818
3819
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3811

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

#need_space_for_hash?(node, closing_brace_token) ⇒ Boolean

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

Returns:

  • (Boolean)


4201
4202
4203
4204
4205
4206
4207
4208
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4201

def need_space_for_hash?(node, closing_brace_token)
  return false unless node[1]

  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)


578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
# File 'lib/ruby_crystal_codemod/formatter.rb', line 578

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)


3950
3951
3952
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3950

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

#next_indentObject



3909
3910
3911
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3909

def next_indent
  @indent + INDENT_SIZE
end

#next_tokenObject



3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3992

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



4015
4016
4017
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4015

def next_token_no_heredoc_check
  @tokens.pop
end

#node_line(node, beginning: true) ⇒ Object



4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4210

def node_line(node, beginning: true)
  # 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
    node_line(node[1], beginning: beginning)
  when :assoc_new
    if beginning
      node_line(node[1], beginning: beginning)
    else
      if node.last == [:string_literal, [:string_content]] || node.last == [:hash, nil]
        # there's no line number for [:string_literal, [:string_content]] or [:hash, nil]
        node_line(node[1], beginning: beginning)
      else
        node_line(node.last, beginning: beginning)
      end
    end
  when :assoclist_from_args
    node_line(beginning ? node[1][0] : node[1].last, beginning: beginning)
  when :dyna_symbol
    if node[1][0].is_a?(Symbol)
      node_line(node[1], beginning: beginning)
    else
      node_line(node[1][0], beginning: beginning)
    end
  when :@label, :@int, :@ident, :@tstring_content, :@kw
    node[2][0]
  end
end

#parse_require_path_from_ruby_code(_node, _ident, _next_level) ⇒ Object

Parses any Ruby code, and attempts to evaluate it

require File.expand_path('./nested_require', File.dirname(__FILE__))


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
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
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
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1207

def parse_require_path_from_ruby_code(_node, _ident, _next_level)
  crystal_path = nil
  (line_no, column_no), _kind = current_token

  # Need to figure out all of the Ruby code to execute, which may span across multiple lines.
  # (This heuristic probably won't work for all valid Ruby code, but it's a good start.)
  paren_count = 0
  require_tokens = []
  @tokens.reverse_each.with_index do |token, i|
    next if i == 0
    _, name = token
    case name
    when :on_nl, :on_semicolon
      break if paren_count == 0
      next if paren_count == 1
    when :on_lparen
      paren_count += 1
    when :on_rparen
      paren_count -= 1
    end

    require_tokens << token[2]
  end

  require_string = require_tokens.join("").strip

  show_error_divider("\n")
  log "WARNING: require statements can only use strings in Crystal. Error at line #{line_no}:#{column_no}:"
  log
  log "#{require_string}"
  log
  unless require_string.include?("File.")
    log "===> require args do not start with 'File.', so not attempting to evaluate the code.\n"
    show_requiring_files_docs
    return false
  end

  show_requiring_files_docs
  log "\n==> Attempting to expand and evaluate the Ruby require path..."

  # Expand __dir__ and __FILE__ into absolute paths
  expanded_dir = File.expand_path(@dir)
  expanded_file = File.expand_path(@filename)
  expanded_require_string = require_string
    .gsub("__dir__", "\"#{expanded_dir}\"")
    .gsub("__FILE__", "\"#{expanded_file}\"")

  log "====> Expanded __dir__ and __FILE__: #{expanded_require_string}"

  evaluated_path = nil
  begin
    log "====> Evaluating Ruby code: #{expanded_require_string}"
    # rubocop:disable Security/Eval
    evaluated_path = eval(expanded_require_string)
    # rubocop:enable Security/Eval
  rescue StandardError => e
    log "ERROR: We tried to evaluate and expand the path, but it crashed with an error:"
    log e
  end

  if evaluated_path == nil || evaluated_path == ""
    log "ERROR: We tried to evaluate and expand the path, but it didn't return anything."
  elsif !evaluated_path.is_a?(String)
    log "====> Evaluated path was not a string! Please fix this require statement manually."
    log "====> Result of Ruby evaluation: #{evaluated_path}"
    return nil
  else
    if !evaluated_path.to_s.match?(/\.rb$/)
      evaluated_path = "#{evaluated_path}.rb"
    end
    log "====> Evaluated Ruby path: #{evaluated_path}"

    if File.exist?(evaluated_path)
      expanded_evaluated_path = File.expand_path(evaluated_path)
      crystal_path = expanded_evaluated_path.sub("#{Dir.getwd}/", "").sub(/\.rb$/, "")
      log "======> Successfully expanded the require path and found the file: #{evaluated_path}"
      log "======> Crystal require: #{crystal_path}"
    else
      log "======> ERROR: Could not find #{evaluated_path}! Please fix this require statement manually."
    end
  end

  if crystal_path.nil? || crystal_path == ""
    log "ERROR: Couldn't parse and evaluate the Ruby require statement! Please update the require statement manually."
    return nil
  end
  show_error_divider("", "\n")

  crystal_path
end

#parse_simple_require_path(_node, _ident, next_level) ⇒ Object

Parses:

require "test"
require_relative "test"
require_relative("test")
require("test")


1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1303

def parse_simple_require_path(_node, _ident, next_level)
  return unless next_level.is_a?(Array)

  if next_level[0] == :arg_paren
    return unless (next_level = next_level[1]) && next_level.is_a?(Array)
  end
  return unless next_level[0] == :args_add_block && (next_level = next_level[1]) && next_level.is_a?(Array)
  return unless (next_level = next_level[0]) && next_level.is_a?(Array)
  return unless next_level[0] == :string_literal && (next_level = next_level[1]) && next_level.is_a?(Array)
  return unless next_level[0] == :string_content && (next_level = next_level[1]) && next_level.is_a?(Array)

  if next_level[0] == :string_embexpr
    show_error_divider("\n")
    (line_no, column_no), _kind = current_token
    log "ERROR: String interpolation is not supported for Crystal require statements! " \
        "Please update the require statement manually."
    log "Error at line #{line_no}:#{column_no}:"
    log
    log @code_lines[line_no - 1]
    log
    show_requiring_files_docs
    show_error_divider("", "\n")
    return false
  end

  return unless next_level[0] == :@tstring_content && (next_level = next_level[1]) && next_level.is_a?(String)
  next_level
end

#push_call(node) ⇒ Object



4023
4024
4025
4026
4027
4028
4029
4030
4031
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4023

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



4042
4043
4044
4045
4046
4047
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4042

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

#push_node(node) ⇒ Object



4033
4034
4035
4036
4037
4038
4039
4040
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4033

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

  yield

  @current_node = old_node
end

#push_type(node) ⇒ Object



4049
4050
4051
4052
4053
4054
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4049

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?



655
656
657
# File 'lib/ruby_crystal_codemod/formatter.rb', line 655

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

#remove_current_command_from_tokensObject



1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1372

def remove_current_command_from_tokens
  paren_count = 0
  loop do
    token = @tokens.last
    raise "[Infinite loop bug] Ran out of tokens!" unless token
    _, name = token
    case name
    when :on_nl, :on_semicolon
      if paren_count == 0
        @tokens.pop
        break
      end
    when :on_lparen
      paren_count += 1
    when :on_rparen
      paren_count -= 1
    end
    @tokens.pop
  end
end

#remove_lines_before_inline_declarationsObject



4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4182

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

#replace_require_statement(node, ident, args) ⇒ Object



1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
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
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1393

def replace_require_statement(node, ident, args)
  # RubyCrystalCodemod doesn't replace single quotes with double quotes for require statements, so
  # we have to fix that manually here. (The double quote replacement seems to work everywhere else.)
  require_path = require_path_from_args(node, ident, args)
  return false if require_path == false

  unless require_path
    show_error_divider("\n")
    (line_no, column_no), _kind = current_token
    log "ERROR: Couldn't find a valid path argument for require! Error at line #{line_no}:#{column_no}:"
    log
    log @code_lines[line_no - 1]
    log
    show_requiring_files_docs
    show_error_divider("", "\n")
    return false
  end

  if ident == "require_relative" && !require_path.match?(/^..\//) && !require_path.match?(/^.\//)
    require_path = "./#{require_path}"
  end

  crystal_path = require_path

  # Rewrite all the tokens with the Crystal require statement.
  remove_current_command_from_tokens

  @tokens += [
    [[0, 0], :on_nl, "\n", nil],
    [[0, 0], :on_tstring_end, '"', nil],
    [[0, 0], :on_tstring_content, crystal_path, nil],
    [[0, 0], :on_tstring_beg, '"', nil],
    [[0, 0], :on_sp, " ", nil],
    [[0, 0], :on_ident, "require", nil],
  ]

  node = [:command, [:@ident, "require", [0, 0]], [:args_add_block,
                                                   [[:string_literal,
                                                     [:string_content,
                                                      [:@tstring_content, crystal_path, [0, 0]]]]], false]]
  _, name, args = node

  base_column = current_token_column

  push_call(node) do
    visit name
    consume_space_after_command_name
  end
  push_call(node) do
    visit_command_args(args, base_column)
  end
  true
end

#require_path_from_args(node, ident, args) ⇒ Object



1332
1333
1334
1335
1336
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
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1332

def require_path_from_args(node, ident, args)
  simple_path = parse_simple_require_path(node, ident, args)
  return false if simple_path == false

  if simple_path
    # We now know that this was a simple string arg (either in parens, or after a space)
    # So now we need to see if it's a single or double quoted string.
    quote_char = nil
    @tokens.reverse_each.with_index do |token, _i|
      (_line_no, _column_no), kind = token
      case kind
      when :on_tstring_beg
        quote_char = token[2]
      when :on_nl
        break
      end
    end
    unless quote_char
      raise "Couldn't figure out the quote type for this string!"
    end

    # Now fix the quote escaping
    if quote_char == "'"
      simple_path = simple_path.gsub('"', "\\\"").gsub("\\'", "'")
    end
    return simple_path
  end

  parse_require_path_from_ruby_code(node, ident, args)
end

#resultObject



4196
4197
4198
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4196

def result
  @output
end

#semicolon?Boolean

Returns:

  • (Boolean)


3958
3959
3960
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3958

def semicolon?
  current_token_kind == :on_semicolon
end

#should_format_string?(string) ⇒ Boolean

should we format this string according to :quote_style?

Returns:

  • (Boolean)


660
661
662
663
664
665
666
667
668
669
670
# File 'lib/ruby_crystal_codemod/formatter.rb', line 660

def should_format_string?(string)
  # 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

#show_error_divider(prefix = "", suffix = "") ⇒ Object



1368
1369
1370
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1368

def show_error_divider(prefix = "", suffix = "")
  log "#{prefix}-------------------------------------------------------------------------------\n#{suffix}"
end

#show_requiring_files_docsObject



1363
1364
1365
1366
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1363

def show_requiring_files_docs
  log "===> Read the 'Requiring files' page in the Crystal docs:"
  log "===> https://crystal-lang.org/reference/syntax_and_semantics/requiring_files.html"
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.



644
645
646
647
648
649
650
651
652
# File 'lib/ruby_crystal_codemod/formatter.rb', line 644

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



3361
3362
3363
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3361

def skip_ignored_space
  next_token while current_token_kind == :on_ignored_sp
end

#skip_semicolonsObject



3430
3431
3432
3433
3434
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3430

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

#skip_spaceObject



3355
3356
3357
3358
3359
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3355

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

#skip_space_backslashObject



3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3373

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



3365
3366
3367
3368
3369
3370
3371
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3365

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



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
3420
3421
3422
3423
3424
3425
3426
3427
3428
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3385

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



3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3850

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)


3966
3967
3968
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3966

def space?
  current_token_kind == :on_sp
end

#to_ary(node) ⇒ Object



4056
4057
4058
# File 'lib/ruby_crystal_codemod/formatter.rb', line 4056

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

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



976
977
978
979
980
981
982
983
984
985
986
987
# File 'lib/ruby_crystal_codemod/formatter.rb', line 976

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



968
969
970
# File 'lib/ruby_crystal_codemod/formatter.rb', line 968

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

#track_case_whenObject



972
973
974
# File 'lib/ruby_crystal_codemod/formatter.rb', line 972

def track_case_when
  track_alignment :case_whem, @case_when_positions
end

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



959
960
961
962
963
964
965
966
# File 'lib/ruby_crystal_codemod/formatter.rb', line 959

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



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
488
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
# File 'lib/ruby_crystal_codemod/formatter.rb', line 208

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
    #
    # [:@int, "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 :@gvar
    # [:@gvar, "$abc", [1, 0]]
    write node[1]
    next_token
  when :@backref
    # [:@backref, "$1", [1, 0]]
    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 && @last_was_newline
        unless (current_token_value == "\n" ||
                current_token_kind == :on_heredoc_end)
          write_indent(next_indent)
        end
        skip_ignored_space
        if current_token_kind == :on_tstring_content
          check :on_tstring_content
          consume_token_value(current_token_value)
          next_token
        end
      else
        while (current_token_kind == :on_ignored_sp) ||
              (current_token_kind == :on_tstring_content) ||
              (current_token_kind == :on_embexpr_beg)
          check current_token_kind
          break if current_token_kind == :on_embexpr_beg
          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_token :on_ident
  when :var_ref
    # [:var_ref, exp]
    visit node[1]
  when :var_field
    # [:var_field, exp]
    visit node[1]
  when :@kw
    # [:@kw, "nil", [1, 0]]
    consume_token :on_kw
  when :@ivar
    # [:@ivar, "@foo", [1, 0]]
    consume_token :on_ivar
  when :@cvar
    # [:@cvar, "@@foo", [1, 0]]
    consume_token :on_cvar
  when :@const
    # [:@const, "FOO", [1, 0]]
    consume_token :on_const
  when :const_ref
    # [:const_ref, [:@const, "Foo", [1, 8]]]
    visit node[1]
  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
    visit_path(node)
  when :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 :vcall
    # [:vcall, exp]
    visit node[1]
  when :fcall
    # [:fcall, [:@ident, "foo", [1, 0]]]
    visit node[1]
  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
    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_paren
    visit_mlhs_paren(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 :@label
    # [:@label, "foo:", [1, 3]]
    write node[1]
    next_token
  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 :@op
    # [:@op, "*", [1, 1]]
    write node[1]
    next_token
  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 :mlhs_add_star
    visit_mlhs_add_star(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)
  else
    bug "Unhandled node: #{node.first}"
  end
ensure
  @node_level -= 1
end

#visit_alias(node) ⇒ Object



2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2997

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



1833
1834
1835
1836
1837
1838
1839
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
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1833

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

  write_params_comma if comma?
  write_indent(base_column) if needs_indent
  consume_op "*"
  skip_space_or_newline
  visit star

  if post_args && !post_args.empty?
    write_params_comma
    visit_comma_separated_list post_args, needs_indent: needs_indent, base_column: base_column
  end
end

#visit_array(node) ⇒ Object



2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2524

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

  token_column = current_token_column

  check :on_lbracket
  write "["
  next_token

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

  check :on_rbracket
  write "]"
  next_token
end

#visit_array_access(node) ⇒ Object



2736
2737
2738
2739
2740
2741
2742
2743
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2736

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



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
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2755

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_setter(node) ⇒ Object



2745
2746
2747
2748
2749
2750
2751
2752
2753
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2745

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



848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
# File 'lib/ruby_crystal_codemod/formatter.rb', line 848

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



914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
# File 'lib/ruby_crystal_codemod/formatter.rb', line 914

def visit_assign_value(value)
  has_slash_newline, _first_space = skip_space_backslash

  sticky = indentable_value?(value)

  # Remove backslash after equal + newline (it's useless)
  if has_slash_newline
    skip_space_or_newline
    write_line
    indent(next_indent) do
      write_indent
      visit(value)
    end
  else
    indent_after_space value, sticky: sticky,
                              want_space: true
  end
end

#visit_begin(node) ⇒ Object



1866
1867
1868
1869
1870
1871
1872
1873
1874
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1866

def visit_begin(node)
  # begin
  #   body
  # end
  #
  # [:begin, [:bodystmt, body, rescue_body, else_body, ensure_body]]
  consume_keyword "begin"
  visit node[1]
end

#visit_begin_node(node) ⇒ Object



2046
2047
2048
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2046

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

#visit_begin_or_end(node, keyword) ⇒ Object



2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2054

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



2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
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
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2203

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



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
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1754

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
    if current_token_value == "|"
      next_token
      skip_space_or_newline
      check :on_op
      next_token
    else
      next_token
    end
    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



1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
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
1926
1927
1928
1929
1930
1931
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1876

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

  indent_body body

  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"
end

#visit_brace_block(node) ⇒ Object



1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
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
1717
1718
1719
1720
1721
1722
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1672

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



2853
2854
2855
2856
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2853

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

#visit_call_args(node) ⇒ Object



1806
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
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1806

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
    skip_space_or_newline

    if comma?
      indent(next_indent) do
        write_params_comma
      end
    end

    # Block operator changed from &: to &. in Crystal
    consume_op "&"
    skip_space_or_newline
    visit block_arg
  end
end

#visit_call_at_paren(node, args) ⇒ Object



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
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1118

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
      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?

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

        next_token
        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
        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
        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
    write_line
  end
  consume_token :on_rparen
end

#visit_call_with_block(node) ⇒ Object



1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1655

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



1025
1026
1027
1028
1029
1030
1031
1032
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
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1025

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



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
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1081

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

  if name.is_a?(Array) && name[1].is_a?(Array)
    ident = name[1][1]
    case ident
    when "require", "require_relative"
      return if replace_require_statement(node, ident, args)
    end
  end

  @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



3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3215

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

  consume_keyword "case"

  if cond
    consume_space
    visit cond
  end

  consume_end_of_line

  write_indent
  visit case_when

  write_indent
  consume_keyword "end"
end

#visit_class(node) ⇒ Object



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
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2273

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



2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2079

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



1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1447

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

  if name.is_a?(Array) && name[0] == :@ident
    ident = name[1]
    case ident
    when "require", "require_relative"
      return if replace_require_statement(node, ident, args)
    end
  end

  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



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
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1583

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



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
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1503

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

  # Check for $: var and LOAD_PATH, which are unsupported in Crystal
  if receiver[0] == :var_ref && receiver[1][0] == :@gvar
    # byebug
    var_name = receiver[1][1]
    case var_name
    when "$:", "$LOAD_PATH"
      show_error_divider("\n")
      (line_no, column_no), _kind = current_token
      log "ERROR: Can't use #{var_name} in a Crystal program! Error at line #{line_no}:#{column_no}:"
      log
      log @code_lines[line_no - 1]
      log
      log "Removing this line from the Crystal code."
      log "You might be able to replace this with CRYSTAL_PATH if needed."
      log "See: https://github.com/crystal-lang/crystal/wiki/Compiler-internals#the-compiler-class"
      show_error_divider("", "\n")

      remove_current_command_from_tokens
      return
    end
  end

  # if name.is_a?(Array) && name[0] == :@ident
  #   ident = name[1]
  #   case ident
  #   when "require", "require_relative"
  #     return if replace_require_statement(node, ident, args)
  #   end
  # end

  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



1471
1472
1473
1474
1475
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1471

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



2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2868

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



2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2317

def visit_def(node)
  # [:def,
  #   [:@ident, "foo", [1, 6]],
  #   [:params, nil, nil, nil, nil, nil, nil, nil],
  #   [:bodystmt, [[:void_stmt]], nil, nil, nil]]
  _, 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



2355
2356
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
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2355

def visit_def_from_name(name, params, body)
  visit name

  params = params[1] if params[0] == :paren

  skip_space

  if current_token_kind == :on_lparen
    next_token
    skip_space
    skip_semicolons

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

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

      skip_space_or_newline
      check :on_rparen
      write ")"
      next_token
    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

  visit body
end

#visit_def_with_receiver(node) ⇒ Object



2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2332

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



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
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2966

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



1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1724

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



2050
2051
2052
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2050

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



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
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# File 'lib/ruby_crystal_codemod/formatter.rb', line 515

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_for(node) ⇒ Object



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
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2016

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



2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2640

def visit_hash(node)
  # [:hash, elements]
  _, elements = node
  token_column = current_token_column

  closing_brace_token, _ = find_closing_brace_token
  need_space = need_space_for_hash?(node, 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: token_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



2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2676

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
  consume_space

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

  visit value
end

#visit_if(node) ⇒ Object



3143
3144
3145
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3143

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

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



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
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3151

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



2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2153

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

  _, name = node

  if name
    skip_space_or_newline
    visit name
  end
end

#visit_lambda(node) ⇒ Object



2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
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
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2882

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:) ⇒ Object



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
3084
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
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3017

def visit_literal_elements(elements, inside_hash: false, inside_array: false, token_column:)
  base_column = @column
  base_line = @line
  needs_final_space = (inside_hash || inside_array) && space?
  first_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

  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) { visit elem }
    else
      visit 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
    wrote_comma = check_heredocs_in_literal_elements(is_last, wrote_comma)

    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
    wrote_comma = check_heredocs_in_literal_elements(is_last, wrote_comma)

    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
    write "," unless wrote_comma || !trailing_commas || @last_was_heredoc

    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
    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



1960
1961
1962
1963
1964
1965
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1960

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

  visit_mlhs_or_mlhs_paren(args)
end

#visit_mlhs_add_star(node) ⇒ Object



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
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2111

def visit_mlhs_add_star(node)
  # [:mlhs_add_star, before, star, after]
  _, before, star, after = node

  if before && !before.empty?
    # Maybe a Ripper bug, but if there's something before a star
    # then a star shouldn't be here... but if it is... handle it
    # somehow...
    if current_token_kind == :on_op && current_token_value == "*"
      star = before
    else
      visit_comma_separated_list to_ary(before)
      write_params_comma
    end
  end

  consume_op "*"

  if star
    skip_space_or_newline
    visit star
  end

  if after && !after.empty?
    write_params_comma
    visit_comma_separated_list after
  end
end

#visit_mlhs_or_mlhs_paren(args) ⇒ Object



1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1967

def visit_mlhs_or_mlhs_paren(args)
  # 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_mlhs_paren(node) ⇒ Object



1951
1952
1953
1954
1955
1956
1957
1958
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1951

def visit_mlhs_paren(node)
  # [:mlhs_paren,
  #   [[:mlhs_paren, [:@ident, "x", [1, 12]]]]
  # ]
  _, args = node

  visit_mlhs_or_mlhs_paren(args)
end

#visit_module(node) ⇒ Object



2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2300

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



2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2000

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



1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1937

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



893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
# File 'lib/ruby_crystal_codemod/formatter.rb', line 893

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



2858
2859
2860
2861
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2858

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

#visit_op_assign(node) ⇒ Object



866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
# File 'lib/ruby_crystal_codemod/formatter.rb', line 866

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



2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
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
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2432

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
    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
    consume_op "**"
    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

  if blockarg
    # [:blockarg, [:@ident, "block", [1, 16]]]
    write_params_comma if needs_comma
    skip_space_or_newline
    consume_op "&"
    skip_space_or_newline
    visit blockarg[1]
  end
end

#visit_paren(node) ⇒ Object



2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2413

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



832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
# File 'lib/ruby_crystal_codemod/formatter.rb', line 832

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



2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
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/ruby_crystal_codemod/formatter.rb', line 2553

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
    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)
        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



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

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



2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2710

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

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

#visit_regexp_literal(node) ⇒ Object



2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2721

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



1933
1934
1935
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1933

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

#visit_rest_param(node) ⇒ Object



2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2140

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



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

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

#visit_sclass(node) ⇒ Object



2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2800

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



2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2818

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



2701
2702
2703
2704
2705
2706
2707
2708
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2701

def visit_splat_inside_hash(node)
  # **exp
  #
  # [:assoc_splat, exp]
  consume_op "**"
  skip_space_or_newline
  visit node[1]
end

#visit_string_concat(node) ⇒ Object



736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
# File 'lib/ruby_crystal_codemod/formatter.rb', line 736

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



781
782
783
784
785
# File 'lib/ruby_crystal_codemod/formatter.rb', line 781

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



769
770
771
772
773
774
775
776
777
778
779
# File 'lib/ruby_crystal_codemod/formatter.rb', line 769

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



606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
# File 'lib/ruby_crystal_codemod/formatter.rb', line 606

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
      write ","
      @last_was_heredoc = true
    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



707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
# File 'lib/ruby_crystal_codemod/formatter.rb', line 707

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



1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
# File 'lib/ruby_crystal_codemod/formatter.rb', line 1006

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



2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2950

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



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

def visit_symbol(node)
  # :foo
  #
  # [:symbol, [:@ident, "foo", [1, 1]]]

  # Block arg calls changed from &: to &. in Crystal
  if @prev_token && @prev_token[2] == "&"
    current_token[1] = :on_period
    current_token[2] = "."
    consume_token :on_period
  else
    consume_token :on_symbeg
  end
  visit_exps node[1..-1], with_lines: false
end

#visit_symbol_literal(node) ⇒ Object



787
788
789
790
791
792
793
794
795
796
# File 'lib/ruby_crystal_codemod/formatter.rb', line 787

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



989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
# File 'lib/ruby_crystal_codemod/formatter.rb', line 989

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



2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2164

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

  # Crystal doesn't support and/or/not
  if current_token[2] == "not"
    current_token[2] = "!"
  end

  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
      skip_space_or_newline
      # 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



3008
3009
3010
3011
3012
3013
3014
3015
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3008

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

  consume_keyword "undef"
  consume_space
  visit_comma_separated_list exps
end

#visit_unless(node) ⇒ Object



3147
3148
3149
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3147

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

#visit_until(node) ⇒ Object



3194
3195
3196
3197
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3194

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

#visit_when(node) ⇒ Object



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
3324
3325
3326
3327
3328
3329
3330
3331
3332
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3235

def visit_when(node)
  # [:when, conds, body, next_exp]
  _, conds, body, next_exp = node

  consume_keyword "when"
  consume_space

  indent(@column) do
    visit_comma_separated_list conds
    skip_space
  end

  then_keyword = keyword?("then")
  inline = then_keyword || semicolon?
  if then_keyword
    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

      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
  elsif semicolon?
    skip_semicolons

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

  if inline
    indent do
      visit_exps body
    end
  else
    indent_body body
  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



3189
3190
3191
3192
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3189

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

#visit_while_or_until(node, keyword) ⇒ Object



3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3199

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



2863
2864
2865
2866
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2863

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

#void_exps?(node) ⇒ Boolean

Returns:

  • (Boolean)


3970
3971
3972
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3970

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.



699
700
701
702
703
704
705
# File 'lib/ruby_crystal_codemod/formatter.rb', line 699

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

#write(value) ⇒ Object



3830
3831
3832
3833
3834
3835
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3830

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

#write_indent(indent = @indent) ⇒ Object



3869
3870
3871
3872
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3869

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

#write_lineObject



3862
3863
3864
3865
3866
3867
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3862

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

#write_params_commaObject



2516
2517
2518
2519
2520
2521
2522
# File 'lib/ruby_crystal_codemod/formatter.rb', line 2516

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

#write_space(value = " ") ⇒ Object



3837
3838
3839
3840
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3837

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

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



3842
3843
3844
3845
3846
3847
3848
# File 'lib/ruby_crystal_codemod/formatter.rb', line 3842

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