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,150 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Cli
|
6
|
+
module Srb
|
7
|
+
class Tc < Thor
|
8
|
+
include Helper
|
9
|
+
|
10
|
+
default_task :tc
|
11
|
+
|
12
|
+
SORT_CODE = "code"
|
13
|
+
SORT_LOC = "loc"
|
14
|
+
SORT_ENUM = [SORT_CODE, SORT_LOC]
|
15
|
+
|
16
|
+
DEFAULT_FORMAT = "%C - %F:%L: %M"
|
17
|
+
|
18
|
+
desc "tc", "Run `srb tc`"
|
19
|
+
option :limit, type: :numeric, aliases: :l, desc: "Limit displayed errors"
|
20
|
+
option :code, type: :numeric, aliases: :c, desc: "Filter displayed errors by code"
|
21
|
+
option :sort, type: :string, aliases: :s, desc: "Sort errors", enum: SORT_ENUM, default: SORT_LOC
|
22
|
+
option :format, type: :string, aliases: :f, desc: "Format line output"
|
23
|
+
option :uniq, type: :boolean, aliases: :u, desc: "Remove duplicated lines"
|
24
|
+
option :count, type: :boolean, default: true, desc: "Show errors count"
|
25
|
+
option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
|
26
|
+
option :sorbet_options, type: :string, default: "", desc: "Pass options to Sorbet"
|
27
|
+
def tc(*paths_to_select)
|
28
|
+
context = context_requiring_sorbet!
|
29
|
+
limit = options[:limit]
|
30
|
+
sort = options[:sort]
|
31
|
+
code = options[:code]
|
32
|
+
uniq = options[:uniq]
|
33
|
+
format = options[:format]
|
34
|
+
count = options[:count]
|
35
|
+
sorbet = options[:sorbet]
|
36
|
+
|
37
|
+
unless limit || code || sort
|
38
|
+
result = T.unsafe(context).srb_tc(
|
39
|
+
*options[:sorbet_options].split(" "),
|
40
|
+
capture_err: false,
|
41
|
+
sorbet_bin: sorbet,
|
42
|
+
)
|
43
|
+
|
44
|
+
say_error(result.err, status: nil, nl: false)
|
45
|
+
exit(result.status)
|
46
|
+
end
|
47
|
+
|
48
|
+
error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
|
49
|
+
result = T.unsafe(context).srb_tc(
|
50
|
+
*options[:sorbet_options].split(" "),
|
51
|
+
"--error-url-base=#{error_url_base}",
|
52
|
+
capture_err: true,
|
53
|
+
sorbet_bin: sorbet,
|
54
|
+
)
|
55
|
+
|
56
|
+
if result.status
|
57
|
+
say_error(result.err, status: nil, nl: false)
|
58
|
+
exit(0)
|
59
|
+
end
|
60
|
+
|
61
|
+
unless result.exit_code == 100
|
62
|
+
# Sorbet will return exit code 100 if there are type checking errors.
|
63
|
+
# If Sorbet returned something else, it means it didn't terminate normally.
|
64
|
+
say_error(result.err, status: nil, nl: false)
|
65
|
+
exit(1)
|
66
|
+
end
|
67
|
+
|
68
|
+
errors = Spoom::Sorbet::Errors::Parser.parse_string(result.err, error_url_base: error_url_base)
|
69
|
+
errors_count = errors.size
|
70
|
+
|
71
|
+
errors = errors.select { |e| e.code == code } if code
|
72
|
+
|
73
|
+
unless paths_to_select.empty?
|
74
|
+
errors.select! do |error|
|
75
|
+
paths_to_select.any? { |path_to_select| error.file&.start_with?(path_to_select) }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
errors = case sort
|
80
|
+
when SORT_CODE
|
81
|
+
Spoom::Sorbet::Errors.sort_errors_by_code(errors)
|
82
|
+
when SORT_LOC
|
83
|
+
errors.sort
|
84
|
+
else
|
85
|
+
errors # preserve natural sort
|
86
|
+
end
|
87
|
+
|
88
|
+
errors = T.must(errors.slice(0, limit)) if limit
|
89
|
+
|
90
|
+
lines = errors.map { |e| format_error(e, format || DEFAULT_FORMAT) }
|
91
|
+
lines = lines.uniq if uniq
|
92
|
+
|
93
|
+
lines.each do |line|
|
94
|
+
say_error(line, status: nil)
|
95
|
+
end
|
96
|
+
|
97
|
+
if count
|
98
|
+
if errors_count == errors.size
|
99
|
+
say_error("Errors: #{errors_count}", status: nil)
|
100
|
+
else
|
101
|
+
say_error("Errors: #{errors.size} shown, #{errors_count} total", status: nil)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
exit(1)
|
106
|
+
rescue Spoom::Sorbet::Error::Segfault => error
|
107
|
+
say_error(<<~ERR, status: nil)
|
108
|
+
#{red("!!! Sorbet exited with code #{error.result.exit_code} - SEGFAULT !!!")}
|
109
|
+
|
110
|
+
This is most likely related to a bug in Sorbet.
|
111
|
+
ERR
|
112
|
+
|
113
|
+
exit(error.result.exit_code)
|
114
|
+
rescue Spoom::Sorbet::Error::Killed => error
|
115
|
+
say_error(<<~ERR, status: nil)
|
116
|
+
#{red("!!! Sorbet exited with code #{error.result.exit_code} - KILLED !!!")}
|
117
|
+
ERR
|
118
|
+
|
119
|
+
exit(error.result.exit_code)
|
120
|
+
end
|
121
|
+
|
122
|
+
no_commands do
|
123
|
+
def format_error(error, format)
|
124
|
+
line = format
|
125
|
+
line = line.gsub("%C", yellow(error.code.to_s))
|
126
|
+
line = line.gsub("%F", error.file)
|
127
|
+
line = line.gsub("%L", error.line.to_s)
|
128
|
+
line = line.gsub("%M", colorize_message(error.message))
|
129
|
+
line
|
130
|
+
end
|
131
|
+
|
132
|
+
def colorize_message(message)
|
133
|
+
return message unless color?
|
134
|
+
|
135
|
+
cyan = T.let(false, T::Boolean)
|
136
|
+
word = StringIO.new
|
137
|
+
message.chars.each do |c|
|
138
|
+
if c == "`"
|
139
|
+
cyan = !cyan
|
140
|
+
next
|
141
|
+
end
|
142
|
+
word << (cyan ? cyan(c) : red(c))
|
143
|
+
end
|
144
|
+
word.string
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "srb/bump"
|
5
|
+
require_relative "srb/coverage"
|
6
|
+
require_relative "srb/lsp"
|
7
|
+
require_relative "srb/tc"
|
8
|
+
|
9
|
+
module Spoom
|
10
|
+
module Cli
|
11
|
+
module Srb
|
12
|
+
class Main < Thor
|
13
|
+
desc "lsp", "Send LSP requests to Sorbet"
|
14
|
+
subcommand "lsp", Spoom::Cli::Srb::LSP
|
15
|
+
|
16
|
+
desc "coverage", "Collect metrics related to Sorbet coverage"
|
17
|
+
subcommand "coverage", Spoom::Cli::Srb::Coverage
|
18
|
+
|
19
|
+
desc "bump", "Change Sorbet sigils from one strictness to another when no errors"
|
20
|
+
subcommand "bump", Spoom::Cli::Srb::Bump
|
21
|
+
|
22
|
+
desc "tc", "Run typechecking with advanced options"
|
23
|
+
subcommand "tc", Spoom::Cli::Srb::Tc
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/spoom/cli.rb
CHANGED
@@ -4,12 +4,8 @@
|
|
4
4
|
require "thor"
|
5
5
|
|
6
6
|
require_relative "cli/helper"
|
7
|
-
|
8
|
-
require_relative "cli/
|
9
|
-
require_relative "cli/config"
|
10
|
-
require_relative "cli/lsp"
|
11
|
-
require_relative "cli/coverage"
|
12
|
-
require_relative "cli/run"
|
7
|
+
require_relative "cli/deadcode"
|
8
|
+
require_relative "cli/srb"
|
13
9
|
|
14
10
|
module Spoom
|
15
11
|
module Cli
|
@@ -22,39 +18,83 @@ module Spoom
|
|
22
18
|
|
23
19
|
map T.unsafe(["--version", "-v"] => :__print_version)
|
24
20
|
|
25
|
-
desc "
|
26
|
-
subcommand "
|
21
|
+
desc "srb", "Sorbet related commands"
|
22
|
+
subcommand "srb", Spoom::Cli::Srb::Main
|
27
23
|
|
28
|
-
desc "
|
29
|
-
|
24
|
+
desc "bump", "Bump Sorbet sigils from `false` to `true` when no errors"
|
25
|
+
option :from,
|
26
|
+
type: :string,
|
27
|
+
default: Spoom::Sorbet::Sigils::STRICTNESS_FALSE,
|
28
|
+
desc: "Change only files from this strictness"
|
29
|
+
option :to,
|
30
|
+
type: :string,
|
31
|
+
default: Spoom::Sorbet::Sigils::STRICTNESS_TRUE,
|
32
|
+
desc: "Change files to this strictness"
|
33
|
+
option :force,
|
34
|
+
type: :boolean,
|
35
|
+
default: false,
|
36
|
+
aliases: :f,
|
37
|
+
desc: "Change strictness without type checking"
|
38
|
+
option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
|
39
|
+
option :dry,
|
40
|
+
type: :boolean,
|
41
|
+
default: false,
|
42
|
+
aliases: :d,
|
43
|
+
desc: "Only display what would happen, do not actually change sigils"
|
44
|
+
option :only,
|
45
|
+
type: :string,
|
46
|
+
default: nil,
|
47
|
+
aliases: :o,
|
48
|
+
desc: "Only change specified list (one file by line)"
|
49
|
+
option :suggest_bump_command,
|
50
|
+
type: :string,
|
51
|
+
desc: "Command to suggest if files can be bumped"
|
52
|
+
option :count_errors,
|
53
|
+
type: :boolean,
|
54
|
+
default: false,
|
55
|
+
desc: "Count the number of errors if all files were bumped"
|
56
|
+
option :sorbet_options, type: :string, default: "", desc: "Pass options to Sorbet"
|
57
|
+
sig { params(directory: String).void }
|
58
|
+
def bump(directory = ".")
|
59
|
+
say_warning("This command is deprecated. Please use `spoom srb bump` instead.")
|
60
|
+
|
61
|
+
invoke(Cli::Srb::Bump, :bump, [directory], options)
|
62
|
+
end
|
30
63
|
|
31
64
|
desc "coverage", "Collect metrics related to Sorbet coverage"
|
32
|
-
|
65
|
+
def coverage(*args)
|
66
|
+
say_warning("This command is deprecated. Please use `spoom srb bump` instead.")
|
67
|
+
|
68
|
+
invoke(Cli::Srb::Coverage, args, options)
|
69
|
+
end
|
70
|
+
|
71
|
+
desc "deadcode", "Analyze code to find deadcode"
|
72
|
+
subcommand "deadcode", Spoom::Cli::Deadcode
|
33
73
|
|
34
74
|
desc "lsp", "Send LSP requests to Sorbet"
|
35
|
-
|
75
|
+
def lsp(*args)
|
76
|
+
say_warning("This command is deprecated. Please use `spoom srb bump` instead.")
|
36
77
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
desc "files", "List all the files typechecked by Sorbet"
|
41
|
-
option :tree, type: :boolean, default: true, desc: "Display list as an indented tree"
|
42
|
-
option :rbi, type: :boolean, default: false, desc: "Show RBI files"
|
43
|
-
def files
|
44
|
-
context = context_requiring_sorbet!
|
45
|
-
|
46
|
-
files = context.srb_files(include_rbis: options[:rbi])
|
47
|
-
if files.empty?
|
48
|
-
say_error("No file matching `#{Sorbet::CONFIG_PATH}`")
|
49
|
-
exit(1)
|
50
|
-
end
|
78
|
+
invoke(Cli::Srb::LSP, args, options)
|
79
|
+
end
|
51
80
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
81
|
+
SORT_CODE = "code"
|
82
|
+
SORT_LOC = "loc"
|
83
|
+
SORT_ENUM = [SORT_CODE, SORT_LOC]
|
84
|
+
|
85
|
+
desc "tc", "Run Sorbet and parses its output"
|
86
|
+
option :limit, type: :numeric, aliases: :l, desc: "Limit displayed errors"
|
87
|
+
option :code, type: :numeric, aliases: :c, desc: "Filter displayed errors by code"
|
88
|
+
option :sort, type: :string, aliases: :s, desc: "Sort errors", enum: SORT_ENUM, default: SORT_LOC
|
89
|
+
option :format, type: :string, aliases: :f, desc: "Format line output"
|
90
|
+
option :uniq, type: :boolean, aliases: :u, desc: "Remove duplicated lines"
|
91
|
+
option :count, type: :boolean, default: true, desc: "Show errors count"
|
92
|
+
option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
|
93
|
+
option :sorbet_options, type: :string, default: "", desc: "Pass options to Sorbet"
|
94
|
+
def tc(*paths_to_select)
|
95
|
+
say_warning("This command is deprecated. Please use `spoom srb tc` instead.")
|
96
|
+
|
97
|
+
invoke(Cli::Srb::Tc, :tc, paths_to_select, options)
|
58
98
|
end
|
59
99
|
|
60
100
|
desc "--version", "Show version"
|
data/lib/spoom/context/git.rb
CHANGED
@@ -9,7 +9,7 @@ module Spoom
|
|
9
9
|
class << self
|
10
10
|
extend T::Sig
|
11
11
|
|
12
|
-
# Parse a line
|
12
|
+
# Parse a line formatted as `%h %at` into a `Commit`
|
13
13
|
sig { params(string: String).returns(T.nilable(Commit)) }
|
14
14
|
def parse_line(string)
|
15
15
|
sha, epoch = string.split(" ", 2)
|
@@ -127,7 +127,7 @@ module Spoom
|
|
127
127
|
git("show #{arg.join(" ")}")
|
128
128
|
end
|
129
129
|
|
130
|
-
# Is there
|
130
|
+
# Is there uncommitted changes in this context directory?
|
131
131
|
sig { params(path: String).returns(T::Boolean) }
|
132
132
|
def git_workdir_clean?(path: ".")
|
133
133
|
git_diff("HEAD").out.empty?
|
data/lib/spoom/context/sorbet.rb
CHANGED
@@ -75,13 +75,13 @@ module Spoom
|
|
75
75
|
# From Sorbet docs on `--ignore`:
|
76
76
|
# > Ignores input files that contain the given string in their paths (relative to the input path passed to
|
77
77
|
# > Sorbet). Strings beginning with / match against the prefix of these relative paths; others are substring
|
78
|
-
# >
|
78
|
+
# > matches. Matches must be against whole folder and file names, so `foo` matches `/foo/bar.rb` and
|
79
79
|
# > `/bar/foo/baz.rb` but not `/foo.rb` or `/foo2/bar.rb`.
|
80
80
|
string = if string.start_with?("/")
|
81
81
|
# Strings beginning with / match against the prefix of these relative paths
|
82
82
|
File.join(absolute_path, string)
|
83
83
|
else
|
84
|
-
# Others are substring
|
84
|
+
# Others are substring matches
|
85
85
|
File.join(absolute_path, "**", string)
|
86
86
|
end
|
87
87
|
# Matches must be against whole folder and file names
|
@@ -93,6 +93,17 @@ module Spoom
|
|
93
93
|
def ignored!
|
94
94
|
@status = Status::IGNORED
|
95
95
|
end
|
96
|
+
|
97
|
+
# Utils
|
98
|
+
|
99
|
+
sig { params(args: T.untyped).returns(String) }
|
100
|
+
def to_json(*args)
|
101
|
+
{
|
102
|
+
kind: kind,
|
103
|
+
name: name,
|
104
|
+
location: location.to_s,
|
105
|
+
}.to_json
|
106
|
+
end
|
96
107
|
end
|
97
108
|
end
|
98
109
|
end
|