spoom 1.2.3 → 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 (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
@@ -2,8 +2,9 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "erubi"
5
- require "syntax_tree"
5
+ require "prism"
6
6
 
7
+ require_relative "deadcode/visitor"
7
8
  require_relative "deadcode/erb"
8
9
  require_relative "deadcode/index"
9
10
  require_relative "deadcode/indexer"
@@ -13,14 +14,20 @@ require_relative "deadcode/definition"
13
14
  require_relative "deadcode/reference"
14
15
  require_relative "deadcode/send"
15
16
  require_relative "deadcode/plugins"
17
+ require_relative "deadcode/remover"
16
18
 
17
19
  module Spoom
18
20
  module Deadcode
19
21
  class Error < Spoom::Error
20
- extend T::Sig
21
22
  extend T::Helpers
22
23
 
23
24
  abstract!
25
+ end
26
+
27
+ class ParserError < Error; end
28
+
29
+ class IndexerError < Error
30
+ extend T::Sig
24
31
 
25
32
  sig { params(message: String, parent: Exception).void }
26
33
  def initialize(message, parent:)
@@ -29,23 +36,47 @@ module Spoom
29
36
  end
30
37
  end
31
38
 
32
- class ParserError < Error; end
33
- class IndexerError < Error; end
34
-
35
39
  class << self
36
40
  extend T::Sig
37
41
 
38
- sig { params(index: Index, ruby: String, file: String, plugins: T::Array[Deadcode::Plugins::Base]).void }
39
- def index_ruby(index, ruby, file:, plugins: [])
40
- node = SyntaxTree.parse(ruby)
42
+ sig { params(ruby: String, file: String).returns(Prism::Node) }
43
+ def parse_ruby(ruby, file:)
44
+ result = Prism.parse(ruby)
45
+ unless result.success?
46
+ message = +"Error while parsing #{file}:\n"
47
+
48
+ result.errors.each do |e|
49
+ message << "- #{e.message} (at #{e.location.start_line}:#{e.location.start_column})\n"
50
+ end
51
+
52
+ raise ParserError, message
53
+ end
54
+
55
+ result.value
56
+ end
57
+
58
+ sig do
59
+ params(
60
+ index: Index,
61
+ node: Prism::Node,
62
+ ruby: String,
63
+ file: String,
64
+ plugins: T::Array[Deadcode::Plugins::Base],
65
+ ).void
66
+ end
67
+ def index_node(index, node, ruby, file:, plugins: [])
41
68
  visitor = Spoom::Deadcode::Indexer.new(file, ruby, index, plugins: plugins)
42
69
  visitor.visit(node)
43
- rescue SyntaxTree::Parser::ParseError => e
44
- raise ParserError.new("Error while parsing #{file} (#{e.message} at #{e.lineno}:#{e.column})", parent: e)
45
70
  rescue => e
46
71
  raise IndexerError.new("Error while indexing #{file} (#{e.message})", parent: e)
47
72
  end
48
73
 
74
+ sig { params(index: Index, ruby: String, file: String, plugins: T::Array[Deadcode::Plugins::Base]).void }
75
+ def index_ruby(index, ruby, file:, plugins: [])
76
+ node = parse_ruby(ruby, file: file)
77
+ index_node(index, node, ruby, file: file, plugins: plugins)
78
+ end
79
+
49
80
  sig { params(index: Index, erb: String, file: String, plugins: T::Array[Deadcode::Plugins::Base]).void }
50
81
  def index_erb(index, erb, file:, plugins: [])
51
82
  ruby = ERB.new(erb).src
@@ -54,14 +54,6 @@ module Spoom
54
54
  nodes.map(&:path)
55
55
  end
56
56
 
57
- # Return a map of strictnesses for each node in the tree
58
- sig { params(context: Context).returns(T::Hash[Node, T.nilable(String)]) }
59
- def nodes_strictnesses(context)
60
- v = CollectStrictnesses.new(context)
61
- v.visit_tree(self)
62
- v.strictnesses
63
- end
64
-
65
57
  # Return a map of typing scores for each node in the tree
66
58
  sig { params(context: Context).returns(T::Hash[Node, Float]) }
67
59
  def nodes_strictness_scores(context)
@@ -82,14 +74,6 @@ module Spoom
82
74
  printer.visit_tree(self)
83
75
  end
84
76
 
85
- sig { params(context: Context, out: T.any(IO, StringIO), colors: T::Boolean).void }
86
- def print_with_strictnesses(context, out: $stdout, colors: true)
87
- strictnesses = nodes_strictnesses(context)
88
-
89
- printer = Printer.new(strictnesses, out: out, colors: colors)
90
- printer.visit_tree(self)
91
- end
92
-
93
77
  # A node representing either a file or a directory inside a FileTree
94
78
  class Node < T::Struct
95
79
  extend T::Sig
@@ -76,7 +76,7 @@ module Spoom
76
76
  ^ # match beginning of line
77
77
  (\S[^:]*) # capture filename as something that starts with a non-space character
78
78
  # followed by anything that is not a colon character
79
- : # match the filename - line number seperator
79
+ : # match the filename - line number separator
80
80
  (\d+) # capture the line number
81
81
  :\s # match the line number - error message separator
82
82
  (.*) # capture the error message
@@ -182,7 +182,7 @@ module Spoom
182
182
  const :range, LSP::Range
183
183
  const :code, Integer
184
184
  const :message, String
185
- const :informations, Object
185
+ const :information, Object
186
186
 
187
187
  class << self
188
188
  extend T::Sig
@@ -193,7 +193,7 @@ module Spoom
193
193
  range: Range.from_json(json["range"]),
194
194
  code: json["code"].to_i,
195
195
  message: json["message"],
196
- informations: json["relatedInformation"],
196
+ information: json["relatedInformation"],
197
197
  )
198
198
  end
199
199
  end
data/lib/spoom/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Spoom
5
- VERSION = "1.2.3"
5
+ VERSION = "1.3.0"
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spoom
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.3
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexandre Terrasa
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-08-08 00:00:00.000000000 Z
11
+ date: 2024-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 13.0.1
61
+ version: 13.1.0
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 13.0.1
68
+ version: 13.1.0
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: erubi
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -81,33 +81,33 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: 1.10.0
83
83
  - !ruby/object:Gem::Dependency
84
- name: sorbet-static-and-runtime
84
+ name: prism
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: 0.5.10187
89
+ version: 0.19.0
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: 0.5.10187
96
+ version: 0.19.0
97
97
  - !ruby/object:Gem::Dependency
98
- name: syntax_tree
98
+ name: sorbet-static-and-runtime
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - ">="
102
102
  - !ruby/object:Gem::Version
103
- version: 6.1.1
103
+ version: 0.5.10187
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
- version: 6.1.1
110
+ version: 0.5.10187
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: thor
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -135,13 +135,16 @@ files:
135
135
  - Rakefile
136
136
  - exe/spoom
137
137
  - lib/spoom.rb
138
+ - lib/spoom/backtrace_filter/minitest.rb
138
139
  - lib/spoom/cli.rb
139
- - lib/spoom/cli/bump.rb
140
140
  - lib/spoom/cli/config.rb
141
- - lib/spoom/cli/coverage.rb
141
+ - lib/spoom/cli/deadcode.rb
142
142
  - lib/spoom/cli/helper.rb
143
- - lib/spoom/cli/lsp.rb
144
- - lib/spoom/cli/run.rb
143
+ - lib/spoom/cli/srb.rb
144
+ - lib/spoom/cli/srb/bump.rb
145
+ - lib/spoom/cli/srb/coverage.rb
146
+ - lib/spoom/cli/srb/lsp.rb
147
+ - lib/spoom/cli/srb/tc.rb
145
148
  - lib/spoom/colors.rb
146
149
  - lib/spoom/context.rb
147
150
  - lib/spoom/context/bundle.rb
@@ -164,10 +167,28 @@ files:
164
167
  - lib/spoom/deadcode/indexer.rb
165
168
  - lib/spoom/deadcode/location.rb
166
169
  - lib/spoom/deadcode/plugins.rb
170
+ - lib/spoom/deadcode/plugins/action_mailer.rb
171
+ - lib/spoom/deadcode/plugins/action_mailer_preview.rb
172
+ - lib/spoom/deadcode/plugins/actionpack.rb
173
+ - lib/spoom/deadcode/plugins/active_job.rb
174
+ - lib/spoom/deadcode/plugins/active_model.rb
175
+ - lib/spoom/deadcode/plugins/active_record.rb
176
+ - lib/spoom/deadcode/plugins/active_support.rb
167
177
  - lib/spoom/deadcode/plugins/base.rb
178
+ - lib/spoom/deadcode/plugins/graphql.rb
179
+ - lib/spoom/deadcode/plugins/minitest.rb
180
+ - lib/spoom/deadcode/plugins/namespaces.rb
181
+ - lib/spoom/deadcode/plugins/rails.rb
182
+ - lib/spoom/deadcode/plugins/rake.rb
183
+ - lib/spoom/deadcode/plugins/rspec.rb
184
+ - lib/spoom/deadcode/plugins/rubocop.rb
168
185
  - lib/spoom/deadcode/plugins/ruby.rb
186
+ - lib/spoom/deadcode/plugins/sorbet.rb
187
+ - lib/spoom/deadcode/plugins/thor.rb
169
188
  - lib/spoom/deadcode/reference.rb
189
+ - lib/spoom/deadcode/remover.rb
170
190
  - lib/spoom/deadcode/send.rb
191
+ - lib/spoom/deadcode/visitor.rb
171
192
  - lib/spoom/file_collector.rb
172
193
  - lib/spoom/file_tree.rb
173
194
  - lib/spoom/printer.rb
@@ -205,7 +226,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
205
226
  - !ruby/object:Gem::Version
206
227
  version: '0'
207
228
  requirements: []
208
- rubygems_version: 3.4.17
229
+ rubygems_version: 3.5.7
209
230
  signing_key:
210
231
  specification_version: 4
211
232
  summary: Useful tools for Sorbet enthusiasts.
@@ -1,198 +0,0 @@
1
- # typed: true
2
- # frozen_string_literal: true
3
-
4
- require "find"
5
- require "open3"
6
-
7
- module Spoom
8
- module Cli
9
- class Bump < Thor
10
- extend T::Sig
11
- include Helper
12
-
13
- default_task :bump
14
-
15
- desc "bump DIRECTORY", "Change Sorbet sigils from one strictness to another when no errors"
16
- option :from,
17
- type: :string,
18
- default: Spoom::Sorbet::Sigils::STRICTNESS_FALSE,
19
- desc: "Change only files from this strictness"
20
- option :to,
21
- type: :string,
22
- default: Spoom::Sorbet::Sigils::STRICTNESS_TRUE,
23
- desc: "Change files to this strictness"
24
- option :force,
25
- type: :boolean,
26
- default: false,
27
- aliases: :f,
28
- desc: "Change strictness without type checking"
29
- option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
30
- option :dry,
31
- type: :boolean,
32
- default: false,
33
- aliases: :d,
34
- desc: "Only display what would happen, do not actually change sigils"
35
- option :only,
36
- type: :string,
37
- default: nil,
38
- aliases: :o,
39
- desc: "Only change specified list (one file by line)"
40
- option :suggest_bump_command,
41
- type: :string,
42
- desc: "Command to suggest if files can be bumped"
43
- option :count_errors,
44
- type: :boolean,
45
- default: false,
46
- desc: "Count the number of errors if all files were bumped"
47
- option :sorbet_options, type: :string, default: "", desc: "Pass options to Sorbet"
48
- sig { params(directory: String).void }
49
- def bump(directory = ".")
50
- context = context_requiring_sorbet!
51
- from = options[:from]
52
- to = options[:to]
53
- force = options[:force]
54
- dry = options[:dry]
55
- only = options[:only]
56
- cmd = options[:suggest_bump_command]
57
- directory = File.expand_path(directory)
58
- exec_path = File.expand_path(self.exec_path)
59
-
60
- unless Sorbet::Sigils.valid_strictness?(from)
61
- say_error("Invalid strictness `#{from}` for option `--from`")
62
- exit(1)
63
- end
64
-
65
- unless Sorbet::Sigils.valid_strictness?(to)
66
- say_error("Invalid strictness `#{to}` for option `--to`")
67
- exit(1)
68
- end
69
-
70
- if options[:count_errors] && !dry
71
- say_error("`--count-errors` can only be used with `--dry`")
72
- exit(1)
73
- end
74
-
75
- say("Checking files...")
76
-
77
- files_to_bump = context.srb_files_with_strictness(from, include_rbis: false)
78
- .map { |file| File.expand_path(file, context.absolute_path) }
79
- .select { |file| file.start_with?(directory) }
80
-
81
- if only
82
- list = File.read(only).lines.map { |file| File.expand_path(file.strip) }
83
- files_to_bump.select! { |file| list.include?(File.expand_path(file)) }
84
- end
85
-
86
- say("\n")
87
-
88
- if files_to_bump.empty?
89
- say("No files to bump from `#{from}` to `#{to}`")
90
- exit(0)
91
- end
92
-
93
- Sorbet::Sigils.change_sigil_in_files(files_to_bump, to)
94
-
95
- if force
96
- print_changes(files_to_bump, command: cmd, from: from, to: to, dry: dry, path: exec_path)
97
- undo_changes(files_to_bump, from) if dry
98
- exit(files_to_bump.empty?)
99
- end
100
-
101
- error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
102
- result = begin
103
- T.unsafe(context).srb_tc(
104
- *options[:sorbet_options].split(" "),
105
- "--error-url-base=#{error_url_base}",
106
- capture_err: true,
107
- sorbet_bin: options[:sorbet],
108
- )
109
- rescue Spoom::Sorbet::Error::Segfault => error
110
- say_error(<<~ERR, status: nil)
111
- !!! Sorbet exited with code #{Spoom::Sorbet::SEGFAULT_CODE} - SEGFAULT !!!
112
-
113
- This is most likely related to a bug in Sorbet.
114
- It means one of the file bumped to `typed: #{to}` made Sorbet crash.
115
- Run `spoom bump -f` locally followed by `bundle exec srb tc` to investigate the problem.
116
- ERR
117
- undo_changes(files_to_bump, from)
118
- exit(error.result.exit_code)
119
- rescue Spoom::Sorbet::Error::Killed => error
120
- say_error(<<~ERR, status: nil)
121
- !!! Sorbet exited with code #{Spoom::Sorbet::KILLED_CODE} - KILLED !!!
122
-
123
- It means Sorbet was killed while executing. Changes to files have not been applied.
124
- Re-run `spoom bump` to try again.
125
- ERR
126
- undo_changes(files_to_bump, from)
127
- exit(error.result.exit_code)
128
- end
129
-
130
- if result.status
131
- print_changes(files_to_bump, command: cmd, from: from, to: to, dry: dry, path: exec_path)
132
- undo_changes(files_to_bump, from) if dry
133
- exit(files_to_bump.empty?)
134
- end
135
-
136
- unless result.exit_code == 100
137
- # Sorbet will return exit code 100 if there are type checking errors.
138
- # If Sorbet returned something else, it means it didn't terminate normally.
139
- say_error(result.err, status: nil, nl: false)
140
- undo_changes(files_to_bump, from)
141
- exit(1)
142
- end
143
-
144
- errors = Sorbet::Errors::Parser.parse_string(result.err, error_url_base: error_url_base)
145
-
146
- all_files = errors.flat_map do |err|
147
- [err.file, *err.files_from_error_sections]
148
- end
149
-
150
- files_with_errors = all_files.map do |file|
151
- path = File.expand_path(file)
152
- next unless path.start_with?(directory)
153
- next unless File.file?(path)
154
- next unless files_to_bump.include?(path)
155
-
156
- path
157
- end.compact.uniq
158
-
159
- undo_changes(files_with_errors, from)
160
-
161
- say("Found #{errors.length} type checking error#{"s" if errors.length > 1}") if options[:count_errors]
162
-
163
- files_changed = files_to_bump - files_with_errors
164
- print_changes(files_changed, command: cmd, from: from, to: to, dry: dry, path: exec_path)
165
- undo_changes(files_to_bump, from) if dry
166
- exit(files_changed.empty?)
167
- end
168
-
169
- no_commands do
170
- def print_changes(files, command:, from: "false", to: "true", dry: false, path: File.expand_path("."))
171
- files_count = files.size
172
- if files_count.zero?
173
- say("No files to bump from `#{from}` to `#{to}`")
174
- return
175
- end
176
- message = StringIO.new
177
- message << (dry ? "Can bump" : "Bumped")
178
- message << " `#{files_count}` file#{"s" if files_count > 1}"
179
- message << " from `#{from}` to `#{to}`:"
180
- say(message.string)
181
- files.each do |file|
182
- file_path = Pathname.new(file).relative_path_from(path)
183
- say(" + #{file_path}")
184
- end
185
- if dry && command
186
- say("\nRun `#{command}` to bump #{files_count > 1 ? "them" : "it"}")
187
- elsif dry
188
- say("\nRun `spoom bump --from #{from} --to #{to}` locally then `commit the changes` and `push them`")
189
- end
190
- end
191
-
192
- def undo_changes(files, from_strictness)
193
- Sorbet::Sigils.change_sigil_in_files(files, from_strictness)
194
- end
195
- end
196
- end
197
- end
198
- end