ruby_crystal_codemod 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.circleci/config.yml +103 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +4 -0
- data/.gitignore +24 -0
- data/.rspec +2 -0
- data/.rubocop.yml +15 -0
- data/.rufo +2 -0
- data/.vscode/settings.json +4 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +141 -0
- data/Rakefile +8 -0
- data/bin/compile_post_process +4 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/developing-ruby-crystal-codemod.md +3 -0
- data/docs/releasing-a-gem.md +17 -0
- data/exe/ruby_crystal_codemod +4 -0
- data/lib/ruby_crystal_codemod.rb +19 -0
- data/lib/ruby_crystal_codemod/command.rb +200 -0
- data/lib/ruby_crystal_codemod/dot_file.rb +52 -0
- data/lib/ruby_crystal_codemod/file_finder.rb +62 -0
- data/lib/ruby_crystal_codemod/formatter.rb +4239 -0
- data/lib/ruby_crystal_codemod/logger.rb +39 -0
- data/lib/ruby_crystal_codemod/settings.rb +29 -0
- data/lib/ruby_crystal_codemod/version.rb +5 -0
- data/rakelib/ruby_crystal_codemod.rake +22 -0
- data/ruby_crystal_codemod.gemspec +34 -0
- data/run_test +5 -0
- data/util/post_process_crystal/shard.lock +14 -0
- data/util/post_process_crystal/shard.yml +11 -0
- data/util/post_process_crystal/spec/fixtures/example.rb +13 -0
- data/util/post_process_crystal/spec/post_process_crystal_spec.cr +124 -0
- data/util/post_process_crystal/spec/spec_helper.cr +4 -0
- data/util/post_process_crystal/src/command.cr +16 -0
- data/util/post_process_crystal/src/post_process_crystal.cr +48 -0
- metadata +208 -0
@@ -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
|