spoom 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +0 -1
- data/README.md +45 -2
- data/lib/spoom.rb +20 -2
- data/lib/spoom/cli.rb +25 -14
- data/lib/spoom/cli/bump.rb +106 -13
- data/lib/spoom/cli/config.rb +3 -3
- data/lib/spoom/cli/coverage.rb +57 -42
- data/lib/spoom/cli/helper.rb +88 -9
- data/lib/spoom/cli/lsp.rb +20 -20
- data/lib/spoom/cli/run.rb +55 -25
- data/lib/spoom/coverage.rb +22 -6
- data/lib/spoom/coverage/report.rb +3 -3
- data/lib/spoom/file_tree.rb +1 -1
- data/lib/spoom/git.rb +2 -1
- data/lib/spoom/printer.rb +0 -1
- data/lib/spoom/sorbet.rb +97 -58
- data/lib/spoom/sorbet/config.rb +30 -0
- data/lib/spoom/sorbet/errors.rb +8 -0
- data/lib/spoom/sorbet/lsp.rb +2 -6
- data/lib/spoom/sorbet/sigils.rb +3 -3
- data/lib/spoom/test_helpers/project.rb +9 -0
- data/lib/spoom/version.rb +2 -2
- data/templates/card.erb +8 -0
- data/templates/card_snapshot.erb +22 -0
- data/templates/page.erb +50 -0
- metadata +9 -7
- data/lib/spoom/config.rb +0 -11
data/lib/spoom/cli/helper.rb
CHANGED
@@ -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
|
|
@@ -10,16 +11,32 @@ module Spoom
|
|
10
11
|
extend T::Sig
|
11
12
|
include Thor::Shell
|
12
13
|
|
14
|
+
# Print `message` on `$stdout`
|
15
|
+
sig { params(message: String).void }
|
16
|
+
def say(message)
|
17
|
+
buffer = StringIO.new
|
18
|
+
buffer << highlight(message)
|
19
|
+
buffer << "\n" unless message.end_with?("\n")
|
20
|
+
|
21
|
+
$stdout.print(buffer.string)
|
22
|
+
$stdout.flush
|
23
|
+
end
|
24
|
+
|
13
25
|
# Print `message` on `$stderr`
|
14
26
|
#
|
15
27
|
# The message is prefixed by a status (default: `Error`).
|
16
|
-
sig
|
17
|
-
|
18
|
-
|
19
|
-
|
28
|
+
sig do
|
29
|
+
params(
|
30
|
+
message: String,
|
31
|
+
status: T.nilable(String),
|
32
|
+
nl: T::Boolean
|
33
|
+
).void
|
34
|
+
end
|
35
|
+
def say_error(message, status: "Error", nl: true)
|
20
36
|
buffer = StringIO.new
|
21
|
-
buffer << "#{status}:
|
22
|
-
buffer <<
|
37
|
+
buffer << "#{red(status)}: " if status
|
38
|
+
buffer << highlight(message)
|
39
|
+
buffer << "\n" if nl && !message.end_with?("\n")
|
23
40
|
|
24
41
|
$stderr.print(buffer.string)
|
25
42
|
$stderr.flush
|
@@ -28,7 +45,7 @@ module Spoom
|
|
28
45
|
# Is `spoom` ran inside a project with a `sorbet/config` file?
|
29
46
|
sig { returns(T::Boolean) }
|
30
47
|
def in_sorbet_project?
|
31
|
-
File.file?(
|
48
|
+
File.file?(sorbet_config_file)
|
32
49
|
end
|
33
50
|
|
34
51
|
# Enforce that `spoom` is ran inside a project with a `sorbet/config` file
|
@@ -37,7 +54,11 @@ module Spoom
|
|
37
54
|
sig { void }
|
38
55
|
def in_sorbet_project!
|
39
56
|
unless in_sorbet_project?
|
40
|
-
say_error(
|
57
|
+
say_error(
|
58
|
+
"not in a Sorbet project (`#{sorbet_config_file}` not found)\n\n" \
|
59
|
+
"When running spoom from another path than the project's root, " \
|
60
|
+
"use `--path PATH` to specify the path to the root."
|
61
|
+
)
|
41
62
|
Kernel.exit(1)
|
42
63
|
end
|
43
64
|
end
|
@@ -49,22 +70,80 @@ module Spoom
|
|
49
70
|
end
|
50
71
|
|
51
72
|
sig { returns(String) }
|
73
|
+
def sorbet_config_file
|
74
|
+
Pathname.new("#{exec_path}/#{Spoom::Sorbet::CONFIG_PATH}").cleanpath.to_s
|
75
|
+
end
|
76
|
+
|
77
|
+
sig { returns(Sorbet::Config) }
|
52
78
|
def sorbet_config
|
53
|
-
|
79
|
+
Sorbet::Config.parse_file(sorbet_config_file)
|
54
80
|
end
|
55
81
|
|
82
|
+
# Colors
|
83
|
+
|
84
|
+
# Color used to highlight expressions in backticks
|
85
|
+
HIGHLIGHT_COLOR = :blue
|
86
|
+
|
56
87
|
# Is the `--color` option true?
|
57
88
|
sig { returns(T::Boolean) }
|
58
89
|
def color?
|
59
90
|
T.unsafe(self).options[:color] # TODO: requires_ancestor
|
60
91
|
end
|
61
92
|
|
93
|
+
sig { params(string: String).returns(String) }
|
94
|
+
def highlight(string)
|
95
|
+
return string unless color?
|
96
|
+
|
97
|
+
res = StringIO.new
|
98
|
+
word = StringIO.new
|
99
|
+
in_ticks = T.let(false, T::Boolean)
|
100
|
+
string.chars.each do |c|
|
101
|
+
if c == '`' && !in_ticks
|
102
|
+
in_ticks = true
|
103
|
+
elsif c == '`' && in_ticks
|
104
|
+
in_ticks = false
|
105
|
+
res << colorize(word.string, HIGHLIGHT_COLOR)
|
106
|
+
word = StringIO.new
|
107
|
+
elsif in_ticks
|
108
|
+
word << c
|
109
|
+
else
|
110
|
+
res << c
|
111
|
+
end
|
112
|
+
end
|
113
|
+
res.string
|
114
|
+
end
|
115
|
+
|
62
116
|
# Colorize a string if `color?`
|
63
117
|
sig { params(string: String, color: Symbol).returns(String) }
|
64
118
|
def colorize(string, color)
|
65
119
|
return string unless color?
|
66
120
|
string.colorize(color)
|
67
121
|
end
|
122
|
+
|
123
|
+
sig { params(string: String).returns(String) }
|
124
|
+
def blue(string)
|
125
|
+
colorize(string, :blue)
|
126
|
+
end
|
127
|
+
|
128
|
+
sig { params(string: String).returns(String) }
|
129
|
+
def gray(string)
|
130
|
+
colorize(string, :light_black)
|
131
|
+
end
|
132
|
+
|
133
|
+
sig { params(string: String).returns(String) }
|
134
|
+
def green(string)
|
135
|
+
colorize(string, :green)
|
136
|
+
end
|
137
|
+
|
138
|
+
sig { params(string: String).returns(String) }
|
139
|
+
def red(string)
|
140
|
+
colorize(string, :red)
|
141
|
+
end
|
142
|
+
|
143
|
+
sig { params(string: String).returns(String) }
|
144
|
+
def yellow(string)
|
145
|
+
colorize(string, :yellow)
|
146
|
+
end
|
68
147
|
end
|
69
148
|
end
|
70
149
|
end
|
data/lib/spoom/cli/lsp.rb
CHANGED
@@ -12,7 +12,7 @@ module Spoom
|
|
12
12
|
|
13
13
|
default_task :show
|
14
14
|
|
15
|
-
desc "interactive", "
|
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", "
|
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|
|
@@ -28,82 +28,82 @@ 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
|
-
|
31
|
+
say("Symbols from `#{file}`:")
|
32
32
|
printer.print_objects(res)
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
-
desc "hover", "
|
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|
|
41
41
|
res = client.hover(to_uri(file), line.to_i, col.to_i)
|
42
|
-
say
|
42
|
+
say("Hovering `#{file}:#{line}:#{col}`:")
|
43
43
|
if res
|
44
44
|
symbol_printer.print_object(res)
|
45
45
|
else
|
46
|
-
|
46
|
+
say("<no data>")
|
47
47
|
end
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
|
-
desc "defs", "
|
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|
|
55
55
|
res = client.definitions(to_uri(file), line.to_i, col.to_i)
|
56
|
-
|
56
|
+
say("Definitions for `#{file}:#{line}:#{col}`:")
|
57
57
|
symbol_printer.print_list(res)
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
-
desc "find", "
|
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|
|
65
65
|
res = client.symbols(query).reject { |symbol| symbol.location.uri.start_with?("https") }
|
66
|
-
|
66
|
+
say("Symbols matching `#{query}`:")
|
67
67
|
symbol_printer.print_objects(res)
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
|
-
desc "symbols", "
|
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|
|
75
75
|
res = client.document_symbols(to_uri(file))
|
76
|
-
|
76
|
+
say("Symbols from `#{file}`:")
|
77
77
|
symbol_printer.print_objects(res)
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
-
desc "refs", "
|
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|
|
85
85
|
res = client.references(to_uri(file), line.to_i, col.to_i)
|
86
|
-
|
86
|
+
say("References to `#{file}:#{line}:#{col}`:")
|
87
87
|
symbol_printer.print_list(res)
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
91
|
-
desc "sigs", "
|
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|
|
95
95
|
res = client.signatures(to_uri(file), line.to_i, col.to_i)
|
96
|
-
|
96
|
+
say("Signature for `#{file}:#{line}:#{col}`:")
|
97
97
|
symbol_printer.print_list(res)
|
98
98
|
end
|
99
99
|
end
|
100
100
|
|
101
|
-
desc "types", "
|
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|
|
105
105
|
res = client.type_definitions(to_uri(file), line.to_i, col.to_i)
|
106
|
-
say
|
106
|
+
say("Type for `#{file}:#{line}:#{col}`:")
|
107
107
|
symbol_printer.print_list(res)
|
108
108
|
end
|
109
109
|
end
|
@@ -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::
|
116
|
+
Spoom::Sorbet::BIN_PATH,
|
117
117
|
"--lsp",
|
118
118
|
"--enable-all-experimental-lsp-features",
|
119
119
|
"--disable-watchman",
|
@@ -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
@@ -8,59 +8,89 @@ module Spoom
|
|
8
8
|
|
9
9
|
default_task :tc
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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, 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
|
-
|
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
|
-
|
38
|
+
output, status = T.unsafe(Spoom::Sorbet).srb_tc(*arg, path: path, capture_err: false, sorbet_bin: sorbet)
|
39
|
+
say_error(output, status: nil, nl: false)
|
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
|
-
|
31
|
-
|
45
|
+
say_error(output, status: nil, nl: false)
|
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 =
|
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.
|
42
|
-
|
43
|
-
|
44
|
-
|
64
|
+
lines = errors.map { |e| format_error(e, format || DEFAULT_FORMAT) }
|
65
|
+
lines = lines.uniq if uniq
|
66
|
+
|
67
|
+
lines.each do |line|
|
68
|
+
say_error(line, status: nil)
|
45
69
|
end
|
46
70
|
|
47
|
-
if
|
48
|
-
|
49
|
-
|
50
|
-
|
71
|
+
if count
|
72
|
+
if errors_count == errors.size
|
73
|
+
say_error("Errors: #{errors_count}", status: nil)
|
74
|
+
else
|
75
|
+
say_error("Errors: #{errors.size} shown, #{errors_count} total", status: nil)
|
76
|
+
end
|
51
77
|
end
|
52
78
|
|
53
|
-
1
|
79
|
+
exit(1)
|
54
80
|
end
|
55
81
|
|
56
82
|
no_commands do
|
57
|
-
def
|
58
|
-
|
59
|
-
code.to_s
|
83
|
+
def format_error(error, format)
|
84
|
+
line = format
|
85
|
+
line = line.gsub(/%C/, yellow(error.code.to_s))
|
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
|
63
|
-
return message unless
|
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
|
data/lib/spoom/coverage.rb
CHANGED
@@ -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
|
-
|
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::
|
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::
|
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::
|
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
|