spoom 1.8.4 → 1.8.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f138cb37a9cdcc10a7195440a948f6f9a3d2225ac032e2179823cf327b3d803
4
- data.tar.gz: 9d8b55599e6de489d29362353f9ac3802b12ad85b6e9e34b40c09b07127ace47
3
+ metadata.gz: 80c79d30a04cc756ee5992c7ced516b76b7cf9887a68b63862118a439404f972
4
+ data.tar.gz: c2f33b188c0972d39afe6c1b5b1ff789ab13afd89e473c621798bf7c7bca97b3
5
5
  SHA512:
6
- metadata.gz: 708cb8b80b385e27c7d5a3e9cc05fbc475df994977a3cbf3ef4a1ea2cf592c91376bf9f1000aa7991ed0c154bb0e3ecc5120bd5ab01c94ca6e20a9a1bac98322
7
- data.tar.gz: 262a4f6b42009e39c91a6e312ed54aba0056e6dd71d5ce136448c4f1343ee1037159a13ebb77d6f68113d64db7ebc3de61bad289440f0fa8b5efcca5e3444ee6
6
+ metadata.gz: e3769e883671aca623fdc186ca25e86a3c10e02e70206d6f3a8e282d5355a9a0c55ae82ea760994746d923738151d923e519337b64352bb06ce4fae4d7139ea1
7
+ data.tar.gz: 2fd05b69029f46c77ae5716ebdbff8e887ebd894a20d60520299a47bee1d6a69531f910a3663d64963fbdac0353a2cabde8b1d891b53637d5188336cea5f3c5a
data/README.md CHANGED
@@ -1,174 +1,190 @@
1
1
  # Spoom
2
2
 
3
- Useful tools for Sorbet enthusiasts.
3
+ Useful tools for Sorbet projects.
4
+
5
+ Spoom provides a CLI and a Ruby API to inspect Sorbet projects, improve typing coverage, translate signatures, query Sorbet LSP, and find dead code.
4
6
 
5
7
  ## Installation
6
8
 
7
- Add this line to your application's Gemfile:
9
+ Add Spoom to your application's Gemfile:
8
10
 
9
11
  ```ruby
10
- gem 'spoom'
12
+ gem "spoom"
11
13
  ```
12
14
 
13
- And then execute:
15
+ Then install it:
14
16
 
15
- $ bundle
17
+ ```sh
18
+ bundle install
19
+ ```
16
20
 
17
- Or install it yourself as:
21
+ Or install it directly:
18
22
 
19
- $ gem install spoom
23
+ ```sh
24
+ gem install spoom
25
+ ```
20
26
 
21
- ## Usage
27
+ Spoom requires Ruby 3.3 or newer.
22
28
 
23
- `spoom` provides both a CLI and an API to interact with Sorbet.
29
+ ## Command line interface
24
30
 
25
- ### Generate a typing coverage report
31
+ Run `spoom help` or `spoom help COMMAND` to list all available commands.
26
32
 
27
- Spoom can create a typing coverage report from Sorbet and Git data:
33
+ ### Typechecking errors
28
34
 
29
- ![Coverage Report](docs/report.png)
35
+ `spoom srb tc` runs `srb tc` and can sort, filter, format, and export errors.
30
36
 
31
- After installing the `spoom` gem, run the `timeline` command to collect the history data:
37
+ List errors sorted by location:
32
38
 
33
- ```
34
- $ spoom srb coverage timeline --save
39
+ ```sh
40
+ spoom srb tc --sort loc
35
41
  ```
36
42
 
37
- Then create the HTML page with `report`:
43
+ List errors sorted by error code:
38
44
 
39
- ```
40
- $ spoom srb coverage report
45
+ ```sh
46
+ spoom srb tc --sort code
41
47
  ```
42
48
 
43
- Your report will be generated under `spoom_report.html`.
49
+ List only errors with a specific code:
44
50
 
45
- See all the [Typing Coverage](#typing-coverage) CLI commands for more details.
51
+ ```sh
52
+ spoom srb tc --code 7004
53
+ ```
46
54
 
47
- ### Command Line Interface
55
+ Limit the number of displayed errors:
48
56
 
49
- #### Errors sorting and filtering
57
+ ```sh
58
+ spoom srb tc --limit 10
59
+ ```
50
60
 
51
- List all typechecking errors sorted by location:
61
+ Options can be combined:
52
62
 
53
- ```
54
- $ spoom srb tc -s loc
63
+ ```sh
64
+ spoom srb tc --sort code --code 7004 --limit 10
55
65
  ```
56
66
 
57
- List all typechecking errors sorted by error code first:
67
+ Remove duplicated error lines:
58
68
 
59
- ```
60
- $ spoom srb tc -s code
69
+ ```sh
70
+ spoom srb tc --uniq
61
71
  ```
62
72
 
63
- List only typechecking errors from a specific error code:
73
+ Format each error line:
64
74
 
65
- ```
66
- $ spoom srb tc -c 7004
75
+ ```sh
76
+ spoom srb tc --format "%C - %F:%L: %M"
67
77
  ```
68
78
 
69
- List only the first 10 typechecking errors
79
+ Format tokens:
70
80
 
71
- ```
72
- $ spoom srb tc -l 10
73
- ```
81
+ * `%C`: error code
82
+ * `%F`: file path
83
+ * `%L`: line number
84
+ * `%M`: error message
74
85
 
75
- These options can be combined:
86
+ Hide the final `Errors: X` count:
76
87
 
77
- ```
78
- $ spoom srb tc -s -c 7004 -l 10
88
+ ```sh
89
+ spoom srb tc --no-count
79
90
  ```
80
91
 
81
- Remove duplicated error lines:
92
+ List only errors from specific files or directories:
82
93
 
83
- ```
84
- $ spoom srb tc -u
94
+ ```sh
95
+ spoom srb tc file1.rb path1/ path2/
85
96
  ```
86
97
 
87
- Format each error line:
98
+ Write errors to a JUnit XML file:
88
99
 
100
+ ```sh
101
+ spoom srb tc --junit-output-path junit.xml
89
102
  ```
90
- $ spoom srb tc -f '%C - %F:%L: %M'
103
+
104
+ Pass extra options to Sorbet:
105
+
106
+ ```sh
107
+ spoom srb tc --sorbet-options="--typed=true"
91
108
  ```
92
109
 
93
- Where:
110
+ ### Typing coverage
94
111
 
95
- * `%C` is the error code
96
- * `%F` is the file the error is from
97
- * `%L` is the line the error is from
98
- * `%M` is the error message
112
+ `spoom srb coverage` collects Sorbet coverage metrics and can generate an HTML report from Sorbet and Git data.
99
113
 
100
- Hide the `Errors: X` at the end of the list:
114
+ ![Coverage Report](docs/report.png)
101
115
 
102
- ```
103
- $ spoom srb tc --no-count
116
+ Show a coverage snapshot:
117
+
118
+ ```sh
119
+ spoom srb coverage
104
120
  ```
105
121
 
106
- List only the errors coming from specific directories or files:
122
+ Save a snapshot under `spoom_data/`:
107
123
 
124
+ ```sh
125
+ spoom srb coverage --save
108
126
  ```
109
- $ spoom srb tc file1.rb path1/ path2/
110
- ```
111
-
112
- #### Typing coverage
113
127
 
114
- Show metrics about the project contents and the typing coverage:
128
+ Save a snapshot under a specific directory:
115
129
 
116
- ```
117
- $ spoom srb coverage
130
+ ```sh
131
+ spoom srb coverage --save my_data/
118
132
  ```
119
133
 
120
- Save coverage data under `spoom_data/`:
134
+ Show typing coverage evolution based on Git history:
121
135
 
122
- ```
123
- $ spoom srb coverage --save
136
+ ```sh
137
+ spoom srb coverage timeline
124
138
  ```
125
139
 
126
- Save coverage data under a specific directory:
140
+ Replay a specific date range:
127
141
 
128
- ```
129
- $ spoom srb coverage --save my_data/
142
+ ```sh
143
+ spoom srb coverage timeline --from YYYY-MM-DD --to YYYY-MM-DD
130
144
  ```
131
145
 
132
- Show typing coverage evolution based on the commits history:
146
+ Save timeline snapshots under `spoom_data/`:
133
147
 
134
- ```
135
- $ spoom srb coverage timeline
148
+ ```sh
149
+ spoom srb coverage timeline --save
136
150
  ```
137
151
 
138
- Show typing coverage evolution based on the commits history between specific dates:
152
+ Save timeline snapshots under a specific directory:
139
153
 
140
- ```
141
- $ spoom srb coverage timeline --from YYYY-MM-DD --to YYYY-MM-DD
154
+ ```sh
155
+ spoom srb coverage timeline --save my_data/
142
156
  ```
143
157
 
144
- Save the typing coverage evolution as JSON under `spoom_data/`:
158
+ Run `bundle install` before collecting each timeline snapshot:
145
159
 
146
- ```
147
- $ spoom srb coverage timeline --save
160
+ ```sh
161
+ spoom srb coverage timeline --bundle-install
148
162
  ```
149
163
 
150
- Save the typing coverage evolution as JSON in a specific directory:
164
+ Generate an HTML coverage report from saved snapshots:
151
165
 
152
- ```
153
- $ spoom srb coverage timeline --save my_data/
166
+ ```sh
167
+ spoom srb coverage report
154
168
  ```
155
169
 
156
- Run `bundle install` for each commit of the timeline (may solve errors due to different Sorbet versions):
170
+ The report is generated at `spoom_report.html` by default.
157
171
 
158
- ```
159
- $ spoom srb coverage timeline --bundle-install
172
+ Generate a report from a custom data directory:
173
+
174
+ ```sh
175
+ spoom srb coverage report --data my_data/
160
176
  ```
161
177
 
162
- Generate an HTML typing coverage report:
178
+ Change the generated report path:
163
179
 
164
- ```
165
- $ spoom srb coverage report
180
+ ```sh
181
+ spoom srb coverage report --file coverage.html
166
182
  ```
167
183
 
168
- Change the colors used for strictnesses (useful for colorblind folks):
184
+ Change report colors:
169
185
 
170
- ```
171
- $ spoom srb coverage report \
186
+ ```sh
187
+ spoom srb coverage report \
172
188
  --color-true "#648ffe" \
173
189
  --color-false "#fe6002" \
174
190
  --color-ignore "#feb000" \
@@ -176,123 +192,213 @@ $ spoom srb coverage report \
176
192
  --color-strong "#6444f1"
177
193
  ```
178
194
 
179
- Open the HTML typing coverage report:
195
+ Open the HTML coverage report:
180
196
 
197
+ ```sh
198
+ spoom srb coverage open
181
199
  ```
182
- $ spoom srb coverage open
200
+
201
+ Open a report at a custom path:
202
+
203
+ ```sh
204
+ spoom srb coverage open coverage.html
183
205
  ```
184
206
 
185
- #### Change the sigil used in files
207
+ ### Sorbet sigils
186
208
 
187
- Bump the strictness from all files currently at `typed: false` to `typed: true` where it does not create typechecking errors:
209
+ `spoom srb bump` changes `# typed:` sigils when the change does not introduce typechecking errors.
188
210
 
189
- ```
190
- $ spoom srb bump --from false --to true
211
+ Bump files from `typed: false` to `typed: true`:
212
+
213
+ ```sh
214
+ spoom srb bump --from false --to true
191
215
  ```
192
216
 
193
- Bump the strictness from all files currently at `typed: false` to `typed: true` even if it creates typechecking errors:
217
+ Force the change without typechecking:
194
218
 
195
- ```
196
- $ spoom srb bump --from false --to true -f
219
+ ```sh
220
+ spoom srb bump --from false --to true --force
197
221
  ```
198
222
 
199
- Bump the strictness from a list of files (one file by line):
223
+ Bump only files listed in a file, one path per line:
200
224
 
201
- ```
202
- $ spoom srb bump --from false --to true -o list.txt
225
+ ```sh
226
+ spoom srb bump --from false --to true --only list.txt
203
227
  ```
204
228
 
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):
229
+ Check which files can be bumped without applying changes:
207
230
 
208
- ```
209
- $ spoom srb bump --from false --to true --dry
231
+ ```sh
232
+ spoom srb bump --from false --to true --dry
210
233
  ```
211
234
 
212
- Bump files using a custom instance of Sorbet:
235
+ This command exits with a non-zero status when files can be bumped, which is useful in CI.
213
236
 
214
- ```
215
- $ spoom srb bump --from false --to true --sorbet /path/to/sorbet/bin
237
+ Use a custom Sorbet executable:
238
+
239
+ ```sh
240
+ spoom srb bump --from false --to true --sorbet /path/to/sorbet/bin
216
241
  ```
217
242
 
218
- Count the number of type-checking errors if all files were bumped to true:
243
+ Count typechecking errors if all files were bumped:
219
244
 
245
+ ```sh
246
+ spoom srb bump --from false --to true --count-errors --dry
220
247
  ```
221
- $ spoom srb bump --count-errors --dry
248
+
249
+ ### Signatures and type assertions
250
+
251
+ `spoom srb sigs` translates signatures between Sorbet RBI syntax and RBS comments.
252
+
253
+ Translate signatures from RBI to RBS comments:
254
+
255
+ ```sh
256
+ spoom srb sigs translate
222
257
  ```
223
258
 
224
- #### Translate sigs between RBI and RBS
259
+ Translate signatures from RBS comments to RBI:
260
+
261
+ ```sh
262
+ spoom srb sigs translate --from rbs --to rbi path/to/file.rb
263
+ ```
225
264
 
226
- Translate all file sigs from RBI to RBS:
265
+ Strip Sorbet signatures from files:
227
266
 
267
+ ```sh
268
+ spoom srb sigs strip path/to/file.rb
228
269
  ```
229
- $ spoom srb sigs translate
270
+
271
+ Export gem signatures to an RBI file:
272
+
273
+ ```sh
274
+ spoom srb sigs export
230
275
  ```
231
276
 
232
- Translate one file's sigs from RBS to RBI:
277
+ Check that the exported RBI file is up to date:
233
278
 
279
+ ```sh
280
+ spoom srb sigs export --check-sync
234
281
  ```
235
- $ spoom srb sigs translate --from rbs --to rbi /path/to/file.rb
282
+
283
+ `spoom srb assertions` translates Sorbet type assertions to RBS comments:
284
+
285
+ ```sh
286
+ spoom srb assertions translate path/to/file.rb
236
287
  ```
237
288
 
238
- #### Interact with Sorbet LSP mode
289
+ ### Sorbet LSP
290
+
291
+ `spoom srb lsp` sends requests to Sorbet LSP.
239
292
 
240
- **Experimental**
293
+ This command group is experimental.
241
294
 
242
- Find all definitions for `Foo`:
295
+ Find symbols matching `Foo`:
243
296
 
297
+ ```sh
298
+ spoom srb lsp find Foo
244
299
  ```
245
- $ spoom srb lsp find Foo
300
+
301
+ List symbols in a file:
302
+
303
+ ```sh
304
+ spoom srb lsp symbols file.rb
246
305
  ```
247
306
 
248
- List all symbols in a file:
307
+ List definitions for a code location:
249
308
 
309
+ ```sh
310
+ spoom srb lsp defs file.rb 10 4
250
311
  ```
251
- $ spoom srb lsp symbols <file.rb>
312
+
313
+ List references for a code location:
314
+
315
+ ```sh
316
+ spoom srb lsp refs file.rb 10 4
252
317
  ```
253
318
 
254
- List all definitions for a specific code location:
319
+ Show hover information for a code location:
255
320
 
321
+ ```sh
322
+ spoom srb lsp hover file.rb 10 4
256
323
  ```
257
- $ spoom srb lsp defs <file.rb> <line> <column>
324
+
325
+ Show signature information for a code location:
326
+
327
+ ```sh
328
+ spoom srb lsp sigs file.rb 10 4
258
329
  ```
259
330
 
260
- List all references for a specific code location:
331
+ Show type information for a code location:
261
332
 
333
+ ```sh
334
+ spoom srb lsp types file.rb 10 4
262
335
  ```
263
- $ spoom srb lsp refs <file.rb> <line> <column>
336
+
337
+ ### Sorbet code metrics
338
+
339
+ `spoom srb metrics` collects metrics about Sorbet usage in Ruby files.
340
+
341
+ Show metrics for the current project:
342
+
343
+ ```sh
344
+ spoom srb metrics
264
345
  ```
265
346
 
266
- Show hover information for a specific code location:
347
+ Show metrics for specific files or directories:
267
348
 
349
+ ```sh
350
+ spoom srb metrics lib/ test/foo_test.rb
268
351
  ```
269
- $ spoom srb lsp hover <file.rb> <line> <column>
352
+
353
+ Dump raw metric keys and values:
354
+
355
+ ```sh
356
+ spoom srb metrics --dump
270
357
  ```
271
358
 
272
- Show signature information for a specific code location:
359
+ ### Dead code
273
360
 
361
+ `spoom deadcode` indexes a project and reports definitions that do not appear to be referenced.
362
+
363
+ Analyze the current project:
364
+
365
+ ```sh
366
+ spoom deadcode
274
367
  ```
275
- $ spoom srb lsp sig <file.rb> <line> <column>
368
+
369
+ Analyze specific paths:
370
+
371
+ ```sh
372
+ spoom deadcode lib/ app/models/
276
373
  ```
277
374
 
278
- Show type information for a specific code location:
375
+ Show files, loaded plugins, definitions, or references used during analysis:
279
376
 
377
+ ```sh
378
+ spoom deadcode --show-files
379
+ spoom deadcode --show-plugins
380
+ spoom deadcode --show-defs
381
+ spoom deadcode --show-refs
280
382
  ```
281
- $ spoom srb lsp sig <file.rb> <line> <column>
383
+
384
+ Remove a reported dead code candidate:
385
+
386
+ ```sh
387
+ spoom deadcode remove path/to/file.rb:42:18-47:23
282
388
  ```
283
389
 
284
- ### API
390
+ ## Ruby API
285
391
 
286
- #### Parsing Sorbet config
392
+ ### Parsing Sorbet config
287
393
 
288
- Parses a Sorbet config file:
394
+ Parse a Sorbet config file:
289
395
 
290
396
  ```ruby
291
397
  config = Spoom::Sorbet::Config.parse_file("sorbet/config")
292
- puts config.paths # "."
398
+ puts config.paths
293
399
  ```
294
400
 
295
- Parses a Sorbet config string:
401
+ Parse a Sorbet config string:
296
402
 
297
403
  ```ruby
298
404
  config = Spoom::Sorbet::Config.parse_string(<<~CONFIG)
@@ -300,30 +406,31 @@ config = Spoom::Sorbet::Config.parse_string(<<~CONFIG)
300
406
  --file=b
301
407
  --ignore=c
302
408
  CONFIG
303
- puts config.paths # "a", "b"
304
- puts config.ignore # "c"
409
+
410
+ puts config.paths
411
+ puts config.ignore
305
412
  ```
306
413
 
307
- List all files typchecked by Sorbet:
414
+ List all files typechecked by Sorbet:
308
415
 
309
416
  ```ruby
310
417
  config = Spoom::Sorbet::Config.parse_file("sorbet/config")
311
- puts Spoom::Sorbet.srb_files(config)
418
+ puts Spoom::Context.new(".").srb_files(with_config: config)
312
419
  ```
313
420
 
314
- #### Parsing Sorbet metrics
421
+ ### Parsing Sorbet metrics
315
422
 
316
423
  Display metrics collected during typechecking:
317
424
 
318
425
  ```ruby
319
- puts Spoom::Sorbet.srb_metrics(capture_err: false)
426
+ puts Spoom::Context.new(".").srb_metrics(capture_err: false)
320
427
  ```
321
428
 
322
- #### Interacting with LSP
429
+ ### Interacting with LSP
323
430
 
324
431
  Create an LSP client:
325
432
 
326
- ```rb
433
+ ```ruby
327
434
  client = Spoom::LSP::Client.new(
328
435
  Spoom::Sorbet::BIN_PATH,
329
436
  "--lsp",
@@ -333,71 +440,88 @@ client = Spoom::LSP::Client.new(
333
440
  client.open(".")
334
441
  ```
335
442
 
336
- Find all the symbols matching a string:
443
+ Find symbols matching a string:
337
444
 
338
- ```rb
445
+ ```ruby
339
446
  puts client.symbols("Foo")
340
447
  ```
341
448
 
342
- Find all the symbols for a file:
449
+ Find symbols in a file:
343
450
 
344
- ```rb
451
+ ```ruby
345
452
  puts client.document_symbols("file://path/to/my/file.rb")
346
453
  ```
347
454
 
348
- ### Backtrace Filtering
455
+ ## Backtrace filtering
456
+
457
+ Spoom provides a Minitest backtrace filter that removes Sorbet frames from test failures.
349
458
 
350
- Spoom provides a backtrace filter for Minitest to remove the Sorbet frames from test failures, giving a more readable output. To enable it:
459
+ Enable it in your test helper:
351
460
 
352
461
  ```ruby
353
462
  # test/test_helper.rb
354
463
  require "spoom/backtrace_filter/minitest"
464
+
355
465
  Minitest.backtrace_filter = Spoom::BacktraceFilter::Minitest.new
356
466
  ```
357
467
 
358
- ### Dead code removal
468
+ ## Development
359
469
 
360
- Run dead code detection in your project with:
470
+ After checking out the repo, install dependencies:
361
471
 
472
+ ```sh
473
+ bin/setup
362
474
  ```
363
- $ spoom deadcode
364
- ```
365
-
366
- This will list all the methods and constants that do not appear to be used in your project.
367
475
 
368
- You can remove them with Spoom:
476
+ Run the tests:
369
477
 
478
+ ```sh
479
+ bin/test
370
480
  ```
371
- $ spoom deadcode remove path/to/file.rb:42:18-47:23
481
+
482
+ Run an interactive console:
483
+
484
+ ```sh
485
+ bin/console
372
486
  ```
373
487
 
374
- ## Development
488
+ Run the full local sanity check before pushing:
375
489
 
376
- 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.
490
+ ```sh
491
+ bin/sanity
492
+ ```
493
+
494
+ Install this gem locally:
377
495
 
378
- To install this gem onto your local machine, run `bundle exec rake install`.
496
+ ```sh
497
+ bundle exec rake install
498
+ ```
379
499
 
380
500
  ## Releasing
381
501
 
382
502
  ### Bump the gem version
383
503
 
384
- - [ ] Locally, update the version number in [`version.rb`](https://github.com/Shopify/spoom/blob/main/lib/spoom/version.rb)
385
- - [ ] Run `bundle install` to update the version number in `Gemfile.lock`
386
- - [ ] Commit this change with the message `Bump version to vx.y.z`
387
- - [ ] Push this change directly to main or open a PR
504
+ * [ ] Update the version number in [`lib/spoom/version.rb`](https://github.com/Shopify/spoom/blob/main/lib/spoom/version.rb)
505
+ * [ ] Run `bundle install` to update the version number in `Gemfile.lock`
506
+ * [ ] Commit the change with `Bump version to vx.y.z`
507
+ * [ ] Push the change directly to `main` or open a pull request
388
508
 
389
509
  ### Create a new tag
390
510
 
391
- - [ ] Locally, create a new tag with the new version number: `git tag vx.y.z`
392
- - [ ] Push this tag up to the remote `git push origin vx.y.z`
511
+ * [ ] Create a tag with the new version number: `git tag vx.y.z`
512
+ * [ ] Push the tag: `git push origin vx.y.z`
393
513
 
394
- ### Release workflow will run automatically
514
+ ### Publish the release
395
515
 
396
- We have a [release workflow](https://github.com/Shopify/spoom/actions/workflows/release.yml) that will publish your new gem version to rubygems.org via [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/). This workflow must be approved by a member of the Ruby and Rails Infrastructure team at Shopify before it will run. Once it is approved, it will automatically publish a new gem version to rubygems.org and create a new GitHub release.
516
+ The [release workflow](https://github.com/Shopify/spoom/actions/workflows/release.yml) publishes new gem versions to RubyGems through [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/).
517
+
518
+ A member of the Ruby and Rails Infrastructure team at Shopify must approve the workflow before it runs. Once approved, it publishes the gem and creates a GitHub release.
397
519
 
398
520
  ## Contributing
399
521
 
400
- Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/spoom. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
522
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/spoom.
523
+
524
+ This project is intended to be a safe, welcoming space for collaboration. Contributors are expected to follow the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
401
525
 
402
526
  ## License
403
527
 
@@ -405,4 +529,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
405
529
 
406
530
  ## Code of Conduct
407
531
 
408
- Everyone interacting in the Spoom project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/Shopify/spoom/blob/main/CODE_OF_CONDUCT.md).
532
+ Everyone interacting in Spoom's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [code of conduct](https://github.com/Shopify/spoom/blob/main/CODE_OF_CONDUCT.md).
@@ -12,7 +12,7 @@ module Spoom
12
12
  #: (Array[String]? bt) -> Array[String]
13
13
  def filter(bt)
14
14
  super.select do |line|
15
- SORBET_PATHS.none? { |path| line.include?(path) }
15
+ !SORBET_PATHS.intersect?(line)
16
16
  end
17
17
  end
18
18
  end
@@ -26,6 +26,10 @@ module Spoom
26
26
  option :translate_generics, type: :boolean, desc: "Translate generics", default: false
27
27
  option :translate_helpers, type: :boolean, desc: "Translate helpers", default: false
28
28
  option :translate_abstract_methods, type: :boolean, desc: "Translate abstract methods", default: false
29
+ option :erase_generic_types,
30
+ type: :boolean,
31
+ desc: "Drop generic types when translating from RBS to RBI",
32
+ default: false
29
33
  def translate(*paths)
30
34
  from = options[:from]
31
35
  to = options[:to]
@@ -70,6 +74,7 @@ module Spoom
70
74
  contents,
71
75
  file: file,
72
76
  max_line_length: max_line_length,
77
+ erase_generic_types: options[:erase_generic_types],
73
78
  )
74
79
  end
75
80
  end
@@ -40,6 +40,15 @@ module Spoom
40
40
  super
41
41
  end
42
42
 
43
+ #: -> String
44
+ def wrapped_src
45
+ <<~RUBY
46
+ def __spoom_deadcode_erb_template__
47
+ #{src}
48
+ end
49
+ RUBY
50
+ end
51
+
43
52
  private
44
53
 
45
54
  # @override
@@ -50,32 +59,34 @@ module Spoom
50
59
  if text == "\n"
51
60
  @newline_pending += 1
52
61
  else
53
- src << bufvar << ".safe_append='"
54
- src << "\n" * @newline_pending if @newline_pending > 0
55
- src << text.gsub(/['\\]/, '\\\\\&')
56
- src << "'.freeze;"
57
-
62
+ with_buffer do
63
+ src << ".safe_append='"
64
+ src << "\n" * @newline_pending if @newline_pending > 0
65
+ src << text.gsub(/['\\]/, '\\\\\&') << @text_end
66
+ end
58
67
  @newline_pending = 0
59
68
  end
60
69
  end
61
70
 
62
- BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
71
+ BLOCK_EXPR = /((\s|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
63
72
 
64
73
  # @override
65
74
  #: (untyped indicator, untyped code) -> void
66
75
  def add_expression(indicator, code)
67
76
  flush_newline_if_pending(src)
68
77
 
69
- src << bufvar << if (indicator == "==") || @escape
70
- ".safe_expr_append="
71
- else
72
- ".append="
73
- end
78
+ with_buffer do
79
+ src << if (indicator == "==") || @escape
80
+ ".safe_expr_append="
81
+ else
82
+ ".append="
83
+ end
74
84
 
75
- if BLOCK_EXPR.match?(code)
76
- src << " " << code
77
- else
78
- src << "(" << code << ");"
85
+ if BLOCK_EXPR.match?(code)
86
+ src << " " << code
87
+ else
88
+ src << "(" << code << ")"
89
+ end
79
90
  end
80
91
  end
81
92
 
@@ -96,7 +107,7 @@ module Spoom
96
107
  #: (untyped src) -> void
97
108
  def flush_newline_if_pending(src)
98
109
  if @newline_pending > 0
99
- src << bufvar << ".safe_append='#{"\n" * @newline_pending}'.freeze;"
110
+ with_buffer { src << ".safe_append='#{"\n" * @newline_pending}" << @text_end }
100
111
  @newline_pending = 0
101
112
  end
102
113
  end
@@ -44,7 +44,7 @@ module Spoom
44
44
 
45
45
  #: (String erb, file: String, ?plugins: Array[Plugins::Base]) -> void
46
46
  def index_erb(erb, file:, plugins: [])
47
- index_ruby(Deadcode::ERB.new(erb).src, file: file, plugins: plugins)
47
+ index_ruby(Deadcode::ERB.new(erb).wrapped_src, file: file, plugins: plugins)
48
48
  end
49
49
 
50
50
  #: (String rb, file: String, ?plugins: Array[Plugins::Base]) -> void
@@ -55,6 +55,11 @@ module Spoom
55
55
  model_builder = Model::Builder.new(@model, file)
56
56
  model_builder.visit(node)
57
57
 
58
+ # Ignore the special synthetic method generated by the ERB engine
59
+ if (spoom_erb_wrapper = @model.symbols["__spoom_deadcode_erb_template__"])
60
+ @ignored.merge(spoom_erb_wrapper.definitions)
61
+ end
62
+
58
63
  # Index references
59
64
  refs_visitor = Model::ReferencesVisitor.new(file)
60
65
  refs_visitor.visit(node)
@@ -117,6 +117,7 @@ module Spoom
117
117
  Prism::ConstantPathAndWriteNode, Prism::ConstantPathOrWriteNode
118
118
  # Nesting node is an assign, it means only one constant is assigned on the line
119
119
  # so we can remove the whole assign
120
+ remove_constant_visibility_call(context)
120
121
  delete_node_and_comments_and_sigs(context)
121
122
  return
122
123
  end
@@ -127,6 +128,7 @@ module Spoom
127
128
  if parent_node.is_a?(Prism::ConstantWriteNode)
128
129
  # Nesting node is an assign, it means only one constant is assigned on the line
129
130
  # so we can remove the whole assign
131
+ remove_constant_visibility_call(parent_context)
130
132
  delete_node_and_comments_and_sigs(parent_context)
131
133
  return
132
134
  elsif parent_node.is_a?(Prism::MultiWriteNode) && parent_node.lefts.size == 1
@@ -190,6 +192,64 @@ module Spoom
190
192
  end
191
193
  end
192
194
 
195
+ # A dead constant is often followed by a `private_constant`/`public_constant` call naming it.
196
+ # That call references the now-removed constant (a load-time `NameError` if left behind), so
197
+ # remove the reference too: delete the whole call when the constant is its only argument, or
198
+ # drop just that symbol when the call lists several constants.
199
+ #: (NodeContext context) -> void
200
+ def remove_constant_visibility_call(context)
201
+ node = context.node
202
+ return unless node.is_a?(Prism::ConstantWriteNode)
203
+
204
+ name = node.name
205
+ call = context.next_nodes.find { |sibling| constant_visibility_call?(sibling, name) }
206
+ return unless call.is_a?(Prism::CallNode)
207
+
208
+ call_context = NodeContext.new(@old_source, @node_context.comments, call, context.nesting)
209
+ arguments = call.arguments&.arguments #: Array[Prism::Node]?
210
+ if arguments && arguments.size > 1
211
+ delete_symbol_argument(call_context, name)
212
+ else
213
+ delete_node_and_comments_and_sigs(call_context)
214
+ end
215
+ end
216
+
217
+ # Whether `node` is a bare `private_constant`/`public_constant` call listing `name`.
218
+ #: (Prism::Node node, Symbol name) -> bool
219
+ def constant_visibility_call?(node, name)
220
+ return false unless node.is_a?(Prism::CallNode)
221
+ return false unless node.receiver.nil?
222
+ return false unless node.name == :private_constant || node.name == :public_constant
223
+
224
+ arguments = node.arguments&.arguments
225
+ return false unless arguments
226
+
227
+ arguments.any? { |argument| argument.is_a?(Prism::SymbolNode) && argument.value == name.to_s }
228
+ end
229
+
230
+ # Drop the `:name` symbol from a `private_constant`/`public_constant` call that lists several
231
+ # constants, keeping the call and the other names intact.
232
+ #: (NodeContext context, Symbol name) -> void
233
+ def delete_symbol_argument(context, name)
234
+ arguments = T.cast(context.node, Prism::CallNode).arguments&.arguments
235
+ return unless arguments
236
+
237
+ index = arguments.index { |argument| argument.is_a?(Prism::SymbolNode) && argument.value == name.to_s }
238
+ return unless index
239
+
240
+ argument = arguments.fetch(index)
241
+ prev_argument = arguments[index - 1] if index.positive?
242
+ next_argument = arguments[index + 1]
243
+
244
+ if prev_argument && next_argument
245
+ replace_chars(prev_argument.location.end_offset, next_argument.location.start_offset, ", ")
246
+ elsif prev_argument
247
+ delete_chars(prev_argument.location.end_offset, argument.location.end_offset)
248
+ elsif next_argument
249
+ delete_chars(argument.location.start_offset, next_argument.location.start_offset)
250
+ end
251
+ end
252
+
193
253
  #: (NodeContext context) -> void
194
254
  def delete_attr_accessor(context)
195
255
  args_context = context.parent_context
@@ -26,7 +26,8 @@ module Spoom
26
26
 
27
27
  @overloads_strategy = options.overloads_strategy #: Symbol
28
28
  @translate_abstract_methods = options.translate_abstract_methods #: bool
29
- @type_translator = RBI::RBS::TypeTranslator.new #: RBI::RBS::TypeTranslator
29
+ @options = options #: Options
30
+ @type_translator = RBI::RBS::TypeTranslator.new(options: options.rbi_options) #: RBI::RBS::TypeTranslator
30
31
  end
31
32
 
32
33
  # @override
@@ -163,7 +164,7 @@ module Spoom
163
164
  next
164
165
  end
165
166
 
166
- translator = RBI::RBS::MethodTypeTranslator.new(rbi_node)
167
+ translator = RBI::RBS::MethodTypeTranslator.new(rbi_node, options: @options.rbi_options)
167
168
 
168
169
  begin
169
170
  translator.visit(method_type)
@@ -273,6 +274,18 @@ module Spoom
273
274
  rewrite_type_params_signature(signature, type_params:)
274
275
  next if type_params.empty?
275
276
 
277
+ if @options.erase_generic_types
278
+ type_params.each do |type_param|
279
+ insert_type_member(
280
+ "#{type_param.name} = ::T.type_alias { ::T.anything }",
281
+ parent_node: node,
282
+ insert_pos:,
283
+ )
284
+ end
285
+
286
+ next
287
+ end
288
+
276
289
  unless already_extends?(node, /^(::)?T::Generic$/)
277
290
  extend_with("T::Generic", into: node, at: insert_pos)
278
291
  end
@@ -462,7 +475,7 @@ module Spoom
462
475
  )
463
476
 
464
477
  @rewriter << Source::Delete.new(from, to)
465
- content = "#{indent}#{alias_name} = T.type_alias { #{sorbet_type.to_rbi} }\n"
478
+ content = "#{indent}#{alias_name} = ::T.type_alias { #{sorbet_type.to_rbi} }\n"
466
479
  content = pad_out_line_count(of: content, to_height_of: type_alias)
467
480
  @rewriter << Source::Insert.new(insert_pos, content)
468
481
  rescue ::RBS::ParsingError, ::RBI::Error
@@ -44,6 +44,12 @@ module Spoom
44
44
 
45
45
  ALLOWED_OVERLOAD_STRATEGIES = [:translate_all, :translate_last, :raise].freeze
46
46
 
47
+ #: bool
48
+ attr_reader :erase_generic_types
49
+
50
+ #: RBI::RBS::MethodTypeTranslator::Options
51
+ attr_reader :rbi_options
52
+
47
53
  #: BaseRBIFormat
48
54
  attr_reader :output_format
49
55
 
@@ -52,11 +58,13 @@ module Spoom
52
58
 
53
59
  #: (
54
60
  #| ?overloads_strategy: Symbol,
61
+ #| ?erase_generic_types: bool,
55
62
  #| ?output_format: BaseRBIFormat,
56
63
  #| ?translate_abstract_methods: bool,
57
64
  #| ) -> void
58
65
  def initialize(
59
66
  overloads_strategy: :translate_all,
67
+ erase_generic_types: false,
60
68
  output_format: HumanReadableRBIFormat.default,
61
69
  translate_abstract_methods: true
62
70
  )
@@ -66,6 +74,8 @@ module Spoom
66
74
  end
67
75
 
68
76
  @overloads_strategy = overloads_strategy
77
+ @erase_generic_types = erase_generic_types
78
+ @rbi_options = RBI::RBS::MethodTypeTranslator::Options.new(erase_generic_types:) #: RBI::RBS::MethodTypeTranslator::Options
69
79
  @output_format = output_format
70
80
  @translate_abstract_methods = translate_abstract_methods
71
81
 
@@ -28,17 +28,20 @@ module Spoom
28
28
  #| String ruby_contents,
29
29
  #| file: String,
30
30
  #| ?max_line_length: Integer?,
31
- #| ?overloads_strategy: Symbol) -> String
31
+ #| ?overloads_strategy: Symbol,
32
+ #| ?erase_generic_types: bool) -> String
32
33
  def rewrite_if_needed(
33
34
  ruby_contents,
34
35
  file:,
35
36
  max_line_length: nil,
36
- overloads_strategy: :translate_all
37
+ overloads_strategy: :translate_all,
38
+ erase_generic_types: false
37
39
  )
38
40
  return ruby_contents unless contains_rbs_syntax?(ruby_contents)
39
41
 
40
42
  options = Options.new(
41
43
  overloads_strategy:,
44
+ erase_generic_types:,
42
45
  output_format: HumanReadableRBIFormat.new(
43
46
  max_line_length:,
44
47
  ),
@@ -59,13 +59,16 @@ module Spoom
59
59
 
60
60
  # Converts all the RBS comments in the given Ruby code to `sig` nodes.
61
61
  # It also handles type members and class annotations.
62
- #: (String ruby_contents, file: String, ?max_line_length: Integer?, ?overloads_strategy: Symbol) -> String
63
- def rbs_comments_to_sorbet_sigs(ruby_contents, file:, max_line_length: nil, overloads_strategy: :translate_all)
62
+ #: (String ruby_contents, file: String, ?max_line_length: Integer?,
63
+ #| ?overloads_strategy: Symbol, ?erase_generic_types: bool) -> String
64
+ def rbs_comments_to_sorbet_sigs(ruby_contents, file:, max_line_length: nil, overloads_strategy: :translate_all,
65
+ erase_generic_types: false)
64
66
  RBSCommentsToSorbetSigs.rewrite_if_needed(
65
67
  ruby_contents,
66
68
  file: file,
67
69
  max_line_length: max_line_length,
68
70
  overloads_strategy: overloads_strategy,
71
+ erase_generic_types: erase_generic_types,
69
72
  )
70
73
  end
71
74
 
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.8.4"
5
+ VERSION = "1.8.5"
6
6
  end
data/rbi/spoom.rbi CHANGED
@@ -1024,6 +1024,9 @@ class Spoom::Deadcode::ERB < ::Erubi::Engine
1024
1024
  sig { params(input: T.untyped, properties: T.untyped).void }
1025
1025
  def initialize(input, properties = T.unsafe(nil)); end
1026
1026
 
1027
+ sig { returns(::String) }
1028
+ def wrapped_src; end
1029
+
1027
1030
  private
1028
1031
 
1029
1032
  sig { override.params(code: T.untyped).void }
@@ -1497,6 +1500,9 @@ class Spoom::Deadcode::Remover::NodeRemover
1497
1500
 
1498
1501
  private
1499
1502
 
1503
+ sig { params(node: ::Prism::Node, name: ::Symbol).returns(T::Boolean) }
1504
+ def constant_visibility_call?(node, name); end
1505
+
1500
1506
  sig { params(context: ::Spoom::Deadcode::Remover::NodeContext).void }
1501
1507
  def delete_attr_accessor(context); end
1502
1508
 
@@ -1512,6 +1518,9 @@ class Spoom::Deadcode::Remover::NodeRemover
1512
1518
  sig { params(context: ::Spoom::Deadcode::Remover::NodeContext).void }
1513
1519
  def delete_node_and_comments_and_sigs(context); end
1514
1520
 
1521
+ sig { params(context: ::Spoom::Deadcode::Remover::NodeContext, name: ::Symbol).void }
1522
+ def delete_symbol_argument(context, name); end
1523
+
1515
1524
  sig do
1516
1525
  params(
1517
1526
  node: ::Prism::Node,
@@ -1524,6 +1533,9 @@ class Spoom::Deadcode::Remover::NodeRemover
1524
1533
  sig { params(def_node: ::Prism::DefNode).returns(T.nilable(::Spoom::Deadcode::Remover::NodeContext)) }
1525
1534
  def modifier_call_context(def_node); end
1526
1535
 
1536
+ sig { params(context: ::Spoom::Deadcode::Remover::NodeContext).void }
1537
+ def remove_constant_visibility_call(context); end
1538
+
1527
1539
  sig { params(start_char: ::Integer, end_char: ::Integer, replacement: ::String).void }
1528
1540
  def replace_chars(start_char, end_char, replacement); end
1529
1541
 
@@ -2926,10 +2938,11 @@ module Spoom::Sorbet::Translate
2926
2938
  ruby_contents: ::String,
2927
2939
  file: ::String,
2928
2940
  max_line_length: T.nilable(::Integer),
2929
- overloads_strategy: ::Symbol
2941
+ overloads_strategy: ::Symbol,
2942
+ erase_generic_types: T::Boolean
2930
2943
  ).returns(::String)
2931
2944
  end
2932
- def rbs_comments_to_sorbet_sigs(ruby_contents, file:, max_line_length: T.unsafe(nil), overloads_strategy: T.unsafe(nil)); end
2945
+ def rbs_comments_to_sorbet_sigs(ruby_contents, file:, max_line_length: T.unsafe(nil), overloads_strategy: T.unsafe(nil), erase_generic_types: T.unsafe(nil)); end
2933
2946
 
2934
2947
  sig do
2935
2948
  params(
@@ -3008,10 +3021,11 @@ module Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs
3008
3021
  ruby_contents: ::String,
3009
3022
  file: ::String,
3010
3023
  max_line_length: T.nilable(::Integer),
3011
- overloads_strategy: ::Symbol
3024
+ overloads_strategy: ::Symbol,
3025
+ erase_generic_types: T::Boolean
3012
3026
  ).returns(::String)
3013
3027
  end
3014
- def rewrite_if_needed(ruby_contents, file:, max_line_length: T.unsafe(nil), overloads_strategy: T.unsafe(nil)); end
3028
+ def rewrite_if_needed(ruby_contents, file:, max_line_length: T.unsafe(nil), overloads_strategy: T.unsafe(nil), erase_generic_types: T.unsafe(nil)); end
3015
3029
  end
3016
3030
  end
3017
3031
 
@@ -3238,11 +3252,15 @@ class Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::Options
3238
3252
  sig do
3239
3253
  params(
3240
3254
  overloads_strategy: ::Symbol,
3255
+ erase_generic_types: T::Boolean,
3241
3256
  output_format: ::Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::BaseRBIFormat,
3242
3257
  translate_abstract_methods: T::Boolean
3243
3258
  ).void
3244
3259
  end
3245
- def initialize(overloads_strategy: T.unsafe(nil), output_format: T.unsafe(nil), translate_abstract_methods: T.unsafe(nil)); end
3260
+ def initialize(overloads_strategy: T.unsafe(nil), erase_generic_types: T.unsafe(nil), output_format: T.unsafe(nil), translate_abstract_methods: T.unsafe(nil)); end
3261
+
3262
+ sig { returns(T::Boolean) }
3263
+ def erase_generic_types; end
3246
3264
 
3247
3265
  sig { returns(::Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::BaseRBIFormat) }
3248
3266
  def output_format; end
@@ -3250,6 +3268,9 @@ class Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::Options
3250
3268
  sig { returns(::Symbol) }
3251
3269
  def overloads_strategy; end
3252
3270
 
3271
+ sig { returns(::RBI::RBS::MethodTypeTranslator::Options) }
3272
+ def rbi_options; end
3273
+
3253
3274
  sig { returns(T::Boolean) }
3254
3275
  def translate_abstract_methods; end
3255
3276
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spoom
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.4
4
+ version: 1.8.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexandre Terrasa
@@ -85,14 +85,14 @@ dependencies:
85
85
  requirements:
86
86
  - - ">="
87
87
  - !ruby/object:Gem::Version
88
- version: 0.3.15
88
+ version: 0.4.1
89
89
  type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - ">="
94
94
  - !ruby/object:Gem::Version
95
- version: 0.3.15
95
+ version: 0.4.1
96
96
  - !ruby/object:Gem::Dependency
97
97
  name: rbs
98
98
  requirement: !ruby/object:Gem::Requirement