ruby_crystal_codemod 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "open3"
5
+
6
+ class RubyCrystalCodemod::Command
7
+ CODE_OK = 0
8
+ CODE_ERROR = 1
9
+ CODE_CHANGE = 3
10
+
11
+ def self.run(argv)
12
+ want_check, exit_code, filename_for_dot_ruby_crystal_codemod, loglevel = parse_options(argv)
13
+ new(want_check, exit_code, filename_for_dot_ruby_crystal_codemod, loglevel).run(argv)
14
+ end
15
+
16
+ def initialize(want_check, exit_code, filename_for_dot_ruby_crystal_codemod, loglevel)
17
+ @want_check = want_check
18
+ @exit_code = exit_code
19
+ @filename_for_dot_ruby_crystal_codemod = filename_for_dot_ruby_crystal_codemod
20
+ @dot_file = RubyCrystalCodemod::DotFile.new
21
+ @squiggly_warning_files = []
22
+ @logger = RubyCrystalCodemod::Logger.new(loglevel)
23
+ end
24
+
25
+ def exit_code(status_code)
26
+ if @exit_code
27
+ status_code
28
+ else
29
+ case status_code
30
+ when CODE_OK, CODE_CHANGE
31
+ 0
32
+ else
33
+ 1
34
+ end
35
+ end
36
+ end
37
+
38
+ def run(argv)
39
+ status_code = if argv.empty?
40
+ format_stdin
41
+ else
42
+ format_args argv
43
+ end
44
+ exit exit_code(status_code)
45
+ end
46
+
47
+ def format_stdin
48
+ code = STDIN.read
49
+
50
+ result = format(code, nil, @filename_for_dot_ruby_crystal_codemod || Dir.getwd)
51
+
52
+ print(result) if !@want_check
53
+
54
+ code == result ? CODE_OK : CODE_CHANGE
55
+ rescue RubyCrystalCodemod::SyntaxError
56
+ logger.error("Error: the given text is not a valid ruby program (it has syntax errors)")
57
+ CODE_ERROR
58
+ rescue => e
59
+ logger.error("You've found a bug!")
60
+ logger.error("Please report it to https://github.com/DocSpring/ruby_crystal_codemod/issues with code that triggers it\n")
61
+ raise e
62
+ end
63
+
64
+ def format_args(args)
65
+ file_finder = RubyCrystalCodemod::FileFinder.new(args)
66
+ files = file_finder.to_a
67
+
68
+ changed = false
69
+ syntax_error = false
70
+ files_exist = false
71
+
72
+ files.each do |(exists, file)|
73
+ if exists
74
+ files_exist = true
75
+ else
76
+ logger.warn("Error: file or directory not found: #{file}")
77
+ next
78
+ end
79
+ result = format_file(file)
80
+
81
+ changed |= result == CODE_CHANGE
82
+ syntax_error |= result == CODE_ERROR
83
+ end
84
+
85
+ return CODE_ERROR unless files_exist
86
+
87
+ case
88
+ when syntax_error then CODE_ERROR
89
+ when changed then CODE_CHANGE
90
+ else CODE_OK
91
+ end
92
+ end
93
+
94
+ def format_file(filename)
95
+ logger.debug("Formatting: #{filename}")
96
+ code = File.read(filename)
97
+
98
+ begin
99
+ result = format(code, filename, @filename_for_dot_ruby_crystal_codemod || File.dirname(filename))
100
+ rescue RubyCrystalCodemod::SyntaxError
101
+ # We ignore syntax errors as these might be template files
102
+ # with .rb extension
103
+ logger.warn("Error: #{filename} has syntax errors")
104
+ return CODE_ERROR
105
+ end
106
+
107
+ # if code.force_encoding(result.encoding) != result
108
+ if @want_check
109
+ logger.warn("Formatting #{filename} produced changes")
110
+ else
111
+ crystal_filename = filename.sub(/\.rb$/, ".cr")
112
+ File.write(crystal_filename, result)
113
+ logger.log("Format: #{filename} => #{crystal_filename}")
114
+ end
115
+
116
+ # Run the post-processing command to handle BEGIN and END comments for Ruby / Crystal.
117
+ post_process_cmd = File.expand_path(File.join(__dir__, "../../util/post_process"))
118
+ unless File.exist?(post_process_cmd)
119
+ raise "Please run ./bin/compile_post_process to compile the post-processing command " \
120
+ "at: #{post_process_cmd}"
121
+ end
122
+ stdout, stderr, status = Open3.capture3(post_process_cmd, crystal_filename)
123
+ unless status.success?
124
+ warn "'./util/post_process' failed with status: #{status.exitstatus}\n\n" \
125
+ "stdout: #{stdout}\n\n" \
126
+ "stderr: #{stderr}"
127
+ end
128
+
129
+ # Format the Crystal file with the Crystal code formatter
130
+ stdout, stderr, status = Open3.capture3("crystal", "tool", "format", crystal_filename)
131
+ unless status.success?
132
+ warn "'crystal tool format' failed with status: #{status.exitstatus}\n\n" \
133
+ "stdout: #{stdout}\n\n" \
134
+ "stderr: #{stderr}"
135
+ puts "(This probably means that you will have to fix some errors manually.)"
136
+ end
137
+
138
+ return CODE_CHANGE
139
+ # end
140
+ rescue RubyCrystalCodemod::SyntaxError
141
+ logger.error("Error: the given text in #{filename} is not a valid ruby program (it has syntax errors)")
142
+ CODE_ERROR
143
+ rescue => e
144
+ logger.error("You've found a bug!")
145
+ logger.error("It happened while trying to format the file #{filename}")
146
+ logger.error("Please report it to https://github.com/DocSpring/ruby_crystal_codemod/issues with code that triggers it\n")
147
+ raise e
148
+ end
149
+
150
+ def format(code, filename, dir)
151
+ @squiggly_warning = false
152
+ formatter = RubyCrystalCodemod::Formatter.new(code, filename, dir)
153
+
154
+ options = @dot_file.get_config_in(dir)
155
+ unless options.nil?
156
+ formatter.init_settings(options)
157
+ end
158
+ formatter.format
159
+ result = formatter.result
160
+ result
161
+ end
162
+
163
+ def self.parse_options(argv)
164
+ exit_code, want_check = true, false
165
+ filename_for_dot_ruby_crystal_codemod = nil
166
+ loglevel = :log
167
+
168
+ OptionParser.new do |opts|
169
+ opts.version = RubyCrystalCodemod::VERSION
170
+ opts.banner = "Usage: ruby_crystal_codemod files or dirs [options]"
171
+
172
+ opts.on("-c", "--check", "Only check formating changes") do
173
+ want_check = true
174
+ end
175
+
176
+ opts.on("--filename=value", "Filename to use to lookup .ruby_crystal_codemod (useful for STDIN formatting)") do |value|
177
+ filename_for_dot_ruby_crystal_codemod = value
178
+ end
179
+
180
+ opts.on("-x", "--simple-exit", "Return 1 in the case of failure, else 0") do
181
+ exit_code = false
182
+ end
183
+
184
+ opts.on(RubyCrystalCodemod::Logger::LEVELS, "--loglevel[=LEVEL]", "Change the level of logging for the CLI. Options are: error, warn, log (default), debug, silent") do |value|
185
+ loglevel = value.to_sym
186
+ end
187
+
188
+ opts.on("-h", "--help", "Show this help") do
189
+ puts opts
190
+ exit
191
+ end
192
+ end.parse!(argv)
193
+
194
+ [want_check, exit_code, filename_for_dot_ruby_crystal_codemod, loglevel]
195
+ end
196
+
197
+ private
198
+
199
+ attr_reader :logger
200
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RubyCrystalCodemod::DotFile
4
+ def initialize
5
+ @cache = {}
6
+ end
7
+
8
+ def get_config_in(dir)
9
+ dot_ruby_crystal_codemod = find_in(dir)
10
+ if dot_ruby_crystal_codemod
11
+ return parse(dot_ruby_crystal_codemod)
12
+ end
13
+ end
14
+
15
+ def find_in(dir)
16
+ @cache.fetch(dir) do
17
+ @cache[dir] = internal_find_in(dir)
18
+ end
19
+ end
20
+
21
+ def parse(file_contents)
22
+ file_contents.lines
23
+ .map { |s| s.strip.split(/\s+/, 2) }
24
+ .each_with_object({}) do |(name, value), acc|
25
+ value ||= ""
26
+ if value.start_with?(":")
27
+ value = value[1..-1].to_sym
28
+ elsif value == "true"
29
+ value = true
30
+ elsif value == "false"
31
+ value = false
32
+ else
33
+ $stderr.puts "Unknown config value=#{value.inspect} for #{name.inspect}"
34
+ next
35
+ end
36
+ acc[name.to_sym] = value
37
+ end
38
+ end
39
+
40
+ def internal_find_in(dir)
41
+ dir = File.expand_path(dir)
42
+ file = File.join(dir, ".ruby_crystal_codemod")
43
+ if File.exist?(file)
44
+ return File.read(file)
45
+ end
46
+
47
+ parent_dir = File.dirname(dir)
48
+ return if parent_dir == dir
49
+
50
+ find_in(parent_dir)
51
+ end
52
+ end
@@ -0,0 +1,62 @@
1
+ require "find"
2
+
3
+ class RubyCrystalCodemod::FileFinder
4
+ include Enumerable
5
+
6
+ # Taken from https://github.com/ruby/rake/blob/f0a897e3fb557f64f5da59785b1a4464826f77b2/lib/rake/application.rb#L41
7
+ # RAKEFILES = [
8
+ # "rakefile",
9
+ # "Rakefile",
10
+ # "rakefile.rb",
11
+ # "Rakefile.rb",
12
+ # ]
13
+
14
+ # FILENAMES = [
15
+ # "Gemfile",
16
+ # *RAKEFILES,
17
+ # ]
18
+
19
+ EXTENSIONS = [
20
+ ".rb",
21
+ # ".gemspec",
22
+ # ".rake",
23
+ # ".jbuilder",
24
+ ]
25
+
26
+ EXCLUDED_DIRS = [
27
+ "vendor",
28
+ ]
29
+
30
+ def initialize(files_or_dirs)
31
+ @files_or_dirs = files_or_dirs
32
+ end
33
+
34
+ def each
35
+ files_or_dirs.each do |file_or_dir|
36
+ if Dir.exist?(file_or_dir)
37
+ all_rb_files(file_or_dir).each { |file| yield [true, file] }
38
+ else
39
+ yield [File.exist?(file_or_dir), file_or_dir]
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :files_or_dirs
47
+
48
+ def all_rb_files(file_or_dir)
49
+ files = []
50
+ Find.find(file_or_dir) do |path|
51
+ basename = File.basename(path)
52
+ if File.directory?(path)
53
+ Find.prune if EXCLUDED_DIRS.include?(basename)
54
+ else
55
+ if EXTENSIONS.include?(File.extname(basename)) #|| FILENAMES.include?(basename)
56
+ files << path
57
+ end
58
+ end
59
+ end
60
+ files
61
+ end
62
+ end
@@ -0,0 +1,4239 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ripper"
4
+ require "byebug"
5
+ require "awesome_print"
6
+
7
+ class RubyCrystalCodemod::Formatter
8
+ include RubyCrystalCodemod::Settings
9
+
10
+ attr_accessor :logs
11
+
12
+ INDENT_SIZE = 2
13
+
14
+ def self.format(code, filename, dir, **options)
15
+ formatter = new(code, filename, dir, **options)
16
+ formatter.format
17
+ formatter.result
18
+ end
19
+
20
+ def initialize(code, filename, dir, **options)
21
+ @options = options
22
+ @filename = filename
23
+ @dir = dir
24
+
25
+ @code = code
26
+ @code_lines = code.lines
27
+ @prev_token = nil
28
+ @tokens = Ripper.lex(code).reverse!
29
+ @sexp = Ripper.sexp(code)
30
+
31
+ # ap @tokens
32
+ ap @sexp if ENV["SHOW_SEXP"]
33
+
34
+ unless @sexp
35
+ raise ::RubyCrystalCodemod::SyntaxError.new
36
+ end
37
+
38
+ @indent = 0
39
+ @line = 0
40
+ @column = 0
41
+ @last_was_newline = true
42
+ @output = +""
43
+
44
+ # The column of a `obj.method` call, so we can align
45
+ # calls to that dot
46
+ @dot_column = nil
47
+
48
+ # Same as above, but the column of the original dot, not
49
+ # the one we finally wrote
50
+ @original_dot_column = nil
51
+
52
+ # Did this line already set the `@dot_column` variable?
53
+ @line_has_dot_column = nil
54
+
55
+ # The column of a `obj.method` call, but only the name part,
56
+ # so we can also align arguments accordingly
57
+ @name_dot_column = nil
58
+
59
+ # Heredocs list, associated with calls ([heredoc, tilde])
60
+ @heredocs = []
61
+
62
+ # Current node, to be able to associate it to heredocs
63
+ @current_node = nil
64
+
65
+ # The current heredoc being printed
66
+ @current_heredoc = nil
67
+
68
+ # The current hash or call or method that has hash-like parameters
69
+ @current_hash = nil
70
+
71
+ @current_type = nil
72
+
73
+ # Are we inside a type body?
74
+ @inside_type_body = false
75
+
76
+ # Map lines to commands that start at the begining of a line with the following info:
77
+ # - line indent
78
+ # - first param indent
79
+ # - first line ends with '(', '[' or '{'?
80
+ # - line of matching pair of the previous item
81
+ # - last line of that call
82
+ #
83
+ # This is needed to dedent some calls that look like this:
84
+ #
85
+ # foo bar(
86
+ # 2,
87
+ # )
88
+ #
89
+ # Without the dedent it would normally look like this:
90
+ #
91
+ # foo bar(
92
+ # 2,
93
+ # )
94
+ #
95
+ # Because the formatter aligns this to the first parameter in the call.
96
+ # However, for these cases it's better to not align it like that.
97
+ @line_to_call_info = {}
98
+
99
+ # Lists [first_line, last_line, indent] of lines that need an indent because
100
+ # of alignment of literals. For example this:#
101
+ #
102
+ # foo [
103
+ # 1,
104
+ # ]
105
+ #
106
+ # is normally formatted to:
107
+ #
108
+ # foo [
109
+ # 1,
110
+ # ]
111
+ #
112
+ # However, if it's already formatted like the above we preserve it.
113
+ @literal_indents = []
114
+
115
+ # First non-space token in this line
116
+ @first_token_in_line = nil
117
+
118
+ # Do we want to compute the above?
119
+ @want_first_token_in_line = false
120
+
121
+ # Each line that belongs to a string literal besides the first
122
+ # go here, so we don't break them when indenting/dedenting stuff
123
+ @unmodifiable_string_lines = {}
124
+
125
+ # Position of comments that occur at the end of a line
126
+ @comments_positions = []
127
+
128
+ # Token for the last comment found
129
+ @last_comment = nil
130
+
131
+ # Actual column of the last comment written
132
+ @last_comment_column = nil
133
+
134
+ # Associate lines to alignments
135
+ # Associate a line to an index inside @comments_position
136
+ # becuase when aligning something to the left of a comment
137
+ # we need to adjust the relative comment
138
+ @line_to_alignments_positions = Hash.new { |h, k| h[k] = [] }
139
+
140
+ # Position of assignments
141
+ @assignments_positions = []
142
+
143
+ # Range of assignment (line => end_line)
144
+ #
145
+ # We need this because when we have to format:
146
+ #
147
+ # ```
148
+ # abc = 1
149
+ # a = foo bar: 2
150
+ # baz: #
151
+ # ```
152
+ #
153
+ # Because we'll insert two spaces after `a`, this will
154
+ # result in a mis-alignment for baz (and possibly other lines
155
+ # below it). So, we remember the line ranges of an assignment,
156
+ # and once we align the first one we fix the other ones.
157
+ @assignments_ranges = {}
158
+
159
+ # Case when positions
160
+ @case_when_positions = []
161
+
162
+ # Declarations that are written in a single line, like:
163
+ #
164
+ # def foo; 1; end
165
+ #
166
+ # We want to track these because we allow consecutive inline defs
167
+ # to be together (without an empty line between them)
168
+ #
169
+ # This is [[line, original_line], ...]
170
+ @inline_declarations = []
171
+
172
+ # This is used to track how far deep we are in the AST.
173
+ # This is useful as it allows you to check if you are inside an array
174
+ # when dealing with heredocs.
175
+ @node_level = 0
176
+
177
+ # This represents the node level of the most recent literal elements list.
178
+ # It is used to track if we are in a list of elements so that commas
179
+ # can be added appropriately for heredocs for example.
180
+ @literal_elements_level = nil
181
+
182
+ @store_logs = false
183
+ @logs = []
184
+
185
+ init_settings(options)
186
+ end
187
+
188
+ def log(str = "")
189
+ if @store_logs
190
+ @logs << str
191
+ return
192
+ end
193
+ puts str
194
+ end
195
+
196
+ def format
197
+ visit @sexp
198
+ consume_end
199
+ write_line if !@last_was_newline || @output == ""
200
+ @output.chomp! if @output.end_with?("\n\n")
201
+
202
+ dedent_calls
203
+ indent_literals
204
+ do_align_case_when if align_case_when
205
+ remove_lines_before_inline_declarations
206
+ end
207
+
208
+ def visit(node)
209
+ @node_level += 1
210
+ unless node.is_a?(Array)
211
+ bug "unexpected node: #{node} at #{current_token}"
212
+ end
213
+
214
+ case node.first
215
+ when :program
216
+ # Topmost node
217
+ #
218
+ # [:program, exps]
219
+ visit_exps node[1], with_indent: true
220
+ when :void_stmt
221
+ # Empty statement
222
+ #
223
+ # [:void_stmt]
224
+ skip_space_or_newline
225
+ when :@int
226
+ # Integer literal
227
+ #
228
+ # [:@int, "123", [1, 0]]
229
+ consume_token :on_int
230
+ when :@float
231
+ # Float literal
232
+ #
233
+ # [:@int, "123.45", [1, 0]]
234
+ consume_token :on_float
235
+ when :@rational
236
+ # Rational literal
237
+ #
238
+ # [:@rational, "123r", [1, 0]]
239
+ consume_token :on_rational
240
+ when :@imaginary
241
+ # Imaginary literal
242
+ #
243
+ # [:@imaginary, "123i", [1, 0]]
244
+ consume_token :on_imaginary
245
+ when :@CHAR
246
+ # [:@CHAR, "?a", [1, 0]]
247
+ consume_token :on_CHAR
248
+ when :@gvar
249
+ # [:@gvar, "$abc", [1, 0]]
250
+ write node[1]
251
+ next_token
252
+ when :@backref
253
+ # [:@backref, "$1", [1, 0]]
254
+ write node[1]
255
+ next_token
256
+ when :@backtick
257
+ # [:@backtick, "`", [1, 4]]
258
+ consume_token :on_backtick
259
+ when :string_literal, :xstring_literal
260
+ visit_string_literal node
261
+ when :string_concat
262
+ visit_string_concat node
263
+ when :@tstring_content
264
+ # [:@tstring_content, "hello ", [1, 1]]
265
+ heredoc, tilde = @current_heredoc
266
+ looking_at_newline = current_token_kind == :on_tstring_content && current_token_value == "\n"
267
+ if heredoc && tilde && !@last_was_newline && looking_at_newline
268
+ check :on_tstring_content
269
+ consume_token_value(current_token_value)
270
+ next_token
271
+ else
272
+ # For heredocs with tilde we sometimes need to align the contents
273
+ if heredoc && tilde && @last_was_newline
274
+ unless (current_token_value == "\n" ||
275
+ current_token_kind == :on_heredoc_end)
276
+ write_indent(next_indent)
277
+ end
278
+ skip_ignored_space
279
+ if current_token_kind == :on_tstring_content
280
+ check :on_tstring_content
281
+ consume_token_value(current_token_value)
282
+ next_token
283
+ end
284
+ else
285
+ while (current_token_kind == :on_ignored_sp) ||
286
+ (current_token_kind == :on_tstring_content) ||
287
+ (current_token_kind == :on_embexpr_beg)
288
+ check current_token_kind
289
+ break if current_token_kind == :on_embexpr_beg
290
+ consume_token current_token_kind
291
+ end
292
+ end
293
+ end
294
+ when :string_content
295
+ # [:string_content, exp]
296
+ visit_exps node[1..-1], with_lines: false
297
+ when :string_embexpr
298
+ # String interpolation piece ( #{exp} )
299
+ visit_string_interpolation node
300
+ when :string_dvar
301
+ visit_string_dvar(node)
302
+ when :symbol_literal
303
+ visit_symbol_literal(node)
304
+ when :symbol
305
+ visit_symbol(node)
306
+ when :dyna_symbol
307
+ visit_quoted_symbol_literal(node)
308
+ when :@ident
309
+ consume_token :on_ident
310
+ when :var_ref
311
+ # [:var_ref, exp]
312
+ visit node[1]
313
+ when :var_field
314
+ # [:var_field, exp]
315
+ visit node[1]
316
+ when :@kw
317
+ # [:@kw, "nil", [1, 0]]
318
+ consume_token :on_kw
319
+ when :@ivar
320
+ # [:@ivar, "@foo", [1, 0]]
321
+ consume_token :on_ivar
322
+ when :@cvar
323
+ # [:@cvar, "@@foo", [1, 0]]
324
+ consume_token :on_cvar
325
+ when :@const
326
+ # [:@const, "FOO", [1, 0]]
327
+ consume_token :on_const
328
+ when :const_ref
329
+ # [:const_ref, [:@const, "Foo", [1, 8]]]
330
+ visit node[1]
331
+ when :top_const_ref
332
+ # [:top_const_ref, [:@const, "Foo", [1, 2]]]
333
+ consume_op "::"
334
+ skip_space_or_newline
335
+ visit node[1]
336
+ when :top_const_field
337
+ # [:top_const_field, [:@const, "Foo", [1, 2]]]
338
+ consume_op "::"
339
+ visit node[1]
340
+ when :const_path_ref
341
+ visit_path(node)
342
+ when :const_path_field
343
+ visit_path(node)
344
+ when :assign
345
+ visit_assign(node)
346
+ when :opassign
347
+ visit_op_assign(node)
348
+ when :massign
349
+ visit_multiple_assign(node)
350
+ when :ifop
351
+ visit_ternary_if(node)
352
+ when :if_mod
353
+ visit_suffix(node, "if")
354
+ when :unless_mod
355
+ visit_suffix(node, "unless")
356
+ when :while_mod
357
+ visit_suffix(node, "while")
358
+ when :until_mod
359
+ visit_suffix(node, "until")
360
+ when :rescue_mod
361
+ visit_suffix(node, "rescue")
362
+ when :vcall
363
+ # [:vcall, exp]
364
+ visit node[1]
365
+ when :fcall
366
+ # [:fcall, [:@ident, "foo", [1, 0]]]
367
+ visit node[1]
368
+ when :command
369
+ visit_command(node)
370
+ when :command_call
371
+ visit_command_call(node)
372
+ when :args_add_block
373
+ visit_call_args(node)
374
+ when :args_add_star
375
+ visit_args_add_star(node)
376
+ when :bare_assoc_hash
377
+ # [:bare_assoc_hash, exps]
378
+
379
+ # Align hash elements to the first key
380
+ indent(@column) do
381
+ visit_comma_separated_list node[1]
382
+ end
383
+ when :method_add_arg
384
+ visit_call_without_receiver(node)
385
+ when :method_add_block
386
+ visit_call_with_block(node)
387
+ when :call
388
+ visit_call_with_receiver(node)
389
+ when :brace_block
390
+ visit_brace_block(node)
391
+ when :do_block
392
+ visit_do_block(node)
393
+ when :block_var
394
+ visit_block_arguments(node)
395
+ when :begin
396
+ visit_begin(node)
397
+ when :bodystmt
398
+ visit_bodystmt(node)
399
+ when :if
400
+ visit_if(node)
401
+ when :unless
402
+ visit_unless(node)
403
+ when :while
404
+ visit_while(node)
405
+ when :until
406
+ visit_until(node)
407
+ when :case
408
+ visit_case(node)
409
+ when :when
410
+ visit_when(node)
411
+ when :unary
412
+ visit_unary(node)
413
+ when :binary
414
+ visit_binary(node)
415
+ when :class
416
+ visit_class(node)
417
+ when :module
418
+ visit_module(node)
419
+ when :mrhs_new_from_args
420
+ visit_mrhs_new_from_args(node)
421
+ when :mlhs_paren
422
+ visit_mlhs_paren(node)
423
+ when :mlhs
424
+ visit_mlhs(node)
425
+ when :mrhs_add_star
426
+ visit_mrhs_add_star(node)
427
+ when :def
428
+ visit_def(node)
429
+ when :defs
430
+ visit_def_with_receiver(node)
431
+ when :paren
432
+ visit_paren(node)
433
+ when :params
434
+ visit_params(node)
435
+ when :array
436
+ visit_array(node)
437
+ when :hash
438
+ visit_hash(node)
439
+ when :assoc_new
440
+ visit_hash_key_value(node)
441
+ when :assoc_splat
442
+ visit_splat_inside_hash(node)
443
+ when :@label
444
+ # [:@label, "foo:", [1, 3]]
445
+ write node[1]
446
+ next_token
447
+ when :dot2
448
+ visit_range(node, true)
449
+ when :dot3
450
+ visit_range(node, false)
451
+ when :regexp_literal
452
+ visit_regexp_literal(node)
453
+ when :aref
454
+ visit_array_access(node)
455
+ when :aref_field
456
+ visit_array_setter(node)
457
+ when :sclass
458
+ visit_sclass(node)
459
+ when :field
460
+ visit_setter(node)
461
+ when :return0
462
+ consume_keyword "return"
463
+ when :return
464
+ visit_return(node)
465
+ when :break
466
+ visit_break(node)
467
+ when :next
468
+ visit_next(node)
469
+ when :yield0
470
+ consume_keyword "yield"
471
+ when :yield
472
+ visit_yield(node)
473
+ when :@op
474
+ # [:@op, "*", [1, 1]]
475
+ write node[1]
476
+ next_token
477
+ when :lambda
478
+ visit_lambda(node)
479
+ when :zsuper
480
+ # [:zsuper]
481
+ consume_keyword "super"
482
+ when :super
483
+ visit_super(node)
484
+ when :defined
485
+ visit_defined(node)
486
+ when :alias, :var_alias
487
+ visit_alias(node)
488
+ when :undef
489
+ visit_undef(node)
490
+ when :mlhs_add_star
491
+ visit_mlhs_add_star(node)
492
+ when :rest_param
493
+ visit_rest_param(node)
494
+ when :kwrest_param
495
+ visit_kwrest_param(node)
496
+ when :retry
497
+ # [:retry]
498
+ consume_keyword "retry"
499
+ when :redo
500
+ # [:redo]
501
+ consume_keyword "redo"
502
+ when :for
503
+ visit_for(node)
504
+ when :BEGIN
505
+ visit_begin_node(node)
506
+ when :END
507
+ visit_end_node(node)
508
+ else
509
+ bug "Unhandled node: #{node.first}"
510
+ end
511
+ ensure
512
+ @node_level -= 1
513
+ end
514
+
515
+ def visit_exps(exps, with_indent: false, with_lines: true, want_trailing_multiline: false)
516
+ consume_end_of_line(at_prefix: true)
517
+
518
+ line_before_endline = nil
519
+
520
+ exps.each_with_index do |exp, i|
521
+ next if exp == :string_content
522
+
523
+ exp_kind = exp[0]
524
+
525
+ # Skip voids to avoid extra indentation
526
+ if exp_kind == :void_stmt
527
+ next
528
+ end
529
+
530
+ if with_indent
531
+ # Don't indent if this exp is in the same line as the previous
532
+ # one (this happens when there's a semicolon between the exps)
533
+ unless line_before_endline && line_before_endline == @line
534
+ write_indent
535
+ end
536
+ end
537
+
538
+ line_before_exp = @line
539
+ original_line = current_token_line
540
+
541
+ push_node(exp) do
542
+ visit exp
543
+ end
544
+
545
+ if declaration?(exp) && @line == line_before_exp
546
+ @inline_declarations << [@line, original_line]
547
+ end
548
+
549
+ is_last = last?(i, exps)
550
+
551
+ line_before_endline = @line
552
+
553
+ if with_lines
554
+ exp_needs_two_lines = needs_two_lines?(exp)
555
+
556
+ consume_end_of_line(want_semicolon: !is_last, want_multiline: !is_last || want_trailing_multiline, needs_two_lines_on_comment: exp_needs_two_lines)
557
+
558
+ # Make sure to put two lines before defs, class and others
559
+ if !is_last && (exp_needs_two_lines || needs_two_lines?(exps[i + 1])) && @line <= line_before_endline + 1
560
+ write_line
561
+ end
562
+ elsif !is_last
563
+ skip_space
564
+
565
+ has_semicolon = semicolon?
566
+ skip_semicolons
567
+ if newline?
568
+ write_line
569
+ write_indent(next_indent)
570
+ elsif has_semicolon
571
+ write "; "
572
+ end
573
+ skip_space_or_newline
574
+ end
575
+ end
576
+ end
577
+
578
+ def needs_two_lines?(exp)
579
+ kind = exp[0]
580
+ case kind
581
+ when :def, :class, :module
582
+ return true
583
+ when :vcall
584
+ # Check if it's private/protected/public
585
+ nested = exp[1]
586
+ if nested[0] == :@ident
587
+ case nested[1]
588
+ when "private", "protected", "public"
589
+ return true
590
+ end
591
+ end
592
+ end
593
+
594
+ false
595
+ end
596
+
597
+ def declaration?(exp)
598
+ case exp[0]
599
+ when :def, :class, :module
600
+ true
601
+ else
602
+ false
603
+ end
604
+ end
605
+
606
+ def visit_string_literal(node)
607
+ # [:string_literal, [:string_content, exps]]
608
+ heredoc = current_token_kind == :on_heredoc_beg
609
+ tilde = current_token_value.include?("~")
610
+
611
+ if heredoc
612
+ write current_token_value.rstrip
613
+ # Accumulate heredoc: we'll write it once
614
+ # we find a newline.
615
+ @heredocs << [node, tilde]
616
+ # Get the next_token while capturing any output.
617
+ # This is needed so that we can add a comma if one is not already present.
618
+ captured_output = capture_output { next_token }
619
+
620
+ inside_literal_elements_list = !@literal_elements_level.nil? &&
621
+ (@node_level - @literal_elements_level) == 2
622
+ needs_comma = !comma? && trailing_commas
623
+
624
+ if inside_literal_elements_list && needs_comma
625
+ write ","
626
+ @last_was_heredoc = true
627
+ end
628
+
629
+ @output << captured_output
630
+ return
631
+ elsif current_token_kind == :on_backtick
632
+ consume_token :on_backtick
633
+ else
634
+ return if format_simple_string(node)
635
+ consume_token :on_tstring_beg
636
+ end
637
+
638
+ visit_string_literal_end(node)
639
+ end
640
+
641
+ # For simple string formatting, look for nodes like:
642
+ # [:string_literal, [:string_content, [:@tstring_content, "abc", [...]]]]
643
+ # and return the simple string inside.
644
+ def simple_string(node)
645
+ inner = node[1][1..-1]
646
+ return if inner.length > 1
647
+ inner = inner[0]
648
+ return "" if !inner
649
+ return if inner[0] != :@tstring_content
650
+ string = inner[1]
651
+ string
652
+ end
653
+
654
+ # Which quote character are we using?
655
+ def quote_char
656
+ (quote_style == :double) ? '"' : "'"
657
+ end
658
+
659
+ # should we format this string according to :quote_style?
660
+ def should_format_string?(string)
661
+ # don't format %q or %Q
662
+ return unless current_token_value == "'" || current_token_value == '"'
663
+ # don't format strings containing slashes
664
+ return if string.include?("\\")
665
+ # don't format strings that contain our quote character
666
+ return if string.include?(quote_char)
667
+ return if string.include?('#{')
668
+ return if string.include?('#$')
669
+ true
670
+ end
671
+
672
+ def format_simple_string(node)
673
+ # is it a simple string node?
674
+ string = simple_string(node)
675
+ return if !string
676
+
677
+ # is it eligible for formatting?
678
+ return if !should_format_string?(string)
679
+
680
+ # success!
681
+ write quote_char
682
+ next_token
683
+ with_unmodifiable_string_lines do
684
+ inner = node[1][1..-1]
685
+ visit_exps(inner, with_lines: false)
686
+ end
687
+ write quote_char
688
+ next_token
689
+
690
+ true
691
+ end
692
+
693
+ # Every line between the first line and end line of this string (excluding the
694
+ # first line) must remain like it is now (we don't want to mess with that when
695
+ # indenting/dedenting)
696
+ #
697
+ # This can happen with heredocs, but also with string literals spanning
698
+ # multiple lines.
699
+ def with_unmodifiable_string_lines
700
+ line = @line
701
+ yield
702
+ (line + 1..@line).each do |i|
703
+ @unmodifiable_string_lines[i] = true
704
+ end
705
+ end
706
+
707
+ def visit_string_literal_end(node)
708
+ inner = node[1]
709
+ inner = inner[1..-1] unless node[0] == :xstring_literal
710
+
711
+ with_unmodifiable_string_lines do
712
+ visit_exps(inner, with_lines: false)
713
+ end
714
+
715
+ case current_token_kind
716
+ when :on_heredoc_end
717
+ heredoc, tilde = @current_heredoc
718
+ if heredoc && tilde
719
+ write_indent
720
+ write current_token_value.strip
721
+ else
722
+ write current_token_value.rstrip
723
+ end
724
+ next_token
725
+ skip_space
726
+
727
+ # Simulate a newline after the heredoc
728
+ @tokens << [[0, 0], :on_ignored_nl, "\n"]
729
+ when :on_backtick
730
+ consume_token :on_backtick
731
+ else
732
+ consume_token :on_tstring_end
733
+ end
734
+ end
735
+
736
+ def visit_string_concat(node)
737
+ # string1 string2
738
+ # [:string_concat, string1, string2]
739
+ _, string1, string2 = node
740
+
741
+ token_column = current_token_column
742
+ base_column = @column
743
+
744
+ visit string1
745
+
746
+ has_backslash, _ = skip_space_backslash
747
+ if has_backslash
748
+ write " \\"
749
+ write_line
750
+
751
+ # If the strings are aligned, like in:
752
+ #
753
+ # foo bar, "hello" \
754
+ # "world"
755
+ #
756
+ # then keep it aligned.
757
+ if token_column == current_token_column
758
+ write_indent(base_column)
759
+ else
760
+ write_indent
761
+ end
762
+ else
763
+ consume_space
764
+ end
765
+
766
+ visit string2
767
+ end
768
+
769
+ def visit_string_interpolation(node)
770
+ # [:string_embexpr, exps]
771
+ consume_token :on_embexpr_beg
772
+ skip_space_or_newline
773
+ if current_token_kind == :on_tstring_content
774
+ next_token
775
+ end
776
+ visit_exps(node[1], with_lines: false)
777
+ skip_space_or_newline
778
+ consume_token :on_embexpr_end
779
+ end
780
+
781
+ def visit_string_dvar(node)
782
+ # [:string_dvar, [:var_ref, [:@ivar, "@foo", [1, 2]]]]
783
+ consume_token :on_embvar
784
+ visit node[1]
785
+ end
786
+
787
+ def visit_symbol_literal(node)
788
+ # :foo
789
+ #
790
+ # [:symbol_literal, [:symbol, [:@ident, "foo", [1, 1]]]]
791
+ #
792
+ # A symbol literal not necessarily begins with `:`.
793
+ # For example, an `alias foo bar` will treat `foo`
794
+ # a as symbol_literal but without a `:symbol` child.
795
+ visit node[1]
796
+ end
797
+
798
+ def visit_symbol(node)
799
+ # :foo
800
+ #
801
+ # [:symbol, [:@ident, "foo", [1, 1]]]
802
+
803
+ # Block arg calls changed from &: to &. in Crystal
804
+ if @prev_token && @prev_token[2] == "&"
805
+ current_token[1] = :on_period
806
+ current_token[2] = "."
807
+ consume_token :on_period
808
+ else
809
+ consume_token :on_symbeg
810
+ end
811
+ visit_exps node[1..-1], with_lines: false
812
+ end
813
+
814
+ def visit_quoted_symbol_literal(node)
815
+ # :"foo"
816
+ #
817
+ # [:dyna_symbol, exps]
818
+ _, exps = node
819
+
820
+ # This is `"...":` as a hash key
821
+ if current_token_kind == :on_tstring_beg
822
+ consume_token :on_tstring_beg
823
+ visit exps
824
+ consume_token :on_label_end
825
+ else
826
+ consume_token :on_symbeg
827
+ visit_exps exps, with_lines: false
828
+ consume_token :on_tstring_end
829
+ end
830
+ end
831
+
832
+ def visit_path(node)
833
+ # Foo::Bar
834
+ #
835
+ # [:const_path_ref,
836
+ # [:var_ref, [:@const, "Foo", [1, 0]]],
837
+ # [:@const, "Bar", [1, 5]]]
838
+ pieces = node[1..-1]
839
+ pieces.each_with_index do |piece, i|
840
+ visit piece
841
+ unless last?(i, pieces)
842
+ consume_op "::"
843
+ skip_space_or_newline
844
+ end
845
+ end
846
+ end
847
+
848
+ def visit_assign(node)
849
+ # target = value
850
+ #
851
+ # [:assign, target, value]
852
+ _, target, value = node
853
+
854
+ line = @line
855
+
856
+ visit target
857
+ consume_space
858
+
859
+ track_assignment
860
+ consume_op "="
861
+ visit_assign_value value
862
+
863
+ @assignments_ranges[line] = @line if @line != line
864
+ end
865
+
866
+ def visit_op_assign(node)
867
+ # target += value
868
+ #
869
+ # [:opassign, target, op, value]
870
+ _, target, op, value = node
871
+
872
+ line = @line
873
+
874
+ visit target
875
+ consume_space
876
+
877
+ # [:@op, "+=", [1, 2]],
878
+ check :on_op
879
+
880
+ before = op[1][0...-1]
881
+ after = op[1][-1]
882
+
883
+ write before
884
+ track_assignment before.size
885
+ write after
886
+ next_token
887
+
888
+ visit_assign_value value
889
+
890
+ @assignments_ranges[line] = @line if @line != line
891
+ end
892
+
893
+ def visit_multiple_assign(node)
894
+ # [:massign, lefts, right]
895
+ _, lefts, right = node
896
+
897
+ visit_comma_separated_list lefts
898
+
899
+ first_space = skip_space
900
+
901
+ # A trailing comma can come after the left hand side
902
+ if comma?
903
+ consume_token :on_comma
904
+ first_space = skip_space
905
+ end
906
+
907
+ write_space_using_setting(first_space, :one)
908
+
909
+ track_assignment
910
+ consume_op "="
911
+ visit_assign_value right
912
+ end
913
+
914
+ def visit_assign_value(value)
915
+ has_slash_newline, _first_space = skip_space_backslash
916
+
917
+ sticky = indentable_value?(value)
918
+
919
+ # Remove backslash after equal + newline (it's useless)
920
+ if has_slash_newline
921
+ skip_space_or_newline
922
+ write_line
923
+ indent(next_indent) do
924
+ write_indent
925
+ visit(value)
926
+ end
927
+ else
928
+ indent_after_space value, sticky: sticky,
929
+ want_space: true
930
+ end
931
+ end
932
+
933
+ def indentable_value?(value)
934
+ return unless current_token_kind == :on_kw
935
+
936
+ case current_token_value
937
+ when "if", "unless", "case"
938
+ true
939
+ when "begin"
940
+ # Only indent if it's begin/rescue
941
+ return false unless value[0] == :begin
942
+
943
+ body = value[1]
944
+ return false unless body[0] == :bodystmt
945
+
946
+ _, _, rescue_body, else_body, ensure_body = body
947
+ rescue_body || else_body || ensure_body
948
+ else
949
+ false
950
+ end
951
+ end
952
+
953
+ def current_comment_aligned_to_previous_one?
954
+ @last_comment &&
955
+ @last_comment[0][0] + 1 == current_token_line &&
956
+ @last_comment[0][1] == current_token_column
957
+ end
958
+
959
+ def track_comment(id: nil, match_previous_id: false)
960
+ if match_previous_id && !@comments_positions.empty?
961
+ id = @comments_positions.last[3]
962
+ end
963
+
964
+ @line_to_alignments_positions[@line] << [:comment, @column, @comments_positions, @comments_positions.size]
965
+ @comments_positions << [@line, @column, 0, id, 0]
966
+ end
967
+
968
+ def track_assignment(offset = 0)
969
+ track_alignment :assign, @assignments_positions, offset
970
+ end
971
+
972
+ def track_case_when
973
+ track_alignment :case_whem, @case_when_positions
974
+ end
975
+
976
+ def track_alignment(key, target, offset = 0, id = nil)
977
+ last = target.last
978
+ if last && last[0] == @line
979
+ # Track only the first alignment in a line
980
+ return
981
+ end
982
+
983
+ @line_to_alignments_positions[@line] << [key, @column, target, target.size]
984
+ info = [@line, @column, @indent, id, offset]
985
+ target << info
986
+ info
987
+ end
988
+
989
+ def visit_ternary_if(node)
990
+ # cond ? then : else
991
+ #
992
+ # [:ifop, cond, then_body, else_body]
993
+ _, cond, then_body, else_body = node
994
+
995
+ visit cond
996
+ consume_space
997
+ consume_op "?"
998
+ consume_space_or_newline
999
+ visit then_body
1000
+ consume_space
1001
+ consume_op ":"
1002
+ consume_space_or_newline
1003
+ visit else_body
1004
+ end
1005
+
1006
+ def visit_suffix(node, suffix)
1007
+ # then if cond
1008
+ # then unless cond
1009
+ # exp rescue handler
1010
+ #
1011
+ # [:if_mod, cond, body]
1012
+ _, body, cond = node
1013
+
1014
+ if suffix != "rescue"
1015
+ body, cond = cond, body
1016
+ end
1017
+
1018
+ visit body
1019
+ consume_space
1020
+ consume_keyword(suffix)
1021
+ consume_space_or_newline
1022
+ visit cond
1023
+ end
1024
+
1025
+ def visit_call_with_receiver(node)
1026
+ # [:call, obj, :".", name]
1027
+ _, obj, _, name = node
1028
+
1029
+ @dot_column = nil
1030
+ visit obj
1031
+
1032
+ first_space = skip_space
1033
+
1034
+ if newline? || comment?
1035
+ consume_end_of_line
1036
+
1037
+ # If align_chained_calls is off, we still want to preserve alignment if it's already there
1038
+ if align_chained_calls || (@original_dot_column && @original_dot_column == current_token_column)
1039
+ @name_dot_column = @dot_column || next_indent
1040
+ write_indent(@dot_column || next_indent)
1041
+ else
1042
+ # Make sure to reset dot_column so next lines don't align to the first dot
1043
+ @dot_column = next_indent
1044
+ @name_dot_column = next_indent
1045
+ write_indent(next_indent)
1046
+ end
1047
+ else
1048
+ write_space_using_setting(first_space, :no)
1049
+ end
1050
+
1051
+ # Remember dot column, but only if there isn't one already set
1052
+ unless @dot_column
1053
+ dot_column = @column
1054
+ original_dot_column = current_token_column
1055
+ end
1056
+
1057
+ consume_call_dot
1058
+
1059
+ skip_space_or_newline_using_setting(:no, next_indent)
1060
+
1061
+ if name == :call
1062
+ # :call means it's .()
1063
+ else
1064
+ visit name
1065
+ end
1066
+
1067
+ # Only set it after we visit the call after the dot,
1068
+ # so we remember the outmost dot position
1069
+ @dot_column = dot_column if dot_column
1070
+ @original_dot_column = original_dot_column if original_dot_column
1071
+ end
1072
+
1073
+ def consume_call_dot
1074
+ if current_token_kind == :on_op
1075
+ consume_token :on_op
1076
+ else
1077
+ consume_token :on_period
1078
+ end
1079
+ end
1080
+
1081
+ def visit_call_without_receiver(node)
1082
+ # foo(arg1, ..., argN)
1083
+ #
1084
+ # [:method_add_arg,
1085
+ # [:fcall, [:@ident, "foo", [1, 0]]],
1086
+ # [:arg_paren, [:args_add_block, [[:@int, "1", [1, 6]]], false]]]
1087
+ _, name, args = node
1088
+
1089
+ if name.is_a?(Array) && name[1].is_a?(Array)
1090
+ ident = name[1][1]
1091
+ case ident
1092
+ when "require", "require_relative"
1093
+ return if replace_require_statement(node, ident, args)
1094
+ end
1095
+ end
1096
+
1097
+ @name_dot_column = nil
1098
+ visit name
1099
+
1100
+ # Some times a call comes without parens (should probably come as command, but well...)
1101
+ return if args.empty?
1102
+
1103
+ # Remember dot column so it's not affected by args
1104
+ dot_column = @dot_column
1105
+ original_dot_column = @original_dot_column
1106
+
1107
+ want_indent = @name_dot_column && @name_dot_column > @indent
1108
+
1109
+ maybe_indent(want_indent, @name_dot_column) do
1110
+ visit_call_at_paren(node, args)
1111
+ end
1112
+
1113
+ # Restore dot column so it's not affected by args
1114
+ @dot_column = dot_column
1115
+ @original_dot_column = original_dot_column
1116
+ end
1117
+
1118
+ def visit_call_at_paren(node, args)
1119
+ consume_token :on_lparen
1120
+
1121
+ # If there's a trailing comma then comes [:arg_paren, args],
1122
+ # which is a bit unexpected, so we fix it
1123
+ if args[1].is_a?(Array) && args[1][0].is_a?(Array)
1124
+ args_node = [:args_add_block, args[1], false]
1125
+ else
1126
+ args_node = args[1]
1127
+ end
1128
+
1129
+ if args_node
1130
+ skip_space
1131
+
1132
+ needs_trailing_newline = newline? || comment?
1133
+ if needs_trailing_newline && (call_info = @line_to_call_info[@line])
1134
+ call_info << true
1135
+ end
1136
+
1137
+ want_trailing_comma = true
1138
+
1139
+ # Check if there's a block arg and if the call ends with hash key/values
1140
+ if args_node[0] == :args_add_block
1141
+ _, args, block_arg = args_node
1142
+ want_trailing_comma = !block_arg
1143
+ if args.is_a?(Array) && (last_arg = args.last) && last_arg.is_a?(Array) &&
1144
+ last_arg[0].is_a?(Symbol) && last_arg[0] != :bare_assoc_hash
1145
+ want_trailing_comma = false
1146
+ end
1147
+ end
1148
+
1149
+ push_call(node) do
1150
+ visit args_node
1151
+ skip_space
1152
+ end
1153
+
1154
+ found_comma = comma?
1155
+
1156
+ if found_comma
1157
+ if needs_trailing_newline
1158
+ write "," if trailing_commas && !block_arg
1159
+
1160
+ next_token
1161
+ indent(next_indent) do
1162
+ consume_end_of_line
1163
+ end
1164
+ write_indent
1165
+ else
1166
+ next_token
1167
+ skip_space
1168
+ end
1169
+ end
1170
+
1171
+ if newline? || comment?
1172
+ if needs_trailing_newline
1173
+ write "," if trailing_commas && want_trailing_comma
1174
+
1175
+ indent(next_indent) do
1176
+ consume_end_of_line
1177
+ end
1178
+ write_indent
1179
+ else
1180
+ skip_space_or_newline
1181
+ end
1182
+ else
1183
+ if needs_trailing_newline && !found_comma
1184
+ write "," if trailing_commas && want_trailing_comma
1185
+ consume_end_of_line
1186
+ write_indent
1187
+ end
1188
+ end
1189
+ else
1190
+ skip_space_or_newline
1191
+ end
1192
+
1193
+ # If the closing parentheses matches the indent of the first parameter,
1194
+ # keep it like that. Otherwise dedent.
1195
+ if call_info && call_info[1] != current_token_column
1196
+ call_info << @line
1197
+ end
1198
+
1199
+ if @last_was_heredoc
1200
+ write_line
1201
+ end
1202
+ consume_token :on_rparen
1203
+ end
1204
+
1205
+ # Parses any Ruby code, and attempts to evaluate it
1206
+ # require File.expand_path('./nested_require', File.dirname(__FILE__))
1207
+ def parse_require_path_from_ruby_code(_node, _ident, _next_level)
1208
+ crystal_path = nil
1209
+ (line_no, column_no), _kind = current_token
1210
+
1211
+ # Need to figure out all of the Ruby code to execute, which may span across multiple lines.
1212
+ # (This heuristic probably won't work for all valid Ruby code, but it's a good start.)
1213
+ paren_count = 0
1214
+ require_tokens = []
1215
+ @tokens.reverse_each.with_index do |token, i|
1216
+ next if i == 0
1217
+ _, name = token
1218
+ case name
1219
+ when :on_nl, :on_semicolon
1220
+ break if paren_count == 0
1221
+ next if paren_count == 1
1222
+ when :on_lparen
1223
+ paren_count += 1
1224
+ when :on_rparen
1225
+ paren_count -= 1
1226
+ end
1227
+
1228
+ require_tokens << token[2]
1229
+ end
1230
+
1231
+ require_string = require_tokens.join("").strip
1232
+
1233
+ show_error_divider("\n")
1234
+ log "WARNING: require statements can only use strings in Crystal. Error at line #{line_no}:#{column_no}:"
1235
+ log
1236
+ log "#{require_string}"
1237
+ log
1238
+ unless require_string.include?("File.")
1239
+ log "===> require args do not start with 'File.', so not attempting to evaluate the code.\n"
1240
+ show_requiring_files_docs
1241
+ return false
1242
+ end
1243
+
1244
+ show_requiring_files_docs
1245
+ log "\n==> Attempting to expand and evaluate the Ruby require path..."
1246
+
1247
+ # Expand __dir__ and __FILE__ into absolute paths
1248
+ expanded_dir = File.expand_path(@dir)
1249
+ expanded_file = File.expand_path(@filename)
1250
+ expanded_require_string = require_string
1251
+ .gsub("__dir__", "\"#{expanded_dir}\"")
1252
+ .gsub("__FILE__", "\"#{expanded_file}\"")
1253
+
1254
+ log "====> Expanded __dir__ and __FILE__: #{expanded_require_string}"
1255
+
1256
+ evaluated_path = nil
1257
+ begin
1258
+ log "====> Evaluating Ruby code: #{expanded_require_string}"
1259
+ # rubocop:disable Security/Eval
1260
+ evaluated_path = eval(expanded_require_string)
1261
+ # rubocop:enable Security/Eval
1262
+ rescue StandardError => e
1263
+ log "ERROR: We tried to evaluate and expand the path, but it crashed with an error:"
1264
+ log e
1265
+ end
1266
+
1267
+ if evaluated_path == nil || evaluated_path == ""
1268
+ log "ERROR: We tried to evaluate and expand the path, but it didn't return anything."
1269
+ elsif !evaluated_path.is_a?(String)
1270
+ log "====> Evaluated path was not a string! Please fix this require statement manually."
1271
+ log "====> Result of Ruby evaluation: #{evaluated_path}"
1272
+ return nil
1273
+ else
1274
+ if !evaluated_path.to_s.match?(/\.rb$/)
1275
+ evaluated_path = "#{evaluated_path}.rb"
1276
+ end
1277
+ log "====> Evaluated Ruby path: #{evaluated_path}"
1278
+
1279
+ if File.exist?(evaluated_path)
1280
+ expanded_evaluated_path = File.expand_path(evaluated_path)
1281
+ crystal_path = expanded_evaluated_path.sub("#{Dir.getwd}/", "").sub(/\.rb$/, "")
1282
+ log "======> Successfully expanded the require path and found the file: #{evaluated_path}"
1283
+ log "======> Crystal require: #{crystal_path}"
1284
+ else
1285
+ log "======> ERROR: Could not find #{evaluated_path}! Please fix this require statement manually."
1286
+ end
1287
+ end
1288
+
1289
+ if crystal_path.nil? || crystal_path == ""
1290
+ log "ERROR: Couldn't parse and evaluate the Ruby require statement! Please update the require statement manually."
1291
+ return nil
1292
+ end
1293
+ show_error_divider("", "\n")
1294
+
1295
+ crystal_path
1296
+ end
1297
+
1298
+ # Parses:
1299
+ # require "test"
1300
+ # require_relative "test"
1301
+ # require_relative("test")
1302
+ # require("test")
1303
+ def parse_simple_require_path(_node, _ident, next_level)
1304
+ return unless next_level.is_a?(Array)
1305
+
1306
+ if next_level[0] == :arg_paren
1307
+ return unless (next_level = next_level[1]) && next_level.is_a?(Array)
1308
+ end
1309
+ return unless next_level[0] == :args_add_block && (next_level = next_level[1]) && next_level.is_a?(Array)
1310
+ return unless (next_level = next_level[0]) && next_level.is_a?(Array)
1311
+ return unless next_level[0] == :string_literal && (next_level = next_level[1]) && next_level.is_a?(Array)
1312
+ return unless next_level[0] == :string_content && (next_level = next_level[1]) && next_level.is_a?(Array)
1313
+
1314
+ if next_level[0] == :string_embexpr
1315
+ show_error_divider("\n")
1316
+ (line_no, column_no), _kind = current_token
1317
+ log "ERROR: String interpolation is not supported for Crystal require statements! " \
1318
+ "Please update the require statement manually."
1319
+ log "Error at line #{line_no}:#{column_no}:"
1320
+ log
1321
+ log @code_lines[line_no - 1]
1322
+ log
1323
+ show_requiring_files_docs
1324
+ show_error_divider("", "\n")
1325
+ return false
1326
+ end
1327
+
1328
+ return unless next_level[0] == :@tstring_content && (next_level = next_level[1]) && next_level.is_a?(String)
1329
+ next_level
1330
+ end
1331
+
1332
+ def require_path_from_args(node, ident, args)
1333
+ simple_path = parse_simple_require_path(node, ident, args)
1334
+ return false if simple_path == false
1335
+
1336
+ if simple_path
1337
+ # We now know that this was a simple string arg (either in parens, or after a space)
1338
+ # So now we need to see if it's a single or double quoted string.
1339
+ quote_char = nil
1340
+ @tokens.reverse_each.with_index do |token, _i|
1341
+ (_line_no, _column_no), kind = token
1342
+ case kind
1343
+ when :on_tstring_beg
1344
+ quote_char = token[2]
1345
+ when :on_nl
1346
+ break
1347
+ end
1348
+ end
1349
+ unless quote_char
1350
+ raise "Couldn't figure out the quote type for this string!"
1351
+ end
1352
+
1353
+ # Now fix the quote escaping
1354
+ if quote_char == "'"
1355
+ simple_path = simple_path.gsub('"', "\\\"").gsub("\\'", "'")
1356
+ end
1357
+ return simple_path
1358
+ end
1359
+
1360
+ parse_require_path_from_ruby_code(node, ident, args)
1361
+ end
1362
+
1363
+ def show_requiring_files_docs
1364
+ log "===> Read the 'Requiring files' page in the Crystal docs:"
1365
+ log "===> https://crystal-lang.org/reference/syntax_and_semantics/requiring_files.html"
1366
+ end
1367
+
1368
+ def show_error_divider(prefix = "", suffix = "")
1369
+ log "#{prefix}-------------------------------------------------------------------------------\n#{suffix}"
1370
+ end
1371
+
1372
+ def remove_current_command_from_tokens
1373
+ paren_count = 0
1374
+ loop do
1375
+ token = @tokens.last
1376
+ raise "[Infinite loop bug] Ran out of tokens!" unless token
1377
+ _, name = token
1378
+ case name
1379
+ when :on_nl, :on_semicolon
1380
+ if paren_count == 0
1381
+ @tokens.pop
1382
+ break
1383
+ end
1384
+ when :on_lparen
1385
+ paren_count += 1
1386
+ when :on_rparen
1387
+ paren_count -= 1
1388
+ end
1389
+ @tokens.pop
1390
+ end
1391
+ end
1392
+
1393
+ def replace_require_statement(node, ident, args)
1394
+ # RubyCrystalCodemod doesn't replace single quotes with double quotes for require statements, so
1395
+ # we have to fix that manually here. (The double quote replacement seems to work everywhere else.)
1396
+ require_path = require_path_from_args(node, ident, args)
1397
+ return false if require_path == false
1398
+
1399
+ unless require_path
1400
+ show_error_divider("\n")
1401
+ (line_no, column_no), _kind = current_token
1402
+ log "ERROR: Couldn't find a valid path argument for require! Error at line #{line_no}:#{column_no}:"
1403
+ log
1404
+ log @code_lines[line_no - 1]
1405
+ log
1406
+ show_requiring_files_docs
1407
+ show_error_divider("", "\n")
1408
+ return false
1409
+ end
1410
+
1411
+ if ident == "require_relative" && !require_path.match?(/^..\//) && !require_path.match?(/^.\//)
1412
+ require_path = "./#{require_path}"
1413
+ end
1414
+
1415
+ crystal_path = require_path
1416
+
1417
+ # Rewrite all the tokens with the Crystal require statement.
1418
+ remove_current_command_from_tokens
1419
+
1420
+ @tokens += [
1421
+ [[0, 0], :on_nl, "\n", nil],
1422
+ [[0, 0], :on_tstring_end, '"', nil],
1423
+ [[0, 0], :on_tstring_content, crystal_path, nil],
1424
+ [[0, 0], :on_tstring_beg, '"', nil],
1425
+ [[0, 0], :on_sp, " ", nil],
1426
+ [[0, 0], :on_ident, "require", nil],
1427
+ ]
1428
+
1429
+ node = [:command, [:@ident, "require", [0, 0]], [:args_add_block,
1430
+ [[:string_literal,
1431
+ [:string_content,
1432
+ [:@tstring_content, crystal_path, [0, 0]]]]], false]]
1433
+ _, name, args = node
1434
+
1435
+ base_column = current_token_column
1436
+
1437
+ push_call(node) do
1438
+ visit name
1439
+ consume_space_after_command_name
1440
+ end
1441
+ push_call(node) do
1442
+ visit_command_args(args, base_column)
1443
+ end
1444
+ true
1445
+ end
1446
+
1447
+ def visit_command(node)
1448
+ # foo arg1, ..., argN
1449
+ #
1450
+ # [:command, name, args]
1451
+ _, name, args = node
1452
+
1453
+ if name.is_a?(Array) && name[0] == :@ident
1454
+ ident = name[1]
1455
+ case ident
1456
+ when "require", "require_relative"
1457
+ return if replace_require_statement(node, ident, args)
1458
+ end
1459
+ end
1460
+
1461
+ base_column = current_token_column
1462
+
1463
+ push_call(node) do
1464
+ visit name
1465
+ consume_space_after_command_name
1466
+ end
1467
+
1468
+ visit_command_end(node, args, base_column)
1469
+ end
1470
+
1471
+ def visit_command_end(node, args, base_column)
1472
+ push_call(node) do
1473
+ visit_command_args(args, base_column)
1474
+ end
1475
+ end
1476
+
1477
+ def flush_heredocs
1478
+ if comment?
1479
+ write_space unless @output[-1] == " "
1480
+ write current_token_value.rstrip
1481
+ next_token
1482
+ write_line
1483
+ if @heredocs.last[1]
1484
+ write_indent(next_indent)
1485
+ end
1486
+ end
1487
+
1488
+ printed = false
1489
+
1490
+ until @heredocs.empty?
1491
+ heredoc, tilde = @heredocs.first
1492
+
1493
+ @heredocs.shift
1494
+ @current_heredoc = [heredoc, tilde]
1495
+ visit_string_literal_end(heredoc)
1496
+ @current_heredoc = nil
1497
+ printed = true
1498
+ end
1499
+
1500
+ @last_was_heredoc = true if printed
1501
+ end
1502
+
1503
+ def visit_command_call(node)
1504
+ # [:command_call,
1505
+ # receiver
1506
+ # :".",
1507
+ # name
1508
+ # [:args_add_block, [[:@int, "1", [1, 8]]], block]]
1509
+ _, receiver, _, name, args = node
1510
+
1511
+ # Check for $: var and LOAD_PATH, which are unsupported in Crystal
1512
+ if receiver[0] == :var_ref && receiver[1][0] == :@gvar
1513
+ # byebug
1514
+ var_name = receiver[1][1]
1515
+ case var_name
1516
+ when "$:", "$LOAD_PATH"
1517
+ show_error_divider("\n")
1518
+ (line_no, column_no), _kind = current_token
1519
+ log "ERROR: Can't use #{var_name} in a Crystal program! Error at line #{line_no}:#{column_no}:"
1520
+ log
1521
+ log @code_lines[line_no - 1]
1522
+ log
1523
+ log "Removing this line from the Crystal code."
1524
+ log "You might be able to replace this with CRYSTAL_PATH if needed."
1525
+ log "See: https://github.com/crystal-lang/crystal/wiki/Compiler-internals#the-compiler-class"
1526
+ show_error_divider("", "\n")
1527
+
1528
+ remove_current_command_from_tokens
1529
+ return
1530
+ end
1531
+ end
1532
+
1533
+ # if name.is_a?(Array) && name[0] == :@ident
1534
+ # ident = name[1]
1535
+ # case ident
1536
+ # when "require", "require_relative"
1537
+ # return if replace_require_statement(node, ident, args)
1538
+ # end
1539
+ # end
1540
+
1541
+ base_column = current_token_column
1542
+
1543
+ visit receiver
1544
+
1545
+ skip_space_or_newline
1546
+
1547
+ # Remember dot column
1548
+ dot_column = @column
1549
+ original_dot_column = @original_dot_column
1550
+
1551
+ consume_call_dot
1552
+
1553
+ skip_space
1554
+
1555
+ if newline? || comment?
1556
+ consume_end_of_line
1557
+ write_indent(next_indent)
1558
+ else
1559
+ skip_space_or_newline
1560
+ end
1561
+
1562
+ visit name
1563
+ consume_space_after_command_name
1564
+ visit_command_args(args, base_column)
1565
+
1566
+ # Only set it after we visit the call after the dot,
1567
+ # so we remember the outmost dot position
1568
+ @dot_column = dot_column
1569
+ @original_dot_column = original_dot_column
1570
+ end
1571
+
1572
+ def consume_space_after_command_name
1573
+ has_backslash, first_space = skip_space_backslash
1574
+ if has_backslash
1575
+ write " \\"
1576
+ write_line
1577
+ write_indent(next_indent)
1578
+ else
1579
+ write_space_using_setting(first_space, :one)
1580
+ end
1581
+ end
1582
+
1583
+ def visit_command_args(args, base_column)
1584
+ needed_indent = @column
1585
+ args_is_def_class_or_module = false
1586
+ param_column = current_token_column
1587
+
1588
+ # Check if there's a single argument and it's
1589
+ # a def, class or module. In that case we don't
1590
+ # want to align the content to the position of
1591
+ # that keyword.
1592
+ if args[0] == :args_add_block
1593
+ nested_args = args[1]
1594
+ if nested_args.is_a?(Array) && nested_args.size == 1
1595
+ first = nested_args[0]
1596
+ if first.is_a?(Array)
1597
+ case first[0]
1598
+ when :def, :class, :module
1599
+ needed_indent = @indent
1600
+ args_is_def_class_or_module = true
1601
+ end
1602
+ end
1603
+ end
1604
+ end
1605
+
1606
+ base_line = @line
1607
+ call_info = @line_to_call_info[@line]
1608
+ if call_info
1609
+ call_info = nil
1610
+ else
1611
+ call_info = [@indent, @column]
1612
+ @line_to_call_info[@line] = call_info
1613
+ end
1614
+
1615
+ old_want_first_token_in_line = @want_first_token_in_line
1616
+ @want_first_token_in_line = true
1617
+
1618
+ # We align call parameters to the first paramter
1619
+ indent(needed_indent) do
1620
+ visit_exps to_ary(args), with_lines: false
1621
+ end
1622
+
1623
+ if call_info && call_info.size > 2
1624
+ # A call like:
1625
+ #
1626
+ # foo, 1, [
1627
+ # 2,
1628
+ # ]
1629
+ #
1630
+ # would normally be aligned like this (with the first parameter):
1631
+ #
1632
+ # foo, 1, [
1633
+ # 2,
1634
+ # ]
1635
+ #
1636
+ # However, the first style is valid too and we preserve it if it's
1637
+ # already formatted like that.
1638
+ call_info << @line
1639
+ elsif !args_is_def_class_or_module && @first_token_in_line && param_column == @first_token_in_line[0][1]
1640
+ # If the last line of the call is aligned with the first parameter, leave it like that:
1641
+ #
1642
+ # foo 1,
1643
+ # 2
1644
+ elsif !args_is_def_class_or_module && @first_token_in_line && base_column + INDENT_SIZE == @first_token_in_line[0][1]
1645
+ # Otherwise, align it just by two spaces (so we need to dedent, we fake a dedent here)
1646
+ #
1647
+ # foo 1,
1648
+ # 2
1649
+ @line_to_call_info[base_line] = [0, needed_indent - next_indent, true, @line, @line]
1650
+ end
1651
+
1652
+ @want_first_token_in_line = old_want_first_token_in_line
1653
+ end
1654
+
1655
+ def visit_call_with_block(node)
1656
+ # [:method_add_block, call, block]
1657
+ _, call, block = node
1658
+
1659
+ visit call
1660
+
1661
+ consume_space
1662
+
1663
+ old_dot_column = @dot_column
1664
+ old_original_dot_column = @original_dot_column
1665
+
1666
+ visit block
1667
+
1668
+ @dot_column = old_dot_column
1669
+ @original_dot_column = old_original_dot_column
1670
+ end
1671
+
1672
+ def visit_brace_block(node)
1673
+ # [:brace_block, args, body]
1674
+ _, args, body = node
1675
+
1676
+ # This is for the empty `{ }` block
1677
+ if void_exps?(body)
1678
+ consume_token :on_lbrace
1679
+ consume_block_args args
1680
+ consume_space
1681
+ consume_token :on_rbrace
1682
+ return
1683
+ end
1684
+
1685
+ closing_brace_token, _ = find_closing_brace_token
1686
+
1687
+ # If the whole block fits into a single line, use braces
1688
+ if current_token_line == closing_brace_token[0][0]
1689
+ consume_token :on_lbrace
1690
+ consume_block_args args
1691
+ consume_space
1692
+ visit_exps body, with_lines: false
1693
+
1694
+ while semicolon?
1695
+ next_token
1696
+ end
1697
+
1698
+ consume_space
1699
+
1700
+ consume_token :on_rbrace
1701
+ return
1702
+ end
1703
+
1704
+ # Otherwise it's multiline
1705
+ consume_token :on_lbrace
1706
+ consume_block_args args
1707
+
1708
+ if (call_info = @line_to_call_info[@line])
1709
+ call_info << true
1710
+ end
1711
+
1712
+ indent_body body, force_multiline: true
1713
+ write_indent
1714
+
1715
+ # If the closing bracket matches the indent of the first parameter,
1716
+ # keep it like that. Otherwise dedent.
1717
+ if call_info && call_info[1] != current_token_column
1718
+ call_info << @line
1719
+ end
1720
+
1721
+ consume_token :on_rbrace
1722
+ end
1723
+
1724
+ def visit_do_block(node)
1725
+ # [:brace_block, args, body]
1726
+ _, args, body = node
1727
+
1728
+ line = @line
1729
+
1730
+ consume_keyword "do"
1731
+
1732
+ consume_block_args args
1733
+
1734
+ if body.first == :bodystmt
1735
+ visit_bodystmt body
1736
+ else
1737
+ indent_body body
1738
+ write_indent unless @line == line
1739
+ consume_keyword "end"
1740
+ end
1741
+ end
1742
+
1743
+ def consume_block_args(args)
1744
+ if args
1745
+ consume_space_or_newline
1746
+ # + 1 because of |...|
1747
+ # ^
1748
+ indent(@column + 1) do
1749
+ visit args
1750
+ end
1751
+ end
1752
+ end
1753
+
1754
+ def visit_block_arguments(node)
1755
+ # [:block_var, params, local_params]
1756
+ _, params, local_params = node
1757
+
1758
+ empty_params = empty_params?(params)
1759
+
1760
+ check :on_op
1761
+
1762
+ # check for ||
1763
+ if empty_params && !local_params
1764
+ # Don't write || as it's meaningless
1765
+ if current_token_value == "|"
1766
+ next_token
1767
+ skip_space_or_newline
1768
+ check :on_op
1769
+ next_token
1770
+ else
1771
+ next_token
1772
+ end
1773
+ return
1774
+ end
1775
+
1776
+ consume_token :on_op
1777
+ found_semicolon = skip_space_or_newline(_want_semicolon: true, write_first_semicolon: true)
1778
+
1779
+ if found_semicolon
1780
+ # Nothing
1781
+ elsif empty_params && local_params
1782
+ consume_token :on_semicolon
1783
+ end
1784
+
1785
+ skip_space_or_newline
1786
+
1787
+ unless empty_params
1788
+ visit params
1789
+ skip_space
1790
+ end
1791
+
1792
+ if local_params
1793
+ if semicolon?
1794
+ consume_token :on_semicolon
1795
+ consume_space
1796
+ end
1797
+
1798
+ visit_comma_separated_list local_params
1799
+ else
1800
+ skip_space_or_newline
1801
+ end
1802
+
1803
+ consume_op "|"
1804
+ end
1805
+
1806
+ def visit_call_args(node)
1807
+ # [:args_add_block, args, block]
1808
+ _, args, block_arg = node
1809
+
1810
+ if !args.empty? && args[0] == :args_add_star
1811
+ # arg1, ..., *star
1812
+ visit args
1813
+ else
1814
+ visit_comma_separated_list args
1815
+ end
1816
+
1817
+ if block_arg
1818
+ skip_space_or_newline
1819
+
1820
+ if comma?
1821
+ indent(next_indent) do
1822
+ write_params_comma
1823
+ end
1824
+ end
1825
+
1826
+ # Block operator changed from &: to &. in Crystal
1827
+ consume_op "&"
1828
+ skip_space_or_newline
1829
+ visit block_arg
1830
+ end
1831
+ end
1832
+
1833
+ def visit_args_add_star(node)
1834
+ # [:args_add_star, args, star, post_args]
1835
+ _, args, star, *post_args = node
1836
+
1837
+ if newline? || comment?
1838
+ needs_indent = true
1839
+ base_column = next_indent
1840
+ else
1841
+ base_column = @column
1842
+ end
1843
+ if !args.empty? && args[0] == :args_add_star
1844
+ # arg1, ..., *star
1845
+ visit args
1846
+ elsif !args.empty?
1847
+ visit_comma_separated_list args
1848
+ else
1849
+ consume_end_of_line if needs_indent
1850
+ end
1851
+
1852
+ skip_space
1853
+
1854
+ write_params_comma if comma?
1855
+ write_indent(base_column) if needs_indent
1856
+ consume_op "*"
1857
+ skip_space_or_newline
1858
+ visit star
1859
+
1860
+ if post_args && !post_args.empty?
1861
+ write_params_comma
1862
+ visit_comma_separated_list post_args, needs_indent: needs_indent, base_column: base_column
1863
+ end
1864
+ end
1865
+
1866
+ def visit_begin(node)
1867
+ # begin
1868
+ # body
1869
+ # end
1870
+ #
1871
+ # [:begin, [:bodystmt, body, rescue_body, else_body, ensure_body]]
1872
+ consume_keyword "begin"
1873
+ visit node[1]
1874
+ end
1875
+
1876
+ def visit_bodystmt(node)
1877
+ # [:bodystmt, body, rescue_body, else_body, ensure_body]
1878
+ # [:bodystmt, [[:@int, "1", [2, 1]]], nil, [[:@int, "2", [4, 1]]], nil] (2.6.0)
1879
+ _, body, rescue_body, else_body, ensure_body = node
1880
+
1881
+ @inside_type_body = false
1882
+
1883
+ line = @line
1884
+
1885
+ indent_body body
1886
+
1887
+ while rescue_body
1888
+ # [:rescue, type, name, body, more_rescue]
1889
+ _, type, name, body, more_rescue = rescue_body
1890
+ write_indent
1891
+ consume_keyword "rescue"
1892
+ if type
1893
+ skip_space
1894
+ write_space
1895
+ indent(@column) do
1896
+ visit_rescue_types(type)
1897
+ end
1898
+ end
1899
+
1900
+ if name
1901
+ skip_space
1902
+ write_space
1903
+ consume_op "=>"
1904
+ skip_space
1905
+ write_space
1906
+ visit name
1907
+ end
1908
+
1909
+ indent_body body
1910
+ rescue_body = more_rescue
1911
+ end
1912
+
1913
+ if else_body
1914
+ # [:else, body]
1915
+ # [[:@int, "2", [4, 1]]] (2.6.0)
1916
+ write_indent
1917
+ consume_keyword "else"
1918
+ else_body = else_body[1] if else_body[0] == :else
1919
+ indent_body else_body
1920
+ end
1921
+
1922
+ if ensure_body
1923
+ # [:ensure, body]
1924
+ write_indent
1925
+ consume_keyword "ensure"
1926
+ indent_body ensure_body[1]
1927
+ end
1928
+
1929
+ write_indent if @line != line
1930
+ consume_keyword "end"
1931
+ end
1932
+
1933
+ def visit_rescue_types(node)
1934
+ visit_exps to_ary(node), with_lines: false
1935
+ end
1936
+
1937
+ def visit_mrhs_new_from_args(node)
1938
+ # Multiple exception types
1939
+ # [:mrhs_new_from_args, exps, final_exp]
1940
+ _, exps, final_exp = node
1941
+
1942
+ if final_exp
1943
+ visit_comma_separated_list exps
1944
+ write_params_comma
1945
+ visit final_exp
1946
+ else
1947
+ visit_comma_separated_list to_ary(exps)
1948
+ end
1949
+ end
1950
+
1951
+ def visit_mlhs_paren(node)
1952
+ # [:mlhs_paren,
1953
+ # [[:mlhs_paren, [:@ident, "x", [1, 12]]]]
1954
+ # ]
1955
+ _, args = node
1956
+
1957
+ visit_mlhs_or_mlhs_paren(args)
1958
+ end
1959
+
1960
+ def visit_mlhs(node)
1961
+ # [:mlsh, *args]
1962
+ _, *args = node
1963
+
1964
+ visit_mlhs_or_mlhs_paren(args)
1965
+ end
1966
+
1967
+ def visit_mlhs_or_mlhs_paren(args)
1968
+ # Sometimes a paren comes, some times not, so act accordingly.
1969
+ has_paren = current_token_kind == :on_lparen
1970
+ if has_paren
1971
+ consume_token :on_lparen
1972
+ skip_space_or_newline
1973
+ end
1974
+
1975
+ # For some reason there's nested :mlhs_paren for
1976
+ # a single parentheses. It seems when there's
1977
+ # a nested array we need parens, otherwise we
1978
+ # just output whatever's inside `args`.
1979
+ if args.is_a?(Array) && args[0].is_a?(Array)
1980
+ indent(@column) do
1981
+ visit_comma_separated_list args
1982
+ skip_space_or_newline
1983
+ end
1984
+ else
1985
+ visit args
1986
+ end
1987
+
1988
+ if has_paren
1989
+ # Ripper has a bug where parsing `|(w, *x, y), z|`,
1990
+ # the "y" isn't returned. In this case we just consume
1991
+ # all tokens until we find a `)`.
1992
+ while current_token_kind != :on_rparen
1993
+ consume_token current_token_kind
1994
+ end
1995
+
1996
+ consume_token :on_rparen
1997
+ end
1998
+ end
1999
+
2000
+ def visit_mrhs_add_star(node)
2001
+ # [:mrhs_add_star, [], [:vcall, [:@ident, "x", [3, 8]]]]
2002
+ _, x, y = node
2003
+
2004
+ if x.empty?
2005
+ consume_op "*"
2006
+ visit y
2007
+ else
2008
+ visit x
2009
+ write_params_comma
2010
+ consume_space
2011
+ consume_op "*"
2012
+ visit y
2013
+ end
2014
+ end
2015
+
2016
+ def visit_for(node)
2017
+ #[:for, var, collection, body]
2018
+ _, var, collection, body = node
2019
+
2020
+ line = @line
2021
+
2022
+ consume_keyword "for"
2023
+ consume_space
2024
+
2025
+ visit_comma_separated_list to_ary(var)
2026
+ skip_space
2027
+ if comma?
2028
+ check :on_comma
2029
+ write ","
2030
+ next_token
2031
+ skip_space_or_newline
2032
+ end
2033
+
2034
+ consume_space
2035
+ consume_keyword "in"
2036
+ consume_space
2037
+ visit collection
2038
+ skip_space
2039
+
2040
+ indent_body body
2041
+
2042
+ write_indent if @line != line
2043
+ consume_keyword "end"
2044
+ end
2045
+
2046
+ def visit_begin_node(node)
2047
+ visit_begin_or_end node, "BEGIN"
2048
+ end
2049
+
2050
+ def visit_end_node(node)
2051
+ visit_begin_or_end node, "END"
2052
+ end
2053
+
2054
+ def visit_begin_or_end(node, keyword)
2055
+ # [:BEGIN, body]
2056
+ _, body = node
2057
+
2058
+ consume_keyword(keyword)
2059
+ consume_space
2060
+
2061
+ closing_brace_token, _index = find_closing_brace_token
2062
+
2063
+ # If the whole block fits into a single line, format
2064
+ # in a single line
2065
+ if current_token_line == closing_brace_token[0][0]
2066
+ consume_token :on_lbrace
2067
+ consume_space
2068
+ visit_exps body, with_lines: false
2069
+ consume_space
2070
+ consume_token :on_rbrace
2071
+ else
2072
+ consume_token :on_lbrace
2073
+ indent_body body
2074
+ write_indent
2075
+ consume_token :on_rbrace
2076
+ end
2077
+ end
2078
+
2079
+ def visit_comma_separated_list(nodes, needs_indent: false, base_column: nil)
2080
+ if newline? || comment?
2081
+ indent { consume_end_of_line }
2082
+ needs_indent = true
2083
+ base_column = next_indent
2084
+ write_indent(base_column)
2085
+ elsif needs_indent
2086
+ write_indent(base_column)
2087
+ else
2088
+ base_column ||= @column
2089
+ end
2090
+
2091
+ nodes = to_ary(nodes)
2092
+ nodes.each_with_index do |exp, i|
2093
+ maybe_indent(needs_indent, base_column) do
2094
+ if block_given?
2095
+ yield exp
2096
+ else
2097
+ visit exp
2098
+ end
2099
+ end
2100
+
2101
+ next if last?(i, nodes)
2102
+
2103
+ skip_space
2104
+ check :on_comma
2105
+ write ","
2106
+ next_token
2107
+ skip_space_or_newline_using_setting(:one, base_column)
2108
+ end
2109
+ end
2110
+
2111
+ def visit_mlhs_add_star(node)
2112
+ # [:mlhs_add_star, before, star, after]
2113
+ _, before, star, after = node
2114
+
2115
+ if before && !before.empty?
2116
+ # Maybe a Ripper bug, but if there's something before a star
2117
+ # then a star shouldn't be here... but if it is... handle it
2118
+ # somehow...
2119
+ if current_token_kind == :on_op && current_token_value == "*"
2120
+ star = before
2121
+ else
2122
+ visit_comma_separated_list to_ary(before)
2123
+ write_params_comma
2124
+ end
2125
+ end
2126
+
2127
+ consume_op "*"
2128
+
2129
+ if star
2130
+ skip_space_or_newline
2131
+ visit star
2132
+ end
2133
+
2134
+ if after && !after.empty?
2135
+ write_params_comma
2136
+ visit_comma_separated_list after
2137
+ end
2138
+ end
2139
+
2140
+ def visit_rest_param(node)
2141
+ # [:rest_param, name]
2142
+
2143
+ _, name = node
2144
+
2145
+ consume_op "*"
2146
+
2147
+ if name
2148
+ skip_space_or_newline
2149
+ visit name
2150
+ end
2151
+ end
2152
+
2153
+ def visit_kwrest_param(node)
2154
+ # [:kwrest_param, name]
2155
+
2156
+ _, name = node
2157
+
2158
+ if name
2159
+ skip_space_or_newline
2160
+ visit name
2161
+ end
2162
+ end
2163
+
2164
+ def visit_unary(node)
2165
+ # [:unary, :-@, [:vcall, [:@ident, "x", [1, 2]]]]
2166
+ _, op, exp = node
2167
+
2168
+ # Crystal doesn't support and/or/not
2169
+ if current_token[2] == "not"
2170
+ current_token[2] = "!"
2171
+ end
2172
+
2173
+ consume_op_or_keyword
2174
+
2175
+ first_space = space?
2176
+ skip_space_or_newline
2177
+
2178
+ if op == :not
2179
+ has_paren = current_token_kind == :on_lparen
2180
+
2181
+ if has_paren && !first_space
2182
+ write "("
2183
+ next_token
2184
+ skip_space_or_newline
2185
+ elsif !has_paren
2186
+ skip_space_or_newline
2187
+ # write_space
2188
+ end
2189
+
2190
+ visit exp
2191
+
2192
+ if has_paren && !first_space
2193
+ skip_space_or_newline
2194
+ check :on_rparen
2195
+ write ")"
2196
+ next_token
2197
+ end
2198
+ else
2199
+ visit exp
2200
+ end
2201
+ end
2202
+
2203
+ def visit_binary(node)
2204
+ # [:binary, left, op, right]
2205
+ _, left, _, right = node
2206
+
2207
+ # If this binary is not at the beginning of a line, if there's
2208
+ # a newline following the op we want to align it with the left
2209
+ # value. So for example:
2210
+ #
2211
+ # var = left_exp ||
2212
+ # right_exp
2213
+ #
2214
+ # But:
2215
+ #
2216
+ # def foo
2217
+ # left_exp ||
2218
+ # right_exp
2219
+ # end
2220
+ needed_indent = @column == @indent ? next_indent : @column
2221
+ base_column = @column
2222
+ token_column = current_token_column
2223
+
2224
+ visit left
2225
+ needs_space = space?
2226
+
2227
+ has_backslash, _ = skip_space_backslash
2228
+ if has_backslash
2229
+ needs_space = true
2230
+ write " \\"
2231
+ write_line
2232
+ write_indent(next_indent)
2233
+ else
2234
+ write_space
2235
+ end
2236
+
2237
+ consume_op_or_keyword
2238
+
2239
+ skip_space
2240
+
2241
+ if newline? || comment?
2242
+ indent_after_space right,
2243
+ want_space: needs_space,
2244
+ needed_indent: needed_indent,
2245
+ token_column: token_column,
2246
+ base_column: base_column
2247
+ else
2248
+ write_space
2249
+ visit right
2250
+ end
2251
+ end
2252
+
2253
+ def consume_op_or_keyword
2254
+ # Crystal doesn't have and / or
2255
+ # See: https://crystal-lang.org/reference/syntax_and_semantics/operators.html
2256
+ value = current_token_value
2257
+ case value
2258
+ when "and"
2259
+ value = "&&"
2260
+ when "or"
2261
+ value = "||"
2262
+ end
2263
+
2264
+ case current_token_kind
2265
+ when :on_op, :on_kw
2266
+ write value
2267
+ next_token
2268
+ else
2269
+ bug "Expected op or kw, not #{current_token_kind}"
2270
+ end
2271
+ end
2272
+
2273
+ def visit_class(node)
2274
+ # [:class,
2275
+ # name
2276
+ # superclass
2277
+ # [:bodystmt, body, nil, nil, nil]]
2278
+ _, name, superclass, body = node
2279
+
2280
+ push_type(node) do
2281
+ consume_keyword "class"
2282
+ skip_space_or_newline
2283
+ write_space
2284
+ visit name
2285
+
2286
+ if superclass
2287
+ skip_space_or_newline
2288
+ write_space
2289
+ consume_op "<"
2290
+ skip_space_or_newline
2291
+ write_space
2292
+ visit superclass
2293
+ end
2294
+
2295
+ @inside_type_body = true
2296
+ visit body
2297
+ end
2298
+ end
2299
+
2300
+ def visit_module(node)
2301
+ # [:module,
2302
+ # name
2303
+ # [:bodystmt, body, nil, nil, nil]]
2304
+ _, name, body = node
2305
+
2306
+ push_type(node) do
2307
+ consume_keyword "module"
2308
+ skip_space_or_newline
2309
+ write_space
2310
+ visit name
2311
+
2312
+ @inside_type_body = true
2313
+ visit body
2314
+ end
2315
+ end
2316
+
2317
+ def visit_def(node)
2318
+ # [:def,
2319
+ # [:@ident, "foo", [1, 6]],
2320
+ # [:params, nil, nil, nil, nil, nil, nil, nil],
2321
+ # [:bodystmt, [[:void_stmt]], nil, nil, nil]]
2322
+ _, name, params, body = node
2323
+
2324
+ consume_keyword "def"
2325
+ consume_space
2326
+
2327
+ push_hash(node) do
2328
+ visit_def_from_name name, params, body
2329
+ end
2330
+ end
2331
+
2332
+ def visit_def_with_receiver(node)
2333
+ # [:defs,
2334
+ # [:vcall, [:@ident, "foo", [1, 5]]],
2335
+ # [:@period, ".", [1, 8]],
2336
+ # [:@ident, "bar", [1, 9]],
2337
+ # [:params, nil, nil, nil, nil, nil, nil, nil],
2338
+ # [:bodystmt, [[:void_stmt]], nil, nil, nil]]
2339
+ _, receiver, _, name, params, body = node
2340
+
2341
+ consume_keyword "def"
2342
+ consume_space
2343
+ visit receiver
2344
+ skip_space_or_newline
2345
+
2346
+ consume_call_dot
2347
+
2348
+ skip_space_or_newline
2349
+
2350
+ push_hash(node) do
2351
+ visit_def_from_name name, params, body
2352
+ end
2353
+ end
2354
+
2355
+ def visit_def_from_name(name, params, body)
2356
+ visit name
2357
+
2358
+ params = params[1] if params[0] == :paren
2359
+
2360
+ skip_space
2361
+
2362
+ if current_token_kind == :on_lparen
2363
+ next_token
2364
+ skip_space
2365
+ skip_semicolons
2366
+
2367
+ if empty_params?(params)
2368
+ skip_space_or_newline
2369
+ check :on_rparen
2370
+ next_token
2371
+ write "()"
2372
+ else
2373
+ write "("
2374
+
2375
+ if newline? || comment?
2376
+ column = @column
2377
+ indent(column) do
2378
+ consume_end_of_line
2379
+ write_indent
2380
+ visit params
2381
+ end
2382
+ else
2383
+ indent(@column) do
2384
+ visit params
2385
+ end
2386
+ end
2387
+
2388
+ skip_space_or_newline
2389
+ check :on_rparen
2390
+ write ")"
2391
+ next_token
2392
+ end
2393
+ elsif !empty_params?(params)
2394
+ if parens_in_def == :yes
2395
+ write "("
2396
+ else
2397
+ write_space
2398
+ end
2399
+
2400
+ visit params
2401
+ write ")" if parens_in_def == :yes
2402
+ skip_space
2403
+ end
2404
+
2405
+ visit body
2406
+ end
2407
+
2408
+ def empty_params?(node)
2409
+ _, a, b, c, d, e, f, g = node
2410
+ !a && !b && !c && !d && !e && !f && !g
2411
+ end
2412
+
2413
+ def visit_paren(node)
2414
+ # ( exps )
2415
+ #
2416
+ # [:paren, exps]
2417
+ _, exps = node
2418
+
2419
+ consume_token :on_lparen
2420
+ skip_space_or_newline
2421
+
2422
+ heredoc = current_token_kind == :on_heredoc_beg
2423
+ if exps
2424
+ visit_exps to_ary(exps), with_lines: false
2425
+ end
2426
+
2427
+ skip_space_or_newline
2428
+ write "\n" if heredoc
2429
+ consume_token :on_rparen
2430
+ end
2431
+
2432
+ def visit_params(node)
2433
+ # (def params)
2434
+ #
2435
+ # [:params, pre_rest_params, args_with_default, rest_param, post_rest_params, label_params, double_star_param, blockarg]
2436
+ _, pre_rest_params, args_with_default, rest_param, post_rest_params, label_params, double_star_param, blockarg = node
2437
+
2438
+ needs_comma = false
2439
+
2440
+ if pre_rest_params
2441
+ visit_comma_separated_list pre_rest_params
2442
+ needs_comma = true
2443
+ end
2444
+
2445
+ if args_with_default
2446
+ write_params_comma if needs_comma
2447
+ visit_comma_separated_list(args_with_default) do |arg, default|
2448
+ visit arg
2449
+ consume_space
2450
+ consume_op "="
2451
+ consume_space
2452
+ visit default
2453
+ end
2454
+ needs_comma = true
2455
+ end
2456
+
2457
+ if rest_param
2458
+ # check for trailing , |x, | (may be [:excessed_comma] in 2.6.0)
2459
+ case rest_param
2460
+ when 0, [:excessed_comma]
2461
+ write_params_comma
2462
+ else
2463
+ # [:rest_param, [:@ident, "x", [1, 15]]]
2464
+ _, rest = rest_param
2465
+ write_params_comma if needs_comma
2466
+ consume_op "*"
2467
+ skip_space_or_newline
2468
+ visit rest if rest
2469
+ needs_comma = true
2470
+ end
2471
+ end
2472
+
2473
+ if post_rest_params
2474
+ write_params_comma if needs_comma
2475
+ visit_comma_separated_list post_rest_params
2476
+ needs_comma = true
2477
+ end
2478
+
2479
+ if label_params
2480
+ # [[label, value], ...]
2481
+ write_params_comma if needs_comma
2482
+ visit_comma_separated_list(label_params) do |label, value|
2483
+ # [:@label, "b:", [1, 20]]
2484
+ write label[1]
2485
+ next_token
2486
+ skip_space_or_newline
2487
+ if value
2488
+ consume_space
2489
+ visit value
2490
+ end
2491
+ end
2492
+ needs_comma = true
2493
+ end
2494
+
2495
+ if double_star_param
2496
+ write_params_comma if needs_comma
2497
+ consume_op "**"
2498
+ skip_space_or_newline
2499
+
2500
+ # A nameless double star comes as an... Integer? :-S
2501
+ visit double_star_param if double_star_param.is_a?(Array)
2502
+ skip_space_or_newline
2503
+ needs_comma = true
2504
+ end
2505
+
2506
+ if blockarg
2507
+ # [:blockarg, [:@ident, "block", [1, 16]]]
2508
+ write_params_comma if needs_comma
2509
+ skip_space_or_newline
2510
+ consume_op "&"
2511
+ skip_space_or_newline
2512
+ visit blockarg[1]
2513
+ end
2514
+ end
2515
+
2516
+ def write_params_comma
2517
+ skip_space
2518
+ check :on_comma
2519
+ write ","
2520
+ next_token
2521
+ skip_space_or_newline_using_setting(:one)
2522
+ end
2523
+
2524
+ def visit_array(node)
2525
+ # [:array, elements]
2526
+
2527
+ # Check if it's `%w(...)` or `%i(...)`
2528
+ case current_token_kind
2529
+ when :on_qwords_beg, :on_qsymbols_beg, :on_words_beg, :on_symbols_beg
2530
+ visit_q_or_i_array(node)
2531
+ return
2532
+ end
2533
+
2534
+ _, elements = node
2535
+
2536
+ token_column = current_token_column
2537
+
2538
+ check :on_lbracket
2539
+ write "["
2540
+ next_token
2541
+
2542
+ if elements
2543
+ visit_literal_elements to_ary(elements), inside_array: true, token_column: token_column
2544
+ else
2545
+ skip_space_or_newline
2546
+ end
2547
+
2548
+ check :on_rbracket
2549
+ write "]"
2550
+ next_token
2551
+ end
2552
+
2553
+ def visit_q_or_i_array(node)
2554
+ _, elements = node
2555
+
2556
+ # For %W it seems elements appear inside other arrays
2557
+ # for some reason, so we flatten them
2558
+ if elements[0].is_a?(Array) && elements[0][0].is_a?(Array)
2559
+ elements = elements.flat_map { |x| x }
2560
+ end
2561
+
2562
+ has_space = current_token_value.end_with?(" ")
2563
+ write current_token_value.strip
2564
+
2565
+ # (pre 2.5.0) If there's a newline after `%w(`, write line and indent
2566
+ if current_token_value.include?("\n") && elements # "%w[\n"
2567
+ write_line
2568
+ write_indent next_indent
2569
+ end
2570
+
2571
+ next_token
2572
+
2573
+ # fix for 2.5.0 ripper change
2574
+ if current_token_kind == :on_words_sep && elements && !elements.empty?
2575
+ value = current_token_value
2576
+ has_space = value.start_with?(" ")
2577
+ if value.include?("\n") && elements # "\n "
2578
+ write_line
2579
+ write_indent next_indent
2580
+ end
2581
+ next_token
2582
+ has_space = true if current_token_value.start_with?(" ")
2583
+ end
2584
+
2585
+ if elements && !elements.empty?
2586
+ write_space if has_space
2587
+ column = @column
2588
+
2589
+ elements.each_with_index do |elem, i|
2590
+ if elem[0] == :@tstring_content
2591
+ # elem is [:@tstring_content, string, [1, 5]
2592
+ write elem[1].strip
2593
+ next_token
2594
+ else
2595
+ visit elem
2596
+ end
2597
+
2598
+ if !last?(i, elements) && current_token_kind == :on_words_sep
2599
+ # On a newline, write line and indent
2600
+ if current_token_value.include?("\n")
2601
+ next_token
2602
+ write_line
2603
+ write_indent(column)
2604
+ else
2605
+ next_token
2606
+ write_space
2607
+ end
2608
+ end
2609
+ end
2610
+ end
2611
+
2612
+ has_newline = false
2613
+ last_token = nil
2614
+
2615
+ while current_token_kind == :on_words_sep
2616
+ has_newline ||= current_token_value.include?("\n")
2617
+
2618
+ unless current_token[2].strip.empty?
2619
+ last_token = current_token
2620
+ end
2621
+
2622
+ next_token
2623
+ end
2624
+
2625
+ if has_newline
2626
+ write_line
2627
+ write_indent
2628
+ elsif has_space && elements && !elements.empty?
2629
+ write_space
2630
+ end
2631
+
2632
+ if last_token
2633
+ write last_token[2].strip
2634
+ else
2635
+ write current_token_value.strip
2636
+ next_token
2637
+ end
2638
+ end
2639
+
2640
+ def visit_hash(node)
2641
+ # [:hash, elements]
2642
+ _, elements = node
2643
+ token_column = current_token_column
2644
+
2645
+ closing_brace_token, _ = find_closing_brace_token
2646
+ need_space = need_space_for_hash?(node, closing_brace_token)
2647
+
2648
+ check :on_lbrace
2649
+ write "{"
2650
+ brace_position = @output.length - 1
2651
+ write " " if need_space
2652
+ next_token
2653
+
2654
+ if elements
2655
+ # [:assoclist_from_args, elements]
2656
+ push_hash(node) do
2657
+ visit_literal_elements(elements[1], inside_hash: true, token_column: token_column)
2658
+ end
2659
+ char_after_brace = @output[brace_position + 1]
2660
+ # Check that need_space is set correctly.
2661
+ if !need_space && !["\n", " "].include?(char_after_brace)
2662
+ need_space = true
2663
+ # Add a space in the missing position.
2664
+ @output.insert(brace_position + 1, " ")
2665
+ end
2666
+ else
2667
+ skip_space_or_newline
2668
+ end
2669
+
2670
+ check :on_rbrace
2671
+ write " " if need_space
2672
+ write "}"
2673
+ next_token
2674
+ end
2675
+
2676
+ def visit_hash_key_value(node)
2677
+ # key => value
2678
+ #
2679
+ # [:assoc_new, key, value]
2680
+ _, key, value = node
2681
+
2682
+ # If a symbol comes it means it's something like
2683
+ # `:foo => 1` or `:"foo" => 1` and a `=>`
2684
+ # always follows
2685
+ symbol = current_token_kind == :on_symbeg
2686
+ arrow = symbol || !(key[0] == :@label || key[0] == :dyna_symbol)
2687
+
2688
+ visit key
2689
+ consume_space
2690
+
2691
+ # Don't output `=>` for keys that are `label: value`
2692
+ # or `"label": value`
2693
+ if arrow
2694
+ consume_op "=>"
2695
+ consume_space
2696
+ end
2697
+
2698
+ visit value
2699
+ end
2700
+
2701
+ def visit_splat_inside_hash(node)
2702
+ # **exp
2703
+ #
2704
+ # [:assoc_splat, exp]
2705
+ consume_op "**"
2706
+ skip_space_or_newline
2707
+ visit node[1]
2708
+ end
2709
+
2710
+ def visit_range(node, inclusive)
2711
+ # [:dot2, left, right]
2712
+ _, left, right = node
2713
+
2714
+ visit left
2715
+ skip_space_or_newline
2716
+ consume_op(inclusive ? ".." : "...")
2717
+ skip_space_or_newline
2718
+ visit right unless right.nil?
2719
+ end
2720
+
2721
+ def visit_regexp_literal(node)
2722
+ # [:regexp_literal, pieces, [:@regexp_end, "/", [1, 1]]]
2723
+ _, pieces = node
2724
+
2725
+ check :on_regexp_beg
2726
+ write current_token_value
2727
+ next_token
2728
+
2729
+ visit_exps pieces, with_lines: false
2730
+
2731
+ check :on_regexp_end
2732
+ write current_token_value
2733
+ next_token
2734
+ end
2735
+
2736
+ def visit_array_access(node)
2737
+ # exp[arg1, ..., argN]
2738
+ #
2739
+ # [:aref, name, args]
2740
+ _, name, args = node
2741
+
2742
+ visit_array_getter_or_setter name, args
2743
+ end
2744
+
2745
+ def visit_array_setter(node)
2746
+ # exp[arg1, ..., argN]
2747
+ # (followed by `=`, though not included in this node)
2748
+ #
2749
+ # [:aref_field, name, args]
2750
+ _, name, args = node
2751
+
2752
+ visit_array_getter_or_setter name, args
2753
+ end
2754
+
2755
+ def visit_array_getter_or_setter(name, args)
2756
+ visit name
2757
+
2758
+ token_column = current_token_column
2759
+
2760
+ skip_space
2761
+ check :on_lbracket
2762
+ write "["
2763
+ next_token
2764
+
2765
+ column = @column
2766
+
2767
+ first_space = skip_space
2768
+
2769
+ # Sometimes args comes with an array...
2770
+ if args && args[0].is_a?(Array)
2771
+ visit_literal_elements args, token_column: token_column
2772
+ else
2773
+ if newline? || comment?
2774
+ needed_indent = next_indent
2775
+ if args
2776
+ consume_end_of_line
2777
+ write_indent(needed_indent)
2778
+ else
2779
+ skip_space_or_newline
2780
+ end
2781
+ else
2782
+ write_space_using_setting(first_space, :never)
2783
+ needed_indent = column
2784
+ end
2785
+
2786
+ if args
2787
+ indent(needed_indent) do
2788
+ visit args
2789
+ end
2790
+ end
2791
+ end
2792
+
2793
+ skip_space_or_newline_using_setting(:never)
2794
+
2795
+ check :on_rbracket
2796
+ write "]"
2797
+ next_token
2798
+ end
2799
+
2800
+ def visit_sclass(node)
2801
+ # class << self
2802
+ #
2803
+ # [:sclass, target, body]
2804
+ _, target, body = node
2805
+
2806
+ push_type(node) do
2807
+ consume_keyword "class"
2808
+ consume_space
2809
+ consume_op "<<"
2810
+ consume_space
2811
+ visit target
2812
+
2813
+ @inside_type_body = true
2814
+ visit body
2815
+ end
2816
+ end
2817
+
2818
+ def visit_setter(node)
2819
+ # foo.bar
2820
+ # (followed by `=`, though not included in this node)
2821
+ #
2822
+ # [:field, receiver, :".", name]
2823
+ _, receiver, _, name = node
2824
+
2825
+ @dot_column = nil
2826
+ @original_dot_column = nil
2827
+
2828
+ visit receiver
2829
+
2830
+ skip_space_or_newline_using_setting(:no, @dot_column || next_indent)
2831
+
2832
+ # Remember dot column
2833
+ dot_column = @column
2834
+ original_dot_column = current_token_column
2835
+
2836
+ consume_call_dot
2837
+
2838
+ skip_space_or_newline_using_setting(:no, next_indent)
2839
+
2840
+ visit name
2841
+
2842
+ # Only set it after we visit the call after the dot,
2843
+ # so we remember the outmost dot position
2844
+ @dot_column = dot_column
2845
+ @original_dot_column = original_dot_column
2846
+ end
2847
+
2848
+ def visit_return(node)
2849
+ # [:return, exp]
2850
+ visit_control_keyword node, "return"
2851
+ end
2852
+
2853
+ def visit_break(node)
2854
+ # [:break, exp]
2855
+ visit_control_keyword node, "break"
2856
+ end
2857
+
2858
+ def visit_next(node)
2859
+ # [:next, exp]
2860
+ visit_control_keyword node, "next"
2861
+ end
2862
+
2863
+ def visit_yield(node)
2864
+ # [:yield, exp]
2865
+ visit_control_keyword node, "yield"
2866
+ end
2867
+
2868
+ def visit_control_keyword(node, keyword)
2869
+ _, exp = node
2870
+
2871
+ consume_keyword keyword
2872
+
2873
+ if exp && !exp.empty?
2874
+ consume_space if space?
2875
+
2876
+ indent(@column) do
2877
+ visit_exps to_ary(node[1]), with_lines: false
2878
+ end
2879
+ end
2880
+ end
2881
+
2882
+ def visit_lambda(node)
2883
+ # [:lambda, [:params, nil, nil, nil, nil, nil, nil, nil], [[:void_stmt]]]
2884
+ # [:lambda, [:params, nil, nil, nil, nil, nil, nil, nil], [[:@int, "1", [2, 2]], [:@int, "2", [3, 2]]]]
2885
+ # [: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)
2886
+ _, params, body = node
2887
+
2888
+ body = body[1] if body[0] == :bodystmt
2889
+ check :on_tlambda
2890
+ write "->"
2891
+ next_token
2892
+
2893
+ skip_space
2894
+
2895
+ if empty_params?(params)
2896
+ if current_token_kind == :on_lparen
2897
+ next_token
2898
+ skip_space_or_newline
2899
+ check :on_rparen
2900
+ next_token
2901
+ skip_space_or_newline
2902
+ end
2903
+ else
2904
+ visit params
2905
+ end
2906
+
2907
+ if void_exps?(body)
2908
+ consume_space
2909
+ consume_token :on_tlambeg
2910
+ consume_space
2911
+ consume_token :on_rbrace
2912
+ return
2913
+ end
2914
+
2915
+ consume_space
2916
+
2917
+ brace = current_token_value == "{"
2918
+
2919
+ if brace
2920
+ closing_brace_token, _ = find_closing_brace_token
2921
+
2922
+ # Check if the whole block fits into a single line
2923
+ if current_token_line == closing_brace_token[0][0]
2924
+ consume_token :on_tlambeg
2925
+
2926
+ consume_space
2927
+ visit_exps body, with_lines: false
2928
+ consume_space
2929
+
2930
+ consume_token :on_rbrace
2931
+ return
2932
+ end
2933
+
2934
+ consume_token :on_tlambeg
2935
+ else
2936
+ consume_keyword "do"
2937
+ end
2938
+
2939
+ indent_body body, force_multiline: true
2940
+
2941
+ write_indent
2942
+
2943
+ if brace
2944
+ consume_token :on_rbrace
2945
+ else
2946
+ consume_keyword "end"
2947
+ end
2948
+ end
2949
+
2950
+ def visit_super(node)
2951
+ # [:super, args]
2952
+ _, args = node
2953
+
2954
+ base_column = current_token_column
2955
+
2956
+ consume_keyword "super"
2957
+
2958
+ if space?
2959
+ consume_space
2960
+ visit_command_end node, args, base_column
2961
+ else
2962
+ visit_call_at_paren node, args
2963
+ end
2964
+ end
2965
+
2966
+ def visit_defined(node)
2967
+ # [:defined, exp]
2968
+ _, exp = node
2969
+
2970
+ consume_keyword "defined?"
2971
+ has_space = space?
2972
+
2973
+ if has_space
2974
+ consume_space
2975
+ else
2976
+ skip_space_or_newline
2977
+ end
2978
+
2979
+ has_paren = current_token_kind == :on_lparen
2980
+
2981
+ if has_paren && !has_space
2982
+ write "("
2983
+ next_token
2984
+ skip_space_or_newline
2985
+ end
2986
+
2987
+ visit exp
2988
+
2989
+ if has_paren && !has_space
2990
+ skip_space_or_newline
2991
+ check :on_rparen
2992
+ write ")"
2993
+ next_token
2994
+ end
2995
+ end
2996
+
2997
+ def visit_alias(node)
2998
+ # [:alias, from, to]
2999
+ _, from, to = node
3000
+
3001
+ consume_keyword "alias"
3002
+ consume_space
3003
+ visit from
3004
+ consume_space
3005
+ visit to
3006
+ end
3007
+
3008
+ def visit_undef(node)
3009
+ # [:undef, exps]
3010
+ _, exps = node
3011
+
3012
+ consume_keyword "undef"
3013
+ consume_space
3014
+ visit_comma_separated_list exps
3015
+ end
3016
+
3017
+ def visit_literal_elements(elements, inside_hash: false, inside_array: false, token_column:)
3018
+ base_column = @column
3019
+ base_line = @line
3020
+ needs_final_space = (inside_hash || inside_array) && space?
3021
+ first_space = skip_space
3022
+
3023
+ if inside_hash
3024
+ needs_final_space = false
3025
+ end
3026
+
3027
+ if inside_array
3028
+ needs_final_space = false
3029
+ end
3030
+
3031
+ if newline? || comment?
3032
+ needs_final_space = false
3033
+ end
3034
+
3035
+ # If there's a newline right at the beginning,
3036
+ # write it, and we'll indent element and always
3037
+ # add a trailing comma to the last element
3038
+ needs_trailing_comma = newline? || comment?
3039
+ if needs_trailing_comma
3040
+ if (call_info = @line_to_call_info[@line])
3041
+ call_info << true
3042
+ end
3043
+
3044
+ needed_indent = next_indent
3045
+ indent { consume_end_of_line }
3046
+ write_indent(needed_indent)
3047
+ else
3048
+ needed_indent = base_column
3049
+ end
3050
+
3051
+ wrote_comma = false
3052
+ first_space = nil
3053
+
3054
+ elements.each_with_index do |elem, i|
3055
+ @literal_elements_level = @node_level
3056
+
3057
+ is_last = last?(i, elements)
3058
+ wrote_comma = false
3059
+
3060
+ if needs_trailing_comma
3061
+ indent(needed_indent) { visit elem }
3062
+ else
3063
+ visit elem
3064
+ end
3065
+
3066
+ # We have to be careful not to aumatically write a heredoc on next_token,
3067
+ # because we miss the chance to write a comma to separate elements
3068
+ first_space = skip_space_no_heredoc_check
3069
+ wrote_comma = check_heredocs_in_literal_elements(is_last, wrote_comma)
3070
+
3071
+ next unless comma?
3072
+
3073
+ unless is_last
3074
+ write ","
3075
+ wrote_comma = true
3076
+ end
3077
+
3078
+ # We have to be careful not to aumatically write a heredoc on next_token,
3079
+ # because we miss the chance to write a comma to separate elements
3080
+ next_token_no_heredoc_check
3081
+
3082
+ first_space = skip_space_no_heredoc_check
3083
+ wrote_comma = check_heredocs_in_literal_elements(is_last, wrote_comma)
3084
+
3085
+ if newline? || comment?
3086
+ if is_last
3087
+ # Nothing
3088
+ else
3089
+ indent(needed_indent) do
3090
+ consume_end_of_line(first_space: first_space)
3091
+ write_indent
3092
+ end
3093
+ end
3094
+ else
3095
+ write_space unless is_last
3096
+ end
3097
+ end
3098
+ @literal_elements_level = nil
3099
+
3100
+ if needs_trailing_comma
3101
+ write "," unless wrote_comma || !trailing_commas || @last_was_heredoc
3102
+
3103
+ consume_end_of_line(first_space: first_space)
3104
+ write_indent
3105
+ elsif comment?
3106
+ consume_end_of_line(first_space: first_space)
3107
+ else
3108
+ if needs_final_space
3109
+ consume_space
3110
+ else
3111
+ skip_space_or_newline
3112
+ end
3113
+ end
3114
+
3115
+ if current_token_column == token_column && needed_indent < token_column
3116
+ # If the closing token is aligned with the opening token, we want to
3117
+ # keep it like that, for example in:
3118
+ #
3119
+ # foo([
3120
+ # 2,
3121
+ # ])
3122
+ @literal_indents << [base_line, @line, token_column + INDENT_SIZE - needed_indent]
3123
+ elsif call_info && call_info[0] == current_token_column
3124
+ # If the closing literal position matches the column where
3125
+ # the call started, we want to preserve it like that
3126
+ # (otherwise we align it to the first parameter)
3127
+ call_info << @line
3128
+ end
3129
+ end
3130
+
3131
+ def check_heredocs_in_literal_elements(is_last, wrote_comma)
3132
+ if (newline? || comment?) && !@heredocs.empty?
3133
+ if is_last && trailing_commas
3134
+ write "," unless wrote_comma
3135
+ wrote_comma = true
3136
+ end
3137
+
3138
+ flush_heredocs
3139
+ end
3140
+ wrote_comma
3141
+ end
3142
+
3143
+ def visit_if(node)
3144
+ visit_if_or_unless node, "if"
3145
+ end
3146
+
3147
+ def visit_unless(node)
3148
+ visit_if_or_unless node, "unless"
3149
+ end
3150
+
3151
+ def visit_if_or_unless(node, keyword, check_end: true)
3152
+ # if cond
3153
+ # then_body
3154
+ # else
3155
+ # else_body
3156
+ # end
3157
+ #
3158
+ # [:if, cond, then, else]
3159
+ line = @line
3160
+
3161
+ consume_keyword(keyword)
3162
+ consume_space
3163
+ visit node[1]
3164
+ skip_space
3165
+
3166
+ indent_body node[2]
3167
+ if (else_body = node[3])
3168
+ # [:else, else_contents]
3169
+ # [:elsif, cond, then, else]
3170
+ write_indent if @line != line
3171
+
3172
+ case else_body[0]
3173
+ when :else
3174
+ consume_keyword "else"
3175
+ indent_body else_body[1]
3176
+ when :elsif
3177
+ visit_if_or_unless else_body, "elsif", check_end: false
3178
+ else
3179
+ bug "expected else or elsif, not #{else_body[0]}"
3180
+ end
3181
+ end
3182
+
3183
+ if check_end
3184
+ write_indent if @line != line
3185
+ consume_keyword "end"
3186
+ end
3187
+ end
3188
+
3189
+ def visit_while(node)
3190
+ # [:while, cond, body]
3191
+ visit_while_or_until node, "while"
3192
+ end
3193
+
3194
+ def visit_until(node)
3195
+ # [:until, cond, body]
3196
+ visit_while_or_until node, "until"
3197
+ end
3198
+
3199
+ def visit_while_or_until(node, keyword)
3200
+ _, cond, body = node
3201
+
3202
+ line = @line
3203
+
3204
+ consume_keyword keyword
3205
+ consume_space
3206
+
3207
+ visit cond
3208
+
3209
+ indent_body body
3210
+
3211
+ write_indent if @line != line
3212
+ consume_keyword "end"
3213
+ end
3214
+
3215
+ def visit_case(node)
3216
+ # [:case, cond, case_when]
3217
+ _, cond, case_when = node
3218
+
3219
+ consume_keyword "case"
3220
+
3221
+ if cond
3222
+ consume_space
3223
+ visit cond
3224
+ end
3225
+
3226
+ consume_end_of_line
3227
+
3228
+ write_indent
3229
+ visit case_when
3230
+
3231
+ write_indent
3232
+ consume_keyword "end"
3233
+ end
3234
+
3235
+ def visit_when(node)
3236
+ # [:when, conds, body, next_exp]
3237
+ _, conds, body, next_exp = node
3238
+
3239
+ consume_keyword "when"
3240
+ consume_space
3241
+
3242
+ indent(@column) do
3243
+ visit_comma_separated_list conds
3244
+ skip_space
3245
+ end
3246
+
3247
+ then_keyword = keyword?("then")
3248
+ inline = then_keyword || semicolon?
3249
+ if then_keyword
3250
+ next_token
3251
+
3252
+ skip_space
3253
+
3254
+ info = track_case_when
3255
+ skip_semicolons
3256
+
3257
+ if newline?
3258
+ inline = false
3259
+
3260
+ # Cancel tracking of `case when ... then` on a nelwine.
3261
+ @case_when_positions.pop
3262
+ else
3263
+ write_space
3264
+
3265
+ write "then"
3266
+
3267
+ # We adjust the column and offset from:
3268
+ #
3269
+ # when 1 then 2
3270
+ # ^ (with offset 0)
3271
+ #
3272
+ # to:
3273
+ #
3274
+ # when 1 then 2
3275
+ # ^ (with offset 5)
3276
+ #
3277
+ # In that way we can align this with an `else` clause.
3278
+ if info
3279
+ offset = @column - info[1]
3280
+ info[1] = @column
3281
+ info[-1] = offset
3282
+ end
3283
+
3284
+ write_space
3285
+ end
3286
+ elsif semicolon?
3287
+ skip_semicolons
3288
+
3289
+ if newline? || comment?
3290
+ inline = false
3291
+ else
3292
+ write ";"
3293
+ track_case_when
3294
+ write " "
3295
+ end
3296
+ end
3297
+
3298
+ if inline
3299
+ indent do
3300
+ visit_exps body
3301
+ end
3302
+ else
3303
+ indent_body body
3304
+ end
3305
+
3306
+ if next_exp
3307
+ write_indent
3308
+
3309
+ if next_exp[0] == :else
3310
+ # [:else, body]
3311
+ consume_keyword "else"
3312
+ track_case_when
3313
+ first_space = skip_space
3314
+
3315
+ if newline? || semicolon? || comment?
3316
+ # Cancel tracking of `else` on a nelwine.
3317
+ @case_when_positions.pop
3318
+
3319
+ indent_body next_exp[1]
3320
+ else
3321
+ if align_case_when
3322
+ write_space
3323
+ else
3324
+ write_space_using_setting(first_space, :one)
3325
+ end
3326
+ visit_exps next_exp[1]
3327
+ end
3328
+ else
3329
+ visit next_exp
3330
+ end
3331
+ end
3332
+ end
3333
+
3334
+ def consume_space(want_preserve_whitespace: false)
3335
+ first_space = skip_space
3336
+ if want_preserve_whitespace && !newline? && !comment? && first_space
3337
+ write_space first_space[2] unless @output[-1] == " "
3338
+ skip_space_or_newline
3339
+ else
3340
+ skip_space_or_newline
3341
+ write_space unless @output[-1] == " "
3342
+ end
3343
+ end
3344
+
3345
+ def consume_space_or_newline
3346
+ skip_space
3347
+ if newline? || comment?
3348
+ consume_end_of_line
3349
+ write_indent(next_indent)
3350
+ else
3351
+ consume_space
3352
+ end
3353
+ end
3354
+
3355
+ def skip_space
3356
+ first_space = space? ? current_token : nil
3357
+ next_token while space?
3358
+ first_space
3359
+ end
3360
+
3361
+ def skip_ignored_space
3362
+ next_token while current_token_kind == :on_ignored_sp
3363
+ end
3364
+
3365
+ def skip_space_no_heredoc_check
3366
+ first_space = space? ? current_token : nil
3367
+ while space?
3368
+ next_token_no_heredoc_check
3369
+ end
3370
+ first_space
3371
+ end
3372
+
3373
+ def skip_space_backslash
3374
+ return [false, false] unless space?
3375
+
3376
+ first_space = current_token
3377
+ has_slash_newline = false
3378
+ while space?
3379
+ has_slash_newline ||= current_token_value == "\\\n"
3380
+ next_token
3381
+ end
3382
+ [has_slash_newline, first_space]
3383
+ end
3384
+
3385
+ def skip_space_or_newline(_want_semicolon: false, write_first_semicolon: false)
3386
+ found_newline = false
3387
+ found_comment = false
3388
+ found_semicolon = false
3389
+ last = nil
3390
+
3391
+ loop do
3392
+ case current_token_kind
3393
+ when :on_sp
3394
+ next_token
3395
+ when :on_nl, :on_ignored_nl
3396
+ next_token
3397
+ last = :newline
3398
+ found_newline = true
3399
+ when :on_semicolon
3400
+ if (!found_newline && !found_comment) || (!found_semicolon && write_first_semicolon)
3401
+ write "; "
3402
+ end
3403
+ next_token
3404
+ last = :semicolon
3405
+ found_semicolon = true
3406
+ when :on_comment
3407
+ write_line if last == :newline
3408
+
3409
+ write_indent if found_comment
3410
+ if current_token_value.end_with?("\n")
3411
+ write_space
3412
+ write current_token_value.rstrip
3413
+ write "\n"
3414
+ write_indent(next_indent)
3415
+ @column = next_indent
3416
+ else
3417
+ write current_token_value
3418
+ end
3419
+ next_token
3420
+ found_comment = true
3421
+ last = :comment
3422
+ else
3423
+ break
3424
+ end
3425
+ end
3426
+
3427
+ found_semicolon
3428
+ end
3429
+
3430
+ def skip_semicolons
3431
+ while semicolon? || space?
3432
+ next_token
3433
+ end
3434
+ end
3435
+
3436
+ def empty_body?(body)
3437
+ body[0] == :bodystmt &&
3438
+ body[1].size == 1 &&
3439
+ body[1][0][0] == :void_stmt
3440
+ end
3441
+
3442
+ def consume_token(kind)
3443
+ check kind
3444
+
3445
+ value = current_token_value
3446
+ if kind == :on_ident
3447
+ # Some of these might be brittle and change too much, but this shouldn't be an issue,
3448
+ # because any mistakes will be caught by the Crystal type-checker.
3449
+ case value
3450
+ when "__dir__"
3451
+ value = "__DIR__"
3452
+ when "include?"
3453
+ value = "includes?"
3454
+ when "key?"
3455
+ value = "has_key?"
3456
+ when "detect"
3457
+ value = "find"
3458
+ when "collect"
3459
+ value = "map"
3460
+ when "respond_to?"
3461
+ value = "responds_to?"
3462
+ when "length", "count"
3463
+ value = "size"
3464
+ when "attr_accessor"
3465
+ value = "property"
3466
+ when "attr_reader"
3467
+ value = "getter"
3468
+ when "attr_writer"
3469
+ value = "setter"
3470
+ end
3471
+ end
3472
+
3473
+ consume_token_value(value)
3474
+ next_token
3475
+ end
3476
+
3477
+ def consume_token_value(value)
3478
+ write value
3479
+
3480
+ # If the value has newlines, we need to adjust line and column
3481
+ number_of_lines = value.count("\n")
3482
+ if number_of_lines > 0
3483
+ @line += number_of_lines
3484
+ last_line_index = value.rindex("\n")
3485
+ @column = value.size - (last_line_index + 1)
3486
+ @last_was_newline = @column == 0
3487
+ end
3488
+ end
3489
+
3490
+ def consume_keyword(value)
3491
+ check :on_kw
3492
+ if current_token_value != value
3493
+ bug "Expected keyword #{value}, not #{current_token_value}"
3494
+ end
3495
+ write value
3496
+ next_token
3497
+ end
3498
+
3499
+ def consume_op(value)
3500
+ check :on_op
3501
+ if current_token_value != value
3502
+ bug "Expected op #{value}, not #{current_token_value}"
3503
+ end
3504
+ write value
3505
+ next_token
3506
+ end
3507
+
3508
+ # Consume and print an end of line, handling semicolons and comments
3509
+ #
3510
+ # - at_prefix: are we at a point before an expression? (if so, we don't need a space before the first comment)
3511
+ # - want_semicolon: do we want do print a semicolon to separate expressions?
3512
+ # - want_multiline: do we want multiple lines to appear, or at most one?
3513
+ def consume_end_of_line(at_prefix: false, want_semicolon: false, want_multiline: true, needs_two_lines_on_comment: false, first_space: nil)
3514
+ found_newline = false # Did we find any newline during this method?
3515
+ found_comment_after_newline = false # Did we find a comment after some newline?
3516
+ last = nil # Last token kind found
3517
+ multilple_lines = false # Did we pass through more than one newline?
3518
+ last_comment_has_newline = false # Does the last comment has a newline?
3519
+ newline_count = 0 # Number of newlines we passed
3520
+ last_space = first_space # Last found space
3521
+
3522
+ loop do
3523
+ case current_token_kind
3524
+ when :on_sp
3525
+ # Ignore spaces
3526
+ last_space = current_token
3527
+ next_token
3528
+ when :on_nl, :on_ignored_nl
3529
+ # I don't know why but sometimes a on_ignored_nl
3530
+ # can appear with nil as the "text", and that's wrong
3531
+ if current_token[2].nil?
3532
+ next_token
3533
+ next
3534
+ end
3535
+
3536
+ if last == :newline
3537
+ # If we pass through consecutive newlines, don't print them
3538
+ # yet, but remember this fact
3539
+ multilple_lines = true unless last_comment_has_newline
3540
+ else
3541
+ # If we just printed a comment that had a newline,
3542
+ # we must print two newlines because we remove newlines from comments (rstrip call)
3543
+ write_line
3544
+ if last == :comment && last_comment_has_newline
3545
+ multilple_lines = true
3546
+ else
3547
+ multilple_lines = false
3548
+ end
3549
+ end
3550
+ found_newline = true
3551
+ next_token
3552
+ last = :newline
3553
+ newline_count += 1
3554
+ when :on_semicolon
3555
+ next_token
3556
+ # If we want to print semicolons and we didn't find a newline yet,
3557
+ # print it, but only if it's not followed by a newline
3558
+ if !found_newline && want_semicolon && last != :semicolon
3559
+ skip_space
3560
+ kind = current_token_kind
3561
+ unless [:on_ignored_nl, :on_eof].include?(kind)
3562
+ return if (kind == :on_kw) &&
3563
+ (%w[class module def].include?(current_token_value))
3564
+ write "; "
3565
+ last = :semicolon
3566
+ end
3567
+ end
3568
+ multilple_lines = false
3569
+ when :on_comment
3570
+ if last == :comment
3571
+ # Since we remove newlines from comments, we must add the last
3572
+ # one if it was a comment
3573
+ write_line
3574
+
3575
+ # If the last comment is in the previous line and it was already
3576
+ # aligned to this comment, keep it aligned. This is useful for
3577
+ # this:
3578
+ #
3579
+ # ```
3580
+ # a = 1 # some comment
3581
+ # # that continues here
3582
+ # ```
3583
+ #
3584
+ # We want to preserve it like that and not change it to:
3585
+ #
3586
+ # ```
3587
+ # a = 1 # some comment
3588
+ # # that continues here
3589
+ # ```
3590
+ if current_comment_aligned_to_previous_one?
3591
+ write_indent(@last_comment_column)
3592
+ track_comment(match_previous_id: true)
3593
+ else
3594
+ write_indent
3595
+ end
3596
+ else
3597
+ if found_newline
3598
+ if newline_count == 1 && needs_two_lines_on_comment
3599
+ if multilple_lines
3600
+ write_line
3601
+ multilple_lines = false
3602
+ else
3603
+ multilple_lines = true
3604
+ end
3605
+ needs_two_lines_on_comment = false
3606
+ end
3607
+
3608
+ # Write line or second line if needed
3609
+ write_line if last != :newline || multilple_lines
3610
+ write_indent
3611
+ track_comment(id: @last_was_newline ? true : nil)
3612
+ else
3613
+ # If we didn't find any newline yet, this is the first comment,
3614
+ # so append a space if needed (for example after an expression)
3615
+ unless at_prefix
3616
+ # Preserve whitespace before comment unless we need to align them
3617
+ if last_space
3618
+ write last_space[2]
3619
+ else
3620
+ write_space
3621
+ end
3622
+ end
3623
+
3624
+ # First we check if the comment was aligned to the previous comment
3625
+ # in the previous line, in order to keep them like that.
3626
+ if current_comment_aligned_to_previous_one?
3627
+ track_comment(match_previous_id: true)
3628
+ else
3629
+ # We want to distinguish comments that appear at the beginning
3630
+ # of a line (which means the line has only a comment) and comments
3631
+ # that appear after some expression. We don't want to align these
3632
+ # and consider them separate entities. So, we use `@last_was_newline`
3633
+ # as an id to distinguish that.
3634
+ #
3635
+ # For example, this:
3636
+ #
3637
+ # # comment 1
3638
+ # # comment 2
3639
+ # call # comment 3
3640
+ #
3641
+ # Should format to:
3642
+ #
3643
+ # # comment 1
3644
+ # # comment 2
3645
+ # call # comment 3
3646
+ #
3647
+ # Instead of:
3648
+ #
3649
+ # # comment 1
3650
+ # # comment 2
3651
+ # call # comment 3
3652
+ #
3653
+ # We still want to track the first two comments to align to the
3654
+ # beginning of the line according to indentation in case they
3655
+ # are not already there.
3656
+ track_comment(id: @last_was_newline ? true : nil)
3657
+ end
3658
+ end
3659
+ end
3660
+ @last_comment = current_token
3661
+ @last_comment_column = @column
3662
+ last_comment_has_newline = current_token_value.end_with?("\n")
3663
+ last = :comment
3664
+ found_comment_after_newline = found_newline
3665
+ multilple_lines = false
3666
+
3667
+ write current_token_value.rstrip
3668
+ next_token
3669
+ when :on_embdoc_beg
3670
+ if multilple_lines || last == :comment
3671
+ write_line
3672
+ end
3673
+
3674
+ consume_embedded_comment
3675
+ last = :comment
3676
+ last_comment_has_newline = true
3677
+ else
3678
+ break
3679
+ end
3680
+ end
3681
+
3682
+ # Output a newline if we didn't do so yet:
3683
+ # either we didn't find a newline and we are at the end of a line (and we didn't just pass a semicolon),
3684
+ # or the last thing was a comment (from which we removed the newline)
3685
+ # or we just passed multiple lines (but printed only one)
3686
+ if (!found_newline && !at_prefix && !(want_semicolon && last == :semicolon)) ||
3687
+ last == :comment ||
3688
+ (multilple_lines && (want_multiline || found_comment_after_newline))
3689
+ write_line
3690
+ end
3691
+ end
3692
+
3693
+ def consume_embedded_comment
3694
+ consume_token_value current_token_value
3695
+ next_token
3696
+
3697
+ while current_token_kind != :on_embdoc_end
3698
+ consume_token_value current_token_value
3699
+ next_token
3700
+ end
3701
+
3702
+ consume_token_value current_token_value.rstrip
3703
+ next_token
3704
+ end
3705
+
3706
+ def consume_end
3707
+ return unless current_token_kind == :on___end__
3708
+
3709
+ line = current_token_line
3710
+
3711
+ write_line unless @output.empty?
3712
+ consume_token :on___end__
3713
+
3714
+ lines = @code.lines[line..-1]
3715
+ lines.each do |current_line|
3716
+ write current_line.chomp
3717
+ write_line
3718
+ end
3719
+ end
3720
+
3721
+ def indent(value = nil)
3722
+ if value
3723
+ old_indent = @indent
3724
+ @indent = value
3725
+ yield
3726
+ @indent = old_indent
3727
+ else
3728
+ @indent += INDENT_SIZE
3729
+ yield
3730
+ @indent -= INDENT_SIZE
3731
+ end
3732
+ end
3733
+
3734
+ def indent_body(exps, force_multiline: false)
3735
+ first_space = skip_space
3736
+
3737
+ has_semicolon = semicolon?
3738
+
3739
+ if has_semicolon
3740
+ next_token
3741
+ skip_semicolons
3742
+ first_space = nil
3743
+ end
3744
+
3745
+ # If an end follows there's nothing to do
3746
+ if keyword?("end")
3747
+ if has_semicolon
3748
+ write "; "
3749
+ else
3750
+ write_space_using_setting(first_space, :one)
3751
+ end
3752
+ return
3753
+ end
3754
+
3755
+ # A then keyword can appear after a newline after an `if`, `unless`, etc.
3756
+ # Since that's a super weird formatting for if, probably way too obsolete
3757
+ # by now, we just remove it.
3758
+ has_then = keyword?("then")
3759
+ if has_then
3760
+ next_token
3761
+ second_space = skip_space
3762
+ end
3763
+
3764
+ has_do = keyword?("do")
3765
+ if has_do
3766
+ next_token
3767
+ second_space = skip_space
3768
+ end
3769
+
3770
+ # If no newline or comment follows, we format it inline.
3771
+ if !force_multiline && !(newline? || comment?)
3772
+ if has_then
3773
+ write " then "
3774
+ elsif has_do
3775
+ write_space_using_setting(first_space, :one, at_least_one: true)
3776
+ write "do"
3777
+ write_space_using_setting(second_space, :one, at_least_one: true)
3778
+ elsif has_semicolon
3779
+ write "; "
3780
+ else
3781
+ write_space_using_setting(first_space, :one, at_least_one: true)
3782
+ end
3783
+ visit_exps exps, with_indent: false, with_lines: false
3784
+
3785
+ consume_space
3786
+
3787
+ return
3788
+ end
3789
+
3790
+ indent do
3791
+ consume_end_of_line(want_multiline: false)
3792
+ end
3793
+
3794
+ if keyword?("then")
3795
+ next_token
3796
+ skip_space_or_newline
3797
+ end
3798
+
3799
+ # If the body is [[:void_stmt]] it's an empty body
3800
+ # so there's nothing to write
3801
+ if exps.size == 1 && exps[0][0] == :void_stmt
3802
+ skip_space_or_newline
3803
+ else
3804
+ indent do
3805
+ visit_exps exps, with_indent: true
3806
+ end
3807
+ write_line unless @last_was_newline
3808
+ end
3809
+ end
3810
+
3811
+ def maybe_indent(toggle, indent_size)
3812
+ if toggle
3813
+ indent(indent_size) do
3814
+ yield
3815
+ end
3816
+ else
3817
+ yield
3818
+ end
3819
+ end
3820
+
3821
+ def capture_output
3822
+ old_output = @output
3823
+ @output = +""
3824
+ yield
3825
+ result = @output
3826
+ @output = old_output
3827
+ result
3828
+ end
3829
+
3830
+ def write(value)
3831
+ @output << value
3832
+ @last_was_newline = false
3833
+ @last_was_heredoc = false
3834
+ @column += value.size
3835
+ end
3836
+
3837
+ def write_space(value = " ")
3838
+ @output << value
3839
+ @column += value.size
3840
+ end
3841
+
3842
+ def write_space_using_setting(first_space, setting, at_least_one: false)
3843
+ if first_space && setting == :dynamic
3844
+ write_space first_space[2]
3845
+ elsif setting == :one || at_least_one
3846
+ write_space
3847
+ end
3848
+ end
3849
+
3850
+ def skip_space_or_newline_using_setting(setting, indent_size = @indent)
3851
+ indent(indent_size) do
3852
+ first_space = skip_space
3853
+ if newline? || comment?
3854
+ consume_end_of_line(want_multiline: false, first_space: first_space)
3855
+ write_indent
3856
+ else
3857
+ write_space_using_setting(first_space, setting)
3858
+ end
3859
+ end
3860
+ end
3861
+
3862
+ def write_line
3863
+ @output << "\n"
3864
+ @last_was_newline = true
3865
+ @column = 0
3866
+ @line += 1
3867
+ end
3868
+
3869
+ def write_indent(indent = @indent)
3870
+ @output << " " * indent
3871
+ @column += indent
3872
+ end
3873
+
3874
+ def indent_after_space(node, sticky: false, want_space: true, needed_indent: next_indent, token_column: nil, base_column: nil)
3875
+ skip_space
3876
+
3877
+ case current_token_kind
3878
+ when :on_ignored_nl, :on_comment
3879
+ indent(needed_indent) do
3880
+ consume_end_of_line
3881
+ end
3882
+
3883
+ if token_column && base_column && token_column == current_token_column
3884
+ # If the expression is aligned with the one above, keep it like that
3885
+ indent(base_column) do
3886
+ write_indent
3887
+ visit node
3888
+ end
3889
+ else
3890
+ indent(needed_indent) do
3891
+ write_indent
3892
+ visit node
3893
+ end
3894
+ end
3895
+ else
3896
+ if want_space
3897
+ write_space
3898
+ end
3899
+ if sticky
3900
+ indent(@column) do
3901
+ visit node
3902
+ end
3903
+ else
3904
+ visit node
3905
+ end
3906
+ end
3907
+ end
3908
+
3909
+ def next_indent
3910
+ @indent + INDENT_SIZE
3911
+ end
3912
+
3913
+ def check(kind)
3914
+ if current_token_kind != kind
3915
+ bug "Expected token #{kind}, not #{current_token_kind}"
3916
+ end
3917
+ end
3918
+
3919
+ def bug(msg)
3920
+ raise RubyCrystalCodemod::Bug.new("#{msg} at #{current_token}")
3921
+ end
3922
+
3923
+ # [[1, 0], :on_int, "1"]
3924
+ def current_token
3925
+ @tokens.last
3926
+ end
3927
+
3928
+ def current_token_kind
3929
+ tok = current_token
3930
+ tok ? tok[1] : :on_eof
3931
+ end
3932
+
3933
+ def current_token_value
3934
+ tok = current_token
3935
+ tok ? tok[2] : ""
3936
+ end
3937
+
3938
+ def current_token_line
3939
+ current_token[0][0]
3940
+ end
3941
+
3942
+ def current_token_column
3943
+ current_token[0][1]
3944
+ end
3945
+
3946
+ def keyword?(keyword)
3947
+ current_token_kind == :on_kw && current_token_value == keyword
3948
+ end
3949
+
3950
+ def newline?
3951
+ current_token_kind == :on_nl || current_token_kind == :on_ignored_nl
3952
+ end
3953
+
3954
+ def comment?
3955
+ current_token_kind == :on_comment
3956
+ end
3957
+
3958
+ def semicolon?
3959
+ current_token_kind == :on_semicolon
3960
+ end
3961
+
3962
+ def comma?
3963
+ current_token_kind == :on_comma
3964
+ end
3965
+
3966
+ def space?
3967
+ current_token_kind == :on_sp
3968
+ end
3969
+
3970
+ def void_exps?(node)
3971
+ node.size == 1 && node[0].size == 1 && node[0][0] == :void_stmt
3972
+ end
3973
+
3974
+ def find_closing_brace_token
3975
+ count = 0
3976
+ i = @tokens.size - 1
3977
+ while i >= 0
3978
+ token = @tokens[i]
3979
+ _, kind = token
3980
+ case kind
3981
+ when :on_lbrace, :on_tlambeg
3982
+ count += 1
3983
+ when :on_rbrace
3984
+ count -= 1
3985
+ return [token, i] if count == 0
3986
+ end
3987
+ i -= 1
3988
+ end
3989
+ nil
3990
+ end
3991
+
3992
+ def next_token
3993
+ @prev_token = self.current_token
3994
+
3995
+ @tokens.pop
3996
+
3997
+ if (newline? || comment?) && !@heredocs.empty?
3998
+ flush_heredocs
3999
+ end
4000
+
4001
+ # First first token in newline if requested
4002
+ if @want_first_token_in_line && @prev_token && (@prev_token[1] == :on_nl || @prev_token[1] == :on_ignored_nl)
4003
+ @tokens.reverse_each do |token|
4004
+ case token[1]
4005
+ when :on_sp
4006
+ next
4007
+ else
4008
+ @first_token_in_line = token
4009
+ break
4010
+ end
4011
+ end
4012
+ end
4013
+ end
4014
+
4015
+ def next_token_no_heredoc_check
4016
+ @tokens.pop
4017
+ end
4018
+
4019
+ def last?(index, array)
4020
+ index == array.size - 1
4021
+ end
4022
+
4023
+ def push_call(node)
4024
+ push_node(node) do
4025
+ # A call can specify hash arguments so it acts as a
4026
+ # hash for key alignment purposes
4027
+ push_hash(node) do
4028
+ yield
4029
+ end
4030
+ end
4031
+ end
4032
+
4033
+ def push_node(node)
4034
+ old_node = @current_node
4035
+ @current_node = node
4036
+
4037
+ yield
4038
+
4039
+ @current_node = old_node
4040
+ end
4041
+
4042
+ def push_hash(node)
4043
+ old_hash = @current_hash
4044
+ @current_hash = node
4045
+ yield
4046
+ @current_hash = old_hash
4047
+ end
4048
+
4049
+ def push_type(node)
4050
+ old_type = @current_type
4051
+ @current_type = node
4052
+ yield
4053
+ @current_type = old_type
4054
+ end
4055
+
4056
+ def to_ary(node)
4057
+ node[0].is_a?(Symbol) ? [node] : node
4058
+ end
4059
+
4060
+ def dedent_calls
4061
+ return if @line_to_call_info.empty?
4062
+
4063
+ lines = @output.lines
4064
+
4065
+ while (line_to_call_info = @line_to_call_info.shift)
4066
+ first_line, call_info = line_to_call_info
4067
+ next unless call_info.size == 5
4068
+
4069
+ indent, first_param_indent, needs_dedent, first_paren_end_line, last_line = call_info
4070
+ next unless needs_dedent
4071
+ next unless first_paren_end_line == last_line
4072
+
4073
+ diff = first_param_indent - indent
4074
+ (first_line + 1..last_line).each do |line|
4075
+ @line_to_call_info.delete(line)
4076
+
4077
+ next if @unmodifiable_string_lines[line]
4078
+
4079
+ current_line = lines[line]
4080
+ current_line = current_line[diff..-1] if diff >= 0
4081
+
4082
+ # It can happen that this line didn't need an indent because
4083
+ # it simply had a newline
4084
+ if current_line
4085
+ lines[line] = current_line
4086
+ adjust_other_alignments nil, line, 0, -diff
4087
+ end
4088
+ end
4089
+ end
4090
+
4091
+ @output = lines.join
4092
+ end
4093
+
4094
+ def indent_literals
4095
+ return if @literal_indents.empty?
4096
+
4097
+ lines = @output.lines
4098
+
4099
+ modified_lines = []
4100
+ @literal_indents.each do |first_line, last_line, indent|
4101
+ (first_line + 1..last_line).each do |line|
4102
+ next if @unmodifiable_string_lines[line]
4103
+
4104
+ current_line = lines[line]
4105
+ current_line = "#{" " * indent}#{current_line}"
4106
+ unless modified_lines[line]
4107
+ modified_lines[line] = current_line
4108
+ lines[line] = current_line
4109
+ adjust_other_alignments nil, line, 0, indent
4110
+ end
4111
+ end
4112
+ end
4113
+
4114
+ @output = lines.join
4115
+ end
4116
+
4117
+ def do_align_case_when
4118
+ do_align @case_when_positions, :case
4119
+ end
4120
+
4121
+ def do_align(components, scope)
4122
+ lines = @output.lines
4123
+
4124
+ # Chunk components that are in consecutive lines
4125
+ chunks = components.chunk_while do |(l1, _c1, i1, id1), (l2, _c2, i2, id2)|
4126
+ l1 + 1 == l2 && i1 == i2 && id1 == id2
4127
+ end
4128
+
4129
+ chunks.each do |elements|
4130
+ next if elements.size == 1
4131
+
4132
+ max_column = elements.map { |_l, c| c }.max
4133
+
4134
+ elements.each do |(line, column, _, _, offset)|
4135
+ next if column == max_column
4136
+
4137
+ split_index = column
4138
+ split_index -= offset if offset
4139
+
4140
+ target_line = lines[line]
4141
+
4142
+ before = target_line[0...split_index]
4143
+ after = target_line[split_index..-1]
4144
+
4145
+ filler_size = max_column - column
4146
+ filler = " " * filler_size
4147
+
4148
+ # Move all lines affected by the assignment shift
4149
+ if scope == :assign && (range = @assignments_ranges[line])
4150
+ (line + 1..range).each do |line_number|
4151
+ lines[line_number] = "#{filler}#{lines[line_number]}"
4152
+
4153
+ # And move other elements too if applicable
4154
+ adjust_other_alignments scope, line_number, column, filler_size
4155
+ end
4156
+ end
4157
+
4158
+ # Move comments to the right if a change happened
4159
+ if scope != :comment
4160
+ adjust_other_alignments scope, line, column, filler_size
4161
+ end
4162
+
4163
+ lines[line] = "#{before}#{filler}#{after}"
4164
+ end
4165
+ end
4166
+
4167
+ @output = lines.join
4168
+ end
4169
+
4170
+ def adjust_other_alignments(scope, line, column, offset)
4171
+ adjustments = @line_to_alignments_positions[line]
4172
+ return unless adjustments
4173
+
4174
+ adjustments.each do |key, adjustment_column, target, index|
4175
+ next if adjustment_column <= column
4176
+ next if scope == key
4177
+
4178
+ target[index][1] += offset if target[index]
4179
+ end
4180
+ end
4181
+
4182
+ def remove_lines_before_inline_declarations
4183
+ return if @inline_declarations.empty?
4184
+
4185
+ lines = @output.lines
4186
+
4187
+ @inline_declarations.reverse.each_cons(2) do |(after, after_original), (before, before_original)|
4188
+ if before + 2 == after && before_original + 1 == after_original && lines[before + 1].strip.empty?
4189
+ lines.delete_at(before + 1)
4190
+ end
4191
+ end
4192
+
4193
+ @output = lines.join
4194
+ end
4195
+
4196
+ def result
4197
+ @output
4198
+ end
4199
+
4200
+ # Check to see if need to add space inside hash literal braces.
4201
+ def need_space_for_hash?(node, closing_brace_token)
4202
+ return false unless node[1]
4203
+
4204
+ left_need_space = current_token_line == node_line(node, beginning: true)
4205
+ right_need_space = closing_brace_token[0][0] == node_line(node, beginning: false)
4206
+
4207
+ left_need_space && right_need_space
4208
+ end
4209
+
4210
+ def node_line(node, beginning: true)
4211
+ # get line of node, it is only used in visit_hash right now,
4212
+ # so handling the following node types is enough.
4213
+ case node.first
4214
+ when :hash, :string_literal, :symbol_literal, :symbol, :vcall, :string_content, :assoc_splat, :var_ref
4215
+ node_line(node[1], beginning: beginning)
4216
+ when :assoc_new
4217
+ if beginning
4218
+ node_line(node[1], beginning: beginning)
4219
+ else
4220
+ if node.last == [:string_literal, [:string_content]] || node.last == [:hash, nil]
4221
+ # there's no line number for [:string_literal, [:string_content]] or [:hash, nil]
4222
+ node_line(node[1], beginning: beginning)
4223
+ else
4224
+ node_line(node.last, beginning: beginning)
4225
+ end
4226
+ end
4227
+ when :assoclist_from_args
4228
+ node_line(beginning ? node[1][0] : node[1].last, beginning: beginning)
4229
+ when :dyna_symbol
4230
+ if node[1][0].is_a?(Symbol)
4231
+ node_line(node[1], beginning: beginning)
4232
+ else
4233
+ node_line(node[1][0], beginning: beginning)
4234
+ end
4235
+ when :@label, :@int, :@ident, :@tstring_content, :@kw
4236
+ node[2][0]
4237
+ end
4238
+ end
4239
+ end