spoom 1.2.4 → 1.3.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 +4 -4
- data/README.md +54 -55
- data/lib/spoom/cli/deadcode.rb +172 -0
- data/lib/spoom/cli/helper.rb +20 -0
- data/lib/spoom/cli/srb/bump.rb +200 -0
- data/lib/spoom/cli/srb/coverage.rb +224 -0
- data/lib/spoom/cli/srb/lsp.rb +159 -0
- data/lib/spoom/cli/srb/tc.rb +150 -0
- data/lib/spoom/cli/srb.rb +27 -0
- data/lib/spoom/cli.rb +72 -32
- data/lib/spoom/context/git.rb +2 -2
- data/lib/spoom/context/sorbet.rb +2 -2
- data/lib/spoom/deadcode/definition.rb +11 -0
- data/lib/spoom/deadcode/indexer.rb +222 -224
- data/lib/spoom/deadcode/location.rb +2 -2
- data/lib/spoom/deadcode/plugins/action_mailer.rb +2 -2
- data/lib/spoom/deadcode/plugins/action_mailer_preview.rb +19 -0
- data/lib/spoom/deadcode/plugins/actionpack.rb +4 -6
- data/lib/spoom/deadcode/plugins/active_model.rb +8 -8
- data/lib/spoom/deadcode/plugins/active_record.rb +9 -12
- data/lib/spoom/deadcode/plugins/active_support.rb +11 -0
- data/lib/spoom/deadcode/plugins/base.rb +1 -1
- data/lib/spoom/deadcode/plugins/graphql.rb +4 -4
- data/lib/spoom/deadcode/plugins/namespaces.rb +2 -4
- data/lib/spoom/deadcode/plugins/ruby.rb +8 -17
- data/lib/spoom/deadcode/plugins/sorbet.rb +4 -10
- data/lib/spoom/deadcode/plugins.rb +1 -0
- data/lib/spoom/deadcode/remover.rb +209 -174
- data/lib/spoom/deadcode/send.rb +9 -10
- data/lib/spoom/deadcode/visitor.rb +755 -0
- data/lib/spoom/deadcode.rb +40 -10
- data/lib/spoom/file_tree.rb +0 -16
- data/lib/spoom/sorbet/errors.rb +1 -1
- data/lib/spoom/sorbet/lsp/structures.rb +2 -2
- data/lib/spoom/version.rb +1 -1
- metadata +19 -15
- data/lib/spoom/cli/bump.rb +0 -198
- data/lib/spoom/cli/coverage.rb +0 -222
- data/lib/spoom/cli/lsp.rb +0 -168
- data/lib/spoom/cli/run.rb +0 -148
@@ -0,0 +1,200 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "find"
|
5
|
+
require "open3"
|
6
|
+
|
7
|
+
module Spoom
|
8
|
+
module Cli
|
9
|
+
module Srb
|
10
|
+
class Bump < Thor
|
11
|
+
extend T::Sig
|
12
|
+
include Helper
|
13
|
+
|
14
|
+
default_task :bump
|
15
|
+
|
16
|
+
desc "bump DIRECTORY", "Change Sorbet sigils from one strictness to another when no errors"
|
17
|
+
option :from,
|
18
|
+
type: :string,
|
19
|
+
default: Spoom::Sorbet::Sigils::STRICTNESS_FALSE,
|
20
|
+
desc: "Change only files from this strictness"
|
21
|
+
option :to,
|
22
|
+
type: :string,
|
23
|
+
default: Spoom::Sorbet::Sigils::STRICTNESS_TRUE,
|
24
|
+
desc: "Change files to this strictness"
|
25
|
+
option :force,
|
26
|
+
type: :boolean,
|
27
|
+
default: false,
|
28
|
+
aliases: :f,
|
29
|
+
desc: "Change strictness without type checking"
|
30
|
+
option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
|
31
|
+
option :dry,
|
32
|
+
type: :boolean,
|
33
|
+
default: false,
|
34
|
+
aliases: :d,
|
35
|
+
desc: "Only display what would happen, do not actually change sigils"
|
36
|
+
option :only,
|
37
|
+
type: :string,
|
38
|
+
default: nil,
|
39
|
+
aliases: :o,
|
40
|
+
desc: "Only change specified list (one file by line)"
|
41
|
+
option :suggest_bump_command,
|
42
|
+
type: :string,
|
43
|
+
desc: "Command to suggest if files can be bumped"
|
44
|
+
option :count_errors,
|
45
|
+
type: :boolean,
|
46
|
+
default: false,
|
47
|
+
desc: "Count the number of errors if all files were bumped"
|
48
|
+
option :sorbet_options, type: :string, default: "", desc: "Pass options to Sorbet"
|
49
|
+
sig { params(directory: String).void }
|
50
|
+
def bump(directory = ".")
|
51
|
+
context = context_requiring_sorbet!
|
52
|
+
from = options[:from]
|
53
|
+
to = options[:to]
|
54
|
+
force = options[:force]
|
55
|
+
dry = options[:dry]
|
56
|
+
only = options[:only]
|
57
|
+
cmd = options[:suggest_bump_command]
|
58
|
+
directory = File.expand_path(directory)
|
59
|
+
exec_path = File.expand_path(self.exec_path)
|
60
|
+
|
61
|
+
unless Sorbet::Sigils.valid_strictness?(from)
|
62
|
+
say_error("Invalid strictness `#{from}` for option `--from`")
|
63
|
+
exit(1)
|
64
|
+
end
|
65
|
+
|
66
|
+
unless Sorbet::Sigils.valid_strictness?(to)
|
67
|
+
say_error("Invalid strictness `#{to}` for option `--to`")
|
68
|
+
exit(1)
|
69
|
+
end
|
70
|
+
|
71
|
+
if options[:count_errors] && !dry
|
72
|
+
say_error("`--count-errors` can only be used with `--dry`")
|
73
|
+
exit(1)
|
74
|
+
end
|
75
|
+
|
76
|
+
say("Checking files...")
|
77
|
+
|
78
|
+
files_to_bump = context.srb_files_with_strictness(from, include_rbis: false)
|
79
|
+
.map { |file| File.expand_path(file, context.absolute_path) }
|
80
|
+
.select { |file| file.start_with?(directory) }
|
81
|
+
|
82
|
+
if only
|
83
|
+
list = File.read(only).lines.map { |file| File.expand_path(file.strip) }
|
84
|
+
files_to_bump.select! { |file| list.include?(File.expand_path(file)) }
|
85
|
+
end
|
86
|
+
|
87
|
+
say("\n")
|
88
|
+
|
89
|
+
if files_to_bump.empty?
|
90
|
+
say("No files to bump from `#{from}` to `#{to}`")
|
91
|
+
exit(0)
|
92
|
+
end
|
93
|
+
|
94
|
+
Sorbet::Sigils.change_sigil_in_files(files_to_bump, to)
|
95
|
+
|
96
|
+
if force
|
97
|
+
print_changes(files_to_bump, command: cmd, from: from, to: to, dry: dry, path: exec_path)
|
98
|
+
undo_changes(files_to_bump, from) if dry
|
99
|
+
exit(files_to_bump.empty?)
|
100
|
+
end
|
101
|
+
|
102
|
+
error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
|
103
|
+
result = begin
|
104
|
+
T.unsafe(context).srb_tc(
|
105
|
+
*options[:sorbet_options].split(" "),
|
106
|
+
"--error-url-base=#{error_url_base}",
|
107
|
+
capture_err: true,
|
108
|
+
sorbet_bin: options[:sorbet],
|
109
|
+
)
|
110
|
+
rescue Spoom::Sorbet::Error::Segfault => error
|
111
|
+
say_error(<<~ERR, status: nil)
|
112
|
+
!!! Sorbet exited with code #{Spoom::Sorbet::SEGFAULT_CODE} - SEGFAULT !!!
|
113
|
+
|
114
|
+
This is most likely related to a bug in Sorbet.
|
115
|
+
It means one of the file bumped to `typed: #{to}` made Sorbet crash.
|
116
|
+
Run `spoom bump -f` locally followed by `bundle exec srb tc` to investigate the problem.
|
117
|
+
ERR
|
118
|
+
undo_changes(files_to_bump, from)
|
119
|
+
exit(error.result.exit_code)
|
120
|
+
rescue Spoom::Sorbet::Error::Killed => error
|
121
|
+
say_error(<<~ERR, status: nil)
|
122
|
+
!!! Sorbet exited with code #{Spoom::Sorbet::KILLED_CODE} - KILLED !!!
|
123
|
+
|
124
|
+
It means Sorbet was killed while executing. Changes to files have not been applied.
|
125
|
+
Re-run `spoom bump` to try again.
|
126
|
+
ERR
|
127
|
+
undo_changes(files_to_bump, from)
|
128
|
+
exit(error.result.exit_code)
|
129
|
+
end
|
130
|
+
|
131
|
+
if result.status
|
132
|
+
print_changes(files_to_bump, command: cmd, from: from, to: to, dry: dry, path: exec_path)
|
133
|
+
undo_changes(files_to_bump, from) if dry
|
134
|
+
exit(files_to_bump.empty?)
|
135
|
+
end
|
136
|
+
|
137
|
+
unless result.exit_code == 100
|
138
|
+
# Sorbet will return exit code 100 if there are type checking errors.
|
139
|
+
# If Sorbet returned something else, it means it didn't terminate normally.
|
140
|
+
say_error(result.err, status: nil, nl: false)
|
141
|
+
undo_changes(files_to_bump, from)
|
142
|
+
exit(1)
|
143
|
+
end
|
144
|
+
|
145
|
+
errors = Sorbet::Errors::Parser.parse_string(result.err, error_url_base: error_url_base)
|
146
|
+
|
147
|
+
all_files = errors.flat_map do |err|
|
148
|
+
[err.file, *err.files_from_error_sections]
|
149
|
+
end
|
150
|
+
|
151
|
+
files_with_errors = all_files.map do |file|
|
152
|
+
path = File.expand_path(file)
|
153
|
+
next unless path.start_with?(directory)
|
154
|
+
next unless File.file?(path)
|
155
|
+
next unless files_to_bump.include?(path)
|
156
|
+
|
157
|
+
path
|
158
|
+
end.compact.uniq
|
159
|
+
|
160
|
+
undo_changes(files_with_errors, from)
|
161
|
+
|
162
|
+
say("Found #{errors.length} type checking error#{"s" if errors.length > 1}") if options[:count_errors]
|
163
|
+
|
164
|
+
files_changed = files_to_bump - files_with_errors
|
165
|
+
print_changes(files_changed, command: cmd, from: from, to: to, dry: dry, path: exec_path)
|
166
|
+
undo_changes(files_to_bump, from) if dry
|
167
|
+
exit(files_changed.empty?)
|
168
|
+
end
|
169
|
+
|
170
|
+
no_commands do
|
171
|
+
def print_changes(files, command:, from: "false", to: "true", dry: false, path: File.expand_path("."))
|
172
|
+
files_count = files.size
|
173
|
+
if files_count.zero?
|
174
|
+
say("No files to bump from `#{from}` to `#{to}`")
|
175
|
+
return
|
176
|
+
end
|
177
|
+
message = StringIO.new
|
178
|
+
message << (dry ? "Can bump" : "Bumped")
|
179
|
+
message << " `#{files_count}` file#{"s" if files_count > 1}"
|
180
|
+
message << " from `#{from}` to `#{to}`:"
|
181
|
+
say(message.string)
|
182
|
+
files.each do |file|
|
183
|
+
file_path = Pathname.new(file).relative_path_from(path)
|
184
|
+
say(" + #{file_path}")
|
185
|
+
end
|
186
|
+
if dry && command
|
187
|
+
say("\nRun `#{command}` to bump #{files_count > 1 ? "them" : "it"}")
|
188
|
+
elsif dry
|
189
|
+
say("\nRun `spoom bump --from #{from} --to #{to}` locally then `commit the changes` and `push them`")
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def undo_changes(files, from_strictness)
|
194
|
+
Sorbet::Sigils.change_sigil_in_files(files, from_strictness)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "../../coverage"
|
5
|
+
require_relative "../../timeline"
|
6
|
+
|
7
|
+
module Spoom
|
8
|
+
module Cli
|
9
|
+
module Srb
|
10
|
+
class Coverage < Thor
|
11
|
+
include Helper
|
12
|
+
|
13
|
+
DATA_DIR = "spoom_data"
|
14
|
+
|
15
|
+
default_task :snapshot
|
16
|
+
|
17
|
+
desc "snapshot", "Run srb tc and display metrics"
|
18
|
+
option :save, type: :string, lazy_default: DATA_DIR, desc: "Save snapshot data as json"
|
19
|
+
option :rbi, type: :boolean, default: true, desc: "Include RBI files in metrics"
|
20
|
+
option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
|
21
|
+
def snapshot
|
22
|
+
context = context_requiring_sorbet!
|
23
|
+
sorbet = options[:sorbet]
|
24
|
+
|
25
|
+
snapshot = Spoom::Coverage.snapshot(context, rbi: options[:rbi], sorbet_bin: sorbet)
|
26
|
+
snapshot.print
|
27
|
+
|
28
|
+
save_dir = options[:save]
|
29
|
+
return unless save_dir
|
30
|
+
|
31
|
+
FileUtils.mkdir_p(save_dir)
|
32
|
+
file = "#{save_dir}/#{snapshot.commit_sha || snapshot.timestamp}.json"
|
33
|
+
File.write(file, snapshot.to_json)
|
34
|
+
say("\nSnapshot data saved under `#{file}`")
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "timeline", "Replay a project and collect metrics"
|
38
|
+
option :from, type: :string, desc: "From commit date"
|
39
|
+
option :to, type: :string, default: Time.now.strftime("%F"), desc: "To commit date"
|
40
|
+
option :save, type: :string, lazy_default: DATA_DIR, desc: "Save snapshot data as json"
|
41
|
+
option :bundle_install, type: :boolean, desc: "Execute `bundle install` before collecting metrics"
|
42
|
+
option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
|
43
|
+
def timeline
|
44
|
+
context = context_requiring_sorbet!
|
45
|
+
path = exec_path
|
46
|
+
sorbet = options[:sorbet]
|
47
|
+
|
48
|
+
ref_before = context.git_current_branch
|
49
|
+
ref_before = context.git_last_commit&.sha unless ref_before
|
50
|
+
unless ref_before
|
51
|
+
say_error("Not in a git repository")
|
52
|
+
say_error("\nSpoom needs to checkout into your previous commits to build the timeline.", status: nil)
|
53
|
+
exit(1)
|
54
|
+
end
|
55
|
+
|
56
|
+
unless context.git_workdir_clean?
|
57
|
+
say_error("Uncommitted changes")
|
58
|
+
say_error(<<~ERR, status: nil)
|
59
|
+
|
60
|
+
Spoom needs to checkout into your previous commits to build the timeline."
|
61
|
+
|
62
|
+
Please `git commit` or `git stash` your changes then try again
|
63
|
+
ERR
|
64
|
+
exit(1)
|
65
|
+
end
|
66
|
+
|
67
|
+
save_dir = options[:save]
|
68
|
+
FileUtils.mkdir_p(save_dir) if save_dir
|
69
|
+
|
70
|
+
from = parse_time(options[:from], "--from")
|
71
|
+
to = parse_time(options[:to], "--to")
|
72
|
+
|
73
|
+
unless from
|
74
|
+
intro_commit = context.sorbet_intro_commit
|
75
|
+
intro_commit = T.must(intro_commit) # we know it's in there since in_sorbet_project!
|
76
|
+
from = intro_commit.time
|
77
|
+
end
|
78
|
+
|
79
|
+
timeline = Spoom::Timeline.new(context, from, to)
|
80
|
+
ticks = timeline.ticks
|
81
|
+
|
82
|
+
if ticks.empty?
|
83
|
+
say_error("No commits to replay, try different `--from` and `--to` options")
|
84
|
+
exit(1)
|
85
|
+
end
|
86
|
+
|
87
|
+
ticks.each_with_index do |commit, i|
|
88
|
+
say("Analyzing commit `#{commit.sha}` - #{commit.time.strftime("%F")} (#{i + 1} / #{ticks.size})")
|
89
|
+
|
90
|
+
context.git_checkout!(ref: commit.sha)
|
91
|
+
|
92
|
+
snapshot = T.let(nil, T.nilable(Spoom::Coverage::Snapshot))
|
93
|
+
if options[:bundle_install]
|
94
|
+
Bundler.with_unbundled_env do
|
95
|
+
next unless bundle_install(path, commit.sha)
|
96
|
+
|
97
|
+
snapshot = Spoom::Coverage.snapshot(context, sorbet_bin: sorbet)
|
98
|
+
end
|
99
|
+
else
|
100
|
+
snapshot = Spoom::Coverage.snapshot(context, sorbet_bin: sorbet)
|
101
|
+
end
|
102
|
+
next unless snapshot
|
103
|
+
|
104
|
+
snapshot.print(indent_level: 2)
|
105
|
+
say("\n")
|
106
|
+
|
107
|
+
next unless save_dir
|
108
|
+
|
109
|
+
file = "#{save_dir}/#{commit.sha}.json"
|
110
|
+
File.write(file, snapshot.to_json)
|
111
|
+
say(" Snapshot data saved under `#{file}`\n\n")
|
112
|
+
end
|
113
|
+
context.git_checkout!(ref: ref_before)
|
114
|
+
end
|
115
|
+
|
116
|
+
desc "report", "Produce a typing coverage report"
|
117
|
+
option :data, type: :string, default: DATA_DIR, desc: "Snapshots JSON data"
|
118
|
+
option :file,
|
119
|
+
type: :string,
|
120
|
+
default: "spoom_report.html",
|
121
|
+
aliases: :f,
|
122
|
+
desc: "Save report to file"
|
123
|
+
option :color_ignore,
|
124
|
+
type: :string,
|
125
|
+
default: Spoom::Coverage::D3::COLOR_IGNORE,
|
126
|
+
desc: "Color used for typed: ignore"
|
127
|
+
option :color_false,
|
128
|
+
type: :string,
|
129
|
+
default: Spoom::Coverage::D3::COLOR_FALSE,
|
130
|
+
desc: "Color used for typed: false"
|
131
|
+
option :color_true,
|
132
|
+
type: :string,
|
133
|
+
default: Spoom::Coverage::D3::COLOR_TRUE,
|
134
|
+
desc: "Color used for typed: true"
|
135
|
+
option :color_strict,
|
136
|
+
type: :string,
|
137
|
+
default: Spoom::Coverage::D3::COLOR_STRICT,
|
138
|
+
desc: "Color used for typed: strict"
|
139
|
+
option :color_strong,
|
140
|
+
type: :string,
|
141
|
+
default: Spoom::Coverage::D3::COLOR_STRONG,
|
142
|
+
desc: "Color used for typed: strong"
|
143
|
+
def report
|
144
|
+
context = context_requiring_sorbet!
|
145
|
+
|
146
|
+
data_dir = options[:data]
|
147
|
+
files = Dir.glob("#{data_dir}/*.json")
|
148
|
+
if files.empty?
|
149
|
+
message_no_data(data_dir)
|
150
|
+
exit(1)
|
151
|
+
end
|
152
|
+
|
153
|
+
snapshots = files.sort.map do |file|
|
154
|
+
json = File.read(file)
|
155
|
+
Spoom::Coverage::Snapshot.from_json(json)
|
156
|
+
end.filter(&:commit_timestamp).sort_by!(&:commit_timestamp)
|
157
|
+
|
158
|
+
palette = Spoom::Coverage::D3::ColorPalette.new(
|
159
|
+
ignore: options[:color_ignore],
|
160
|
+
false: options[:color_false],
|
161
|
+
true: options[:color_true],
|
162
|
+
strict: options[:color_strict],
|
163
|
+
strong: options[:color_strong],
|
164
|
+
)
|
165
|
+
|
166
|
+
report = Spoom::Coverage.report(context, snapshots, palette: palette)
|
167
|
+
file = options[:file]
|
168
|
+
File.write(file, report.html)
|
169
|
+
say("Report generated under `#{file}`")
|
170
|
+
say("\nUse `spoom coverage open` to open it.")
|
171
|
+
end
|
172
|
+
|
173
|
+
desc "open", "Open the typing coverage report"
|
174
|
+
def open(file = "spoom_report.html")
|
175
|
+
unless File.exist?(file)
|
176
|
+
say_error("No report file to open `#{file}`")
|
177
|
+
say_error(<<~ERR, status: nil)
|
178
|
+
|
179
|
+
If you already generated a report under another name use #{blue("spoom coverage open PATH")}.
|
180
|
+
|
181
|
+
To generate a report run #{blue("spoom coverage report")}.
|
182
|
+
ERR
|
183
|
+
exit(1)
|
184
|
+
end
|
185
|
+
|
186
|
+
exec("open #{file}")
|
187
|
+
end
|
188
|
+
|
189
|
+
no_commands do
|
190
|
+
def parse_time(string, option)
|
191
|
+
return unless string
|
192
|
+
|
193
|
+
Time.parse(string)
|
194
|
+
rescue ArgumentError
|
195
|
+
say_error("Invalid date `#{string}` for option `#{option}` (expected format `YYYY-MM-DD`)")
|
196
|
+
exit(1)
|
197
|
+
end
|
198
|
+
|
199
|
+
def bundle_install(path, sha)
|
200
|
+
opts = {}
|
201
|
+
opts[:chdir] = path
|
202
|
+
out, status = Open3.capture2e("bundle install", opts)
|
203
|
+
unless status.success?
|
204
|
+
say_error("Can't run `bundle install` for commit `#{sha}`. Skipping snapshot")
|
205
|
+
say_error(out, status: nil)
|
206
|
+
return false
|
207
|
+
end
|
208
|
+
true
|
209
|
+
end
|
210
|
+
|
211
|
+
def message_no_data(file)
|
212
|
+
say_error("No snapshot files found in `#{file}`")
|
213
|
+
say_error(<<~ERR, status: nil)
|
214
|
+
|
215
|
+
If you already generated snapshot files under another directory use #{blue("spoom coverage report PATH")}.
|
216
|
+
|
217
|
+
To generate snapshot files run #{blue("spoom coverage timeline --save")}.
|
218
|
+
ERR
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "shellwords"
|
5
|
+
|
6
|
+
require_relative "../../sorbet/lsp"
|
7
|
+
|
8
|
+
module Spoom
|
9
|
+
module Cli
|
10
|
+
module Srb
|
11
|
+
class LSP < Thor
|
12
|
+
include Helper
|
13
|
+
|
14
|
+
desc "list", "List all known symbols"
|
15
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
16
|
+
def list
|
17
|
+
run do |client|
|
18
|
+
printer = symbol_printer
|
19
|
+
Dir["**/*.rb"].each do |file|
|
20
|
+
res = client.document_symbols(to_uri(file))
|
21
|
+
next if res.empty?
|
22
|
+
|
23
|
+
say("Symbols from `#{file}`:")
|
24
|
+
printer.print_objects(res)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "hover", "Request hover information"
|
30
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
31
|
+
def hover(file, line, col)
|
32
|
+
run do |client|
|
33
|
+
res = client.hover(to_uri(file), line.to_i, col.to_i)
|
34
|
+
say("Hovering `#{file}:#{line}:#{col}`:")
|
35
|
+
if res
|
36
|
+
symbol_printer.print_object(res)
|
37
|
+
else
|
38
|
+
say("<no data>")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "defs", "List definitions of a symbol"
|
44
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
45
|
+
def defs(file, line, col)
|
46
|
+
run do |client|
|
47
|
+
res = client.definitions(to_uri(file), line.to_i, col.to_i)
|
48
|
+
say("Definitions for `#{file}:#{line}:#{col}`:")
|
49
|
+
symbol_printer.print_list(res)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "find", "Find symbols matching a query"
|
54
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
55
|
+
def find(query)
|
56
|
+
run do |client|
|
57
|
+
res = client.symbols(query).reject { |symbol| symbol.location.uri.start_with?("https") }
|
58
|
+
say("Symbols matching `#{query}`:")
|
59
|
+
symbol_printer.print_objects(res)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "symbols", "List symbols from a file"
|
64
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
65
|
+
def symbols(file)
|
66
|
+
run do |client|
|
67
|
+
res = client.document_symbols(to_uri(file))
|
68
|
+
say("Symbols from `#{file}`:")
|
69
|
+
symbol_printer.print_objects(res)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
desc "refs", "List references to a symbol"
|
74
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
75
|
+
def refs(file, line, col)
|
76
|
+
run do |client|
|
77
|
+
res = client.references(to_uri(file), line.to_i, col.to_i)
|
78
|
+
say("References to `#{file}:#{line}:#{col}`:")
|
79
|
+
symbol_printer.print_list(res)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
desc "sigs", "List signatures for a symbol"
|
84
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
85
|
+
def sigs(file, line, col)
|
86
|
+
run do |client|
|
87
|
+
res = client.signatures(to_uri(file), line.to_i, col.to_i)
|
88
|
+
say("Signature for `#{file}:#{line}:#{col}`:")
|
89
|
+
symbol_printer.print_list(res)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
desc "types", "Display type of a symbol"
|
94
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
95
|
+
def types(file, line, col)
|
96
|
+
run do |client|
|
97
|
+
res = client.type_definitions(to_uri(file), line.to_i, col.to_i)
|
98
|
+
say("Type for `#{file}:#{line}:#{col}`:")
|
99
|
+
symbol_printer.print_list(res)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
no_commands do
|
104
|
+
def lsp_client
|
105
|
+
context_requiring_sorbet!
|
106
|
+
|
107
|
+
path = exec_path
|
108
|
+
client = Spoom::LSP::Client.new(
|
109
|
+
Spoom::Sorbet::BIN_PATH,
|
110
|
+
"--lsp",
|
111
|
+
"--enable-all-experimental-lsp-features",
|
112
|
+
"--disable-watchman",
|
113
|
+
path: path,
|
114
|
+
)
|
115
|
+
client.open(File.expand_path(path))
|
116
|
+
client
|
117
|
+
end
|
118
|
+
|
119
|
+
def symbol_printer
|
120
|
+
Spoom::LSP::SymbolPrinter.new(
|
121
|
+
indent_level: 2,
|
122
|
+
colors: options[:color],
|
123
|
+
prefix: "file://#{File.expand_path(exec_path)}",
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
def run(&block)
|
128
|
+
client = lsp_client
|
129
|
+
block.call(client)
|
130
|
+
rescue Spoom::LSP::Error::Diagnostics => err
|
131
|
+
say_error("Sorbet returned typechecking errors for `#{symbol_printer.clean_uri(err.uri)}`")
|
132
|
+
err.diagnostics.each do |d|
|
133
|
+
say_error("#{d.message} (#{d.code})", status: " #{d.range}")
|
134
|
+
end
|
135
|
+
exit(1)
|
136
|
+
rescue Spoom::LSP::Error::BadHeaders => err
|
137
|
+
say_error("Sorbet didn't answer correctly (#{err.message})")
|
138
|
+
exit(1)
|
139
|
+
rescue Spoom::LSP::Error => err
|
140
|
+
say_error(err.message)
|
141
|
+
exit(1)
|
142
|
+
ensure
|
143
|
+
begin
|
144
|
+
client&.close
|
145
|
+
rescue
|
146
|
+
# We can't do much if Sorbet refuse to close.
|
147
|
+
# We kill the parent process and let the child be killed.
|
148
|
+
exit(1)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def to_uri(path)
|
153
|
+
"file://" + File.join(File.expand_path(exec_path), path)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|