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