spoom 1.0.3 → 1.0.4
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 +4 -0
- data/Rakefile +1 -0
- data/exe/spoom +7 -0
- data/lib/spoom.rb +8 -1
- data/lib/spoom/cli.rb +33 -0
- data/lib/spoom/cli/commands/base.rb +36 -0
- data/lib/spoom/cli/commands/config.rb +67 -0
- data/lib/spoom/cli/commands/lsp.rb +156 -0
- data/lib/spoom/cli/commands/run.rb +92 -0
- data/lib/spoom/cli/symbol_printer.rb +71 -0
- data/lib/spoom/config.rb +11 -0
- data/lib/spoom/sorbet.rb +70 -0
- data/lib/spoom/sorbet/config.rb +21 -9
- data/lib/spoom/sorbet/errors.rb +129 -0
- data/lib/spoom/sorbet/lsp.rb +194 -0
- data/lib/spoom/sorbet/lsp/base.rb +58 -0
- data/lib/spoom/sorbet/lsp/errors.rb +45 -0
- data/lib/spoom/sorbet/lsp/structures.rb +218 -0
- data/lib/spoom/sorbet/metrics.rb +102 -0
- data/lib/spoom/version.rb +2 -1
- metadata +58 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d851bf66dbfb8e1d316b27466fde8ee205e55e68b9b489c3f8f234c03e1763d2
|
4
|
+
data.tar.gz: '068972f0ebb9393400dcbf68837be0a58114cc55560438fd329be3b54acfcaaf'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8315d9c7de9beaf2aa9a5d6963cde78ec6e9e7949659bac85a0ff719080755808fb5a854c2a03f6dcab8134c17bc8d90d145d53d32a3f1632d83a2c6a21ebfaa
|
7
|
+
data.tar.gz: 88fee999fa79caf40dd776ce5253a1337a5acc2507653a2c09adfb3e4723b6a8a06033d34c8f299b3a7a663ef01c2fa4a8243d98fd9f104e9c77ad69f9836ff7
|
data/Gemfile
CHANGED
data/Rakefile
CHANGED
data/exe/spoom
CHANGED
data/lib/spoom.rb
CHANGED
@@ -1,6 +1,13 @@
|
|
1
|
+
# typed: true
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require "sorbet-runtime"
|
4
5
|
|
6
|
+
module Spoom
|
7
|
+
class Error < StandardError; end
|
8
|
+
end
|
9
|
+
|
10
|
+
require "spoom/cli"
|
11
|
+
require "spoom/config"
|
12
|
+
require "spoom/sorbet"
|
5
13
|
require "spoom/version"
|
6
|
-
require "spoom/sorbet/config"
|
data/lib/spoom/cli.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "thor"
|
5
|
+
|
6
|
+
require_relative "cli/commands/config"
|
7
|
+
require_relative "cli/commands/lsp"
|
8
|
+
require_relative "cli/commands/run"
|
9
|
+
|
10
|
+
module Spoom
|
11
|
+
module Cli
|
12
|
+
class Main < Thor
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
class_option :no_color, desc: "Don't use colors", type: :boolean
|
16
|
+
|
17
|
+
desc "config", "manage Sorbet config"
|
18
|
+
subcommand "config", Spoom::Cli::Commands::Config
|
19
|
+
|
20
|
+
desc "lsp", "send LSP requests to Sorbet"
|
21
|
+
subcommand "lsp", Spoom::Cli::Commands::LSP
|
22
|
+
|
23
|
+
desc "tc", "run Sorbet and parses its output"
|
24
|
+
subcommand "tc", Spoom::Cli::Commands::Run
|
25
|
+
|
26
|
+
# Utils
|
27
|
+
|
28
|
+
def self.exit_on_failure?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'stringio'
|
5
|
+
|
6
|
+
module Spoom
|
7
|
+
module Cli
|
8
|
+
module Commands
|
9
|
+
class Base < Thor
|
10
|
+
no_commands do
|
11
|
+
def say_error(message, status = "Error")
|
12
|
+
status = set_color(status, :red)
|
13
|
+
|
14
|
+
buffer = StringIO.new
|
15
|
+
buffer << "#{status}: #{message}"
|
16
|
+
buffer << "\n" unless message.end_with?("\n")
|
17
|
+
|
18
|
+
$stderr.print(buffer.string)
|
19
|
+
$stderr.flush
|
20
|
+
end
|
21
|
+
|
22
|
+
def in_sorbet_project?
|
23
|
+
File.file?(Spoom::Config::SORBET_CONFIG)
|
24
|
+
end
|
25
|
+
|
26
|
+
def in_sorbet_project!
|
27
|
+
unless in_sorbet_project?
|
28
|
+
say_error("not in a Sorbet project (no sorbet/config)")
|
29
|
+
exit(1)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "base"
|
5
|
+
require_relative "../../sorbet/config"
|
6
|
+
|
7
|
+
module Spoom
|
8
|
+
module Cli
|
9
|
+
module Commands
|
10
|
+
class Config < Base
|
11
|
+
default_task :show
|
12
|
+
|
13
|
+
desc "show", "show Sorbet config"
|
14
|
+
def show
|
15
|
+
in_sorbet_project!
|
16
|
+
config = Spoom::Sorbet::Config.parse_file(Spoom::Config::SORBET_CONFIG)
|
17
|
+
|
18
|
+
say("Found Sorbet config at `#{Spoom::Config::SORBET_CONFIG}`.")
|
19
|
+
|
20
|
+
say("\nPaths typechecked:")
|
21
|
+
if config.paths.empty?
|
22
|
+
say(" * (default: .)")
|
23
|
+
else
|
24
|
+
config.paths.each do |path|
|
25
|
+
say(" * #{path}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
say("\nPaths ignored:")
|
30
|
+
if config.ignore.empty?
|
31
|
+
say(" * (default: none)")
|
32
|
+
else
|
33
|
+
config.ignore.each do |path|
|
34
|
+
say(" * #{path}")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
say("\nAllowed extensions:")
|
39
|
+
if config.allowed_extensions.empty?
|
40
|
+
say(" * .rb (default)")
|
41
|
+
say(" * .rbi (default)")
|
42
|
+
else
|
43
|
+
config.allowed_extensions.each do |ext|
|
44
|
+
say(" * #{ext}")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
desc "files", "show files matching Sorbet config"
|
50
|
+
def files
|
51
|
+
in_sorbet_project!
|
52
|
+
config = Spoom::Sorbet::Config.parse_file(Spoom::Config::SORBET_CONFIG)
|
53
|
+
files = Spoom::Sorbet.srb_files(config)
|
54
|
+
|
55
|
+
say("Files matching `#{Spoom::Config::SORBET_CONFIG}`:")
|
56
|
+
if files.empty?
|
57
|
+
say(" NONE")
|
58
|
+
else
|
59
|
+
files.each do |path|
|
60
|
+
say(" * #{path}")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'shellwords'
|
5
|
+
|
6
|
+
require_relative "base"
|
7
|
+
require_relative "../symbol_printer"
|
8
|
+
require_relative "../../sorbet/lsp"
|
9
|
+
|
10
|
+
module Spoom
|
11
|
+
module Cli
|
12
|
+
module Commands
|
13
|
+
class LSP < Base
|
14
|
+
default_task :show
|
15
|
+
|
16
|
+
desc "interactive", "interactive LSP mode"
|
17
|
+
def show
|
18
|
+
in_sorbet_project!
|
19
|
+
lsp = lsp_client
|
20
|
+
# TODO: run interactive mode
|
21
|
+
puts lsp
|
22
|
+
end
|
23
|
+
|
24
|
+
desc "list", "list all known symbols"
|
25
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
26
|
+
def list
|
27
|
+
run do |client|
|
28
|
+
Dir["**/*.rb"].each do |file|
|
29
|
+
res = client.document_symbols(file.to_uri)
|
30
|
+
next if res.empty?
|
31
|
+
puts "Symbols from `#{file}`:"
|
32
|
+
SymbolPrinter.print_object(res, options["no_color"])
|
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(file.to_uri, line.to_i, col.to_i)
|
42
|
+
say "Hovering `#{file}:#{line}:#{col}`:"
|
43
|
+
if res
|
44
|
+
SymbolPrinter.print_object(res, options["no_color"])
|
45
|
+
else
|
46
|
+
puts "<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(file.to_uri, line.to_i, col.to_i)
|
56
|
+
puts "Definitions for `#{file}:#{line}:#{col}`:"
|
57
|
+
SymbolPrinter.print_list(res, options["no_color"])
|
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)
|
66
|
+
puts "Symbols matching `#{query}`:"
|
67
|
+
printer = SymbolPrinter.new(2, options["no_color"])
|
68
|
+
res.each do |symbol|
|
69
|
+
next if symbol.location.uri.start_with?("https")
|
70
|
+
printer.visit(symbol)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
desc "symbols", "list symbols from a file"
|
76
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
77
|
+
def symbols(file)
|
78
|
+
run do |client|
|
79
|
+
res = client.document_symbols(file.to_uri)
|
80
|
+
puts "Symbols from `#{file}`:"
|
81
|
+
SymbolPrinter.print_object(res, options["no_color"])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
desc "refs", "list references to a symbol"
|
86
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
87
|
+
def refs(file, line, col)
|
88
|
+
run do |client|
|
89
|
+
res = client.references(file.to_uri, line.to_i, col.to_i)
|
90
|
+
puts "References to `#{file}:#{line}:#{col}`:"
|
91
|
+
SymbolPrinter.print_list(res, options["no_color"])
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
desc "sigs", "list signatures for a symbol"
|
96
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
97
|
+
def sigs(file, line, col)
|
98
|
+
run do |client|
|
99
|
+
res = client.signatures(file.to_uri, line.to_i, col.to_i)
|
100
|
+
puts "Signature for `#{file}:#{line}:#{col}`:"
|
101
|
+
SymbolPrinter.print_list(res, options["no_color"])
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
desc "types", "display type of a symbol"
|
106
|
+
# TODO: options, filter, limit, kind etc.. filter rbi
|
107
|
+
def types(file, line, col)
|
108
|
+
run do |client|
|
109
|
+
res = client.type_definitions(file.to_uri, line.to_i, col.to_i)
|
110
|
+
say "Type for `#{file}:#{line}:#{col}`:"
|
111
|
+
SymbolPrinter.print_list(res, options["no_color"])
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
no_commands do
|
116
|
+
def lsp_client
|
117
|
+
in_sorbet_project!
|
118
|
+
client = Spoom::LSP::Client.new(
|
119
|
+
Spoom::Config::SORBET_PATH,
|
120
|
+
"--lsp",
|
121
|
+
"--enable-all-experimental-lsp-features",
|
122
|
+
"--disable-watchman",
|
123
|
+
)
|
124
|
+
client.open(Spoom::Config::WORKSPACE_PATH)
|
125
|
+
client
|
126
|
+
end
|
127
|
+
|
128
|
+
def run(&block)
|
129
|
+
client = lsp_client
|
130
|
+
block.call(client)
|
131
|
+
rescue Spoom::LSP::Error::Diagnostics => err
|
132
|
+
say_error("Sorbet returned typechecking errors for `#{err.uri.from_uri}`")
|
133
|
+
err.diagnostics.each do |d|
|
134
|
+
say_error("#{d.message} (#{d.code})", " #{d.range}")
|
135
|
+
end
|
136
|
+
exit(1)
|
137
|
+
rescue Spoom::LSP::Error::BadHeaders => err
|
138
|
+
say_error("Sorbet didn't answer correctly (#{err.message})")
|
139
|
+
exit(1)
|
140
|
+
rescue Spoom::LSP::Error => err
|
141
|
+
say_error(err.message)
|
142
|
+
exit(1)
|
143
|
+
ensure
|
144
|
+
begin
|
145
|
+
client&.close
|
146
|
+
rescue
|
147
|
+
# We can't do much if Sorbet refuse to close.
|
148
|
+
# We kill the parent process and let the child be killed.
|
149
|
+
exit(1)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "base"
|
5
|
+
|
6
|
+
module Spoom
|
7
|
+
module Cli
|
8
|
+
module Commands
|
9
|
+
class Run < Base
|
10
|
+
default_task :tc
|
11
|
+
|
12
|
+
desc "tc", "run srb tc"
|
13
|
+
option :limit, type: :numeric, aliases: :l
|
14
|
+
option :code, type: :numeric, aliases: :c
|
15
|
+
option :sort, type: :string, aliases: :s
|
16
|
+
def tc
|
17
|
+
in_sorbet_project!
|
18
|
+
|
19
|
+
limit = options[:limit]
|
20
|
+
sort = options[:sort]
|
21
|
+
code = options[:code]
|
22
|
+
colors = !options[:no_color]
|
23
|
+
|
24
|
+
unless limit || code || sort
|
25
|
+
return Spoom::Sorbet.srb_tc(capture_err: false).last
|
26
|
+
end
|
27
|
+
|
28
|
+
output, status = Spoom::Sorbet.srb_tc(capture_err: true)
|
29
|
+
if status
|
30
|
+
$stderr.print(output)
|
31
|
+
return 0
|
32
|
+
end
|
33
|
+
|
34
|
+
errors = Spoom::Sorbet::Errors::Parser.parse_string(output)
|
35
|
+
errors_count = errors.size
|
36
|
+
|
37
|
+
errors = sort == "code" ? errors.sort_by { |e| [e.code, e.file, e.line, e.message] } : errors.sort
|
38
|
+
errors = errors.select { |e| e.code == code } if code
|
39
|
+
errors = T.must(errors.slice(0, limit)) if limit
|
40
|
+
|
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}"
|
45
|
+
end
|
46
|
+
|
47
|
+
if errors_count == errors.size
|
48
|
+
$stderr.puts "Errors: #{errors_count}"
|
49
|
+
else
|
50
|
+
$stderr.puts "Errors: #{errors.size} shown, #{errors_count} total"
|
51
|
+
end
|
52
|
+
|
53
|
+
1
|
54
|
+
end
|
55
|
+
|
56
|
+
desc "metrics", "run srb tc and display metrics"
|
57
|
+
def metrics
|
58
|
+
in_sorbet_project!
|
59
|
+
|
60
|
+
metrics = Spoom::Sorbet.srb_metrics(capture_err: false)
|
61
|
+
if metrics
|
62
|
+
metrics.show
|
63
|
+
else
|
64
|
+
puts "no data"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
no_commands do
|
69
|
+
def colorize_code(code, colors = true)
|
70
|
+
return code.to_s unless colors
|
71
|
+
code.to_s.light_black
|
72
|
+
end
|
73
|
+
|
74
|
+
def colorize_message(message, colors = true)
|
75
|
+
return message unless colors
|
76
|
+
|
77
|
+
cyan = T.let(false, T::Boolean)
|
78
|
+
word = StringIO.new
|
79
|
+
message.chars.each do |c|
|
80
|
+
if c == '`'
|
81
|
+
cyan = !cyan
|
82
|
+
next
|
83
|
+
end
|
84
|
+
word << (cyan ? c.cyan : c.red)
|
85
|
+
end
|
86
|
+
word.string
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "colorize"
|
5
|
+
|
6
|
+
module Spoom
|
7
|
+
module Cli
|
8
|
+
class SymbolPrinter
|
9
|
+
attr_accessor :seen, :no_color
|
10
|
+
|
11
|
+
def self.print_list(list, no_color)
|
12
|
+
printer = SymbolPrinter.new(2, no_color)
|
13
|
+
list.each do |item|
|
14
|
+
printer.print(" * ")
|
15
|
+
printer.visit(item)
|
16
|
+
printer.printn
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.print_object(object, no_color)
|
21
|
+
printer = SymbolPrinter.new(2, no_color)
|
22
|
+
printer.visit(object)
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(default_indent, no_color = false)
|
26
|
+
@seen = Set.new
|
27
|
+
@current_indent = default_indent
|
28
|
+
@no_color = no_color
|
29
|
+
String.disable_colorization = no_color
|
30
|
+
end
|
31
|
+
|
32
|
+
def indent
|
33
|
+
@current_indent += 2
|
34
|
+
end
|
35
|
+
|
36
|
+
def dedent
|
37
|
+
@current_indent -= 2
|
38
|
+
end
|
39
|
+
|
40
|
+
def print(string)
|
41
|
+
Kernel.print(string)
|
42
|
+
end
|
43
|
+
|
44
|
+
def printn
|
45
|
+
print("\n")
|
46
|
+
end
|
47
|
+
|
48
|
+
def printt
|
49
|
+
print(" " * @current_indent)
|
50
|
+
end
|
51
|
+
|
52
|
+
def visit(object)
|
53
|
+
if object.is_a?(Array)
|
54
|
+
object.each { |e| visit(e) }
|
55
|
+
else
|
56
|
+
object.accept_printer(self)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class String
|
64
|
+
def to_uri
|
65
|
+
"file://" + File.join(Spoom::Config::WORKSPACE_PATH, self)
|
66
|
+
end
|
67
|
+
|
68
|
+
def from_uri
|
69
|
+
sub("file://#{Spoom::Config::WORKSPACE_PATH}", "")
|
70
|
+
end
|
71
|
+
end
|