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