spoom 1.2.4 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +54 -55
  3. data/lib/spoom/cli/deadcode.rb +172 -0
  4. data/lib/spoom/cli/helper.rb +20 -0
  5. data/lib/spoom/cli/srb/bump.rb +200 -0
  6. data/lib/spoom/cli/srb/coverage.rb +224 -0
  7. data/lib/spoom/cli/srb/lsp.rb +159 -0
  8. data/lib/spoom/cli/srb/tc.rb +150 -0
  9. data/lib/spoom/cli/srb.rb +27 -0
  10. data/lib/spoom/cli.rb +72 -32
  11. data/lib/spoom/context/git.rb +2 -2
  12. data/lib/spoom/context/sorbet.rb +2 -2
  13. data/lib/spoom/deadcode/definition.rb +11 -0
  14. data/lib/spoom/deadcode/indexer.rb +222 -224
  15. data/lib/spoom/deadcode/location.rb +2 -2
  16. data/lib/spoom/deadcode/plugins/action_mailer.rb +2 -2
  17. data/lib/spoom/deadcode/plugins/action_mailer_preview.rb +19 -0
  18. data/lib/spoom/deadcode/plugins/actionpack.rb +4 -6
  19. data/lib/spoom/deadcode/plugins/active_model.rb +8 -8
  20. data/lib/spoom/deadcode/plugins/active_record.rb +9 -12
  21. data/lib/spoom/deadcode/plugins/active_support.rb +11 -0
  22. data/lib/spoom/deadcode/plugins/base.rb +1 -1
  23. data/lib/spoom/deadcode/plugins/graphql.rb +4 -4
  24. data/lib/spoom/deadcode/plugins/namespaces.rb +2 -4
  25. data/lib/spoom/deadcode/plugins/ruby.rb +8 -17
  26. data/lib/spoom/deadcode/plugins/sorbet.rb +4 -10
  27. data/lib/spoom/deadcode/plugins.rb +1 -0
  28. data/lib/spoom/deadcode/remover.rb +209 -174
  29. data/lib/spoom/deadcode/send.rb +9 -10
  30. data/lib/spoom/deadcode/visitor.rb +755 -0
  31. data/lib/spoom/deadcode.rb +40 -10
  32. data/lib/spoom/file_tree.rb +0 -16
  33. data/lib/spoom/sorbet/errors.rb +1 -1
  34. data/lib/spoom/sorbet/lsp/structures.rb +2 -2
  35. data/lib/spoom/version.rb +1 -1
  36. metadata +19 -15
  37. data/lib/spoom/cli/bump.rb +0 -198
  38. data/lib/spoom/cli/coverage.rb +0 -222
  39. data/lib/spoom/cli/lsp.rb +0 -168
  40. data/lib/spoom/cli/run.rb +0 -148
@@ -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"
@@ -18,10 +19,15 @@ require_relative "deadcode/remover"
18
19
  module Spoom
19
20
  module Deadcode
20
21
  class Error < Spoom::Error
21
- extend T::Sig
22
22
  extend T::Helpers
23
23
 
24
24
  abstract!
25
+ end
26
+
27
+ class ParserError < Error; end
28
+
29
+ class IndexerError < Error
30
+ extend T::Sig
25
31
 
26
32
  sig { params(message: String, parent: Exception).void }
27
33
  def initialize(message, parent:)
@@ -30,23 +36,47 @@ module Spoom
30
36
  end
31
37
  end
32
38
 
33
- class ParserError < Error; end
34
- class IndexerError < Error; end
35
-
36
39
  class << self
37
40
  extend T::Sig
38
41
 
39
- sig { params(index: Index, ruby: String, file: String, plugins: T::Array[Deadcode::Plugins::Base]).void }
40
- def index_ruby(index, ruby, file:, plugins: [])
41
- 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: [])
42
68
  visitor = Spoom::Deadcode::Indexer.new(file, ruby, index, plugins: plugins)
43
69
  visitor.visit(node)
44
- rescue SyntaxTree::Parser::ParseError => e
45
- raise ParserError.new("Error while parsing #{file} (#{e.message} at #{e.lineno}:#{e.column})", parent: e)
46
70
  rescue => e
47
71
  raise IndexerError.new("Error while indexing #{file} (#{e.message})", parent: e)
48
72
  end
49
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
+
50
80
  sig { params(index: Index, erb: String, file: String, plugins: T::Array[Deadcode::Plugins::Base]).void }
51
81
  def index_erb(index, erb, file:, plugins: [])
52
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.4"
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.4
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-09-15 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
@@ -137,12 +137,14 @@ files:
137
137
  - lib/spoom.rb
138
138
  - lib/spoom/backtrace_filter/minitest.rb
139
139
  - lib/spoom/cli.rb
140
- - lib/spoom/cli/bump.rb
141
140
  - lib/spoom/cli/config.rb
142
- - lib/spoom/cli/coverage.rb
141
+ - lib/spoom/cli/deadcode.rb
143
142
  - lib/spoom/cli/helper.rb
144
- - lib/spoom/cli/lsp.rb
145
- - 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
146
148
  - lib/spoom/colors.rb
147
149
  - lib/spoom/context.rb
148
150
  - lib/spoom/context/bundle.rb
@@ -166,6 +168,7 @@ files:
166
168
  - lib/spoom/deadcode/location.rb
167
169
  - lib/spoom/deadcode/plugins.rb
168
170
  - lib/spoom/deadcode/plugins/action_mailer.rb
171
+ - lib/spoom/deadcode/plugins/action_mailer_preview.rb
169
172
  - lib/spoom/deadcode/plugins/actionpack.rb
170
173
  - lib/spoom/deadcode/plugins/active_job.rb
171
174
  - lib/spoom/deadcode/plugins/active_model.rb
@@ -185,6 +188,7 @@ files:
185
188
  - lib/spoom/deadcode/reference.rb
186
189
  - lib/spoom/deadcode/remover.rb
187
190
  - lib/spoom/deadcode/send.rb
191
+ - lib/spoom/deadcode/visitor.rb
188
192
  - lib/spoom/file_collector.rb
189
193
  - lib/spoom/file_tree.rb
190
194
  - lib/spoom/printer.rb
@@ -222,7 +226,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
222
226
  - !ruby/object:Gem::Version
223
227
  version: '0'
224
228
  requirements: []
225
- rubygems_version: 3.4.19
229
+ rubygems_version: 3.5.7
226
230
  signing_key:
227
231
  specification_version: 4
228
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
@@ -1,222 +0,0 @@
1
- # typed: true
2
- # frozen_string_literal: true
3
-
4
- require_relative "../coverage"
5
- require_relative "../timeline"
6
-
7
- module Spoom
8
- module Cli
9
- class Coverage < Thor
10
- include Helper
11
-
12
- DATA_DIR = "spoom_data"
13
-
14
- default_task :snapshot
15
-
16
- desc "snapshot", "Run srb tc and display metrics"
17
- option :save, type: :string, lazy_default: DATA_DIR, desc: "Save snapshot data as json"
18
- option :rbi, type: :boolean, default: true, desc: "Include RBI files in metrics"
19
- option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
20
- def snapshot
21
- context = context_requiring_sorbet!
22
- sorbet = options[:sorbet]
23
-
24
- snapshot = Spoom::Coverage.snapshot(context, rbi: options[:rbi], sorbet_bin: sorbet)
25
- snapshot.print
26
-
27
- save_dir = options[:save]
28
- return unless save_dir
29
-
30
- FileUtils.mkdir_p(save_dir)
31
- file = "#{save_dir}/#{snapshot.commit_sha || snapshot.timestamp}.json"
32
- File.write(file, snapshot.to_json)
33
- say("\nSnapshot data saved under `#{file}`")
34
- end
35
-
36
- desc "timeline", "Replay a project and collect metrics"
37
- option :from, type: :string, desc: "From commit date"
38
- option :to, type: :string, default: Time.now.strftime("%F"), desc: "To commit date"
39
- option :save, type: :string, lazy_default: DATA_DIR, desc: "Save snapshot data as json"
40
- option :bundle_install, type: :boolean, desc: "Execute `bundle install` before collecting metrics"
41
- option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
42
- def timeline
43
- context = context_requiring_sorbet!
44
- path = exec_path
45
- sorbet = options[:sorbet]
46
-
47
- ref_before = context.git_current_branch
48
- ref_before = context.git_last_commit&.sha unless ref_before
49
- unless ref_before
50
- say_error("Not in a git repository")
51
- say_error("\nSpoom needs to checkout into your previous commits to build the timeline.", status: nil)
52
- exit(1)
53
- end
54
-
55
- unless context.git_workdir_clean?
56
- say_error("Uncommited changes")
57
- say_error(<<~ERR, status: nil)
58
-
59
- Spoom needs to checkout into your previous commits to build the timeline."
60
-
61
- Please `git commit` or `git stash` your changes then try again
62
- ERR
63
- exit(1)
64
- end
65
-
66
- save_dir = options[:save]
67
- FileUtils.mkdir_p(save_dir) if save_dir
68
-
69
- from = parse_time(options[:from], "--from")
70
- to = parse_time(options[:to], "--to")
71
-
72
- unless from
73
- intro_commit = context.sorbet_intro_commit
74
- intro_commit = T.must(intro_commit) # we know it's in there since in_sorbet_project!
75
- from = intro_commit.time
76
- end
77
-
78
- timeline = Spoom::Timeline.new(context, from, to)
79
- ticks = timeline.ticks
80
-
81
- if ticks.empty?
82
- say_error("No commits to replay, try different `--from` and `--to` options")
83
- exit(1)
84
- end
85
-
86
- ticks.each_with_index do |commit, i|
87
- say("Analyzing commit `#{commit.sha}` - #{commit.time.strftime("%F")} (#{i + 1} / #{ticks.size})")
88
-
89
- context.git_checkout!(ref: commit.sha)
90
-
91
- snapshot = T.let(nil, T.nilable(Spoom::Coverage::Snapshot))
92
- if options[:bundle_install]
93
- Bundler.with_unbundled_env do
94
- next unless bundle_install(path, commit.sha)
95
-
96
- snapshot = Spoom::Coverage.snapshot(context, sorbet_bin: sorbet)
97
- end
98
- else
99
- snapshot = Spoom::Coverage.snapshot(context, sorbet_bin: sorbet)
100
- end
101
- next unless snapshot
102
-
103
- snapshot.print(indent_level: 2)
104
- say("\n")
105
-
106
- next unless save_dir
107
-
108
- file = "#{save_dir}/#{commit.sha}.json"
109
- File.write(file, snapshot.to_json)
110
- say(" Snapshot data saved under `#{file}`\n\n")
111
- end
112
- context.git_checkout!(ref: ref_before)
113
- end
114
-
115
- desc "report", "Produce a typing coverage report"
116
- option :data, type: :string, default: DATA_DIR, desc: "Snapshots JSON data"
117
- option :file,
118
- type: :string,
119
- default: "spoom_report.html",
120
- aliases: :f,
121
- desc: "Save report to file"
122
- option :color_ignore,
123
- type: :string,
124
- default: Spoom::Coverage::D3::COLOR_IGNORE,
125
- desc: "Color used for typed: ignore"
126
- option :color_false,
127
- type: :string,
128
- default: Spoom::Coverage::D3::COLOR_FALSE,
129
- desc: "Color used for typed: false"
130
- option :color_true,
131
- type: :string,
132
- default: Spoom::Coverage::D3::COLOR_TRUE,
133
- desc: "Color used for typed: true"
134
- option :color_strict,
135
- type: :string,
136
- default: Spoom::Coverage::D3::COLOR_STRICT,
137
- desc: "Color used for typed: strict"
138
- option :color_strong,
139
- type: :string,
140
- default: Spoom::Coverage::D3::COLOR_STRONG,
141
- desc: "Color used for typed: strong"
142
- def report
143
- context = context_requiring_sorbet!
144
-
145
- data_dir = options[:data]
146
- files = Dir.glob("#{data_dir}/*.json")
147
- if files.empty?
148
- message_no_data(data_dir)
149
- exit(1)
150
- end
151
-
152
- snapshots = files.sort.map do |file|
153
- json = File.read(file)
154
- Spoom::Coverage::Snapshot.from_json(json)
155
- end.filter(&:commit_timestamp).sort_by!(&:commit_timestamp)
156
-
157
- palette = Spoom::Coverage::D3::ColorPalette.new(
158
- ignore: options[:color_ignore],
159
- false: options[:color_false],
160
- true: options[:color_true],
161
- strict: options[:color_strict],
162
- strong: options[:color_strong],
163
- )
164
-
165
- report = Spoom::Coverage.report(context, snapshots, palette: palette)
166
- file = options[:file]
167
- File.write(file, report.html)
168
- say("Report generated under `#{file}`")
169
- say("\nUse `spoom coverage open` to open it.")
170
- end
171
-
172
- desc "open", "Open the typing coverage report"
173
- def open(file = "spoom_report.html")
174
- unless File.exist?(file)
175
- say_error("No report file to open `#{file}`")
176
- say_error(<<~ERR, status: nil)
177
-
178
- If you already generated a report under another name use #{blue("spoom coverage open PATH")}.
179
-
180
- To generate a report run #{blue("spoom coverage report")}.
181
- ERR
182
- exit(1)
183
- end
184
-
185
- exec("open #{file}")
186
- end
187
-
188
- no_commands do
189
- def parse_time(string, option)
190
- return unless string
191
-
192
- Time.parse(string)
193
- rescue ArgumentError
194
- say_error("Invalid date `#{string}` for option `#{option}` (expected format `YYYY-MM-DD`)")
195
- exit(1)
196
- end
197
-
198
- def bundle_install(path, sha)
199
- opts = {}
200
- opts[:chdir] = path
201
- out, status = Open3.capture2e("bundle install", opts)
202
- unless status.success?
203
- say_error("Can't run `bundle install` for commit `#{sha}`. Skipping snapshot")
204
- say_error(out, status: nil)
205
- return false
206
- end
207
- true
208
- end
209
-
210
- def message_no_data(file)
211
- say_error("No snapshot files found in `#{file}`")
212
- say_error(<<~ERR, status: nil)
213
-
214
- If you already generated snapshot files under another directory use #{blue("spoom coverage report PATH")}.
215
-
216
- To generate snapshot files run #{blue("spoom coverage timeline --save")}.
217
- ERR
218
- end
219
- end
220
- end
221
- end
222
- end