spoom 1.0.4 → 1.0.9
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 +296 -1
- data/Rakefile +1 -0
- data/lib/spoom.rb +21 -2
- data/lib/spoom/cli.rb +56 -10
- data/lib/spoom/cli/bump.rb +138 -0
- data/lib/spoom/cli/config.rb +51 -0
- data/lib/spoom/cli/coverage.rb +206 -0
- data/lib/spoom/cli/helper.rb +149 -0
- data/lib/spoom/cli/lsp.rb +165 -0
- data/lib/spoom/cli/run.rb +109 -0
- data/lib/spoom/coverage.rb +89 -0
- data/lib/spoom/coverage/d3.rb +110 -0
- data/lib/spoom/coverage/d3/base.rb +50 -0
- data/lib/spoom/coverage/d3/circle_map.rb +195 -0
- data/lib/spoom/coverage/d3/pie.rb +175 -0
- data/lib/spoom/coverage/d3/timeline.rb +486 -0
- data/lib/spoom/coverage/report.rb +308 -0
- data/lib/spoom/coverage/snapshot.rb +132 -0
- data/lib/spoom/file_tree.rb +196 -0
- data/lib/spoom/git.rb +98 -0
- data/lib/spoom/printer.rb +80 -0
- data/lib/spoom/sorbet.rb +99 -47
- data/lib/spoom/sorbet/config.rb +30 -0
- data/lib/spoom/sorbet/errors.rb +33 -15
- data/lib/spoom/sorbet/lsp.rb +2 -4
- data/lib/spoom/sorbet/lsp/structures.rb +108 -14
- data/lib/spoom/sorbet/metrics.rb +10 -79
- data/lib/spoom/sorbet/sigils.rb +98 -0
- data/lib/spoom/test_helpers/project.rb +112 -0
- data/lib/spoom/timeline.rb +53 -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 +28 -11
- data/lib/spoom/cli/commands/base.rb +0 -36
- data/lib/spoom/cli/commands/config.rb +0 -67
- data/lib/spoom/cli/commands/lsp.rb +0 -156
- data/lib/spoom/cli/commands/run.rb +0 -92
- data/lib/spoom/cli/symbol_printer.rb +0 -71
- data/lib/spoom/config.rb +0 -11
@@ -0,0 +1,165 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'shellwords'
|
5
|
+
|
6
|
+
require_relative "../sorbet/lsp"
|
7
|
+
|
8
|
+
module Spoom
|
9
|
+
module Cli
|
10
|
+
class LSP < Thor
|
11
|
+
include Helper
|
12
|
+
|
13
|
+
default_task :show
|
14
|
+
|
15
|
+
desc "interactive", "Interactive LSP mode"
|
16
|
+
def show
|
17
|
+
in_sorbet_project!
|
18
|
+
lsp = lsp_client
|
19
|
+
# TODO: run interactive mode
|
20
|
+
puts lsp
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "list", "List all known symbols"
|
24
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
25
|
+
def list
|
26
|
+
run do |client|
|
27
|
+
printer = symbol_printer
|
28
|
+
Dir["**/*.rb"].each do |file|
|
29
|
+
res = client.document_symbols(to_uri(file))
|
30
|
+
next if res.empty?
|
31
|
+
say("Symbols from `#{file}`:")
|
32
|
+
printer.print_objects(res)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "hover", "Request hover informations"
|
38
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
39
|
+
def hover(file, line, col)
|
40
|
+
run do |client|
|
41
|
+
res = client.hover(to_uri(file), line.to_i, col.to_i)
|
42
|
+
say("Hovering `#{file}:#{line}:#{col}`:")
|
43
|
+
if res
|
44
|
+
symbol_printer.print_object(res)
|
45
|
+
else
|
46
|
+
say("<no data>")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
desc "defs", "List definitions of a symbol"
|
52
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
53
|
+
def defs(file, line, col)
|
54
|
+
run do |client|
|
55
|
+
res = client.definitions(to_uri(file), line.to_i, col.to_i)
|
56
|
+
say("Definitions for `#{file}:#{line}:#{col}`:")
|
57
|
+
symbol_printer.print_list(res)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
desc "find", "Find symbols matching a query"
|
62
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
63
|
+
def find(query)
|
64
|
+
run do |client|
|
65
|
+
res = client.symbols(query).reject { |symbol| symbol.location.uri.start_with?("https") }
|
66
|
+
say("Symbols matching `#{query}`:")
|
67
|
+
symbol_printer.print_objects(res)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
desc "symbols", "List symbols from a file"
|
72
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
73
|
+
def symbols(file)
|
74
|
+
run do |client|
|
75
|
+
res = client.document_symbols(to_uri(file))
|
76
|
+
say("Symbols from `#{file}`:")
|
77
|
+
symbol_printer.print_objects(res)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
desc "refs", "List references to a symbol"
|
82
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
83
|
+
def refs(file, line, col)
|
84
|
+
run do |client|
|
85
|
+
res = client.references(to_uri(file), line.to_i, col.to_i)
|
86
|
+
say("References to `#{file}:#{line}:#{col}`:")
|
87
|
+
symbol_printer.print_list(res)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
desc "sigs", "List signatures for a symbol"
|
92
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
93
|
+
def sigs(file, line, col)
|
94
|
+
run do |client|
|
95
|
+
res = client.signatures(to_uri(file), line.to_i, col.to_i)
|
96
|
+
say("Signature for `#{file}:#{line}:#{col}`:")
|
97
|
+
symbol_printer.print_list(res)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
desc "types", "Display type of a symbol"
|
102
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
103
|
+
def types(file, line, col)
|
104
|
+
run do |client|
|
105
|
+
res = client.type_definitions(to_uri(file), line.to_i, col.to_i)
|
106
|
+
say("Type for `#{file}:#{line}:#{col}`:")
|
107
|
+
symbol_printer.print_list(res)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
no_commands do
|
112
|
+
def lsp_client
|
113
|
+
in_sorbet_project!
|
114
|
+
path = exec_path
|
115
|
+
client = Spoom::LSP::Client.new(
|
116
|
+
Spoom::Sorbet::BIN_PATH,
|
117
|
+
"--lsp",
|
118
|
+
"--enable-all-experimental-lsp-features",
|
119
|
+
"--disable-watchman",
|
120
|
+
path: path
|
121
|
+
)
|
122
|
+
client.open(File.expand_path(path))
|
123
|
+
client
|
124
|
+
end
|
125
|
+
|
126
|
+
def symbol_printer
|
127
|
+
Spoom::LSP::SymbolPrinter.new(
|
128
|
+
indent_level: 2,
|
129
|
+
colors: options[:color],
|
130
|
+
prefix: "file://#{File.expand_path(exec_path)}"
|
131
|
+
)
|
132
|
+
end
|
133
|
+
|
134
|
+
def run(&block)
|
135
|
+
client = lsp_client
|
136
|
+
block.call(client)
|
137
|
+
rescue Spoom::LSP::Error::Diagnostics => err
|
138
|
+
say_error("Sorbet returned typechecking errors for `#{symbol_printer.clean_uri(err.uri)}`")
|
139
|
+
err.diagnostics.each do |d|
|
140
|
+
say_error("#{d.message} (#{d.code})", status: " #{d.range}")
|
141
|
+
end
|
142
|
+
exit(1)
|
143
|
+
rescue Spoom::LSP::Error::BadHeaders => err
|
144
|
+
say_error("Sorbet didn't answer correctly (#{err.message})")
|
145
|
+
exit(1)
|
146
|
+
rescue Spoom::LSP::Error => err
|
147
|
+
say_error(err.message)
|
148
|
+
exit(1)
|
149
|
+
ensure
|
150
|
+
begin
|
151
|
+
client&.close
|
152
|
+
rescue
|
153
|
+
# We can't do much if Sorbet refuse to close.
|
154
|
+
# We kill the parent process and let the child be killed.
|
155
|
+
exit(1)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def to_uri(path)
|
160
|
+
"file://" + File.join(File.expand_path(exec_path), path)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Cli
|
6
|
+
class Run < Thor
|
7
|
+
include Helper
|
8
|
+
|
9
|
+
default_task :tc
|
10
|
+
|
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)
|
26
|
+
in_sorbet_project!
|
27
|
+
|
28
|
+
path = exec_path
|
29
|
+
limit = options[:limit]
|
30
|
+
sort = options[:sort]
|
31
|
+
code = options[:code]
|
32
|
+
uniq = options[:uniq]
|
33
|
+
format = options[:format]
|
34
|
+
count = options[:count]
|
35
|
+
sorbet = options[:sorbet]
|
36
|
+
|
37
|
+
unless limit || code || sort
|
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)
|
41
|
+
end
|
42
|
+
|
43
|
+
output, status = T.unsafe(Spoom::Sorbet).srb_tc(*arg, path: path, capture_err: true, sorbet_bin: sorbet)
|
44
|
+
if status
|
45
|
+
say_error(output, status: nil, nl: false)
|
46
|
+
exit(0)
|
47
|
+
end
|
48
|
+
|
49
|
+
errors = Spoom::Sorbet::Errors::Parser.parse_string(output)
|
50
|
+
errors_count = errors.size
|
51
|
+
|
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
|
+
|
61
|
+
errors = errors.select { |e| e.code == code } if code
|
62
|
+
errors = T.must(errors.slice(0, limit)) if limit
|
63
|
+
|
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)
|
69
|
+
end
|
70
|
+
|
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
|
77
|
+
end
|
78
|
+
|
79
|
+
exit(1)
|
80
|
+
end
|
81
|
+
|
82
|
+
no_commands do
|
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
|
90
|
+
end
|
91
|
+
|
92
|
+
def colorize_message(message)
|
93
|
+
return message unless color?
|
94
|
+
|
95
|
+
cyan = T.let(false, T::Boolean)
|
96
|
+
word = StringIO.new
|
97
|
+
message.chars.each do |c|
|
98
|
+
if c == '`'
|
99
|
+
cyan = !cyan
|
100
|
+
next
|
101
|
+
end
|
102
|
+
word << (cyan ? c.cyan : c.red)
|
103
|
+
end
|
104
|
+
word.string
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "coverage/snapshot"
|
5
|
+
require_relative "coverage/report"
|
6
|
+
require_relative "file_tree"
|
7
|
+
|
8
|
+
require "date"
|
9
|
+
|
10
|
+
module Spoom
|
11
|
+
module Coverage
|
12
|
+
extend T::Sig
|
13
|
+
|
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
|
+
|
30
|
+
snapshot = Snapshot.new
|
31
|
+
return snapshot unless metrics
|
32
|
+
|
33
|
+
sha = Spoom::Git.last_commit(path: path)
|
34
|
+
snapshot.commit_sha = sha
|
35
|
+
snapshot.commit_timestamp = Spoom::Git.commit_timestamp(sha, path: path).to_i if sha
|
36
|
+
|
37
|
+
snapshot.files = metrics.fetch("types.input.files", 0)
|
38
|
+
snapshot.modules = metrics.fetch("types.input.modules.total", 0)
|
39
|
+
snapshot.classes = metrics.fetch("types.input.classes.total", 0)
|
40
|
+
snapshot.singleton_classes = metrics.fetch("types.input.singleton_classes.total", 0)
|
41
|
+
snapshot.methods_with_sig = metrics.fetch("types.sig.count", 0)
|
42
|
+
snapshot.methods_without_sig = metrics.fetch("types.input.methods.total", 0) - snapshot.methods_with_sig
|
43
|
+
snapshot.calls_typed = metrics.fetch("types.input.sends.typed", 0)
|
44
|
+
snapshot.calls_untyped = metrics.fetch("types.input.sends.total", 0) - snapshot.calls_typed
|
45
|
+
|
46
|
+
snapshot.duration += metrics.fetch("run.utilization.system_time.us", 0)
|
47
|
+
snapshot.duration += metrics.fetch("run.utilization.user_time.us", 0)
|
48
|
+
|
49
|
+
Snapshot::STRICTNESSES.each do |strictness|
|
50
|
+
next unless metrics.key?("types.input.files.sigil.#{strictness}")
|
51
|
+
snapshot.sigils[strictness] = T.must(metrics["types.input.files.sigil.#{strictness}"])
|
52
|
+
end
|
53
|
+
|
54
|
+
snapshot.version_static = Spoom::Sorbet.version_from_gemfile_lock(gem: "sorbet-static", path: path)
|
55
|
+
snapshot.version_runtime = Spoom::Sorbet.version_from_gemfile_lock(gem: "sorbet-runtime", path: path)
|
56
|
+
|
57
|
+
snapshot
|
58
|
+
end
|
59
|
+
|
60
|
+
sig { params(snapshots: T::Array[Snapshot], palette: D3::ColorPalette, path: String).returns(Report) }
|
61
|
+
def self.report(snapshots, palette:, path: ".")
|
62
|
+
intro_commit = Git.sorbet_intro_commit(path: path)
|
63
|
+
intro_date = intro_commit ? Git.commit_time(intro_commit, path: path) : nil
|
64
|
+
|
65
|
+
Report.new(
|
66
|
+
project_name: File.basename(File.expand_path(path)),
|
67
|
+
palette: palette,
|
68
|
+
snapshots: snapshots,
|
69
|
+
sigils_tree: sigils_tree(path: path),
|
70
|
+
sorbet_intro_commit: intro_commit,
|
71
|
+
sorbet_intro_date: intro_date,
|
72
|
+
)
|
73
|
+
end
|
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
|
+
|
80
|
+
sig { params(path: String).returns(FileTree) }
|
81
|
+
def self.sigils_tree(path: ".")
|
82
|
+
config = sorbet_config(path: path)
|
83
|
+
files = Sorbet.srb_files(config, path: path)
|
84
|
+
files.select! { |file| file =~ /\.rb$/ }
|
85
|
+
files.reject! { |file| file =~ %r{/test/} }
|
86
|
+
FileTree.new(files, strip_prefix: path)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "d3/circle_map"
|
5
|
+
require_relative "d3/pie"
|
6
|
+
require_relative "d3/timeline"
|
7
|
+
|
8
|
+
module Spoom
|
9
|
+
module Coverage
|
10
|
+
module D3
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
COLOR_IGNORE = "#999"
|
14
|
+
COLOR_FALSE = "#db4437"
|
15
|
+
COLOR_TRUE = "#0f9d58"
|
16
|
+
COLOR_STRICT = "#0a7340"
|
17
|
+
COLOR_STRONG = "#064828"
|
18
|
+
|
19
|
+
sig { returns(String) }
|
20
|
+
def self.header_style
|
21
|
+
<<~CSS
|
22
|
+
svg {
|
23
|
+
width: 100%;
|
24
|
+
height: 100%;
|
25
|
+
}
|
26
|
+
|
27
|
+
.tooltip {
|
28
|
+
font: 12px Arial, sans-serif;
|
29
|
+
color: #fff;
|
30
|
+
text-align: center;
|
31
|
+
background: rgba(0, 0, 0, 0.6);
|
32
|
+
padding: 5px;
|
33
|
+
border: 0px;
|
34
|
+
border-radius: 4px;
|
35
|
+
position: absolute;
|
36
|
+
top: 0;
|
37
|
+
left: 0;
|
38
|
+
opacity: 0;
|
39
|
+
}
|
40
|
+
|
41
|
+
.label {
|
42
|
+
font: 14px Arial, sans-serif;
|
43
|
+
font-weight: bold;
|
44
|
+
fill: #fff;
|
45
|
+
text-anchor: middle;
|
46
|
+
pointer-events: none;
|
47
|
+
}
|
48
|
+
|
49
|
+
.label .small {
|
50
|
+
font-size: 10px;
|
51
|
+
}
|
52
|
+
|
53
|
+
#{Pie.header_style}
|
54
|
+
#{CircleMap.header_style}
|
55
|
+
#{Timeline.header_style}
|
56
|
+
CSS
|
57
|
+
end
|
58
|
+
|
59
|
+
sig { params(palette: ColorPalette).returns(String) }
|
60
|
+
def self.header_script(palette)
|
61
|
+
<<~JS
|
62
|
+
var parseDate = d3.timeParse("%s");
|
63
|
+
|
64
|
+
function strictnessColor(strictness) {
|
65
|
+
switch(strictness) {
|
66
|
+
case "ignore":
|
67
|
+
return "#{palette.ignore}";
|
68
|
+
case "false":
|
69
|
+
return "#{palette.false}";
|
70
|
+
case "true":
|
71
|
+
return "#{palette.true}";
|
72
|
+
case "strict":
|
73
|
+
return "#{palette.strict}";
|
74
|
+
case "strong":
|
75
|
+
return "#{palette.strong}";
|
76
|
+
}
|
77
|
+
return "#{palette.false}";
|
78
|
+
}
|
79
|
+
|
80
|
+
function toPercent(value, sum) {
|
81
|
+
return value ? Math.round(value * 100 / sum) : 0;
|
82
|
+
}
|
83
|
+
|
84
|
+
var tooltip = d3.select("body")
|
85
|
+
.append("div")
|
86
|
+
.append("div")
|
87
|
+
.attr("class", "tooltip");
|
88
|
+
|
89
|
+
function moveTooltip(d) {
|
90
|
+
return tooltip
|
91
|
+
.style("left", (d3.event.pageX + 20) + "px")
|
92
|
+
.style("top", (d3.event.pageY) + "px")
|
93
|
+
}
|
94
|
+
|
95
|
+
#{Pie.header_script}
|
96
|
+
#{CircleMap.header_script}
|
97
|
+
#{Timeline.header_script}
|
98
|
+
JS
|
99
|
+
end
|
100
|
+
|
101
|
+
class ColorPalette < T::Struct
|
102
|
+
prop :ignore, String
|
103
|
+
prop :false, String
|
104
|
+
prop :true, String
|
105
|
+
prop :strict, String
|
106
|
+
prop :strong, String
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|