spoom 1.2.3 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +64 -55
  3. data/lib/spoom/backtrace_filter/minitest.rb +21 -0
  4. data/lib/spoom/cli/deadcode.rb +172 -0
  5. data/lib/spoom/cli/helper.rb +20 -0
  6. data/lib/spoom/cli/srb/bump.rb +200 -0
  7. data/lib/spoom/cli/srb/coverage.rb +224 -0
  8. data/lib/spoom/cli/srb/lsp.rb +159 -0
  9. data/lib/spoom/cli/srb/tc.rb +150 -0
  10. data/lib/spoom/cli/srb.rb +27 -0
  11. data/lib/spoom/cli.rb +72 -32
  12. data/lib/spoom/context/git.rb +2 -2
  13. data/lib/spoom/context/sorbet.rb +2 -2
  14. data/lib/spoom/deadcode/definition.rb +11 -0
  15. data/lib/spoom/deadcode/erb.rb +4 -4
  16. data/lib/spoom/deadcode/indexer.rb +266 -200
  17. data/lib/spoom/deadcode/location.rb +30 -2
  18. data/lib/spoom/deadcode/plugins/action_mailer.rb +21 -0
  19. data/lib/spoom/deadcode/plugins/action_mailer_preview.rb +19 -0
  20. data/lib/spoom/deadcode/plugins/actionpack.rb +59 -0
  21. data/lib/spoom/deadcode/plugins/active_job.rb +13 -0
  22. data/lib/spoom/deadcode/plugins/active_model.rb +46 -0
  23. data/lib/spoom/deadcode/plugins/active_record.rb +108 -0
  24. data/lib/spoom/deadcode/plugins/active_support.rb +32 -0
  25. data/lib/spoom/deadcode/plugins/base.rb +165 -12
  26. data/lib/spoom/deadcode/plugins/graphql.rb +47 -0
  27. data/lib/spoom/deadcode/plugins/minitest.rb +28 -0
  28. data/lib/spoom/deadcode/plugins/namespaces.rb +32 -0
  29. data/lib/spoom/deadcode/plugins/rails.rb +31 -0
  30. data/lib/spoom/deadcode/plugins/rake.rb +12 -0
  31. data/lib/spoom/deadcode/plugins/rspec.rb +19 -0
  32. data/lib/spoom/deadcode/plugins/rubocop.rb +41 -0
  33. data/lib/spoom/deadcode/plugins/ruby.rb +10 -18
  34. data/lib/spoom/deadcode/plugins/sorbet.rb +40 -0
  35. data/lib/spoom/deadcode/plugins/thor.rb +21 -0
  36. data/lib/spoom/deadcode/plugins.rb +91 -0
  37. data/lib/spoom/deadcode/remover.rb +651 -0
  38. data/lib/spoom/deadcode/send.rb +27 -6
  39. data/lib/spoom/deadcode/visitor.rb +755 -0
  40. data/lib/spoom/deadcode.rb +41 -10
  41. data/lib/spoom/file_tree.rb +0 -16
  42. data/lib/spoom/sorbet/errors.rb +1 -1
  43. data/lib/spoom/sorbet/lsp/structures.rb +2 -2
  44. data/lib/spoom/version.rb +1 -1
  45. metadata +36 -15
  46. data/lib/spoom/cli/bump.rb +0 -198
  47. data/lib/spoom/cli/coverage.rb +0 -222
  48. data/lib/spoom/cli/lsp.rb +0 -168
  49. data/lib/spoom/cli/run.rb +0 -148
@@ -1,222 +0,0 @@
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
- class Coverage < Thor
10
- include Helper
11
-
12
- DATA_DIR = "spoom_data"
13
-
14
- default_task :snapshot
15
-
16
- desc "snapshot", "Run srb tc and display metrics"
17
- option :save, type: :string, lazy_default: DATA_DIR, desc: "Save snapshot data as json"
18
- option :rbi, type: :boolean, default: true, desc: "Include RBI files in metrics"
19
- option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
20
- def snapshot
21
- context = context_requiring_sorbet!
22
- sorbet = options[:sorbet]
23
-
24
- snapshot = Spoom::Coverage.snapshot(context, rbi: options[:rbi], sorbet_bin: sorbet)
25
- snapshot.print
26
-
27
- save_dir = options[:save]
28
- return unless save_dir
29
-
30
- FileUtils.mkdir_p(save_dir)
31
- file = "#{save_dir}/#{snapshot.commit_sha || snapshot.timestamp}.json"
32
- File.write(file, snapshot.to_json)
33
- say("\nSnapshot data saved under `#{file}`")
34
- end
35
-
36
- desc "timeline", "Replay a project and collect metrics"
37
- option :from, type: :string, desc: "From commit date"
38
- option :to, type: :string, default: Time.now.strftime("%F"), desc: "To commit date"
39
- option :save, type: :string, lazy_default: DATA_DIR, desc: "Save snapshot data as json"
40
- option :bundle_install, type: :boolean, desc: "Execute `bundle install` before collecting metrics"
41
- option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
42
- def timeline
43
- context = context_requiring_sorbet!
44
- path = exec_path
45
- sorbet = options[:sorbet]
46
-
47
- ref_before = context.git_current_branch
48
- ref_before = context.git_last_commit&.sha unless ref_before
49
- unless ref_before
50
- say_error("Not in a git repository")
51
- say_error("\nSpoom needs to checkout into your previous commits to build the timeline.", status: nil)
52
- exit(1)
53
- end
54
-
55
- unless context.git_workdir_clean?
56
- say_error("Uncommited changes")
57
- say_error(<<~ERR, status: nil)
58
-
59
- Spoom needs to checkout into your previous commits to build the timeline."
60
-
61
- Please `git commit` or `git stash` your changes then try again
62
- ERR
63
- exit(1)
64
- end
65
-
66
- save_dir = options[:save]
67
- FileUtils.mkdir_p(save_dir) if save_dir
68
-
69
- from = parse_time(options[:from], "--from")
70
- to = parse_time(options[:to], "--to")
71
-
72
- unless from
73
- intro_commit = context.sorbet_intro_commit
74
- intro_commit = T.must(intro_commit) # we know it's in there since in_sorbet_project!
75
- from = intro_commit.time
76
- end
77
-
78
- timeline = Spoom::Timeline.new(context, from, to)
79
- ticks = timeline.ticks
80
-
81
- if ticks.empty?
82
- say_error("No commits to replay, try different `--from` and `--to` options")
83
- exit(1)
84
- end
85
-
86
- ticks.each_with_index do |commit, i|
87
- say("Analyzing commit `#{commit.sha}` - #{commit.time.strftime("%F")} (#{i + 1} / #{ticks.size})")
88
-
89
- context.git_checkout!(ref: commit.sha)
90
-
91
- snapshot = T.let(nil, T.nilable(Spoom::Coverage::Snapshot))
92
- if options[:bundle_install]
93
- Bundler.with_unbundled_env do
94
- next unless bundle_install(path, commit.sha)
95
-
96
- snapshot = Spoom::Coverage.snapshot(context, sorbet_bin: sorbet)
97
- end
98
- else
99
- snapshot = Spoom::Coverage.snapshot(context, sorbet_bin: sorbet)
100
- end
101
- next unless snapshot
102
-
103
- snapshot.print(indent_level: 2)
104
- say("\n")
105
-
106
- next unless save_dir
107
-
108
- file = "#{save_dir}/#{commit.sha}.json"
109
- File.write(file, snapshot.to_json)
110
- say(" Snapshot data saved under `#{file}`\n\n")
111
- end
112
- context.git_checkout!(ref: ref_before)
113
- end
114
-
115
- desc "report", "Produce a typing coverage report"
116
- option :data, type: :string, default: DATA_DIR, desc: "Snapshots JSON data"
117
- option :file,
118
- type: :string,
119
- default: "spoom_report.html",
120
- aliases: :f,
121
- desc: "Save report to file"
122
- option :color_ignore,
123
- type: :string,
124
- default: Spoom::Coverage::D3::COLOR_IGNORE,
125
- desc: "Color used for typed: ignore"
126
- option :color_false,
127
- type: :string,
128
- default: Spoom::Coverage::D3::COLOR_FALSE,
129
- desc: "Color used for typed: false"
130
- option :color_true,
131
- type: :string,
132
- default: Spoom::Coverage::D3::COLOR_TRUE,
133
- desc: "Color used for typed: true"
134
- option :color_strict,
135
- type: :string,
136
- default: Spoom::Coverage::D3::COLOR_STRICT,
137
- desc: "Color used for typed: strict"
138
- option :color_strong,
139
- type: :string,
140
- default: Spoom::Coverage::D3::COLOR_STRONG,
141
- desc: "Color used for typed: strong"
142
- def report
143
- context = context_requiring_sorbet!
144
-
145
- data_dir = options[:data]
146
- files = Dir.glob("#{data_dir}/*.json")
147
- if files.empty?
148
- message_no_data(data_dir)
149
- exit(1)
150
- end
151
-
152
- snapshots = files.sort.map do |file|
153
- json = File.read(file)
154
- Spoom::Coverage::Snapshot.from_json(json)
155
- end.filter(&:commit_timestamp).sort_by!(&:commit_timestamp)
156
-
157
- palette = Spoom::Coverage::D3::ColorPalette.new(
158
- ignore: options[:color_ignore],
159
- false: options[:color_false],
160
- true: options[:color_true],
161
- strict: options[:color_strict],
162
- strong: options[:color_strong],
163
- )
164
-
165
- report = Spoom::Coverage.report(context, snapshots, palette: palette)
166
- file = options[:file]
167
- File.write(file, report.html)
168
- say("Report generated under `#{file}`")
169
- say("\nUse `spoom coverage open` to open it.")
170
- end
171
-
172
- desc "open", "Open the typing coverage report"
173
- def open(file = "spoom_report.html")
174
- unless File.exist?(file)
175
- say_error("No report file to open `#{file}`")
176
- say_error(<<~ERR, status: nil)
177
-
178
- If you already generated a report under another name use #{blue("spoom coverage open PATH")}.
179
-
180
- To generate a report run #{blue("spoom coverage report")}.
181
- ERR
182
- exit(1)
183
- end
184
-
185
- exec("open #{file}")
186
- end
187
-
188
- no_commands do
189
- def parse_time(string, option)
190
- return unless string
191
-
192
- Time.parse(string)
193
- rescue ArgumentError
194
- say_error("Invalid date `#{string}` for option `#{option}` (expected format `YYYY-MM-DD`)")
195
- exit(1)
196
- end
197
-
198
- def bundle_install(path, sha)
199
- opts = {}
200
- opts[:chdir] = path
201
- out, status = Open3.capture2e("bundle install", opts)
202
- unless status.success?
203
- say_error("Can't run `bundle install` for commit `#{sha}`. Skipping snapshot")
204
- say_error(out, status: nil)
205
- return false
206
- end
207
- true
208
- end
209
-
210
- def message_no_data(file)
211
- say_error("No snapshot files found in `#{file}`")
212
- say_error(<<~ERR, status: nil)
213
-
214
- If you already generated snapshot files under another directory use #{blue("spoom coverage report PATH")}.
215
-
216
- To generate snapshot files run #{blue("spoom coverage timeline --save")}.
217
- ERR
218
- end
219
- end
220
- end
221
- end
222
- end
data/lib/spoom/cli/lsp.rb DELETED
@@ -1,168 +0,0 @@
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
- class LSP < Thor
11
- include Helper
12
-
13
- default_task :show
14
-
15
- desc "interactive", "Interactive LSP mode"
16
- def show
17
- context_requiring_sorbet!
18
-
19
- lsp = lsp_client
20
- # TODO: run interactive mode
21
- puts lsp
22
- end
23
-
24
- desc "list", "List all known symbols"
25
- # TODO: options, filter, limit, kind etc.. filter rbi
26
- def list
27
- run do |client|
28
- printer = symbol_printer
29
- Dir["**/*.rb"].each do |file|
30
- res = client.document_symbols(to_uri(file))
31
- next if res.empty?
32
-
33
- say("Symbols from `#{file}`:")
34
- printer.print_objects(res)
35
- end
36
- end
37
- end
38
-
39
- desc "hover", "Request hover informations"
40
- # TODO: options, filter, limit, kind etc.. filter rbi
41
- def hover(file, line, col)
42
- run do |client|
43
- res = client.hover(to_uri(file), line.to_i, col.to_i)
44
- say("Hovering `#{file}:#{line}:#{col}`:")
45
- if res
46
- symbol_printer.print_object(res)
47
- else
48
- say("<no data>")
49
- end
50
- end
51
- end
52
-
53
- desc "defs", "List definitions of a symbol"
54
- # TODO: options, filter, limit, kind etc.. filter rbi
55
- def defs(file, line, col)
56
- run do |client|
57
- res = client.definitions(to_uri(file), line.to_i, col.to_i)
58
- say("Definitions for `#{file}:#{line}:#{col}`:")
59
- symbol_printer.print_list(res)
60
- end
61
- end
62
-
63
- desc "find", "Find symbols matching a query"
64
- # TODO: options, filter, limit, kind etc.. filter rbi
65
- def find(query)
66
- run do |client|
67
- res = client.symbols(query).reject { |symbol| symbol.location.uri.start_with?("https") }
68
- say("Symbols matching `#{query}`:")
69
- symbol_printer.print_objects(res)
70
- end
71
- end
72
-
73
- desc "symbols", "List symbols from a file"
74
- # TODO: options, filter, limit, kind etc.. filter rbi
75
- def symbols(file)
76
- run do |client|
77
- res = client.document_symbols(to_uri(file))
78
- say("Symbols from `#{file}`:")
79
- symbol_printer.print_objects(res)
80
- end
81
- end
82
-
83
- desc "refs", "List references to a symbol"
84
- # TODO: options, filter, limit, kind etc.. filter rbi
85
- def refs(file, line, col)
86
- run do |client|
87
- res = client.references(to_uri(file), line.to_i, col.to_i)
88
- say("References to `#{file}:#{line}:#{col}`:")
89
- symbol_printer.print_list(res)
90
- end
91
- end
92
-
93
- desc "sigs", "List signatures for a symbol"
94
- # TODO: options, filter, limit, kind etc.. filter rbi
95
- def sigs(file, line, col)
96
- run do |client|
97
- res = client.signatures(to_uri(file), line.to_i, col.to_i)
98
- say("Signature for `#{file}:#{line}:#{col}`:")
99
- symbol_printer.print_list(res)
100
- end
101
- end
102
-
103
- desc "types", "Display type of a symbol"
104
- # TODO: options, filter, limit, kind etc.. filter rbi
105
- def types(file, line, col)
106
- run do |client|
107
- res = client.type_definitions(to_uri(file), line.to_i, col.to_i)
108
- say("Type for `#{file}:#{line}:#{col}`:")
109
- symbol_printer.print_list(res)
110
- end
111
- end
112
-
113
- no_commands do
114
- def lsp_client
115
- context_requiring_sorbet!
116
-
117
- path = exec_path
118
- client = Spoom::LSP::Client.new(
119
- Spoom::Sorbet::BIN_PATH,
120
- "--lsp",
121
- "--enable-all-experimental-lsp-features",
122
- "--disable-watchman",
123
- path: path,
124
- )
125
- client.open(File.expand_path(path))
126
- client
127
- end
128
-
129
- def symbol_printer
130
- Spoom::LSP::SymbolPrinter.new(
131
- indent_level: 2,
132
- colors: options[:color],
133
- prefix: "file://#{File.expand_path(exec_path)}",
134
- )
135
- end
136
-
137
- def run(&block)
138
- client = lsp_client
139
- block.call(client)
140
- rescue Spoom::LSP::Error::Diagnostics => err
141
- say_error("Sorbet returned typechecking errors for `#{symbol_printer.clean_uri(err.uri)}`")
142
- err.diagnostics.each do |d|
143
- say_error("#{d.message} (#{d.code})", status: " #{d.range}")
144
- end
145
- exit(1)
146
- rescue Spoom::LSP::Error::BadHeaders => err
147
- say_error("Sorbet didn't answer correctly (#{err.message})")
148
- exit(1)
149
- rescue Spoom::LSP::Error => err
150
- say_error(err.message)
151
- exit(1)
152
- ensure
153
- begin
154
- client&.close
155
- rescue
156
- # We can't do much if Sorbet refuse to close.
157
- # We kill the parent process and let the child be killed.
158
- exit(1)
159
- end
160
- end
161
-
162
- def to_uri(path)
163
- "file://" + File.join(File.expand_path(exec_path), path)
164
- end
165
- end
166
- end
167
- end
168
- end
data/lib/spoom/cli/run.rb DELETED
@@ -1,148 +0,0 @@
1
- # typed: true
2
- # frozen_string_literal: true
3
-
4
- module Spoom
5
- module Cli
6
- class Run < Thor
7
- include Helper
8
-
9
- default_task :tc
10
-
11
- SORT_CODE = "code"
12
- SORT_LOC = "loc"
13
- SORT_ENUM = [SORT_CODE, SORT_LOC]
14
-
15
- DEFAULT_FORMAT = "%C - %F:%L: %M"
16
-
17
- desc "tc", "Run `srb tc`"
18
- option :limit, type: :numeric, aliases: :l, desc: "Limit displayed errors"
19
- option :code, type: :numeric, aliases: :c, desc: "Filter displayed errors by code"
20
- option :sort, type: :string, aliases: :s, desc: "Sort errors", enum: SORT_ENUM, default: SORT_LOC
21
- option :format, type: :string, aliases: :f, desc: "Format line output"
22
- option :uniq, type: :boolean, aliases: :u, desc: "Remove duplicated lines"
23
- option :count, type: :boolean, default: true, desc: "Show errors count"
24
- option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
25
- option :sorbet_options, type: :string, default: "", desc: "Pass options to Sorbet"
26
- def tc(*paths_to_select)
27
- context = context_requiring_sorbet!
28
- limit = options[:limit]
29
- sort = options[:sort]
30
- code = options[:code]
31
- uniq = options[:uniq]
32
- format = options[:format]
33
- count = options[:count]
34
- sorbet = options[:sorbet]
35
-
36
- unless limit || code || sort
37
- result = T.unsafe(context).srb_tc(
38
- *options[:sorbet_options].split(" "),
39
- capture_err: false,
40
- sorbet_bin: sorbet,
41
- )
42
-
43
- say_error(result.err, status: nil, nl: false)
44
- exit(result.status)
45
- end
46
-
47
- error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
48
- result = T.unsafe(context).srb_tc(
49
- *options[:sorbet_options].split(" "),
50
- "--error-url-base=#{error_url_base}",
51
- capture_err: true,
52
- sorbet_bin: sorbet,
53
- )
54
-
55
- if result.status
56
- say_error(result.err, status: nil, nl: false)
57
- exit(0)
58
- end
59
-
60
- unless result.exit_code == 100
61
- # Sorbet will return exit code 100 if there are type checking errors.
62
- # If Sorbet returned something else, it means it didn't terminate normally.
63
- say_error(result.err, status: nil, nl: false)
64
- exit(1)
65
- end
66
-
67
- errors = Spoom::Sorbet::Errors::Parser.parse_string(result.err, error_url_base: error_url_base)
68
- errors_count = errors.size
69
-
70
- errors = errors.select { |e| e.code == code } if code
71
-
72
- unless paths_to_select.empty?
73
- errors.select! do |error|
74
- paths_to_select.any? { |path_to_select| error.file&.start_with?(path_to_select) }
75
- end
76
- end
77
-
78
- errors = case sort
79
- when SORT_CODE
80
- Spoom::Sorbet::Errors.sort_errors_by_code(errors)
81
- when SORT_LOC
82
- errors.sort
83
- else
84
- errors # preserve natural sort
85
- end
86
-
87
- errors = T.must(errors.slice(0, limit)) if limit
88
-
89
- lines = errors.map { |e| format_error(e, format || DEFAULT_FORMAT) }
90
- lines = lines.uniq if uniq
91
-
92
- lines.each do |line|
93
- say_error(line, status: nil)
94
- end
95
-
96
- if count
97
- if errors_count == errors.size
98
- say_error("Errors: #{errors_count}", status: nil)
99
- else
100
- say_error("Errors: #{errors.size} shown, #{errors_count} total", status: nil)
101
- end
102
- end
103
-
104
- exit(1)
105
- rescue Spoom::Sorbet::Error::Segfault => error
106
- say_error(<<~ERR, status: nil)
107
- #{red("!!! Sorbet exited with code #{error.result.exit_code} - SEGFAULT !!!")}
108
-
109
- This is most likely related to a bug in Sorbet.
110
- ERR
111
-
112
- exit(error.result.exit_code)
113
- rescue Spoom::Sorbet::Error::Killed => error
114
- say_error(<<~ERR, status: nil)
115
- #{red("!!! Sorbet exited with code #{error.result.exit_code} - KILLED !!!")}
116
- ERR
117
-
118
- exit(error.result.exit_code)
119
- end
120
-
121
- no_commands do
122
- def format_error(error, format)
123
- line = format
124
- line = line.gsub(/%C/, yellow(error.code.to_s))
125
- line = line.gsub(/%F/, error.file)
126
- line = line.gsub(/%L/, error.line.to_s)
127
- line = line.gsub(/%M/, colorize_message(error.message))
128
- line
129
- end
130
-
131
- def colorize_message(message)
132
- return message unless color?
133
-
134
- cyan = T.let(false, T::Boolean)
135
- word = StringIO.new
136
- message.chars.each do |c|
137
- if c == "`"
138
- cyan = !cyan
139
- next
140
- end
141
- word << (cyan ? cyan(c) : red(c))
142
- end
143
- word.string
144
- end
145
- end
146
- end
147
- end
148
- end