spoom 1.0.7 → 1.0.8

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: f16907e7c4ce2a3d368bcac828ae7cfb43f0dde8f857bbfa364c4a3a16e0e433
4
- data.tar.gz: b6d2a532d34b0cd5e8e4964d2edc5e279e5481de511a4e61ea764e120e4bae33
3
+ metadata.gz: 866446fc75971d51c7a71d7ed713cf256f682ffbd0fabfaf97c95b601638876b
4
+ data.tar.gz: a38dd3b7859929349c2d84b4119e5f8d8d374c0cf5062adbb3904065a1d6f0f2
5
5
  SHA512:
6
- metadata.gz: ad2f822b0c34fdb51ae8002bdc2c3f35202e219e334723716b141afe9ab00b18c65f74c3129cff6e4a9cb992d3c673ccb8afa522114cf2759af5cdd888df5100
7
- data.tar.gz: f077abc8f9be4b2768776214601dd1c8d8c5709649536202a0ab9765058de9dc8590712b9a6fb96c30a0ac5ed46e7c21d84ab9bff190e48d99d5e0bddab1d44e
6
+ metadata.gz: 257a78cbdf439ee83e23a823ed39918ada7a1b66786967a77604e5dca2bb825aeb8af2f5fcda6c497686f29f9d0c7dba1d051060d984694f22c0064dcbd8c1a9
7
+ data.tar.gz: 4a66b832da23c6237d1dedc16735cce058306ce66327fd2e074d72a481d55412aa7dc8047f0fa25957e1369747478347b77ccf8cd7c85cdb286fcff6a564a412
data/README.md CHANGED
@@ -69,7 +69,7 @@ $ spoom files
69
69
  List all typechecking errors sorted by location:
70
70
 
71
71
  ```
72
- $ spoom tc -s
72
+ $ spoom tc -s loc
73
73
  ```
74
74
 
75
75
  List all typechecking errors sorted by error code first:
@@ -96,6 +96,31 @@ These options can be combined:
96
96
  $ spoom tc -s -c 7004 -l 10
97
97
  ```
98
98
 
99
+ Remove duplicated error lines:
100
+
101
+ ```
102
+ $ spoom tc -u
103
+ ```
104
+
105
+ Format each error line:
106
+
107
+ ```
108
+ $ spoom tc -f '%C - %F:%L: %M'
109
+ ```
110
+
111
+ Where:
112
+
113
+ * `%C` is the error code
114
+ * `%F` is the file the error is from
115
+ * `%L` is the line the error is from
116
+ * `%M` is the error message
117
+
118
+ Hide the `Errors: X` at the end of the list:
119
+
120
+ ```
121
+ $ spoom tc --no-count
122
+ ```
123
+
99
124
  #### Typing coverage
100
125
 
101
126
  Show metrics about the project contents and the typing coverage:
@@ -183,6 +208,24 @@ Bump the strictness from all files currently at `typed: false` to `typed: true`
183
208
  $ spoom bump --from false --to true -f
184
209
  ```
185
210
 
211
+ Bump the strictness from a list of files (one file by line):
212
+
213
+ ```
214
+ $ spoom bump --from false --to true -o list.txt
215
+ ```
216
+
217
+ Check if files can be bumped without applying any change:
218
+
219
+ ```
220
+ $ spoom bump --from false --to true --dry
221
+ ```
222
+
223
+ Bump files using a custom instance of Sorbet:
224
+
225
+ ```
226
+ $ spoom bump --from false --to true --sorbet /path/to/sorbet/bin
227
+ ```
228
+
186
229
  #### Interact with Sorbet LSP mode
187
230
 
188
231
  **Experimental**
@@ -273,7 +316,7 @@ Create an LSP client:
273
316
 
274
317
  ```rb
275
318
  client = Spoom::LSP::Client.new(
276
- Spoom::Config::SORBET_PATH,
319
+ Spoom::Sorbet::BIN_PATH,
277
320
  "--lsp",
278
321
  "--enable-all-experimental-lsp-features",
279
322
  "--disable-watchman",
@@ -4,10 +4,27 @@
4
4
  require "sorbet-runtime"
5
5
 
6
6
  module Spoom
7
+ extend T::Sig
8
+
9
+ SPOOM_PATH = (Pathname.new(__FILE__) / ".." / "..").to_s
10
+
7
11
  class Error < StandardError; end
8
- end
9
12
 
10
- require "spoom/config"
13
+ sig do
14
+ params(
15
+ cmd: String,
16
+ arg: String,
17
+ path: String,
18
+ capture_err: T::Boolean
19
+ ).returns([String, T::Boolean])
20
+ end
21
+ def self.exec(cmd, *arg, path: '.', capture_err: false)
22
+ method = capture_err ? "popen2e" : "popen2"
23
+ Open3.send(method, [cmd, *arg].join(" "), chdir: path) do |_, o, t|
24
+ [o.read, T.cast(t.value, Process::Status).success?]
25
+ end
26
+ end
27
+ end
11
28
 
12
29
  require "spoom/sorbet"
13
30
  require "spoom/cli"
@@ -17,26 +17,27 @@ module Spoom
17
17
  extend T::Sig
18
18
  include Helper
19
19
 
20
- class_option :color, desc: "Use colors", type: :boolean, default: true
21
- class_option :path, desc: "Run spoom in a specific path", type: :string, default: ".", aliases: :p
20
+ class_option :color, type: :boolean, default: true, desc: "Use colors"
21
+ class_option :path, type: :string, default: ".", aliases: :p, desc: "Run spoom in a specific path"
22
+
22
23
  map T.unsafe(%w[--version -v] => :__print_version)
23
24
 
24
- desc "bump", "bump Sorbet sigils from `false` to `true` when no errors"
25
+ desc "bump", "Bump Sorbet sigils from `false` to `true` when no errors"
25
26
  subcommand "bump", Spoom::Cli::Bump
26
27
 
27
- desc "config", "manage Sorbet config"
28
+ desc "config", "Manage Sorbet config"
28
29
  subcommand "config", Spoom::Cli::Config
29
30
 
30
- desc "coverage", "collect metrics related to Sorbet coverage"
31
+ desc "coverage", "Collect metrics related to Sorbet coverage"
31
32
  subcommand "coverage", Spoom::Cli::Coverage
32
33
 
33
- desc "lsp", "send LSP requests to Sorbet"
34
+ desc "lsp", "Send LSP requests to Sorbet"
34
35
  subcommand "lsp", Spoom::Cli::LSP
35
36
 
36
- desc "tc", "run Sorbet and parses its output"
37
+ desc "tc", "Run Sorbet and parses its output"
37
38
  subcommand "tc", Spoom::Cli::Run
38
39
 
39
- desc "files", "list all the files typechecked by Sorbet"
40
+ desc "files", "List all the files typechecked by Sorbet"
40
41
  def files
41
42
  in_sorbet_project!
42
43
 
@@ -53,7 +54,7 @@ module Spoom
53
54
  end
54
55
  end
55
56
 
56
- desc "--version", "show version"
57
+ desc "--version", "Show version"
57
58
  def __print_version
58
59
  puts "Spoom v#{Spoom::VERSION}"
59
60
  end
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'find'
@@ -12,10 +12,18 @@ module Spoom
12
12
 
13
13
  default_task :bump
14
14
 
15
- desc "bump DIRECTORY", "change Sorbet sigils from one strictness to another when no errors"
16
- option :from, type: :string, default: Spoom::Sorbet::Sigils::STRICTNESS_FALSE
17
- option :to, type: :string, default: Spoom::Sorbet::Sigils::STRICTNESS_TRUE
18
- option :force, desc: "change strictness without type checking", type: :boolean, default: false, aliases: :f
15
+ desc "bump DIRECTORY", "Change Sorbet sigils from one strictness to another when no errors"
16
+ option :from, type: :string, default: Spoom::Sorbet::Sigils::STRICTNESS_FALSE,
17
+ desc: "Change only files from this strictness"
18
+ option :to, type: :string, default: Spoom::Sorbet::Sigils::STRICTNESS_TRUE,
19
+ desc: "Change files to this strictness"
20
+ option :force, type: :boolean, default: false, aliases: :f,
21
+ desc: "Change strictness without type checking"
22
+ option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
23
+ option :dry, type: :boolean, default: false, aliases: :d,
24
+ desc: "Only display what would happen, do not actually change sigils"
25
+ option :only, type: :string, default: nil, aliases: :o,
26
+ desc: "Only change specified list (one file by line)"
19
27
  sig { params(directory: String).void }
20
28
  def bump(directory = ".")
21
29
  in_sorbet_project!
@@ -23,6 +31,9 @@ module Spoom
23
31
  from = options[:from]
24
32
  to = options[:to]
25
33
  force = options[:force]
34
+ dry = options[:dry]
35
+ only = options[:only]
36
+ exec_path = File.expand_path(self.exec_path)
26
37
 
27
38
  unless Sorbet::Sigils.valid_strictness?(from)
28
39
  say_error("Invalid strictness #{from} for option --from")
@@ -36,13 +47,32 @@ module Spoom
36
47
 
37
48
  directory = File.expand_path(directory)
38
49
  files_to_bump = Sorbet::Sigils.files_with_sigil_strictness(directory, from)
50
+
51
+ if only
52
+ list = File.read(only).lines.map { |file| File.expand_path(file.strip) }
53
+ files_to_bump.select! { |file| list.include?(File.expand_path(file)) }
54
+ end
55
+
56
+ if files_to_bump.empty?
57
+ $stderr.puts("No file to bump from #{from} to #{to}")
58
+ exit(0)
59
+ end
60
+
39
61
  Sorbet::Sigils.change_sigil_in_files(files_to_bump, to)
40
62
 
41
- return if force
63
+ if force
64
+ print_changes(files_to_bump, from: from, to: to, dry: dry, path: exec_path)
65
+ undo_changes(files_to_bump, from) if dry
66
+ exit(files_to_bump.empty?)
67
+ end
42
68
 
43
- output, no_errors = Sorbet.srb_tc(path: exec_path, capture_err: true)
69
+ output, no_errors = Sorbet.srb_tc(path: exec_path, capture_err: true, sorbet_bin: options[:sorbet])
44
70
 
45
- return if no_errors
71
+ if no_errors
72
+ print_changes(files_to_bump, from: from, to: to, dry: dry, path: exec_path)
73
+ undo_changes(files_to_bump, from) if dry
74
+ exit(files_to_bump.empty?)
75
+ end
46
76
 
47
77
  errors = Sorbet::Errors::Parser.parse_string(output)
48
78
 
@@ -53,7 +83,35 @@ module Spoom
53
83
  path
54
84
  end.compact.uniq
55
85
 
56
- Sorbet::Sigils.change_sigil_in_files(files_with_errors, from)
86
+ undo_changes(files_with_errors, from)
87
+
88
+ files_changed = files_to_bump - files_with_errors
89
+ print_changes(files_changed, from: from, to: to, dry: dry, path: exec_path)
90
+ undo_changes(files_to_bump, from) if dry
91
+ exit(files_changed.empty?)
92
+ end
93
+
94
+ no_commands do
95
+ def print_changes(files, from: "false", to: "true", dry: false, path: File.expand_path("."))
96
+ if files.empty?
97
+ $stderr.puts("No file to bump from #{from} to #{to}")
98
+ return
99
+ 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}:")
103
+ files.each do |file|
104
+ file_path = Pathname.new(file).relative_path_from(path)
105
+ $stderr.puts(" + #{file_path}")
106
+ end
107
+ if dry
108
+ $stderr.puts("\nRun `spoom bump --from #{from} --to #{to}` to bump them")
109
+ end
110
+ end
111
+
112
+ def undo_changes(files, from_strictness)
113
+ Sorbet::Sigils.change_sigil_in_files(files, from_strictness)
114
+ end
57
115
  end
58
116
  end
59
117
  end
@@ -11,7 +11,7 @@ module Spoom
11
11
 
12
12
  default_task :show
13
13
 
14
- desc "show", "show Sorbet config"
14
+ desc "show", "Show Sorbet config"
15
15
  def show
16
16
  in_sorbet_project!
17
17
  config = Spoom::Sorbet::Config.parse_file(sorbet_config)
@@ -13,13 +13,16 @@ module Spoom
13
13
 
14
14
  default_task :snapshot
15
15
 
16
- desc "snapshot", "run srb tc and display metrics"
17
- option :save, type: :string, desc: "Save snapshot data as json", lazy_default: DATA_DIR
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: "Exclude RBI files from metrics"
19
+ option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
18
20
  def snapshot
19
21
  in_sorbet_project!
20
-
21
22
  path = exec_path
22
- snapshot = Spoom::Coverage.snapshot(path: path)
23
+ sorbet = options[:sorbet]
24
+
25
+ snapshot = Spoom::Coverage.snapshot(path: path, rbi: options[:rbi], sorbet_bin: sorbet)
23
26
  snapshot.print
24
27
 
25
28
  save_dir = options[:save]
@@ -30,14 +33,16 @@ module Spoom
30
33
  puts "\nSnapshot data saved under #{file}"
31
34
  end
32
35
 
33
- desc "timeline", "replay a project and collect metrics"
34
- option :from, type: :string
35
- option :to, type: :string, default: Time.now.strftime("%F")
36
- option :save, type: :string, desc: "Save snapshot data as json", lazy_default: DATA_DIR
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"
37
40
  option :bundle_install, type: :boolean, desc: "Execute `bundle install` before collecting metrics"
41
+ option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
38
42
  def timeline
39
43
  in_sorbet_project!
40
44
  path = exec_path
45
+ sorbet = options[:sorbet]
41
46
 
42
47
  sha_before = Spoom::Git.last_commit(path: path)
43
48
  unless sha_before
@@ -83,10 +88,10 @@ module Spoom
83
88
  if options[:bundle_install]
84
89
  Bundler.with_clean_env do
85
90
  next unless bundle_install(path, sha)
86
- snapshot = Spoom::Coverage.snapshot(path: path)
91
+ snapshot = Spoom::Coverage.snapshot(path: path, sorbet_bin: sorbet)
87
92
  end
88
93
  else
89
- snapshot = Spoom::Coverage.snapshot(path: path)
94
+ snapshot = Spoom::Coverage.snapshot(path: path, sorbet_bin: sorbet)
90
95
  end
91
96
  next unless snapshot
92
97
 
@@ -101,14 +106,20 @@ module Spoom
101
106
  Spoom::Git.checkout(sha_before, path: path)
102
107
  end
103
108
 
104
- desc "report", "produce a typing coverage report"
105
- option :data, type: :string, desc: "Snapshots JSON data", default: DATA_DIR
106
- option :file, type: :string, default: "spoom_report.html", aliases: :f
107
- option :color_ignore, type: :string, default: Spoom::Coverage::D3::COLOR_IGNORE
108
- option :color_false, type: :string, default: Spoom::Coverage::D3::COLOR_FALSE
109
- option :color_true, type: :string, default: Spoom::Coverage::D3::COLOR_TRUE
110
- option :color_strict, type: :string, default: Spoom::Coverage::D3::COLOR_STRICT
111
- option :color_strong, type: :string, default: Spoom::Coverage::D3::COLOR_STRONG
109
+ desc "report", "Produce a typing coverage report"
110
+ option :data, type: :string, default: DATA_DIR, desc: "Snapshots JSON data"
111
+ option :file, type: :string, default: "spoom_report.html", aliases: :f,
112
+ desc: "Save report to file"
113
+ option :color_ignore, type: :string, default: Spoom::Coverage::D3::COLOR_IGNORE,
114
+ desc: "Color used for typed: ignore"
115
+ option :color_false, type: :string, default: Spoom::Coverage::D3::COLOR_FALSE,
116
+ desc: "Color used for typed: false"
117
+ option :color_true, type: :string, default: Spoom::Coverage::D3::COLOR_TRUE,
118
+ desc: "Color used for typed: true"
119
+ option :color_strict, type: :string, default: Spoom::Coverage::D3::COLOR_STRICT,
120
+ desc: "Color used for typed: strict"
121
+ option :color_strong, type: :string, default: Spoom::Coverage::D3::COLOR_STRONG,
122
+ desc: "Color used for typed: strong"
112
123
  def report
113
124
  in_sorbet_project!
114
125
 
@@ -139,7 +150,7 @@ module Spoom
139
150
  puts "\nUse #{colorize('spoom coverage open', :blue)} to open it."
140
151
  end
141
152
 
142
- desc "open", "open the typing coverage report"
153
+ desc "open", "Open the typing coverage report"
143
154
  def open(file = "spoom_report.html")
144
155
  unless File.exist?(file)
145
156
  say_error("No report file to open #{colorize(file, :blue)}")
@@ -1,6 +1,7 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "fileutils"
4
5
  require "pathname"
5
6
  require "stringio"
6
7
 
@@ -54,7 +55,7 @@ module Spoom
54
55
 
55
56
  sig { returns(String) }
56
57
  def sorbet_config
57
- Pathname.new("#{exec_path}/#{Spoom::Config::SORBET_CONFIG}").cleanpath.to_s
58
+ Pathname.new("#{exec_path}/#{Spoom::Sorbet::CONFIG_PATH}").cleanpath.to_s
58
59
  end
59
60
 
60
61
  # Is the `--color` option true?
@@ -12,7 +12,7 @@ module Spoom
12
12
 
13
13
  default_task :show
14
14
 
15
- desc "interactive", "interactive LSP mode"
15
+ desc "interactive", "Interactive LSP mode"
16
16
  def show
17
17
  in_sorbet_project!
18
18
  lsp = lsp_client
@@ -20,7 +20,7 @@ module Spoom
20
20
  puts lsp
21
21
  end
22
22
 
23
- desc "list", "list all known symbols"
23
+ desc "list", "List all known symbols"
24
24
  # TODO: options, filter, limit, kind etc.. filter rbi
25
25
  def list
26
26
  run do |client|
@@ -34,7 +34,7 @@ module Spoom
34
34
  end
35
35
  end
36
36
 
37
- desc "hover", "request hover informations"
37
+ desc "hover", "Request hover informations"
38
38
  # TODO: options, filter, limit, kind etc.. filter rbi
39
39
  def hover(file, line, col)
40
40
  run do |client|
@@ -48,7 +48,7 @@ module Spoom
48
48
  end
49
49
  end
50
50
 
51
- desc "defs", "list definitions of a symbol"
51
+ desc "defs", "List definitions of a symbol"
52
52
  # TODO: options, filter, limit, kind etc.. filter rbi
53
53
  def defs(file, line, col)
54
54
  run do |client|
@@ -58,7 +58,7 @@ module Spoom
58
58
  end
59
59
  end
60
60
 
61
- desc "find", "find symbols matching a query"
61
+ desc "find", "Find symbols matching a query"
62
62
  # TODO: options, filter, limit, kind etc.. filter rbi
63
63
  def find(query)
64
64
  run do |client|
@@ -68,7 +68,7 @@ module Spoom
68
68
  end
69
69
  end
70
70
 
71
- desc "symbols", "list symbols from a file"
71
+ desc "symbols", "List symbols from a file"
72
72
  # TODO: options, filter, limit, kind etc.. filter rbi
73
73
  def symbols(file)
74
74
  run do |client|
@@ -78,7 +78,7 @@ module Spoom
78
78
  end
79
79
  end
80
80
 
81
- desc "refs", "list references to a symbol"
81
+ desc "refs", "List references to a symbol"
82
82
  # TODO: options, filter, limit, kind etc.. filter rbi
83
83
  def refs(file, line, col)
84
84
  run do |client|
@@ -88,7 +88,7 @@ module Spoom
88
88
  end
89
89
  end
90
90
 
91
- desc "sigs", "list signatures for a symbol"
91
+ desc "sigs", "List signatures for a symbol"
92
92
  # TODO: options, filter, limit, kind etc.. filter rbi
93
93
  def sigs(file, line, col)
94
94
  run do |client|
@@ -98,7 +98,7 @@ module Spoom
98
98
  end
99
99
  end
100
100
 
101
- desc "types", "display type of a symbol"
101
+ desc "types", "Display type of a symbol"
102
102
  # TODO: options, filter, limit, kind etc.. filter rbi
103
103
  def types(file, line, col)
104
104
  run do |client|
@@ -113,7 +113,7 @@ module Spoom
113
113
  in_sorbet_project!
114
114
  path = exec_path
115
115
  client = Spoom::LSP::Client.new(
116
- Spoom::Config::SORBET_PATH,
116
+ Spoom::Sorbet::BIN_PATH,
117
117
  "--lsp",
118
118
  "--enable-all-experimental-lsp-features",
119
119
  "--disable-watchman",
@@ -8,59 +8,89 @@ module Spoom
8
8
 
9
9
  default_task :tc
10
10
 
11
- desc "tc", "run srb tc"
12
- option :limit, type: :numeric, aliases: :l
13
- option :code, type: :numeric, aliases: :c
14
- option :sort, type: :string, aliases: :s
15
- def tc
11
+ SORT_CODE = "code"
12
+ SORT_LOC = "loc"
13
+ SORT_ENUM = [SORT_CODE, SORT_LOC]
14
+
15
+ DEFAULT_FORMAT = "%C - %F:%L: %M"
16
+
17
+ desc "tc", "Run `srb tc`"
18
+ option :limit, type: :numeric, aliases: :l, desc: "Limit displayed errors"
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
21
+ option :format, type: :string, aliases: :f, desc: "Format line output"
22
+ option :uniq, type: :boolean, aliases: :u, desc: "Remove duplicated lines"
23
+ option :count, type: :boolean, default: true, desc: "Show errors count"
24
+ option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
25
+ def tc(*arg)
16
26
  in_sorbet_project!
17
27
 
18
28
  path = exec_path
19
29
  limit = options[:limit]
20
30
  sort = options[:sort]
21
31
  code = options[:code]
22
- colors = options[:color]
32
+ uniq = options[:uniq]
33
+ format = options[:format]
34
+ count = options[:count]
35
+ sorbet = options[:sorbet]
23
36
 
24
37
  unless limit || code || sort
25
- return Spoom::Sorbet.srb_tc(path: path, capture_err: false).last
38
+ output, status = T.unsafe(Spoom::Sorbet).srb_tc(*arg, path: path, capture_err: false, sorbet_bin: sorbet)
39
+ $stderr.print(output)
40
+ exit(status)
26
41
  end
27
42
 
28
- output, status = Spoom::Sorbet.srb_tc(path: path, capture_err: true)
43
+ output, status = T.unsafe(Spoom::Sorbet).srb_tc(*arg, path: path, capture_err: true, sorbet_bin: sorbet)
29
44
  if status
30
45
  $stderr.print(output)
31
- return 0
46
+ exit(0)
32
47
  end
33
48
 
34
49
  errors = Spoom::Sorbet::Errors::Parser.parse_string(output)
35
50
  errors_count = errors.size
36
51
 
37
- errors = sort == "code" ? errors.sort_by { |e| [e.code, e.file, e.line, e.message] } : errors.sort
52
+ errors = case sort
53
+ when SORT_CODE
54
+ Spoom::Sorbet::Errors.sort_errors_by_code(errors)
55
+ when SORT_LOC
56
+ errors.sort
57
+ else
58
+ errors # preserve natural sort
59
+ end
60
+
38
61
  errors = errors.select { |e| e.code == code } if code
39
62
  errors = T.must(errors.slice(0, limit)) if limit
40
63
 
41
- errors.each do |e|
42
- code = colorize_code(e.code, colors)
43
- message = colorize_message(e.message, colors)
44
- $stderr.puts "#{code} - #{e.file}:#{e.line}: #{message}"
64
+ lines = errors.map { |e| format_error(e, format || DEFAULT_FORMAT) }
65
+ lines = lines.uniq if uniq
66
+
67
+ lines.each do |line|
68
+ $stderr.puts line
45
69
  end
46
70
 
47
- if errors_count == errors.size
48
- $stderr.puts "Errors: #{errors_count}"
49
- else
50
- $stderr.puts "Errors: #{errors.size} shown, #{errors_count} total"
71
+ if count
72
+ if errors_count == errors.size
73
+ $stderr.puts "Errors: #{errors_count}"
74
+ else
75
+ $stderr.puts "Errors: #{errors.size} shown, #{errors_count} total"
76
+ end
51
77
  end
52
78
 
53
- 1
79
+ exit(1)
54
80
  end
55
81
 
56
82
  no_commands do
57
- def colorize_code(code, colors = true)
58
- return code.to_s unless colors
59
- code.to_s.light_black
83
+ def format_error(error, format)
84
+ line = format
85
+ line = line.gsub(/%C/, colorize(error.code.to_s, :yellow))
86
+ line = line.gsub(/%F/, error.file)
87
+ line = line.gsub(/%L/, error.line.to_s)
88
+ line = line.gsub(/%M/, colorize_message(error.message))
89
+ line
60
90
  end
61
91
 
62
- def colorize_message(message, colors = true)
63
- return message unless colors
92
+ def colorize_message(message)
93
+ return message unless color?
64
94
 
65
95
  cyan = T.let(false, T::Boolean)
66
96
  word = StringIO.new
@@ -11,10 +11,23 @@ module Spoom
11
11
  module Coverage
12
12
  extend T::Sig
13
13
 
14
- sig { params(path: String).returns(Snapshot) }
15
- def self.snapshot(path: '.')
14
+ sig { params(path: String, rbi: T::Boolean, sorbet_bin: T.nilable(String)).returns(Snapshot) }
15
+ def self.snapshot(path: '.', rbi: true, sorbet_bin: nil)
16
+ config = sorbet_config(path: path)
17
+ config.allowed_extensions.push(".rb", ".rbi") if config.allowed_extensions.empty?
18
+
19
+ new_config = config.copy
20
+ new_config.allowed_extensions.reject! { |ext| !rbi && ext == ".rbi" }
21
+
22
+ metrics = Spoom::Sorbet.srb_metrics(
23
+ "--no-config",
24
+ new_config.options_string,
25
+ path: path,
26
+ capture_err: true,
27
+ sorbet_bin: sorbet_bin
28
+ )
29
+
16
30
  snapshot = Snapshot.new
17
- metrics = Spoom::Sorbet.srb_metrics(path: path, capture_err: true)
18
31
  return snapshot unless metrics
19
32
 
20
33
  sha = Spoom::Git.last_commit(path: path)
@@ -59,11 +72,14 @@ module Spoom
59
72
  )
60
73
  end
61
74
 
75
+ sig { params(path: String).returns(Sorbet::Config) }
76
+ def self.sorbet_config(path: ".")
77
+ Sorbet::Config.parse_file("#{path}/#{Spoom::Sorbet::CONFIG_PATH}")
78
+ end
79
+
62
80
  sig { params(path: String).returns(FileTree) }
63
81
  def self.sigils_tree(path: ".")
64
- config_file = "#{path}/#{Spoom::Config::SORBET_CONFIG}"
65
- return FileTree.new unless File.exist?(config_file)
66
- config = Sorbet::Config.parse_file(config_file)
82
+ config = sorbet_config(path: path)
67
83
  files = Sorbet.srb_files(config, path: path)
68
84
  files.select! { |file| file =~ /\.rb$/ }
69
85
  files.reject! { |file| file =~ %r{/test/} }
@@ -41,7 +41,7 @@ module Spoom
41
41
 
42
42
  abstract!
43
43
 
44
- TEMPLATE = T.let("#{Spoom::Config::SPOOM_PATH}/templates/page.erb", String)
44
+ TEMPLATE = T.let("#{Spoom::SPOOM_PATH}/templates/page.erb", String)
45
45
 
46
46
  sig { returns(String) }
47
47
  attr_reader :title
@@ -89,7 +89,7 @@ module Spoom
89
89
  class Card < Template
90
90
  extend T::Sig
91
91
 
92
- TEMPLATE = T.let("#{Spoom::Config::SPOOM_PATH}/templates/card.erb", String)
92
+ TEMPLATE = T.let("#{Spoom::SPOOM_PATH}/templates/card.erb", String)
93
93
 
94
94
  sig { returns(T.nilable(String)) }
95
95
  attr_reader :title, :body
@@ -123,7 +123,7 @@ module Spoom
123
123
  class Snapshot < Card
124
124
  extend T::Sig
125
125
 
126
- TEMPLATE = T.let("#{Spoom::Config::SPOOM_PATH}/templates/card_snapshot.erb", String)
126
+ TEMPLATE = T.let("#{Spoom::SPOOM_PATH}/templates/card_snapshot.erb", String)
127
127
 
128
128
  sig { returns(Coverage::Snapshot) }
129
129
  attr_reader :snapshot
@@ -80,7 +80,7 @@ module Spoom
80
80
 
81
81
  private
82
82
 
83
- sig { params(node: FileTree::Node, collected_nodes: T::Array[Node]).returns(T::Array[String]) }
83
+ sig { params(node: FileTree::Node, collected_nodes: T::Array[Node]).returns(T::Array[Node]) }
84
84
  def collect_nodes(node, collected_nodes = [])
85
85
  collected_nodes << node
86
86
  node.children.values.each { |child| collect_nodes(child, collected_nodes) }
@@ -11,73 +11,111 @@ require "open3"
11
11
 
12
12
  module Spoom
13
13
  module Sorbet
14
- extend T::Sig
14
+ CONFIG_PATH = "sorbet/config"
15
+ GEM_PATH = Gem::Specification.find_by_name("sorbet-static").full_gem_path
16
+ BIN_PATH = (Pathname.new(GEM_PATH) / "libexec" / "sorbet").to_s
15
17
 
16
- sig { params(arg: String, path: String, capture_err: T::Boolean).returns([String, T::Boolean]) }
17
- def self.srb(*arg, path: '.', capture_err: false)
18
- opts = {}
19
- opts[:chdir] = path
20
- out = T.let("", T.nilable(String))
21
- res = T.let(false, T::Boolean)
22
- if capture_err
23
- Open3.popen2e(["bundle", "exec", "srb", *arg].join(" "), opts) do |_, o, t|
24
- out = o.read
25
- res = T.cast(t.value, Process::Status).success?
26
- end
27
- else
28
- Open3.popen2(["bundle", "exec", "srb", *arg].join(" "), opts) do |_, o, t|
29
- out = o.read
30
- res = T.cast(t.value, Process::Status).success?
18
+ class << self
19
+ extend T::Sig
20
+
21
+ sig do
22
+ params(
23
+ arg: String,
24
+ path: String,
25
+ capture_err: T::Boolean,
26
+ sorbet_bin: T.nilable(String)
27
+ ).returns([String, T::Boolean])
28
+ end
29
+ def srb(*arg, path: '.', capture_err: false, sorbet_bin: nil)
30
+ if sorbet_bin
31
+ arg.prepend(sorbet_bin)
32
+ else
33
+ arg.prepend("bundle", "exec", "srb")
31
34
  end
35
+ T.unsafe(Spoom).exec(*arg, path: path, capture_err: capture_err)
32
36
  end
33
- [out || "", res]
34
- end
35
37
 
36
- sig { params(arg: String, path: String, capture_err: T::Boolean).returns([String, T::Boolean]) }
37
- def self.srb_tc(*arg, path: '.', capture_err: false)
38
- srb(*T.unsafe(["tc", *arg]), path: path, capture_err: capture_err)
39
- end
38
+ sig do
39
+ params(
40
+ arg: String,
41
+ path: String,
42
+ capture_err: T::Boolean,
43
+ sorbet_bin: T.nilable(String)
44
+ ).returns([String, T::Boolean])
45
+ end
46
+ def srb_tc(*arg, path: '.', capture_err: false, sorbet_bin: nil)
47
+ arg.prepend("tc") unless sorbet_bin
48
+ T.unsafe(self).srb(*arg, path: path, capture_err: capture_err, sorbet_bin: sorbet_bin)
49
+ end
40
50
 
41
- # List all files typechecked by Sorbet from its `config`
42
- sig { params(config: Config, path: String).returns(T::Array[String]) }
43
- def self.srb_files(config, path: '.')
44
- regs = config.ignore.map { |string| Regexp.new(Regexp.escape(string)) }
45
- exts = config.allowed_extensions.empty? ? ['.rb', '.rbi'] : config.allowed_extensions
46
- Dir.glob((Pathname.new(path) / "**/*{#{exts.join(',')}}").to_s).reject do |f|
47
- regs.any? { |re| re.match?(f) }
48
- end.sort
49
- end
51
+ # List all files typechecked by Sorbet from its `config`
52
+ sig { params(config: Config, path: String).returns(T::Array[String]) }
53
+ def srb_files(config, path: '.')
54
+ regs = config.ignore.map { |string| Regexp.new(Regexp.escape(string)) }
55
+ exts = config.allowed_extensions.empty? ? ['.rb', '.rbi'] : config.allowed_extensions
56
+ Dir.glob((Pathname.new(path) / "**/*{#{exts.join(',')}}").to_s).reject do |f|
57
+ regs.any? { |re| re.match?(f) }
58
+ end.sort
59
+ end
50
60
 
51
- sig { params(arg: String, path: String, capture_err: T::Boolean).returns(T.nilable(String)) }
52
- def self.srb_version(*arg, path: '.', capture_err: false)
53
- out, res = srb(*T.unsafe(["--version", *arg]), path: path, capture_err: capture_err)
54
- return nil unless res
55
- out.split(" ")[2]
56
- end
61
+ sig do
62
+ params(
63
+ arg: String,
64
+ path: String,
65
+ capture_err: T::Boolean,
66
+ sorbet_bin: T.nilable(String)
67
+ ).returns(T.nilable(String))
68
+ end
69
+ def srb_version(*arg, path: '.', capture_err: false, sorbet_bin: nil)
70
+ out, res = T.unsafe(self).srb_tc(
71
+ "--version",
72
+ *arg,
73
+ path: path,
74
+ capture_err: capture_err,
75
+ sorbet_bin: sorbet_bin
76
+ )
77
+ return nil unless res
78
+ out.split(" ")[2]
79
+ end
57
80
 
58
- sig { params(arg: String, path: String, capture_err: T::Boolean).returns(T.nilable(T::Hash[String, Integer])) }
59
- def self.srb_metrics(*arg, path: '.', capture_err: false)
60
- metrics_file = "metrics.tmp"
61
- metrics_path = "#{path}/#{metrics_file}"
62
- srb_tc(*T.unsafe(["--metrics-file=#{metrics_file}", *arg]), path: path, capture_err: capture_err)
63
- if File.exist?(metrics_path)
64
- metrics = Spoom::Sorbet::MetricsParser.parse_file(metrics_path)
65
- File.delete(metrics_path)
66
- return metrics
81
+ sig do
82
+ params(
83
+ arg: String,
84
+ path: String,
85
+ capture_err: T::Boolean,
86
+ sorbet_bin: T.nilable(String)
87
+ ).returns(T.nilable(T::Hash[String, Integer]))
88
+ end
89
+ def srb_metrics(*arg, path: '.', capture_err: false, sorbet_bin: nil)
90
+ metrics_file = "metrics.tmp"
91
+ metrics_path = "#{path}/#{metrics_file}"
92
+ T.unsafe(self).srb_tc(
93
+ "--metrics-file",
94
+ metrics_file,
95
+ *arg,
96
+ path: path,
97
+ capture_err: capture_err,
98
+ sorbet_bin: sorbet_bin
99
+ )
100
+ if File.exist?(metrics_path)
101
+ metrics = Spoom::Sorbet::MetricsParser.parse_file(metrics_path)
102
+ File.delete(metrics_path)
103
+ return metrics
104
+ end
105
+ nil
67
106
  end
68
- nil
69
- end
70
107
 
71
- # Get `gem` version from the `Gemfile.lock` content
72
- #
73
- # Returns `nil` if `gem` cannot be found in the Gemfile.
74
- sig { params(gem: String, path: String).returns(T.nilable(String)) }
75
- def self.version_from_gemfile_lock(gem: 'sorbet', path: '.')
76
- gemfile_path = "#{path}/Gemfile.lock"
77
- return nil unless File.exist?(gemfile_path)
78
- content = File.read(gemfile_path).match(/^ #{gem} \(.*(\d+\.\d+\.\d+).*\)/)
79
- return nil unless content
80
- content[1]
108
+ # Get `gem` version from the `Gemfile.lock` content
109
+ #
110
+ # Returns `nil` if `gem` cannot be found in the Gemfile.
111
+ sig { params(gem: String, path: String).returns(T.nilable(String)) }
112
+ def version_from_gemfile_lock(gem: 'sorbet', path: '.')
113
+ gemfile_path = "#{path}/Gemfile.lock"
114
+ return nil unless File.exist?(gemfile_path)
115
+ content = File.read(gemfile_path).match(/^ #{gem} \(.*(\d+\.\d+\.\d+).*\)/)
116
+ return nil unless content
117
+ content[1]
118
+ end
81
119
  end
82
120
  end
83
121
  end
@@ -36,6 +36,36 @@ module Spoom
36
36
  @allowed_extensions = T.let([], T::Array[String])
37
37
  end
38
38
 
39
+ sig { returns(Config) }
40
+ def copy
41
+ new_config = Sorbet::Config.new
42
+ new_config.paths.concat(@paths)
43
+ new_config.ignore.concat(@ignore)
44
+ new_config.allowed_extensions.concat(@allowed_extensions)
45
+ new_config
46
+ end
47
+
48
+ # Returns self as a string of options that can be passed to Sorbet
49
+ #
50
+ # Example:
51
+ # ~~~rb
52
+ # config = Sorbet::Config.new
53
+ # config.paths << "/foo"
54
+ # config.paths << "/bar"
55
+ # config.ignore << "/baz"
56
+ # config.allowed_extensions << ".rb"
57
+ #
58
+ # puts config.options_string # "/foo /bar --ignore /baz --allowed-extension .rb"
59
+ # ~~~
60
+ sig { returns(String) }
61
+ def options_string
62
+ opts = []
63
+ opts.concat(paths)
64
+ opts.concat(ignore.map { |p| "--ignore #{p}" })
65
+ opts.concat(allowed_extensions.map { |ext| "--allowed-extension #{ext}" })
66
+ opts.join(" ")
67
+ end
68
+
39
69
  class << self
40
70
  extend T::Sig
41
71
 
@@ -4,6 +4,8 @@
4
4
  module Spoom
5
5
  module Sorbet
6
6
  module Errors
7
+ extend T::Sig
8
+
7
9
  # Parse errors from Sorbet output
8
10
  class Parser
9
11
  extend T::Sig
@@ -123,6 +125,7 @@ module Spoom
123
125
  @more = more
124
126
  end
125
127
 
128
+ # By default errors are sorted by location
126
129
  sig { params(other: T.untyped).returns(Integer) }
127
130
  def <=>(other)
128
131
  return 0 unless other.is_a?(Error)
@@ -134,6 +137,11 @@ module Spoom
134
137
  "#{file}:#{line}: #{message} (#{code})"
135
138
  end
136
139
  end
140
+
141
+ sig { params(errors: T::Array[Error]).returns(T::Array[Error]) }
142
+ def self.sort_errors_by_code(errors)
143
+ errors.sort_by { |e| [e.code, e.file, e.line, e.message] }
144
+ end
137
145
  end
138
146
  end
139
147
  end
@@ -11,13 +11,9 @@ require_relative 'lsp/errors'
11
11
  module Spoom
12
12
  module LSP
13
13
  class Client
14
- def initialize(sorbet_cmd, *sorbet_args, path: ".")
14
+ def initialize(sorbet_bin, *sorbet_args, path: ".")
15
15
  @id = 0
16
- Bundler.with_clean_env do
17
- opts = {}
18
- opts[:chdir] = path
19
- @in, @out, @err, @status = Open3.popen3([sorbet_cmd, *sorbet_args].join(" "), opts)
20
- end
16
+ @in, @out, @err, @status = T.unsafe(Open3).popen3(sorbet_bin, *sorbet_args, chdir: path)
21
17
  end
22
18
 
23
19
  def next_id
@@ -56,14 +56,14 @@ module Spoom
56
56
  sig { params(path: T.any(String, Pathname)).returns(T.nilable(String)) }
57
57
  def self.file_strictness(path)
58
58
  return nil unless File.exist?(path)
59
- content = File.read(path)
59
+ content = File.read(path, encoding: Encoding::ASCII_8BIT)
60
60
  strictness_in_content(content)
61
61
  end
62
62
 
63
63
  # changes the sigil in the file at the passed path to the specified new strictness
64
64
  sig { params(path: T.any(String, Pathname), new_strictness: String).returns(T::Boolean) }
65
65
  def self.change_sigil_in_file(path, new_strictness)
66
- content = File.read(path)
66
+ content = File.read(path, encoding: Encoding::ASCII_8BIT)
67
67
  new_content = update_sigil(content, new_strictness)
68
68
 
69
69
  File.write(path, new_content)
@@ -87,7 +87,7 @@ module Spoom
87
87
  extension: String
88
88
  ).returns(T::Array[String])
89
89
  end
90
- def self.files_with_sigil_strictness(directory, strictness, extension = ".rb")
90
+ def self.files_with_sigil_strictness(directory, strictness, extension: ".rb")
91
91
  paths = Dir.glob("#{File.expand_path(directory)}/**/*#{extension}").sort.uniq
92
92
  paths.filter do |path|
93
93
  file_strictness(path) == strictness
@@ -1,6 +1,6 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Spoom
5
- VERSION = "1.0.7"
5
+ VERSION = "1.0.8"
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.0.7
4
+ version: 1.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexandre Terrasa
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-30 00:00:00.000000000 Z
11
+ date: 2021-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -128,7 +128,6 @@ files:
128
128
  - lib/spoom/cli/helper.rb
129
129
  - lib/spoom/cli/lsp.rb
130
130
  - lib/spoom/cli/run.rb
131
- - lib/spoom/config.rb
132
131
  - lib/spoom/coverage.rb
133
132
  - lib/spoom/coverage/d3.rb
134
133
  - lib/spoom/coverage/d3/base.rb
@@ -1,13 +0,0 @@
1
- # typed: true
2
- # frozen_string_literal: true
3
-
4
- require 'pathname'
5
-
6
- module Spoom
7
- module Config
8
- SORBET_CONFIG = "sorbet/config"
9
- SORBET_GEM_PATH = Gem::Specification.find_by_name("sorbet-static").full_gem_path
10
- SORBET_PATH = (Pathname.new(SORBET_GEM_PATH) / "libexec" / "sorbet").to_s
11
- SPOOM_PATH = (Pathname.new(__FILE__) / ".." / ".." / "..").to_s
12
- end
13
- end