spoom 1.2.4 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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