spoom 1.0.8 → 1.1.2

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: 866446fc75971d51c7a71d7ed713cf256f682ffbd0fabfaf97c95b601638876b
4
- data.tar.gz: a38dd3b7859929349c2d84b4119e5f8d8d374c0cf5062adbb3904065a1d6f0f2
3
+ metadata.gz: b9b05f4a851903017a55999c9ba093a08e690af7294145dbe6e4401e1cb919f1
4
+ data.tar.gz: 24db18c1f5f061a275576c5a863ec1c190da06eb1f76c1b420266c5c85818641
5
5
  SHA512:
6
- metadata.gz: 257a78cbdf439ee83e23a823ed39918ada7a1b66786967a77604e5dca2bb825aeb8af2f5fcda6c497686f29f9d0c7dba1d051060d984694f22c0064dcbd8c1a9
7
- data.tar.gz: 4a66b832da23c6237d1dedc16735cce058306ce66327fd2e074d72a481d55412aa7dc8047f0fa25957e1369747478347b77ccf8cd7c85cdb286fcff6a564a412
6
+ metadata.gz: 85f76b4b4ece46d92484dee9063ca001114a5231f6371cdac09d403ce8e23120ccd4bbd8c9e51e6ed8de43b4a0f9db0f0453c21e2bf20ab71cc25108745201e1
7
+ data.tar.gz: fb41d83d4dee426c81fb2d2d0786ea0ebe21c42c3fda3ed907b522f6ee0f9745cc860ba86f01e97a224c334ae6d837e97af94f45bfd4cb4e1eba45c5640dfde2
data/Gemfile CHANGED
@@ -8,6 +8,5 @@ gemspec
8
8
  group(:development) do
9
9
  gem('rubocop-shopify', require: false)
10
10
  gem('rubocop-sorbet', require: false)
11
- gem('byebug')
12
11
  gem('pry-byebug')
13
12
  end
data/README.md CHANGED
@@ -226,6 +226,12 @@ Bump files using a custom instance of Sorbet:
226
226
  $ spoom bump --from false --to true --sorbet /path/to/sorbet/bin
227
227
  ```
228
228
 
229
+ Count the number of type-checking errors if all files were bumped to true:
230
+
231
+ ```
232
+ $ spoom bump --count-errors --dry
233
+ ```
234
+
229
235
  #### Interact with Sorbet LSP mode
230
236
 
231
237
  **Experimental**
data/lib/spoom.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "colorize"
4
5
  require "sorbet-runtime"
5
6
 
6
7
  module Spoom
data/lib/spoom/cli.rb CHANGED
@@ -38,19 +38,29 @@ module Spoom
38
38
  subcommand "tc", Spoom::Cli::Run
39
39
 
40
40
  desc "files", "List all the files typechecked by Sorbet"
41
+ option :tree, type: :boolean, default: true, desc: "Display list as an indented tree"
42
+ option :rbi, type: :boolean, default: true, desc: "Show RBI files"
41
43
  def files
42
44
  in_sorbet_project!
43
45
 
44
46
  path = exec_path
45
- config = Spoom::Sorbet::Config.parse_file(sorbet_config)
47
+ config = sorbet_config
46
48
  files = Spoom::Sorbet.srb_files(config, path: path)
47
49
 
48
- say("Files matching `#{sorbet_config}`:")
50
+ unless options[:rbi]
51
+ files = files.reject { |file| file.end_with?(".rbi") }
52
+ end
53
+
49
54
  if files.empty?
50
- say(" NONE")
51
- else
55
+ say_error("No file matching `#{sorbet_config_file}`")
56
+ exit(1)
57
+ end
58
+
59
+ if options[:tree]
52
60
  tree = FileTree.new(files, strip_prefix: path)
53
- tree.print(colors: options[:color], indent_level: 2)
61
+ tree.print(colors: options[:color], indent_level: 0)
62
+ else
63
+ puts files
54
64
  end
55
65
  end
56
66
 
@@ -24,6 +24,10 @@ module Spoom
24
24
  desc: "Only display what would happen, do not actually change sigils"
25
25
  option :only, type: :string, default: nil, aliases: :o,
26
26
  desc: "Only change specified list (one file by line)"
27
+ option :suggest_bump_command, type: :string,
28
+ desc: "Command to suggest if files can be bumped"
29
+ option :count_errors, type: :boolean, default: false,
30
+ desc: "Count the number of errors if all files were bumped"
27
31
  sig { params(directory: String).void }
28
32
  def bump(directory = ".")
29
33
  in_sorbet_project!
@@ -33,43 +37,61 @@ module Spoom
33
37
  force = options[:force]
34
38
  dry = options[:dry]
35
39
  only = options[:only]
40
+ cmd = options[:suggest_bump_command]
36
41
  exec_path = File.expand_path(self.exec_path)
37
42
 
38
43
  unless Sorbet::Sigils.valid_strictness?(from)
39
- say_error("Invalid strictness #{from} for option --from")
44
+ say_error("Invalid strictness `#{from}` for option `--from`")
40
45
  exit(1)
41
46
  end
42
47
 
43
48
  unless Sorbet::Sigils.valid_strictness?(to)
44
- say_error("Invalid strictness #{to} for option --to")
49
+ say_error("Invalid strictness `#{to}` for option `--to`")
45
50
  exit(1)
46
51
  end
47
52
 
53
+ if options[:count_errors] && !dry
54
+ say_error("`--count-errors` can only be used with `--dry`")
55
+ exit(1)
56
+ end
57
+
58
+ say("Checking files...")
59
+
48
60
  directory = File.expand_path(directory)
49
61
  files_to_bump = Sorbet::Sigils.files_with_sigil_strictness(directory, from)
50
62
 
63
+ files_from_config = config_files(path: exec_path)
64
+ files_to_bump.select! { |file| files_from_config.include?(file) }
65
+
51
66
  if only
52
67
  list = File.read(only).lines.map { |file| File.expand_path(file.strip) }
53
68
  files_to_bump.select! { |file| list.include?(File.expand_path(file)) }
54
69
  end
55
70
 
71
+ say("\n")
72
+
56
73
  if files_to_bump.empty?
57
- $stderr.puts("No file to bump from #{from} to #{to}")
74
+ say("No file to bump from `#{from}` to `#{to}`")
58
75
  exit(0)
59
76
  end
60
77
 
61
78
  Sorbet::Sigils.change_sigil_in_files(files_to_bump, to)
62
79
 
63
80
  if force
64
- print_changes(files_to_bump, from: from, to: to, dry: dry, path: exec_path)
81
+ print_changes(files_to_bump, command: cmd, from: from, to: to, dry: dry, path: exec_path)
65
82
  undo_changes(files_to_bump, from) if dry
66
83
  exit(files_to_bump.empty?)
67
84
  end
68
85
 
69
- output, no_errors = Sorbet.srb_tc(path: exec_path, capture_err: true, sorbet_bin: options[:sorbet])
86
+ output, no_errors = Sorbet.srb_tc(
87
+ "--no-error-sections",
88
+ path: exec_path,
89
+ capture_err: true,
90
+ sorbet_bin: options[:sorbet]
91
+ )
70
92
 
71
93
  if no_errors
72
- print_changes(files_to_bump, from: from, to: to, dry: dry, path: exec_path)
94
+ print_changes(files_to_bump, command: cmd, from: from, to: to, dry: dry, path: exec_path)
73
95
  undo_changes(files_to_bump, from) if dry
74
96
  exit(files_to_bump.empty?)
75
97
  end
@@ -85,33 +107,45 @@ module Spoom
85
107
 
86
108
  undo_changes(files_with_errors, from)
87
109
 
110
+ say("Found #{errors.length} type checking error#{'s' if errors.length > 1}") if options[:count_errors]
111
+
88
112
  files_changed = files_to_bump - files_with_errors
89
- print_changes(files_changed, from: from, to: to, dry: dry, path: exec_path)
113
+ print_changes(files_changed, command: cmd, from: from, to: to, dry: dry, path: exec_path)
90
114
  undo_changes(files_to_bump, from) if dry
91
115
  exit(files_changed.empty?)
92
116
  end
93
117
 
94
118
  no_commands do
95
- def print_changes(files, from: "false", to: "true", dry: false, path: File.expand_path("."))
119
+ def print_changes(files, command:, from: "false", to: "true", dry: false, path: File.expand_path("."))
96
120
  if files.empty?
97
- $stderr.puts("No file to bump from #{from} to #{to}")
121
+ say("No file to bump from `#{from}` to `#{to}`")
98
122
  return
99
123
  end
100
- $stderr.write(dry ? "Can bump" : "Bumped")
101
- $stderr.write(" #{files.size} file#{'s' if files.size > 1}")
102
- $stderr.puts(" from #{from} to #{to}:")
124
+ message = StringIO.new
125
+ message << (dry ? "Can bump" : "Bumped")
126
+ message << " `#{files.size}` file#{'s' if files.size > 1}"
127
+ message << " from `#{from}` to `#{to}`:"
128
+ say(message.string)
103
129
  files.each do |file|
104
130
  file_path = Pathname.new(file).relative_path_from(path)
105
- $stderr.puts(" + #{file_path}")
131
+ say(" + #{file_path}")
106
132
  end
107
- if dry
108
- $stderr.puts("\nRun `spoom bump --from #{from} --to #{to}` to bump them")
133
+ if dry && command
134
+ say("\nRun `#{command}` to bump them")
135
+ elsif dry
136
+ say("\nRun `spoom bump --from #{from} --to #{to}` locally then `commit the changes` and `push them`")
109
137
  end
110
138
  end
111
139
 
112
140
  def undo_changes(files, from_strictness)
113
141
  Sorbet::Sigils.change_sigil_in_files(files, from_strictness)
114
142
  end
143
+
144
+ def config_files(path: ".")
145
+ config = sorbet_config
146
+ files = Sorbet.srb_files(config, path: path)
147
+ files.map { |file| File.expand_path(file) }
148
+ end
115
149
  end
116
150
  end
117
151
  end
@@ -14,9 +14,9 @@ module Spoom
14
14
  desc "show", "Show Sorbet config"
15
15
  def show
16
16
  in_sorbet_project!
17
- config = Spoom::Sorbet::Config.parse_file(sorbet_config)
17
+ config = sorbet_config
18
18
 
19
- say("Found Sorbet config at `#{sorbet_config}`.")
19
+ say("Found Sorbet config at `#{sorbet_config_file}`.")
20
20
 
21
21
  say("\nPaths typechecked:")
22
22
  if config.paths.empty?
@@ -15,7 +15,7 @@ module Spoom
15
15
 
16
16
  desc "snapshot", "Run srb tc and display metrics"
17
17
  option :save, type: :string, lazy_default: DATA_DIR, desc: "Save snapshot data as json"
18
- option :rbi, type: :boolean, default: true, desc: "Exclude RBI files from metrics"
18
+ option :rbi, type: :boolean, default: true, desc: "Include RBI files in metrics"
19
19
  option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
20
20
  def snapshot
21
21
  in_sorbet_project!
@@ -30,7 +30,7 @@ module Spoom
30
30
  FileUtils.mkdir_p(save_dir)
31
31
  file = "#{save_dir}/#{snapshot.commit_sha || snapshot.timestamp}.json"
32
32
  File.write(file, snapshot.to_json)
33
- puts "\nSnapshot data saved under #{file}"
33
+ say("\nSnapshot data saved under `#{file}`")
34
34
  end
35
35
 
36
36
  desc "timeline", "Replay a project and collect metrics"
@@ -44,17 +44,22 @@ module Spoom
44
44
  path = exec_path
45
45
  sorbet = options[:sorbet]
46
46
 
47
- sha_before = Spoom::Git.last_commit(path: path)
48
- unless sha_before
47
+ ref_before = Spoom::Git.current_branch
48
+ ref_before = Spoom::Git.last_commit(path: path) unless ref_before
49
+ unless ref_before
49
50
  say_error("Not in a git repository")
50
- $stderr.puts "\nSpoom needs to checkout into your previous commits to build the timeline."
51
+ say_error("\nSpoom needs to checkout into your previous commits to build the timeline.", status: nil)
51
52
  exit(1)
52
53
  end
53
54
 
54
55
  unless Spoom::Git.workdir_clean?(path: path)
55
56
  say_error("Uncommited changes")
56
- $stderr.puts "\nSpoom needs to checkout into your previous commits to build the timeline."
57
- $stderr.puts "\nPlease git commit or git stash your changes then try again."
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
58
63
  exit(1)
59
64
  end
60
65
 
@@ -74,19 +79,19 @@ module Spoom
74
79
  ticks = timeline.ticks
75
80
 
76
81
  if ticks.empty?
77
- say_error("No commits to replay, try different --from and --to options")
82
+ say_error("No commits to replay, try different `--from` and `--to` options")
78
83
  exit(1)
79
84
  end
80
85
 
81
86
  ticks.each_with_index do |sha, i|
82
87
  date = Spoom::Git.commit_time(sha, path: path)
83
- puts "Analyzing commit #{sha} - #{date&.strftime('%F')} (#{i + 1} / #{ticks.size})"
88
+ say("Analyzing commit `#{sha}` - #{date&.strftime('%F')} (#{i + 1} / #{ticks.size})")
84
89
 
85
90
  Spoom::Git.checkout(sha, path: path)
86
91
 
87
92
  snapshot = T.let(nil, T.nilable(Spoom::Coverage::Snapshot))
88
93
  if options[:bundle_install]
89
- Bundler.with_clean_env do
94
+ Bundler.with_unbundled_env do
90
95
  next unless bundle_install(path, sha)
91
96
  snapshot = Spoom::Coverage.snapshot(path: path, sorbet_bin: sorbet)
92
97
  end
@@ -96,14 +101,14 @@ module Spoom
96
101
  next unless snapshot
97
102
 
98
103
  snapshot.print(indent_level: 2)
99
- puts "\n"
104
+ say("\n")
100
105
 
101
106
  next unless save_dir
102
107
  file = "#{save_dir}/#{sha}.json"
103
108
  File.write(file, snapshot.to_json)
104
- puts " Snapshot data saved under #{file}\n\n"
109
+ say(" Snapshot data saved under `#{file}`\n\n")
105
110
  end
106
- Spoom::Git.checkout(sha_before, path: path)
111
+ Spoom::Git.checkout(ref_before, path: path)
107
112
  end
108
113
 
109
114
  desc "report", "Produce a typing coverage report"
@@ -146,20 +151,20 @@ module Spoom
146
151
  report = Spoom::Coverage.report(snapshots, palette: palette, path: exec_path)
147
152
  file = options[:file]
148
153
  File.write(file, report.html)
149
- puts "Report generated under #{file}"
150
- puts "\nUse #{colorize('spoom coverage open', :blue)} to open it."
154
+ say("Report generated under `#{file}`")
155
+ say("\nUse `spoom coverage open` to open it.")
151
156
  end
152
157
 
153
158
  desc "open", "Open the typing coverage report"
154
159
  def open(file = "spoom_report.html")
155
160
  unless File.exist?(file)
156
- say_error("No report file to open #{colorize(file, :blue)}")
157
- $stderr.puts <<~OUT
161
+ say_error("No report file to open `#{file}`")
162
+ say_error(<<~ERR, status: nil)
158
163
 
159
- If you already generated a report under another name use #{colorize('spoom coverage open PATH', :blue)}.
164
+ If you already generated a report under another name use #{blue('spoom coverage open PATH')}.
160
165
 
161
- To generate a report run #{colorize('spoom coverage report', :blue)}.
162
- OUT
166
+ To generate a report run #{blue('spoom coverage report')}.
167
+ ERR
163
168
  exit(1)
164
169
  end
165
170
 
@@ -171,7 +176,7 @@ module Spoom
171
176
  return nil unless string
172
177
  Time.parse(string)
173
178
  rescue ArgumentError
174
- say_error("Invalid date `#{string}` for option #{option} (expected format YYYY-MM-DD)")
179
+ say_error("Invalid date `#{string}` for option `#{option}` (expected format `YYYY-MM-DD`)")
175
180
  exit(1)
176
181
  end
177
182
 
@@ -180,21 +185,21 @@ module Spoom
180
185
  opts[:chdir] = path
181
186
  out, status = Open3.capture2e("bundle install", opts)
182
187
  unless status.success?
183
- say_error("Can't run `bundle install` for commit #{sha}. Skipping snapshot")
184
- $stderr.puts(out)
188
+ say_error("Can't run `bundle install` for commit `#{sha}`. Skipping snapshot")
189
+ say_error(out, status: nil)
185
190
  return false
186
191
  end
187
192
  true
188
193
  end
189
194
 
190
195
  def message_no_data(file)
191
- say_error("No snapshot files found in #{colorize(file, :blue)}")
192
- $stderr.puts <<~OUT
196
+ say_error("No snapshot files found in `#{file}`")
197
+ say_error(<<~ERR, status: nil)
193
198
 
194
- If you already generated snapshot files under another directory use #{colorize('spoom coverage report PATH', :blue)}.
199
+ If you already generated snapshot files under another directory use #{blue('spoom coverage report PATH')}.
195
200
 
196
- To generate snapshot files run #{colorize('spoom coverage timeline --save-dir spoom_data', :blue)}.
197
- OUT
201
+ To generate snapshot files run #{blue('spoom coverage timeline --save-dir spoom_data')}.
202
+ ERR
198
203
  end
199
204
  end
200
205
  end
@@ -9,18 +9,37 @@ module Spoom
9
9
  module Cli
10
10
  module Helper
11
11
  extend T::Sig
12
+ extend T::Helpers
12
13
  include Thor::Shell
13
14
 
15
+ requires_ancestor Thor
16
+
17
+ # Print `message` on `$stdout`
18
+ sig { params(message: String).void }
19
+ def say(message)
20
+ buffer = StringIO.new
21
+ buffer << highlight(message)
22
+ buffer << "\n" unless message.end_with?("\n")
23
+
24
+ $stdout.print(buffer.string)
25
+ $stdout.flush
26
+ end
27
+
14
28
  # Print `message` on `$stderr`
15
29
  #
16
30
  # The message is prefixed by a status (default: `Error`).
17
- sig { params(message: String, status: String).void }
18
- def say_error(message, status = "Error")
19
- status = set_color(status, :red)
20
-
31
+ sig do
32
+ params(
33
+ message: String,
34
+ status: T.nilable(String),
35
+ nl: T::Boolean
36
+ ).void
37
+ end
38
+ def say_error(message, status: "Error", nl: true)
21
39
  buffer = StringIO.new
22
- buffer << "#{status}: #{message}"
23
- buffer << "\n" unless message.end_with?("\n")
40
+ buffer << "#{red(status)}: " if status
41
+ buffer << highlight(message)
42
+ buffer << "\n" if nl && !message.end_with?("\n")
24
43
 
25
44
  $stderr.print(buffer.string)
26
45
  $stderr.flush
@@ -29,7 +48,7 @@ module Spoom
29
48
  # Is `spoom` ran inside a project with a `sorbet/config` file?
30
49
  sig { returns(T::Boolean) }
31
50
  def in_sorbet_project?
32
- File.file?(sorbet_config)
51
+ File.file?(sorbet_config_file)
33
52
  end
34
53
 
35
54
  # Enforce that `spoom` is ran inside a project with a `sorbet/config` file
@@ -39,9 +58,9 @@ module Spoom
39
58
  def in_sorbet_project!
40
59
  unless in_sorbet_project?
41
60
  say_error(
42
- "not in a Sorbet project (#{colorize(sorbet_config, :yellow)} not found)\n\n" \
61
+ "not in a Sorbet project (`#{sorbet_config_file}` not found)\n\n" \
43
62
  "When running spoom from another path than the project's root, " \
44
- "use #{colorize('--path PATH', :blue)} to specify the path to the root."
63
+ "use `--path PATH` to specify the path to the root."
45
64
  )
46
65
  Kernel.exit(1)
47
66
  end
@@ -50,18 +69,51 @@ module Spoom
50
69
  # Return the path specified through `--path`
51
70
  sig { returns(String) }
52
71
  def exec_path
53
- T.unsafe(self).options[:path] # TODO: requires_ancestor
72
+ options[:path]
54
73
  end
55
74
 
56
75
  sig { returns(String) }
57
- def sorbet_config
76
+ def sorbet_config_file
58
77
  Pathname.new("#{exec_path}/#{Spoom::Sorbet::CONFIG_PATH}").cleanpath.to_s
59
78
  end
60
79
 
80
+ sig { returns(Sorbet::Config) }
81
+ def sorbet_config
82
+ Sorbet::Config.parse_file(sorbet_config_file)
83
+ end
84
+
85
+ # Colors
86
+
87
+ # Color used to highlight expressions in backticks
88
+ HIGHLIGHT_COLOR = :blue
89
+
61
90
  # Is the `--color` option true?
62
91
  sig { returns(T::Boolean) }
63
92
  def color?
64
- T.unsafe(self).options[:color] # TODO: requires_ancestor
93
+ options[:color]
94
+ end
95
+
96
+ sig { params(string: String).returns(String) }
97
+ def highlight(string)
98
+ return string unless color?
99
+
100
+ res = StringIO.new
101
+ word = StringIO.new
102
+ in_ticks = T.let(false, T::Boolean)
103
+ string.chars.each do |c|
104
+ if c == '`' && !in_ticks
105
+ in_ticks = true
106
+ elsif c == '`' && in_ticks
107
+ in_ticks = false
108
+ res << colorize(word.string, HIGHLIGHT_COLOR)
109
+ word = StringIO.new
110
+ elsif in_ticks
111
+ word << c
112
+ else
113
+ res << c
114
+ end
115
+ end
116
+ res.string
65
117
  end
66
118
 
67
119
  # Colorize a string if `color?`
@@ -70,6 +122,31 @@ module Spoom
70
122
  return string unless color?
71
123
  string.colorize(color)
72
124
  end
125
+
126
+ sig { params(string: String).returns(String) }
127
+ def blue(string)
128
+ colorize(string, :blue)
129
+ end
130
+
131
+ sig { params(string: String).returns(String) }
132
+ def gray(string)
133
+ colorize(string, :light_black)
134
+ end
135
+
136
+ sig { params(string: String).returns(String) }
137
+ def green(string)
138
+ colorize(string, :green)
139
+ end
140
+
141
+ sig { params(string: String).returns(String) }
142
+ def red(string)
143
+ colorize(string, :red)
144
+ end
145
+
146
+ sig { params(string: String).returns(String) }
147
+ def yellow(string)
148
+ colorize(string, :yellow)
149
+ end
73
150
  end
74
151
  end
75
152
  end
data/lib/spoom/cli/lsp.rb CHANGED
@@ -28,7 +28,7 @@ module Spoom
28
28
  Dir["**/*.rb"].each do |file|
29
29
  res = client.document_symbols(to_uri(file))
30
30
  next if res.empty?
31
- puts "Symbols from `#{file}`:"
31
+ say("Symbols from `#{file}`:")
32
32
  printer.print_objects(res)
33
33
  end
34
34
  end
@@ -39,11 +39,11 @@ module Spoom
39
39
  def hover(file, line, col)
40
40
  run do |client|
41
41
  res = client.hover(to_uri(file), line.to_i, col.to_i)
42
- say "Hovering `#{file}:#{line}:#{col}`:"
42
+ say("Hovering `#{file}:#{line}:#{col}`:")
43
43
  if res
44
44
  symbol_printer.print_object(res)
45
45
  else
46
- puts "<no data>"
46
+ say("<no data>")
47
47
  end
48
48
  end
49
49
  end
@@ -53,7 +53,7 @@ module Spoom
53
53
  def defs(file, line, col)
54
54
  run do |client|
55
55
  res = client.definitions(to_uri(file), line.to_i, col.to_i)
56
- puts "Definitions for `#{file}:#{line}:#{col}`:"
56
+ say("Definitions for `#{file}:#{line}:#{col}`:")
57
57
  symbol_printer.print_list(res)
58
58
  end
59
59
  end
@@ -63,7 +63,7 @@ module Spoom
63
63
  def find(query)
64
64
  run do |client|
65
65
  res = client.symbols(query).reject { |symbol| symbol.location.uri.start_with?("https") }
66
- puts "Symbols matching `#{query}`:"
66
+ say("Symbols matching `#{query}`:")
67
67
  symbol_printer.print_objects(res)
68
68
  end
69
69
  end
@@ -73,7 +73,7 @@ module Spoom
73
73
  def symbols(file)
74
74
  run do |client|
75
75
  res = client.document_symbols(to_uri(file))
76
- puts "Symbols from `#{file}`:"
76
+ say("Symbols from `#{file}`:")
77
77
  symbol_printer.print_objects(res)
78
78
  end
79
79
  end
@@ -83,7 +83,7 @@ module Spoom
83
83
  def refs(file, line, col)
84
84
  run do |client|
85
85
  res = client.references(to_uri(file), line.to_i, col.to_i)
86
- puts "References to `#{file}:#{line}:#{col}`:"
86
+ say("References to `#{file}:#{line}:#{col}`:")
87
87
  symbol_printer.print_list(res)
88
88
  end
89
89
  end
@@ -93,7 +93,7 @@ module Spoom
93
93
  def sigs(file, line, col)
94
94
  run do |client|
95
95
  res = client.signatures(to_uri(file), line.to_i, col.to_i)
96
- puts "Signature for `#{file}:#{line}:#{col}`:"
96
+ say("Signature for `#{file}:#{line}:#{col}`:")
97
97
  symbol_printer.print_list(res)
98
98
  end
99
99
  end
@@ -103,7 +103,7 @@ module Spoom
103
103
  def types(file, line, col)
104
104
  run do |client|
105
105
  res = client.type_definitions(to_uri(file), line.to_i, col.to_i)
106
- say "Type for `#{file}:#{line}:#{col}`:"
106
+ say("Type for `#{file}:#{line}:#{col}`:")
107
107
  symbol_printer.print_list(res)
108
108
  end
109
109
  end
@@ -137,7 +137,7 @@ module Spoom
137
137
  rescue Spoom::LSP::Error::Diagnostics => err
138
138
  say_error("Sorbet returned typechecking errors for `#{symbol_printer.clean_uri(err.uri)}`")
139
139
  err.diagnostics.each do |d|
140
- say_error("#{d.message} (#{d.code})", " #{d.range}")
140
+ say_error("#{d.message} (#{d.code})", status: " #{d.range}")
141
141
  end
142
142
  exit(1)
143
143
  rescue Spoom::LSP::Error::BadHeaders => err
data/lib/spoom/cli/run.rb CHANGED
@@ -17,7 +17,7 @@ module Spoom
17
17
  desc "tc", "Run `srb tc`"
18
18
  option :limit, type: :numeric, aliases: :l, desc: "Limit displayed errors"
19
19
  option :code, type: :numeric, aliases: :c, desc: "Filter displayed errors by code"
20
- option :sort, type: :string, aliases: :s, desc: "Sort errors", enum: SORT_ENUM, lazy_default: SORT_LOC
20
+ option :sort, type: :string, aliases: :s, desc: "Sort errors", enum: SORT_ENUM, default: SORT_LOC
21
21
  option :format, type: :string, aliases: :f, desc: "Format line output"
22
22
  option :uniq, type: :boolean, aliases: :u, desc: "Remove duplicated lines"
23
23
  option :count, type: :boolean, default: true, desc: "Show errors count"
@@ -36,13 +36,13 @@ module Spoom
36
36
 
37
37
  unless limit || code || sort
38
38
  output, status = T.unsafe(Spoom::Sorbet).srb_tc(*arg, path: path, capture_err: false, sorbet_bin: sorbet)
39
- $stderr.print(output)
39
+ say_error(output, status: nil, nl: false)
40
40
  exit(status)
41
41
  end
42
42
 
43
43
  output, status = T.unsafe(Spoom::Sorbet).srb_tc(*arg, path: path, capture_err: true, sorbet_bin: sorbet)
44
44
  if status
45
- $stderr.print(output)
45
+ say_error(output, status: nil, nl: false)
46
46
  exit(0)
47
47
  end
48
48
 
@@ -65,14 +65,14 @@ module Spoom
65
65
  lines = lines.uniq if uniq
66
66
 
67
67
  lines.each do |line|
68
- $stderr.puts line
68
+ say_error(line, status: nil)
69
69
  end
70
70
 
71
71
  if count
72
72
  if errors_count == errors.size
73
- $stderr.puts "Errors: #{errors_count}"
73
+ say_error("Errors: #{errors_count}", status: nil)
74
74
  else
75
- $stderr.puts "Errors: #{errors.size} shown, #{errors_count} total"
75
+ say_error("Errors: #{errors.size} shown, #{errors_count} total", status: nil)
76
76
  end
77
77
  end
78
78
 
@@ -82,7 +82,7 @@ module Spoom
82
82
  no_commands do
83
83
  def format_error(error, format)
84
84
  line = format
85
- line = line.gsub(/%C/, colorize(error.code.to_s, :yellow))
85
+ line = line.gsub(/%C/, yellow(error.code.to_s))
86
86
  line = line.gsub(/%F/, error.file)
87
87
  line = line.gsub(/%L/, error.line.to_s)
88
88
  line = line.gsub(/%M/, colorize_message(error.message))
@@ -21,6 +21,9 @@ module Spoom
21
21
 
22
22
  metrics = Spoom::Sorbet.srb_metrics(
23
23
  "--no-config",
24
+ "--no-error-sections",
25
+ "--no-error-count",
26
+ "--isolate-error-code=0",
24
27
  new_config.options_string,
25
28
  path: path,
26
29
  capture_err: true,
@@ -54,6 +57,9 @@ module Spoom
54
57
  snapshot.version_static = Spoom::Sorbet.version_from_gemfile_lock(gem: "sorbet-static", path: path)
55
58
  snapshot.version_runtime = Spoom::Sorbet.version_from_gemfile_lock(gem: "sorbet-runtime", path: path)
56
59
 
60
+ files = Spoom::Sorbet.srb_files(new_config, path: path)
61
+ snapshot.rbi_files = files.count { |file| file.end_with?(".rbi") }
62
+
57
63
  snapshot
58
64
  end
59
65
 
@@ -81,8 +87,15 @@ module Spoom
81
87
  def self.sigils_tree(path: ".")
82
88
  config = sorbet_config(path: path)
83
89
  files = Sorbet.srb_files(config, path: path)
84
- files.select! { |file| file =~ /\.rb$/ }
90
+
91
+ extensions = config.allowed_extensions
92
+ extensions = [".rb"] if extensions.empty?
93
+ extensions -= [".rbi"]
94
+
95
+ pattern = /\.(#{Regexp.union(extensions.map { |ext| ext[1..-1] })})$/
96
+ files.select! { |file| file =~ pattern }
85
97
  files.reject! { |file| file =~ %r{/test/} }
98
+
86
99
  FileTree.new(files, strip_prefix: path)
87
100
  end
88
101
  end
@@ -36,10 +36,28 @@ module Spoom
36
36
  pointer-events: none;
37
37
  }
38
38
 
39
+ .area {
40
+ fill-opacity: 0.5;
41
+ }
42
+
43
+ .line {
44
+ stroke-width: 2;
45
+ fill: transparent;
46
+ }
47
+
48
+ .dot {
49
+ r: 2;
50
+ fill: #888;
51
+ }
52
+
39
53
  .inverted .grid line {
40
54
  stroke: #777;
41
55
  }
42
56
 
57
+ .inverted .area {
58
+ fill-opacity: 0.9;
59
+ }
60
+
43
61
  .inverted .axis text {
44
62
  fill: #fff;
45
63
  }
@@ -47,6 +65,10 @@ module Spoom
47
65
  .inverted .axis line {
48
66
  stroke: #fff;
49
67
  }
68
+
69
+ .inverted .dot {
70
+ fill: #fff;
71
+ }
50
72
  CSS
51
73
  end
52
74
 
@@ -170,7 +192,6 @@ module Spoom
170
192
  .y1((d) => yScale_#{id}(#{y}))
171
193
  .curve(d3.#{curve}))
172
194
  .attr("fill", "#{color}")
173
- .attr("fill-opacity", 0.5)
174
195
  HTML
175
196
  end
176
197
 
@@ -185,8 +206,6 @@ module Spoom
185
206
  .y((d) => yScale_#{id}(#{y}))
186
207
  .curve(d3.#{curve}))
187
208
  .attr("stroke", "#{color}")
188
- .attr("stroke-width", 3)
189
- .attr("fill", "transparent")
190
209
  HTML
191
210
  end
192
211
 
@@ -198,10 +217,8 @@ module Spoom
198
217
  .enter()
199
218
  .append("circle")
200
219
  .attr("class", "dot")
201
- .attr("r", 3)
202
220
  .attr("cx", (d) => xScale_#{id}(parseDate(d.timestamp)))
203
221
  .attr("cy", (d, i) => yScale_#{id}(#{y}))
204
- .attr("fill", "#aaa")
205
222
  .on("mouseover", (d) => tooltip.style("opacity", 1))
206
223
  .on("mousemove", tooltip_#{id})
207
224
  .on("mouseleave", (d) => tooltip.style("opacity", 0));
@@ -381,18 +398,15 @@ module Spoom
381
398
  layer.append("path")
382
399
  .attr("class", "area")
383
400
  .attr("d", area_#{id})
384
- .attr("fill", (d) => strictnessColor(d.key))
385
- .attr("fill-opacity", 0.9)
401
+ .attr("fill", (d) => #{color})
386
402
 
387
403
  svg_#{id}.selectAll("circle")
388
404
  .data(points_#{id})
389
405
  .enter()
390
406
  .append("circle")
391
407
  .attr("class", "dot")
392
- .attr("r", 2)
393
408
  .attr("cx", (d) => xScale_#{id}(parseDate(#{y})))
394
409
  .attr("cy", (d, i) => yScale_#{id}(d[1]))
395
- .attr("fill", "#fff")
396
410
  .on("mouseover", (d) => tooltip.style("opacity", 1))
397
411
  .on("mousemove", tooltip_#{id})
398
412
  .on("mouseleave", (d) => tooltip.style("opacity", 0));
@@ -480,6 +494,129 @@ module Spoom
480
494
  JS
481
495
  end
482
496
  end
497
+
498
+ class RBIs < Stacked
499
+ extend T::Sig
500
+
501
+ sig { params(id: String, snapshots: T::Array[Snapshot]).void }
502
+ def initialize(id, snapshots)
503
+ keys = ['rbis', 'files']
504
+ data = snapshots.map do |snapshot|
505
+ {
506
+ timestamp: snapshot.commit_timestamp,
507
+ commit: snapshot.commit_sha,
508
+ total: snapshot.files,
509
+ values: { files: snapshot.files - snapshot.rbi_files, rbis: snapshot.rbi_files },
510
+ }
511
+ end
512
+ super(id, data, keys)
513
+ end
514
+
515
+ sig { override.returns(String) }
516
+ def tooltip
517
+ <<~JS
518
+ function tooltip_#{id}(d) {
519
+ moveTooltip(d)
520
+ .html("commit <b>" + d.data.commit + "</b><br>"
521
+ + d3.timeFormat("%y/%m/%d")(parseDate(d.data.timestamp)) + "<br><br>"
522
+ + "Files: <b>" + d.data.values.files + "</b><br>"
523
+ + "RBIs: <b>" + d.data.values.rbis + "</b><br><br>"
524
+ + "Total: <b>" + d.data.total + "</b>")
525
+ }
526
+ JS
527
+ end
528
+
529
+ sig { override.returns(String) }
530
+ def script
531
+ <<~JS
532
+ #{tooltip}
533
+
534
+ var data_#{id} = #{@data.to_json};
535
+ var keys_#{id} = #{T.unsafe(@keys).to_json};
536
+
537
+ var stack_#{id} = d3.stack()
538
+ .keys(keys_#{id})
539
+ .value((d, key) => d.values[key]);
540
+
541
+ var layers_#{id} = stack_#{id}(data_#{id});
542
+
543
+ var points_#{id} = []
544
+ layers_#{id}.forEach(function(d) {
545
+ d.forEach(function(p) {
546
+ p.key = d.key
547
+ points_#{id}.push(p);
548
+ });
549
+ })
550
+
551
+ function draw_#{id}() {
552
+ var width_#{id} = document.getElementById("#{id}").clientWidth;
553
+ var height_#{id} = 200;
554
+
555
+ d3.select("##{id}").selectAll("*").remove()
556
+
557
+ var svg_#{id} = d3.select("##{id}")
558
+ .attr("width", width_#{id})
559
+ .attr("height", height_#{id});
560
+
561
+ #{plot}
562
+ }
563
+
564
+ draw_#{id}();
565
+ window.addEventListener("resize", draw_#{id});
566
+ JS
567
+ end
568
+
569
+ sig { override.params(y: String, color: String, curve: String).returns(String) }
570
+ def line(y:, color: 'strictnessColor(d.key)', curve: 'curveCatmullRom.alpha(1)')
571
+ <<~JS
572
+ var area_#{id} = d3.area()
573
+ .x((d) => xScale_#{id}(parseDate(#{y})))
574
+ .y0((d) => yScale_#{id}(d[0]))
575
+ .y1((d) => yScale_#{id}(d[1]))
576
+ .curve(d3.#{curve});
577
+
578
+ var layer = svg_#{id}.selectAll(".layer")
579
+ .data(layers_#{id})
580
+ .enter().append("g")
581
+ .attr("class", "layer")
582
+
583
+ layer.append("path")
584
+ .attr("class", "area")
585
+ .attr("d", area_#{id})
586
+ .attr("fill", (d) => #{color})
587
+
588
+ layer.append("path")
589
+ .attr("class", "line")
590
+ .attr("d", d3.line()
591
+ .x((d) => xScale_#{id}(parseDate(#{y})))
592
+ .y((d, i) => yScale_#{id}(d[1]))
593
+ .curve(d3.#{curve}))
594
+ .attr("stroke", (d) => #{color})
595
+
596
+ svg_#{id}.selectAll("circle")
597
+ .data(points_#{id})
598
+ .enter()
599
+ .append("circle")
600
+ .attr("class", "dot")
601
+ .attr("cx", (d) => xScale_#{id}(parseDate(#{y})))
602
+ .attr("cy", (d, i) => yScale_#{id}(d[1]))
603
+ .on("mouseover", (d) => tooltip.style("opacity", 1))
604
+ .on("mousemove", tooltip_#{id})
605
+ .on("mouseleave", (d) => tooltip.style("opacity", 0));
606
+ JS
607
+ end
608
+
609
+ sig { override.returns(String) }
610
+ def plot
611
+ <<~JS
612
+ #{x_scale}
613
+ #{y_scale(min: '0', max: "d3.max(data_#{id}, (d) => d.total + 10)", ticks: 'tickValues([0, 25, 50, 75, 100])')}
614
+ #{line(y: 'd.data.timestamp', color: "d.key == 'rbis' ? '#8673ff' : '#007bff'")}
615
+ #{x_ticks}
616
+ #{y_ticks(ticks: 'tickValues([25, 50, 75])', format: 'd', padding: 20)}
617
+ JS
618
+ end
619
+ end
483
620
  end
484
621
  end
485
622
  end
@@ -194,6 +194,15 @@ module Spoom
194
194
  end
195
195
  end
196
196
 
197
+ class RBIs < Timeline
198
+ extend T::Sig
199
+
200
+ sig { params(snapshots: T::Array[Coverage::Snapshot], title: String).void }
201
+ def initialize(snapshots:, title: "RBIs Timeline")
202
+ super(title: title, timeline: D3::Timeline::RBIs.new("timeline_rbis", snapshots))
203
+ end
204
+ end
205
+
197
206
  class Versions < Timeline
198
207
  extend T::Sig
199
208
 
@@ -298,6 +307,7 @@ module Spoom
298
307
  cards << Cards::Timeline::Sigils.new(snapshots: snapshots)
299
308
  cards << Cards::Timeline::Calls.new(snapshots: snapshots)
300
309
  cards << Cards::Timeline::Sigs.new(snapshots: snapshots)
310
+ cards << Cards::Timeline::RBIs.new(snapshots: snapshots)
301
311
  cards << Cards::Timeline::Versions.new(snapshots: snapshots)
302
312
  cards << Cards::Timeline::Runtimes.new(snapshots: snapshots)
303
313
  cards << Cards::SorbetIntro.new(sorbet_intro_commit: sorbet_intro_commit, sorbet_intro_date: sorbet_intro_date)
@@ -13,6 +13,7 @@ module Spoom
13
13
  prop :commit_sha, T.nilable(String), default: nil
14
14
  prop :commit_timestamp, T.nilable(Integer), default: nil
15
15
  prop :files, Integer, default: 0
16
+ prop :rbi_files, Integer, default: 0
16
17
  prop :modules, Integer, default: 0
17
18
  prop :classes, Integer, default: 0
18
19
  prop :singleton_classes, Integer, default: 0
@@ -46,6 +47,7 @@ module Spoom
46
47
  snapshot.commit_sha = obj.fetch("commit_sha", nil)
47
48
  snapshot.commit_timestamp = obj.fetch("commit_timestamp", nil)
48
49
  snapshot.files = obj.fetch("files", 0)
50
+ snapshot.rbi_files = obj.fetch("rbi_files", 0)
49
51
  snapshot.modules = obj.fetch("modules", 0)
50
52
  snapshot.classes = obj.fetch("classes", 0)
51
53
  snapshot.singleton_classes = obj.fetch("singleton_classes", 0)
@@ -86,7 +88,7 @@ module Spoom
86
88
  end
87
89
  printl("Content:")
88
90
  indent
89
- printl("files: #{snapshot.files}")
91
+ printl("files: #{snapshot.files} (including #{snapshot.rbi_files} RBIs)")
90
92
  printl("modules: #{snapshot.modules}")
91
93
  printl("classes: #{snapshot.classes - snapshot.singleton_classes}")
92
94
  printl("methods: #{methods}")
data/lib/spoom/git.rb CHANGED
@@ -14,11 +14,12 @@ module Spoom
14
14
  return "", "Error: `#{path}` is not a directory.", false unless File.directory?(path)
15
15
  opts = {}
16
16
  opts[:chdir] = path
17
- _, o, e, s = Open3.popen3(*T.unsafe([command, *T.unsafe(arg), opts]))
17
+ i, o, e, s = Open3.popen3(*T.unsafe([command, *T.unsafe(arg), opts]))
18
18
  out = o.read.to_s
19
19
  o.close
20
20
  err = e.read.to_s
21
21
  e.close
22
+ i.close
22
23
  [out, err, T.cast(s.value, Process::Status).success?]
23
24
  end
24
25
 
@@ -49,6 +50,13 @@ module Spoom
49
50
  exec("git show #{arg.join(' ')}", path: path)
50
51
  end
51
52
 
53
+ sig { params(path: String).returns(T.nilable(String)) }
54
+ def self.current_branch(path: ".")
55
+ out, _, status = exec("git branch --show-current", path: path)
56
+ return nil unless status
57
+ out.strip
58
+ end
59
+
52
60
  # Utils
53
61
 
54
62
  # Get the commit epoch timestamp for a `sha`
@@ -92,7 +100,19 @@ module Spoom
92
100
  def self.sorbet_intro_commit(path: ".")
93
101
  res, _, status = Spoom::Git.log("--diff-filter=A --format='%h' -1 -- sorbet/config", path: path)
94
102
  return nil unless status
95
- res.strip
103
+ res.strip!
104
+ return nil if res.empty?
105
+ res
106
+ end
107
+
108
+ # Get the hash of the commit removing the `sorbet/config` file
109
+ sig { params(path: String).returns(T.nilable(String)) }
110
+ def self.sorbet_removal_commit(path: ".")
111
+ res, _, status = Spoom::Git.log("--diff-filter=D --format='%h' -1 -- sorbet/config", path: path)
112
+ return nil unless status
113
+ res.strip!
114
+ return nil if res.empty?
115
+ res
96
116
  end
97
117
  end
98
118
  end
data/lib/spoom/printer.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "colorize"
5
4
  require "stringio"
6
5
 
7
6
  module Spoom
data/lib/spoom/sorbet.rb CHANGED
@@ -68,6 +68,7 @@ module Spoom
68
68
  end
69
69
  def srb_version(*arg, path: '.', capture_err: false, sorbet_bin: nil)
70
70
  out, res = T.unsafe(self).srb_tc(
71
+ "--no-config",
71
72
  "--version",
72
73
  *arg,
73
74
  path: path,
@@ -29,11 +29,15 @@ module Spoom
29
29
  sig { returns(T::Array[String]) }
30
30
  attr_reader :paths, :ignore, :allowed_extensions
31
31
 
32
+ sig { returns(T::Boolean) }
33
+ attr_accessor :no_stdlib
34
+
32
35
  sig { void }
33
36
  def initialize
34
37
  @paths = T.let([], T::Array[String])
35
38
  @ignore = T.let([], T::Array[String])
36
39
  @allowed_extensions = T.let([], T::Array[String])
40
+ @no_stdlib = T.let(false, T::Boolean)
37
41
  end
38
42
 
39
43
  sig { returns(Config) }
@@ -42,6 +46,7 @@ module Spoom
42
46
  new_config.paths.concat(@paths)
43
47
  new_config.ignore.concat(@ignore)
44
48
  new_config.allowed_extensions.concat(@allowed_extensions)
49
+ new_config.no_stdlib = @no_stdlib
45
50
  new_config
46
51
  end
47
52
 
@@ -63,6 +68,7 @@ module Spoom
63
68
  opts.concat(paths)
64
69
  opts.concat(ignore.map { |p| "--ignore #{p}" })
65
70
  opts.concat(allowed_extensions.map { |ext| "--allowed-extension #{ext}" })
71
+ opts << "--no-stdlib" if @no_stdlib
66
72
  opts.join(" ")
67
73
  end
68
74
 
@@ -106,12 +112,19 @@ module Spoom
106
112
  when /^--dir=/
107
113
  config.paths << parse_option(line)
108
114
  next
115
+ when /^--no-stdlib$/
116
+ config.no_stdlib = true
117
+ next
109
118
  when /^--.*=/
110
119
  next
111
120
  when /^--/
112
121
  state = :skip
113
122
  when /^-.*=?/
114
123
  next
124
+ when /^#/
125
+ next
126
+ when /^$/
127
+ next
115
128
  else
116
129
  case state
117
130
  when :ignore
@@ -55,7 +55,7 @@ module Spoom
55
55
  # * returns nil if no sigil
56
56
  sig { params(path: T.any(String, Pathname)).returns(T.nilable(String)) }
57
57
  def self.file_strictness(path)
58
- return nil unless File.exist?(path)
58
+ return nil unless File.file?(path)
59
59
  content = File.read(path, encoding: Encoding::ASCII_8BIT)
60
60
  strictness_in_content(content)
61
61
  end
@@ -76,6 +76,15 @@ module Spoom
76
76
  Spoom::Git.exec("GIT_COMMITTER_DATE=\"#{date}\" git commit -m '#{message}' --date '#{date}'", path: path)
77
77
  end
78
78
 
79
+ # Run `bundle install` in this project
80
+ sig { returns([T.nilable(String), T.nilable(String), T::Boolean]) }
81
+ def bundle_install
82
+ opts = {}
83
+ opts[:chdir] = path
84
+ out, err, status = Open3.capture3("bundle", "install", opts)
85
+ [out, err, status.success?]
86
+ end
87
+
79
88
  # Run a command with `bundle exec` in this project
80
89
  sig { params(cmd: String, args: String).returns([T.nilable(String), T.nilable(String), T::Boolean]) }
81
90
  def bundle_exec(cmd, *args)
@@ -91,6 +100,16 @@ module Spoom
91
100
  FileUtils.rm_rf(path)
92
101
  end
93
102
 
103
+ sig { params(name: String).void }
104
+ def create_and_checkout_branch(name)
105
+ Spoom::Git.exec("git checkout -b #{name}", path: path)
106
+ end
107
+
108
+ sig { returns(T.nilable(String)) }
109
+ def current_branch
110
+ Spoom::Git.current_branch(path: path)
111
+ end
112
+
94
113
  private
95
114
 
96
115
  # Create an absolute path from `self.path` and `rel_path`
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.0.8"
5
+ VERSION = "1.1.2"
6
6
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spoom
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.8
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexandre Terrasa
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-01-22 00:00:00.000000000 Z
11
+ date: 2021-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.17'
19
+ version: 2.2.10
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.17'
26
+ version: 2.2.10
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -70,16 +70,16 @@ dependencies:
70
70
  name: sorbet
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - "~>"
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
- version: 0.5.5
75
+ version: 0.5.6347
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - "~>"
80
+ - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: 0.5.5
82
+ version: 0.5.6347
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: thor
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -174,7 +174,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
174
174
  - !ruby/object:Gem::Version
175
175
  version: '0'
176
176
  requirements: []
177
- rubygems_version: 3.0.3
177
+ rubygems_version: 3.2.20
178
178
  signing_key:
179
179
  specification_version: 4
180
180
  summary: Useful tools for Sorbet enthusiasts.