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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e7ef8ebef70bc8827cdcb4b2c0e49ed6c53686e0db409908354f3762ad07c10
4
- data.tar.gz: a6adb405f0379cbe040e743f177e758e2bdaadcdea13ee9c7f75d84f764434b1
3
+ metadata.gz: 1993e4b43cf1c9d97475b654c049e8b14a27189561471b2bfb4bb077ac2f7b9e
4
+ data.tar.gz: ad6a269fe90e77df95564a13e222d58e9993c7ffef389c2be3a0e2fc7a1aed9a
5
5
  SHA512:
6
- metadata.gz: 86d74bc7d9554d67c1421908116bbab7615d77e9cb096d461f7885429d495f2b02f19d6024117f7083423cfe7c48165b18918c143f29574896bfbb76a4fb651f
7
- data.tar.gz: 754d41e8f26425a42288388879fe1658214adea03002421495297486343a827d6dc79a395e895553ed15f67cdc766a16f4a174aab2a3671bd2455e305a8c6f13
6
+ metadata.gz: 8eab661cb5d9aa290bea6dba5ab3808ad4df7847213229616c4eb912faf012208da053bcb4019700e390f57ce156ff7852a9ef4b01ab48c819acc1453a62b8eb
7
+ data.tar.gz: 7f74c0655f781be1c9832d33dcc9a1cb088705f201998f431dae80a90dae259fe09a85cb0ffec1bd53369694b842c5bfe10a3b32ff25a9b1c61ee712fd934900
data/README.md CHANGED
@@ -31,13 +31,13 @@ Spoom can create a typing coverage report from Sorbet and Git data:
31
31
  After installing the `spoom` gem, run the `timeline` command to collect the history data:
32
32
 
33
33
  ```
34
- $ spoom coverage timeline --save
34
+ $ spoom srb coverage timeline --save
35
35
  ```
36
36
 
37
37
  Then create the HTML page with `report`:
38
38
 
39
39
  ```
40
- $ spoom coverage report
40
+ $ spoom srb coverage report
41
41
  ```
42
42
 
43
43
  Your report will be generated under `spoom_report.html`.
@@ -46,66 +46,48 @@ See all the [Typing Coverage](#typing-coverage) CLI commands for more details.
46
46
 
47
47
  ### Command Line Interface
48
48
 
49
- #### Sorbet configuration commands
50
-
51
- Spoom works with your `sorbet/config` file. No additional configuration is needed.
52
-
53
- Show Sorbet config options:
54
-
55
- ```
56
- $ spoom config
57
- ```
58
-
59
- #### Listing files
60
-
61
- List the files (and related strictness) that will be typchecked with the current Sorbet config options:
62
-
63
- ```
64
- $ spoom files
65
- ```
66
-
67
49
  #### Errors sorting and filtering
68
50
 
69
51
  List all typechecking errors sorted by location:
70
52
 
71
53
  ```
72
- $ spoom tc -s loc
54
+ $ spoom srb tc -s loc
73
55
  ```
74
56
 
75
57
  List all typechecking errors sorted by error code first:
76
58
 
77
59
  ```
78
- $ spoom tc -s code
60
+ $ spoom srb tc -s code
79
61
  ```
80
62
 
81
63
  List only typechecking errors from a specific error code:
82
64
 
83
65
  ```
84
- $ spoom tc -c 7004
66
+ $ spoom srb tc -c 7004
85
67
  ```
86
68
 
87
69
  List only the first 10 typechecking errors
88
70
 
89
71
  ```
90
- $ spoom tc -l 10
72
+ $ spoom srb tc -l 10
91
73
  ```
92
74
 
93
75
  These options can be combined:
94
76
 
95
77
  ```
96
- $ spoom tc -s -c 7004 -l 10
78
+ $ spoom srb tc -s -c 7004 -l 10
97
79
  ```
98
80
 
99
81
  Remove duplicated error lines:
100
82
 
101
83
  ```
102
- $ spoom tc -u
84
+ $ spoom srb tc -u
103
85
  ```
104
86
 
105
87
  Format each error line:
106
88
 
107
89
  ```
108
- $ spoom tc -f '%C - %F:%L: %M'
90
+ $ spoom srb tc -f '%C - %F:%L: %M'
109
91
  ```
110
92
 
111
93
  Where:
@@ -118,13 +100,13 @@ Where:
118
100
  Hide the `Errors: X` at the end of the list:
119
101
 
120
102
  ```
121
- $ spoom tc --no-count
103
+ $ spoom srb tc --no-count
122
104
  ```
123
105
 
124
- List only the errors comming from specific directories or files:
106
+ List only the errors coming from specific directories or files:
125
107
 
126
108
  ```
127
- $ spoom tc file1.rb path1/ path2/
109
+ $ spoom srb tc file1.rb path1/ path2/
128
110
  ```
129
111
 
130
112
  #### Typing coverage
@@ -132,61 +114,61 @@ $ spoom tc file1.rb path1/ path2/
132
114
  Show metrics about the project contents and the typing coverage:
133
115
 
134
116
  ```
135
- $ spoom coverage
117
+ $ spoom srb coverage
136
118
  ```
137
119
 
138
120
  Save coverage data under `spoom_data/`:
139
121
 
140
122
  ```
141
- $ spoom coverage --save
123
+ $ spoom srb coverage --save
142
124
  ```
143
125
 
144
126
  Save coverage data under a specific directory:
145
127
 
146
128
  ```
147
- $ spoom coverage --save my_data/
129
+ $ spoom srb coverage --save my_data/
148
130
  ```
149
131
 
150
132
  Show typing coverage evolution based on the commits history:
151
133
 
152
134
  ```
153
- $ spoom coverage timeline
135
+ $ spoom srb coverage timeline
154
136
  ```
155
137
 
156
138
  Show typing coverage evolution based on the commits history between specific dates:
157
139
 
158
140
  ```
159
- $ spoom coverage timeline --from YYYY-MM-DD --to YYYY-MM-DD
141
+ $ spoom srb coverage timeline --from YYYY-MM-DD --to YYYY-MM-DD
160
142
  ```
161
143
 
162
144
  Save the typing coverage evolution as JSON under `spoom_data/`:
163
145
 
164
146
  ```
165
- $ spoom coverage timeline --save
147
+ $ spoom srb coverage timeline --save
166
148
  ```
167
149
 
168
150
  Save the typing coverage evolution as JSON in a specific directory:
169
151
 
170
152
  ```
171
- $ spoom coverage timeline --save my_data/
153
+ $ spoom srb coverage timeline --save my_data/
172
154
  ```
173
155
 
174
156
  Run `bundle install` for each commit of the timeline (may solve errors due to different Sorbet versions):
175
157
 
176
158
  ```
177
- $ spoom coverage timeline --bundle-install
159
+ $ spoom srb coverage timeline --bundle-install
178
160
  ```
179
161
 
180
162
  Generate an HTML typing coverage report:
181
163
 
182
164
  ```
183
- $ spoom coverage report
165
+ $ spoom srb coverage report
184
166
  ```
185
167
 
186
168
  Change the colors used for strictnesses (useful for colorblind folks):
187
169
 
188
170
  ```
189
- $ spoom coverage report \
171
+ $ spoom srb coverage report \
190
172
  --color-true "#648ffe" \
191
173
  --color-false "#fe6002" \
192
174
  --color-ignore "#feb000" \
@@ -197,7 +179,7 @@ $ spoom coverage report \
197
179
  Open the HTML typing coverage report:
198
180
 
199
181
  ```
200
- $ spoom coverage open
182
+ $ spoom srb coverage open
201
183
  ```
202
184
 
203
185
  #### Change the sigil used in files
@@ -205,37 +187,38 @@ $ spoom coverage open
205
187
  Bump the strictness from all files currently at `typed: false` to `typed: true` where it does not create typechecking errors:
206
188
 
207
189
  ```
208
- $ spoom bump --from false --to true
190
+ $ spoom srb bump --from false --to true
209
191
  ```
210
192
 
211
193
  Bump the strictness from all files currently at `typed: false` to `typed: true` even if it creates typechecking errors:
212
194
 
213
195
  ```
214
- $ spoom bump --from false --to true -f
196
+ $ spoom srb bump --from false --to true -f
215
197
  ```
216
198
 
217
199
  Bump the strictness from a list of files (one file by line):
218
200
 
219
201
  ```
220
- $ spoom bump --from false --to true -o list.txt
202
+ $ spoom srb bump --from false --to true -o list.txt
221
203
  ```
222
204
 
223
- Check if files can be bumped without applying any change:
205
+ Check if files can be bumped without applying any change and show the list of files that can be bumped without errors.
206
+ Will exit with a non-zero status if some files can be bumped without errors (useful to check for bumpable files on CI for example):
224
207
 
225
208
  ```
226
- $ spoom bump --from false --to true --dry
209
+ $ spoom srb bump --from false --to true --dry
227
210
  ```
228
211
 
229
212
  Bump files using a custom instance of Sorbet:
230
213
 
231
214
  ```
232
- $ spoom bump --from false --to true --sorbet /path/to/sorbet/bin
215
+ $ spoom srb bump --from false --to true --sorbet /path/to/sorbet/bin
233
216
  ```
234
217
 
235
218
  Count the number of type-checking errors if all files were bumped to true:
236
219
 
237
220
  ```
238
- $ spoom bump --count-errors --dry
221
+ $ spoom srb bump --count-errors --dry
239
222
  ```
240
223
 
241
224
  #### Interact with Sorbet LSP mode
@@ -245,43 +228,43 @@ $ spoom bump --count-errors --dry
245
228
  Find all definitions for `Foo`:
246
229
 
247
230
  ```
248
- $ spoom lsp find Foo
231
+ $ spoom srb lsp find Foo
249
232
  ```
250
233
 
251
234
  List all symbols in a file:
252
235
 
253
236
  ```
254
- $ spoom lsp symbols <file.rb>
237
+ $ spoom srb lsp symbols <file.rb>
255
238
  ```
256
239
 
257
240
  List all definitions for a specific code location:
258
241
 
259
242
  ```
260
- $ spoom lsp defs <file.rb> <line> <column>
243
+ $ spoom srb lsp defs <file.rb> <line> <column>
261
244
  ```
262
245
 
263
246
  List all references for a specific code location:
264
247
 
265
248
  ```
266
- $ spoom lsp refs <file.rb> <line> <column>
249
+ $ spoom srb lsp refs <file.rb> <line> <column>
267
250
  ```
268
251
 
269
252
  Show hover information for a specific code location:
270
253
 
271
254
  ```
272
- $ spoom lsp hover <file.rb> <line> <column>
255
+ $ spoom srb lsp hover <file.rb> <line> <column>
273
256
  ```
274
257
 
275
258
  Show signature information for a specific code location:
276
259
 
277
260
  ```
278
- $ spoom lsp sig <file.rb> <line> <column>
261
+ $ spoom srb lsp sig <file.rb> <line> <column>
279
262
  ```
280
263
 
281
264
  Show type information for a specific code location:
282
265
 
283
266
  ```
284
- $ spoom lsp sig <file.rb> <line> <column>
267
+ $ spoom srb lsp sig <file.rb> <line> <column>
285
268
  ```
286
269
 
287
270
  ### API
@@ -348,6 +331,32 @@ Find all the symbols for a file:
348
331
  puts client.document_symbols("file://path/to/my/file.rb")
349
332
  ```
350
333
 
334
+ ### Backtrace Filtering
335
+
336
+ Spoom provides a backtrace filter for Minitest to remove the Sorbet frames from test failures, giving a more readable output. To enable it:
337
+
338
+ ```ruby
339
+ # test/test_helper.rb
340
+ require "spoom/backtrace_filter/minitest"
341
+ Minitest.backtrace_filter = Spoom::BacktraceFilter::Minitest.new
342
+ ```
343
+
344
+ ### Dead code removal
345
+
346
+ Run dead code detection in your project with:
347
+
348
+ ```
349
+ $ spoom deadcode
350
+ ```
351
+
352
+ This will list all the methods and constants that do not appear to be used in your project.
353
+
354
+ You can remove them with Spoom:
355
+
356
+ ```
357
+ $ spoom deadcode remove path/to/file.rb:42:18-47:23
358
+ ```
359
+
351
360
  ## Development
352
361
 
353
362
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. Don't forget to run `bin/sanity` before pushing your changes.
@@ -0,0 +1,21 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "minitest"
5
+
6
+ module Spoom
7
+ module BacktraceFilter
8
+ class Minitest < ::Minitest::BacktraceFilter
9
+ extend T::Sig
10
+
11
+ SORBET_PATHS = T.let(Gem.loaded_specs["sorbet-runtime"].full_require_paths.freeze, T::Array[String])
12
+
13
+ sig { override.params(bt: T.nilable(T::Array[String])).returns(T::Array[String]) }
14
+ def filter(bt)
15
+ super.select do |line|
16
+ SORBET_PATHS.none? { |path| line.include?(path) }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,172 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../deadcode"
5
+
6
+ module Spoom
7
+ module Cli
8
+ class Deadcode < Thor
9
+ extend T::Sig
10
+ include Helper
11
+
12
+ default_task :deadcode
13
+
14
+ desc "deadcode PATH...", "Analyze PATHS to find dead code"
15
+ option :allowed_extensions,
16
+ type: :array,
17
+ default: [".rb", ".erb", ".gemspec"],
18
+ aliases: :e,
19
+ desc: "Allowed extensions"
20
+ option :allowed_mime_types,
21
+ type: :array,
22
+ default: ["text/x-ruby", "text/x-ruby-script"],
23
+ aliases: :m,
24
+ desc: "Allowed mime types"
25
+ option :exclude,
26
+ type: :array,
27
+ default: ["vendor/", "sorbet/"],
28
+ aliases: :x,
29
+ desc: "Exclude paths"
30
+ option :show_files,
31
+ type: :boolean,
32
+ default: false,
33
+ desc: "Show the files that will be analyzed"
34
+ option :show_plugins,
35
+ type: :boolean,
36
+ default: false,
37
+ desc: "Show the loaded plugins"
38
+ option :show_defs,
39
+ type: :boolean,
40
+ default: false,
41
+ desc: "Show the indexed definitions"
42
+ option :show_refs,
43
+ type: :boolean,
44
+ default: false,
45
+ desc: "Show the indexed references"
46
+ option :sort,
47
+ type: :string,
48
+ default: "name",
49
+ enum: ["name", "location"],
50
+ desc: "Sort the output by name or location"
51
+ sig { params(paths: String).void }
52
+ def deadcode(*paths)
53
+ context = self.context
54
+
55
+ paths << exec_path if paths.empty?
56
+
57
+ $stderr.puts "Collecting files..."
58
+ collector = FileCollector.new(
59
+ allow_extensions: options[:allowed_extensions],
60
+ allow_mime_types: options[:allowed_mime_types],
61
+ exclude_patterns: options[:exclude].map { |p| Pathname.new(File.join(exec_path, p, "**")).cleanpath.to_s },
62
+ )
63
+ collector.visit_paths(paths)
64
+ files = collector.files.sort
65
+
66
+ if options[:show_files]
67
+ $stderr.puts "\nCollected #{blue(files.size.to_s)} files for analysis\n"
68
+ files.each do |file|
69
+ $stderr.puts " #{gray(file)}"
70
+ end
71
+ $stderr.puts
72
+ end
73
+
74
+ plugins = Spoom::Deadcode.plugins_from_gemfile_lock(context)
75
+ if options[:show_plugins]
76
+ $stderr.puts "\nLoaded #{blue(plugins.size.to_s)} plugins\n"
77
+ plugins.each do |plugin|
78
+ $stderr.puts " #{gray(plugin.class.to_s)}"
79
+ end
80
+ $stderr.puts
81
+ end
82
+
83
+ index = Spoom::Deadcode::Index.new
84
+
85
+ $stderr.puts "Indexing #{blue(files.size.to_s)} files..."
86
+ files.each do |file|
87
+ content = File.read(file)
88
+ content = ERB.new(content).src if file.end_with?(".erb")
89
+
90
+ tree = Spoom::Deadcode.parse_ruby(content, file: file)
91
+ Spoom::Deadcode.index_node(index, tree, content, file: file, plugins: plugins)
92
+ rescue Spoom::Deadcode::ParserError => e
93
+ say_error("Error parsing #{file}: #{e.message}")
94
+ next
95
+ rescue Spoom::Deadcode::IndexerError => e
96
+ say_error("Error indexing #{file}: #{e.message}")
97
+ next
98
+ end
99
+
100
+ if options[:show_defs]
101
+ $stderr.puts "\nDefinitions:"
102
+ index.definitions.each do |name, definitions|
103
+ $stderr.puts " #{blue(name)}"
104
+ definitions.each do |definition|
105
+ $stderr.puts " #{yellow(definition.kind.serialize)} #{gray(definition.location.to_s)}"
106
+ end
107
+ end
108
+ $stderr.puts
109
+ end
110
+
111
+ if options[:show_refs]
112
+ $stderr.puts "\nReferences:"
113
+ index.references.values.flatten.sort_by(&:name).each do |references|
114
+ name = references.name
115
+ kind = references.kind.serialize
116
+ loc = references.location.to_s
117
+ $stderr.puts " #{blue(name)} #{yellow(kind)} #{gray(loc)}"
118
+ end
119
+ $stderr.puts
120
+ end
121
+
122
+ definitions_count = index.definitions.size.to_s
123
+ references_count = index.references.size.to_s
124
+ $stderr.puts "Analyzing #{blue(definitions_count)} definitions against #{blue(references_count)} references..."
125
+
126
+ index.finalize!
127
+ dead = index.definitions.values.flatten.select(&:dead?)
128
+
129
+ if options[:sort] == "name"
130
+ dead.sort_by!(&:name)
131
+ else
132
+ dead.sort_by!(&:location)
133
+ end
134
+
135
+ if dead.empty?
136
+ $stderr.puts "\n#{green("No dead code found!")}"
137
+ else
138
+ $stderr.puts "\nCandidates:"
139
+ dead.each do |definition|
140
+ $stderr.puts " #{red(definition.full_name)} #{gray(definition.location.to_s)}"
141
+ end
142
+ $stderr.puts "\n"
143
+ $stderr.puts red(" Found #{dead.size} dead candidates")
144
+
145
+ exit(1)
146
+ end
147
+ end
148
+
149
+ desc "remove LOCATION", "Remove dead code at LOCATION"
150
+ def remove(location_string)
151
+ location = Spoom::Deadcode::Location.from_string(location_string)
152
+ context = self.context
153
+ remover = Spoom::Deadcode::Remover.new(context)
154
+
155
+ new_source = remover.remove_location(nil, location)
156
+ context.write!("PATCH", new_source)
157
+
158
+ diff = context.exec("diff -u #{location.file} PATCH")
159
+ $stderr.puts T.must(diff.out.lines[2..-1]).join
160
+ context.remove!("PATCH")
161
+
162
+ context.write!(location.file, new_source)
163
+ rescue Spoom::Deadcode::Remover::Error => e
164
+ say_error("Can't remove code at #{location_string}: #{e.message}")
165
+ exit(1)
166
+ rescue Spoom::Deadcode::Location::LocationError => e
167
+ say_error(e.message)
168
+ exit(1)
169
+ end
170
+ end
171
+ end
172
+ end
@@ -46,6 +46,26 @@ module Spoom
46
46
  $stderr.flush
47
47
  end
48
48
 
49
+ # Print `message` on `$stderr`
50
+ #
51
+ # The message is prefixed by a status (default: `Warning`).
52
+ sig do
53
+ params(
54
+ message: String,
55
+ status: T.nilable(String),
56
+ nl: T::Boolean,
57
+ ).void
58
+ end
59
+ def say_warning(message, status: "Warning", nl: true)
60
+ buffer = StringIO.new
61
+ buffer << "#{yellow(status)}: " if status
62
+ buffer << highlight(message)
63
+ buffer << "\n" if nl && !message.end_with?("\n")
64
+
65
+ $stderr.print(buffer.string)
66
+ $stderr.flush
67
+ end
68
+
49
69
  # Returns the context at `--path` (by default the current working directory)
50
70
  sig { returns(Context) }
51
71
  def context