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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +54 -55
  3. data/lib/spoom/cli/deadcode.rb +172 -0
  4. data/lib/spoom/cli/helper.rb +20 -0
  5. data/lib/spoom/cli/srb/bump.rb +200 -0
  6. data/lib/spoom/cli/srb/coverage.rb +224 -0
  7. data/lib/spoom/cli/srb/lsp.rb +159 -0
  8. data/lib/spoom/cli/srb/tc.rb +150 -0
  9. data/lib/spoom/cli/srb.rb +27 -0
  10. data/lib/spoom/cli.rb +72 -32
  11. data/lib/spoom/context/git.rb +2 -2
  12. data/lib/spoom/context/sorbet.rb +2 -2
  13. data/lib/spoom/deadcode/definition.rb +11 -0
  14. data/lib/spoom/deadcode/indexer.rb +222 -224
  15. data/lib/spoom/deadcode/location.rb +2 -2
  16. data/lib/spoom/deadcode/plugins/action_mailer.rb +2 -2
  17. data/lib/spoom/deadcode/plugins/action_mailer_preview.rb +19 -0
  18. data/lib/spoom/deadcode/plugins/actionpack.rb +4 -6
  19. data/lib/spoom/deadcode/plugins/active_model.rb +8 -8
  20. data/lib/spoom/deadcode/plugins/active_record.rb +9 -12
  21. data/lib/spoom/deadcode/plugins/active_support.rb +11 -0
  22. data/lib/spoom/deadcode/plugins/base.rb +1 -1
  23. data/lib/spoom/deadcode/plugins/graphql.rb +4 -4
  24. data/lib/spoom/deadcode/plugins/namespaces.rb +2 -4
  25. data/lib/spoom/deadcode/plugins/ruby.rb +8 -17
  26. data/lib/spoom/deadcode/plugins/sorbet.rb +4 -10
  27. data/lib/spoom/deadcode/plugins.rb +1 -0
  28. data/lib/spoom/deadcode/remover.rb +209 -174
  29. data/lib/spoom/deadcode/send.rb +9 -10
  30. data/lib/spoom/deadcode/visitor.rb +755 -0
  31. data/lib/spoom/deadcode.rb +40 -10
  32. data/lib/spoom/file_tree.rb +0 -16
  33. data/lib/spoom/sorbet/errors.rb +1 -1
  34. data/lib/spoom/sorbet/lsp/structures.rb +2 -2
  35. data/lib/spoom/version.rb +1 -1
  36. metadata +19 -15
  37. data/lib/spoom/cli/bump.rb +0 -198
  38. data/lib/spoom/cli/coverage.rb +0 -222
  39. data/lib/spoom/cli/lsp.rb +0 -168
  40. 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/bump"
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 "bump", "Bump Sorbet sigils from `false` to `true` when no errors"
26
- subcommand "bump", Spoom::Cli::Bump
21
+ desc "srb", "Sorbet related commands"
22
+ subcommand "srb", Spoom::Cli::Srb::Main
27
23
 
28
- desc "config", "Manage Sorbet config"
29
- subcommand "config", Spoom::Cli::Config
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
- subcommand "coverage", Spoom::Cli::Coverage
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
- subcommand "lsp", Spoom::Cli::LSP
75
+ def lsp(*args)
76
+ say_warning("This command is deprecated. Please use `spoom srb bump` instead.")
36
77
 
37
- desc "tc", "Run Sorbet and parses its output"
38
- subcommand "tc", Spoom::Cli::Run
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
- if options[:tree]
53
- tree = FileTree.new(files)
54
- tree.print_with_strictnesses(context, colors: options[:color])
55
- else
56
- puts files
57
- end
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"
@@ -9,7 +9,7 @@ module Spoom
9
9
  class << self
10
10
  extend T::Sig
11
11
 
12
- # Parse a line formated as `%h %at` into a `Commit`
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 uncommited changes in this context directory?
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?
@@ -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
- # > matchs. Matches must be against whole folder and file names, so `foo` matches `/foo/bar.rb` and
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 matchs
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