spoom 1.0.4 → 1.0.9
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 +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
|