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
@@ -0,0 +1,58 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module LSP
|
6
|
+
# Base messaging
|
7
|
+
# We don't use T::Struct for those so we can subclass them
|
8
|
+
|
9
|
+
# A general message as defined by JSON-RPC.
|
10
|
+
#
|
11
|
+
# The language server protocol always uses `"2.0"` as the `jsonrpc` version.
|
12
|
+
class Message
|
13
|
+
attr_reader :jsonrpc
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@jsonrpc = '2.0'
|
17
|
+
end
|
18
|
+
|
19
|
+
def as_json
|
20
|
+
instance_variables.each_with_object({}) do |var, obj|
|
21
|
+
val = instance_variable_get(var)
|
22
|
+
obj[var.to_s.delete('@')] = val if val
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_json(*args)
|
27
|
+
as_json.to_json(*args)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# A request message to describe a request between the client and the server.
|
32
|
+
#
|
33
|
+
# Every processed request must send a response back to the sender of the request.
|
34
|
+
class Request < Message
|
35
|
+
attr_reader :id, :method, :params
|
36
|
+
|
37
|
+
def initialize(id, method, params)
|
38
|
+
super()
|
39
|
+
@id = id
|
40
|
+
@method = method
|
41
|
+
@params = params
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# A notification message.
|
46
|
+
#
|
47
|
+
# A processed notification message must not send a response back. They work like events.
|
48
|
+
class Notification < Message
|
49
|
+
attr_reader :method, :params
|
50
|
+
|
51
|
+
def initialize(method, params)
|
52
|
+
super()
|
53
|
+
@method = method
|
54
|
+
@params = params
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module LSP
|
6
|
+
class Error < StandardError
|
7
|
+
class AlreadyOpen < Error; end
|
8
|
+
class BadHeaders < Error; end
|
9
|
+
|
10
|
+
class Diagnostics < Error
|
11
|
+
attr_reader :uri, :diagnostics
|
12
|
+
|
13
|
+
def self.from_json(json)
|
14
|
+
Diagnostics.new(
|
15
|
+
json['uri'],
|
16
|
+
json['diagnostics'].map { |d| Diagnostic.from_json(d) }
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(uri, diagnostics)
|
21
|
+
@uri = uri
|
22
|
+
@diagnostics = diagnostics
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class ResponseError < Error
|
28
|
+
attr_reader :code, :message, :data
|
29
|
+
|
30
|
+
def self.from_json(json)
|
31
|
+
ResponseError.new(
|
32
|
+
json['code'],
|
33
|
+
json['message'],
|
34
|
+
json['data']
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(code, message, data)
|
39
|
+
@code = code
|
40
|
+
@message = message
|
41
|
+
@data = data
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module LSP
|
6
|
+
class Hover < T::Struct
|
7
|
+
const :contents, String
|
8
|
+
const :range, T.nilable(Range)
|
9
|
+
|
10
|
+
def self.from_json(json)
|
11
|
+
Hover.new(
|
12
|
+
contents: json['contents']['value'],
|
13
|
+
range: json['range'] ? Range.from_json(json['range']) : nil
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def accept_printer(printer)
|
18
|
+
printer.print("#{contents}\n")
|
19
|
+
printer.visit(range) if range
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
"#{contents} (#{range})."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Position < T::Struct
|
28
|
+
const :line, Integer
|
29
|
+
const :char, Integer
|
30
|
+
|
31
|
+
def self.from_json(json)
|
32
|
+
Position.new(
|
33
|
+
line: json['line'].to_i,
|
34
|
+
char: json['character'].to_i
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def accept_printer(printer)
|
39
|
+
printer.print("#{line}:#{char}".light_black)
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
"#{line}:#{char}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Range < T::Struct
|
48
|
+
const :start, Position
|
49
|
+
const :end, Position
|
50
|
+
|
51
|
+
def self.from_json(json)
|
52
|
+
Range.new(
|
53
|
+
start: Position.from_json(json['start']),
|
54
|
+
end: Position.from_json(json['end'])
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
def accept_printer(printer)
|
59
|
+
printer.visit(start)
|
60
|
+
printer.print("-".light_black)
|
61
|
+
printer.visit(self.end)
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_s
|
65
|
+
"#{start}-#{self.end}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Location < T::Struct
|
70
|
+
const :uri, String
|
71
|
+
const :range, LSP::Range
|
72
|
+
|
73
|
+
def self.from_json(json)
|
74
|
+
Location.new(
|
75
|
+
uri: json['uri'],
|
76
|
+
range: Range.from_json(json['range'])
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def accept_printer(printer)
|
81
|
+
printer.print("#{uri.from_uri}:".light_black)
|
82
|
+
printer.visit(range)
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_s
|
86
|
+
"#{uri}:#{range})."
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class SignatureHelp < T::Struct
|
91
|
+
const :label, T.nilable(String)
|
92
|
+
const :doc, Object # TODO
|
93
|
+
const :params, T::Array[T.untyped] # TODO
|
94
|
+
|
95
|
+
def self.from_json(json)
|
96
|
+
SignatureHelp.new(
|
97
|
+
label: json['label'],
|
98
|
+
doc: json['documentation'],
|
99
|
+
params: json['parameters'],
|
100
|
+
)
|
101
|
+
end
|
102
|
+
|
103
|
+
def accept_printer(printer)
|
104
|
+
printer.print(label)
|
105
|
+
printer.print("(")
|
106
|
+
printer.print(params.map { |l| "#{l['label']}: #{l['documentation']}" }.join(", "))
|
107
|
+
printer.print(")")
|
108
|
+
end
|
109
|
+
|
110
|
+
def to_s
|
111
|
+
"#{label}(#{params})."
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class Diagnostic < T::Struct
|
116
|
+
const :range, LSP::Range
|
117
|
+
const :code, Integer
|
118
|
+
const :message, String
|
119
|
+
const :informations, Object
|
120
|
+
|
121
|
+
def self.from_json(json)
|
122
|
+
Diagnostic.new(
|
123
|
+
range: Range.from_json(json['range']),
|
124
|
+
code: json['code'].to_i,
|
125
|
+
message: json['message'],
|
126
|
+
informations: json['relatedInformation']
|
127
|
+
)
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_s
|
131
|
+
"Error: #{message} (#{code})."
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class DocumentSymbol < T::Struct
|
136
|
+
const :name, String
|
137
|
+
const :detail, T.nilable(String)
|
138
|
+
const :kind, Integer
|
139
|
+
const :location, T.nilable(Location)
|
140
|
+
const :range, T.nilable(Range)
|
141
|
+
const :children, T::Array[DocumentSymbol]
|
142
|
+
|
143
|
+
def self.from_json(json)
|
144
|
+
DocumentSymbol.new(
|
145
|
+
name: json['name'],
|
146
|
+
detail: json['detail'],
|
147
|
+
kind: json['kind'],
|
148
|
+
location: json['location'] ? Location.from_json(json['location']) : nil,
|
149
|
+
range: json['range'] ? Range.from_json(json['range']) : nil,
|
150
|
+
children: json['children'] ? json['children'].map { |symbol| DocumentSymbol.from_json(symbol) } : [],
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
def accept_printer(printer)
|
155
|
+
h = serialize.hash
|
156
|
+
return if printer.seen.include?(h)
|
157
|
+
printer.seen.add(h)
|
158
|
+
|
159
|
+
printer.printt
|
160
|
+
printer.print(kind_string)
|
161
|
+
printer.print(' ')
|
162
|
+
printer.print(name.blue.bold)
|
163
|
+
printer.print(' ('.light_black)
|
164
|
+
if range
|
165
|
+
printer.visit(range)
|
166
|
+
elsif location
|
167
|
+
printer.visit(location)
|
168
|
+
end
|
169
|
+
printer.print(')'.light_black)
|
170
|
+
printer.printn
|
171
|
+
unless children.empty?
|
172
|
+
printer.indent
|
173
|
+
printer.visit(children)
|
174
|
+
printer.dedent
|
175
|
+
end
|
176
|
+
# TODO: also display details?
|
177
|
+
end
|
178
|
+
|
179
|
+
def to_s
|
180
|
+
"#{name} (#{range})"
|
181
|
+
end
|
182
|
+
|
183
|
+
def kind_string
|
184
|
+
return "<unknown:#{kind}>" unless SYMBOL_KINDS.key?(kind)
|
185
|
+
SYMBOL_KINDS[kind]
|
186
|
+
end
|
187
|
+
|
188
|
+
SYMBOL_KINDS = {
|
189
|
+
1 => "file",
|
190
|
+
2 => "module",
|
191
|
+
3 => "namespace",
|
192
|
+
4 => "package",
|
193
|
+
5 => "class",
|
194
|
+
6 => "def",
|
195
|
+
7 => "property",
|
196
|
+
8 => "field",
|
197
|
+
9 => "constructor",
|
198
|
+
10 => "enum",
|
199
|
+
11 => "interface",
|
200
|
+
12 => "function",
|
201
|
+
13 => "variable",
|
202
|
+
14 => "const",
|
203
|
+
15 => "string",
|
204
|
+
16 => "number",
|
205
|
+
17 => "boolean",
|
206
|
+
18 => "array",
|
207
|
+
19 => "object",
|
208
|
+
20 => "key",
|
209
|
+
21 => "null",
|
210
|
+
22 => "enum_member",
|
211
|
+
23 => "struct",
|
212
|
+
24 => "event",
|
213
|
+
25 => "operator",
|
214
|
+
26 => "type_parameter",
|
215
|
+
}
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Sorbet
|
6
|
+
class Metrics < T::Struct
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
DEFAULT_PREFIX = "ruby_typer.unknown.."
|
10
|
+
SIGILS = T.let(["ignore", "false", "true", "strict", "strong", "__STDLIB_INTERNAL"], T::Array[String])
|
11
|
+
|
12
|
+
const :repo, String
|
13
|
+
const :sha, String
|
14
|
+
const :status, String
|
15
|
+
const :branch, String
|
16
|
+
const :timestamp, Integer
|
17
|
+
const :uuid, String
|
18
|
+
const :metrics, T::Hash[String, T.nilable(Integer)]
|
19
|
+
|
20
|
+
sig { params(path: String, prefix: String).returns(Metrics) }
|
21
|
+
def self.parse_file(path, prefix = DEFAULT_PREFIX)
|
22
|
+
parse_string(File.read(path), prefix)
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { params(string: String, prefix: String).returns(Metrics) }
|
26
|
+
def self.parse_string(string, prefix = DEFAULT_PREFIX)
|
27
|
+
parse_hash(JSON.parse(string), prefix)
|
28
|
+
end
|
29
|
+
|
30
|
+
sig { params(obj: T::Hash[String, T.untyped], prefix: String).returns(Metrics) }
|
31
|
+
def self.parse_hash(obj, prefix = DEFAULT_PREFIX)
|
32
|
+
Metrics.new(
|
33
|
+
repo: obj.fetch("repo"),
|
34
|
+
sha: obj.fetch("sha"),
|
35
|
+
status: obj.fetch("status"),
|
36
|
+
branch: obj.fetch("branch"),
|
37
|
+
timestamp: obj.fetch("timestamp").to_i,
|
38
|
+
uuid: obj.fetch("uuid"),
|
39
|
+
metrics: obj["metrics"].each_with_object({}) do |metric, all|
|
40
|
+
name = metric["name"]
|
41
|
+
name = name.sub(prefix, '')
|
42
|
+
all[name] = metric["value"].to_i
|
43
|
+
end,
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
sig { returns(T::Hash[String, T.nilable(Integer)]) }
|
48
|
+
def files_by_strictness
|
49
|
+
SIGILS.each_with_object({}) do |sigil, map|
|
50
|
+
map[sigil] = metrics["types.input.files.sigil.#{sigil}"]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
sig { returns(Integer) }
|
55
|
+
def files_count
|
56
|
+
files_by_strictness.values.compact.sum
|
57
|
+
end
|
58
|
+
|
59
|
+
sig { params(key: String).returns(T.nilable(Integer)) }
|
60
|
+
def [](key)
|
61
|
+
metrics[key]
|
62
|
+
end
|
63
|
+
|
64
|
+
sig { returns(String) }
|
65
|
+
def to_s
|
66
|
+
"Metrics<#{repo}-#{timestamp}-#{status}>"
|
67
|
+
end
|
68
|
+
|
69
|
+
sig { params(out: T.any(IO, StringIO)).void }
|
70
|
+
def show(out = $stdout)
|
71
|
+
files = files_count
|
72
|
+
|
73
|
+
out.puts "Sigils:"
|
74
|
+
out.puts " files: #{files}"
|
75
|
+
files_by_strictness.each do |sigil, value|
|
76
|
+
next unless value
|
77
|
+
out.puts " #{sigil}: #{value}#{percent(value, files)}"
|
78
|
+
end
|
79
|
+
|
80
|
+
out.puts "\nMethods:"
|
81
|
+
m = metrics['types.input.methods.total']
|
82
|
+
s = metrics['types.sig.count']
|
83
|
+
out.puts " methods: #{m}"
|
84
|
+
out.puts " signatures: #{s}#{percent(s, m)}"
|
85
|
+
|
86
|
+
out.puts "\nSends:"
|
87
|
+
t = metrics['types.input.sends.typed']
|
88
|
+
s = metrics['types.input.sends.total']
|
89
|
+
out.puts " sends: #{s}"
|
90
|
+
out.puts " typed: #{t}#{percent(t, s)}"
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
sig { params(value: T.nilable(Integer), total: T.nilable(Integer)).returns(String) }
|
96
|
+
def percent(value, total)
|
97
|
+
return "" if value.nil? || total.nil? || total == 0
|
98
|
+
" (#{value * 100 / total}%)"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/spoom/version.rb
CHANGED
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spoom
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexandre Terrasa
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: sorbet-runtime
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: bundler
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +52,20 @@ dependencies:
|
|
66
52
|
- - "~>"
|
67
53
|
- !ruby/object:Gem::Version
|
68
54
|
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sorbet-runtime
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: sorbet
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +80,34 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: 0.5.5
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: thor
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.19.2
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.19.2
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: colorize
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
83
111
|
description:
|
84
112
|
email:
|
85
113
|
- alexandre.terrasa@shopify.com
|
@@ -93,7 +121,21 @@ files:
|
|
93
121
|
- Rakefile
|
94
122
|
- exe/spoom
|
95
123
|
- lib/spoom.rb
|
124
|
+
- lib/spoom/cli.rb
|
125
|
+
- lib/spoom/cli/commands/base.rb
|
126
|
+
- lib/spoom/cli/commands/config.rb
|
127
|
+
- lib/spoom/cli/commands/lsp.rb
|
128
|
+
- lib/spoom/cli/commands/run.rb
|
129
|
+
- lib/spoom/cli/symbol_printer.rb
|
130
|
+
- lib/spoom/config.rb
|
131
|
+
- lib/spoom/sorbet.rb
|
96
132
|
- lib/spoom/sorbet/config.rb
|
133
|
+
- lib/spoom/sorbet/errors.rb
|
134
|
+
- lib/spoom/sorbet/lsp.rb
|
135
|
+
- lib/spoom/sorbet/lsp/base.rb
|
136
|
+
- lib/spoom/sorbet/lsp/errors.rb
|
137
|
+
- lib/spoom/sorbet/lsp/structures.rb
|
138
|
+
- lib/spoom/sorbet/metrics.rb
|
97
139
|
- lib/spoom/version.rb
|
98
140
|
homepage: https://github.com/Shopify/spoom
|
99
141
|
licenses:
|