spoom 1.0.8 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
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.