ruby_crystal_codemod 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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