spoom 1.1.6 → 1.1.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 +1 -1
- data/Rakefile +1 -1
- data/exe/spoom +1 -1
- data/lib/spoom/cli/bump.rb +6 -4
- data/lib/spoom/cli/run.rb +9 -9
- data/lib/spoom/git.rb +42 -36
- data/lib/spoom/sorbet/errors.rb +31 -24
- data/lib/spoom/sorbet/lsp/base.rb +28 -5
- data/lib/spoom/sorbet/lsp/errors.rb +22 -3
- data/lib/spoom/sorbet/lsp/structures.rb +26 -7
- data/lib/spoom/sorbet/lsp.rb +44 -6
- data/lib/spoom/sorbet.rb +11 -11
- data/lib/spoom/test_helpers/project.rb +17 -7
- data/lib/spoom/timeline.rb +3 -3
- data/lib/spoom/version.rb +1 -1
- data/lib/spoom.rb +26 -8
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 54a219cd3c4f0e2ea998e4aa5a81a7556c9aed944fdfe38f6aff1267c421336e
|
4
|
+
data.tar.gz: 10671ff002575fc844189cd4371c8f5c35960fc09c3335e7d3a9a234f89fd4ec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3413d1409199b453275c3e41bfa79c4a4b88632d2917c7215eb46812768378a767f6eb9b6b79c3f2eeb7a99d14942bb63c3bdf0f4112cfd8be731ff76706dd06
|
7
|
+
data.tar.gz: 1ccd808b2ed86a945e44c6dc4a5a8bdc231b64eacaa70a3b6a4757b0e415911c9328d70ae148158751e36d320045e8f5d9dc23e4a08aa4305bceb36860f7f346
|
data/Gemfile
CHANGED
data/Rakefile
CHANGED
data/exe/spoom
CHANGED
data/lib/spoom/cli/bump.rb
CHANGED
@@ -83,14 +83,16 @@ module Spoom
|
|
83
83
|
exit(files_to_bump.empty?)
|
84
84
|
end
|
85
85
|
|
86
|
-
|
86
|
+
error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
|
87
|
+
result = Sorbet.srb_tc(
|
87
88
|
"--no-error-sections",
|
89
|
+
"--error-url-base=#{error_url_base}",
|
88
90
|
path: exec_path,
|
89
91
|
capture_err: true,
|
90
92
|
sorbet_bin: options[:sorbet]
|
91
93
|
)
|
92
94
|
|
93
|
-
check_sorbet_segfault(exit_code) do
|
95
|
+
check_sorbet_segfault(result.exit_code) do
|
94
96
|
say_error(<<~ERR, status: nil)
|
95
97
|
It means one of the file bumped to `typed: #{to}` made Sorbet crash.
|
96
98
|
Run `spoom bump -f` locally followed by `bundle exec srb tc` to investigate the problem.
|
@@ -98,13 +100,13 @@ module Spoom
|
|
98
100
|
undo_changes(files_to_bump, from)
|
99
101
|
end
|
100
102
|
|
101
|
-
if status
|
103
|
+
if result.status
|
102
104
|
print_changes(files_to_bump, command: cmd, from: from, to: to, dry: dry, path: exec_path)
|
103
105
|
undo_changes(files_to_bump, from) if dry
|
104
106
|
exit(files_to_bump.empty?)
|
105
107
|
end
|
106
108
|
|
107
|
-
errors = Sorbet::Errors::Parser.parse_string(
|
109
|
+
errors = Sorbet::Errors::Parser.parse_string(result.err, error_url_base: error_url_base)
|
108
110
|
|
109
111
|
files_with_errors = errors.map do |err|
|
110
112
|
path = File.expand_path(err.file)
|
data/lib/spoom/cli/run.rb
CHANGED
@@ -35,33 +35,33 @@ module Spoom
|
|
35
35
|
sorbet = options[:sorbet]
|
36
36
|
|
37
37
|
unless limit || code || sort
|
38
|
-
|
38
|
+
result = T.unsafe(Spoom::Sorbet).srb_tc(
|
39
39
|
*arg,
|
40
40
|
path: path,
|
41
41
|
capture_err: false,
|
42
42
|
sorbet_bin: sorbet
|
43
43
|
)
|
44
44
|
|
45
|
-
check_sorbet_segfault(
|
46
|
-
say_error(
|
47
|
-
exit(status)
|
45
|
+
check_sorbet_segfault(result.code)
|
46
|
+
say_error(result.err, status: nil, nl: false)
|
47
|
+
exit(result.status)
|
48
48
|
end
|
49
49
|
|
50
|
-
|
50
|
+
result = T.unsafe(Spoom::Sorbet).srb_tc(
|
51
51
|
*arg,
|
52
52
|
path: path,
|
53
53
|
capture_err: true,
|
54
54
|
sorbet_bin: sorbet
|
55
55
|
)
|
56
56
|
|
57
|
-
check_sorbet_segfault(exit_code)
|
57
|
+
check_sorbet_segfault(result.exit_code)
|
58
58
|
|
59
|
-
if status
|
60
|
-
say_error(
|
59
|
+
if result.status
|
60
|
+
say_error(result.err, status: nil, nl: false)
|
61
61
|
exit(0)
|
62
62
|
end
|
63
63
|
|
64
|
-
errors = Spoom::Sorbet::Errors::Parser.parse_string(
|
64
|
+
errors = Spoom::Sorbet::Errors::Parser.parse_string(result.err)
|
65
65
|
errors_count = errors.size
|
66
66
|
|
67
67
|
errors = case sort
|
data/lib/spoom/git.rb
CHANGED
@@ -9,52 +9,58 @@ module Spoom
|
|
9
9
|
extend T::Sig
|
10
10
|
|
11
11
|
# Execute a `command`
|
12
|
-
sig { params(command: String, arg: String, path: String).returns(
|
12
|
+
sig { params(command: String, arg: String, path: String).returns(ExecResult) }
|
13
13
|
def self.exec(command, *arg, path: '.')
|
14
|
-
return
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
14
|
+
return ExecResult.new(
|
15
|
+
out: "",
|
16
|
+
err: "Error: `#{path}` is not a directory.",
|
17
|
+
status: false,
|
18
|
+
exit_code: 1
|
19
|
+
) unless File.directory?(path)
|
20
|
+
|
21
|
+
T.unsafe(Open3).popen3(command, *arg, chdir: path) do |_, stdout, stderr, thread|
|
22
|
+
status = T.cast(thread.value, Process::Status)
|
23
|
+
ExecResult.new(
|
24
|
+
out: stdout.read,
|
25
|
+
err: stderr.read,
|
26
|
+
status: T.must(status.success?),
|
27
|
+
exit_code: T.must(status.exitstatus)
|
28
|
+
)
|
29
|
+
end
|
24
30
|
end
|
25
31
|
|
26
32
|
# Git commands
|
27
33
|
|
28
|
-
sig { params(arg: String, path: String).returns(
|
34
|
+
sig { params(arg: String, path: String).returns(ExecResult) }
|
29
35
|
def self.checkout(*arg, path: ".")
|
30
36
|
exec("git checkout -q #{arg.join(' ')}", path: path)
|
31
37
|
end
|
32
38
|
|
33
|
-
sig { params(arg: String, path: String).returns(
|
39
|
+
sig { params(arg: String, path: String).returns(ExecResult) }
|
34
40
|
def self.diff(*arg, path: ".")
|
35
41
|
exec("git diff #{arg.join(' ')}", path: path)
|
36
42
|
end
|
37
43
|
|
38
|
-
sig { params(arg: String, path: String).returns(
|
44
|
+
sig { params(arg: String, path: String).returns(ExecResult) }
|
39
45
|
def self.log(*arg, path: ".")
|
40
46
|
exec("git log #{arg.join(' ')}", path: path)
|
41
47
|
end
|
42
48
|
|
43
|
-
sig { params(arg: String, path: String).returns(
|
49
|
+
sig { params(arg: String, path: String).returns(ExecResult) }
|
44
50
|
def self.rev_parse(*arg, path: ".")
|
45
51
|
exec("git rev-parse --short #{arg.join(' ')}", path: path)
|
46
52
|
end
|
47
53
|
|
48
|
-
sig { params(arg: String, path: String).returns(
|
54
|
+
sig { params(arg: String, path: String).returns(ExecResult) }
|
49
55
|
def self.show(*arg, path: ".")
|
50
56
|
exec("git show #{arg.join(' ')}", path: path)
|
51
57
|
end
|
52
58
|
|
53
59
|
sig { params(path: String).returns(T.nilable(String)) }
|
54
60
|
def self.current_branch(path: ".")
|
55
|
-
|
56
|
-
return nil unless status
|
57
|
-
out.strip
|
61
|
+
result = exec("git branch --show-current", path: path)
|
62
|
+
return nil unless result.status
|
63
|
+
result.out.strip
|
58
64
|
end
|
59
65
|
|
60
66
|
# Utils
|
@@ -62,9 +68,9 @@ module Spoom
|
|
62
68
|
# Get the commit epoch timestamp for a `sha`
|
63
69
|
sig { params(sha: String, path: String).returns(T.nilable(Integer)) }
|
64
70
|
def self.commit_timestamp(sha, path: ".")
|
65
|
-
|
66
|
-
return nil unless status
|
67
|
-
out.strip.to_i
|
71
|
+
result = show("--no-notes --no-patch --pretty=%at #{sha}", path: path)
|
72
|
+
return nil unless result.status
|
73
|
+
result.out.strip.to_i
|
68
74
|
end
|
69
75
|
|
70
76
|
# Get the commit Time for a `sha`
|
@@ -78,9 +84,9 @@ module Spoom
|
|
78
84
|
# Get the last commit sha
|
79
85
|
sig { params(path: String).returns(T.nilable(String)) }
|
80
86
|
def self.last_commit(path: ".")
|
81
|
-
|
82
|
-
return nil unless status
|
83
|
-
out.strip
|
87
|
+
result = rev_parse("HEAD", path: path)
|
88
|
+
return nil unless result.status
|
89
|
+
result.out.strip
|
84
90
|
end
|
85
91
|
|
86
92
|
# Translate a git epoch timestamp into a Time
|
@@ -92,27 +98,27 @@ module Spoom
|
|
92
98
|
# Is there uncommited changes in `path`?
|
93
99
|
sig { params(path: String).returns(T::Boolean) }
|
94
100
|
def self.workdir_clean?(path: ".")
|
95
|
-
diff("HEAD", path: path).
|
101
|
+
diff("HEAD", path: path).out.empty?
|
96
102
|
end
|
97
103
|
|
98
104
|
# Get the hash of the commit introducing the `sorbet/config` file
|
99
105
|
sig { params(path: String).returns(T.nilable(String)) }
|
100
106
|
def self.sorbet_intro_commit(path: ".")
|
101
|
-
|
102
|
-
return nil unless status
|
103
|
-
|
104
|
-
return nil if
|
105
|
-
|
107
|
+
result = Spoom::Git.log("--diff-filter=A --format='%h' -1 -- sorbet/config", path: path)
|
108
|
+
return nil unless result.status
|
109
|
+
out = result.out.strip
|
110
|
+
return nil if out.empty?
|
111
|
+
out
|
106
112
|
end
|
107
113
|
|
108
114
|
# Get the hash of the commit removing the `sorbet/config` file
|
109
115
|
sig { params(path: String).returns(T.nilable(String)) }
|
110
116
|
def self.sorbet_removal_commit(path: ".")
|
111
|
-
|
112
|
-
return nil unless status
|
113
|
-
|
114
|
-
return nil if
|
115
|
-
|
117
|
+
result = Spoom::Git.log("--diff-filter=D --format='%h' -1 -- sorbet/config", path: path)
|
118
|
+
return nil unless result.status
|
119
|
+
out = result.out.strip
|
120
|
+
return nil if out.empty?
|
121
|
+
out
|
116
122
|
end
|
117
123
|
end
|
118
124
|
end
|
data/lib/spoom/sorbet/errors.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
module Spoom
|
@@ -6,41 +6,31 @@ module Spoom
|
|
6
6
|
module Errors
|
7
7
|
extend T::Sig
|
8
8
|
|
9
|
+
DEFAULT_ERROR_URL_BASE = "https://srb.help/"
|
10
|
+
|
9
11
|
# Parse errors from Sorbet output
|
10
12
|
class Parser
|
11
13
|
extend T::Sig
|
12
14
|
|
13
|
-
HEADER = [
|
15
|
+
HEADER = T.let([
|
14
16
|
"👋 Hey there! Heads up that this is not a release build of sorbet.",
|
15
17
|
"Release builds are faster and more well-supported by the Sorbet team.",
|
16
18
|
"Check out the README to learn how to build Sorbet in release mode.",
|
17
19
|
"To forcibly silence this error, either pass --silence-dev-message,",
|
18
20
|
"or set SORBET_SILENCE_DEV_MESSAGE=1 in your shell environment.",
|
19
|
-
]
|
20
|
-
|
21
|
-
ERROR_LINE_MATCH_REGEX = %r{
|
22
|
-
^ # match beginning of line
|
23
|
-
(\S[^:]*) # capture filename as something that starts with a non-space character
|
24
|
-
# followed by anything that is not a colon character
|
25
|
-
: # match the filename - line number seperator
|
26
|
-
(\d+) # capture the line number
|
27
|
-
:\s # match the line number - error message separator
|
28
|
-
(.*) # capture the error message
|
29
|
-
\shttps://srb.help/ # match the error code url prefix
|
30
|
-
(\d+) # capture the error code
|
31
|
-
$ # match end of line
|
32
|
-
}x.freeze
|
21
|
+
], T::Array[String])
|
33
22
|
|
34
|
-
sig { params(output: String).returns(T::Array[Error]) }
|
35
|
-
def self.parse_string(output)
|
36
|
-
parser = Spoom::Sorbet::Errors::Parser.new
|
23
|
+
sig { params(output: String, error_url_base: String).returns(T::Array[Error]) }
|
24
|
+
def self.parse_string(output, error_url_base: DEFAULT_ERROR_URL_BASE)
|
25
|
+
parser = Spoom::Sorbet::Errors::Parser.new(error_url_base: error_url_base)
|
37
26
|
parser.parse(output)
|
38
27
|
end
|
39
28
|
|
40
|
-
sig { void }
|
41
|
-
def initialize
|
42
|
-
@errors = []
|
43
|
-
@
|
29
|
+
sig { params(error_url_base: String).void }
|
30
|
+
def initialize(error_url_base: DEFAULT_ERROR_URL_BASE)
|
31
|
+
@errors = T.let([], T::Array[Error])
|
32
|
+
@error_line_match_regex = T.let(error_line_match_regexp(error_url_base), Regexp)
|
33
|
+
@current_error = T.let(nil, T.nilable(Error))
|
44
34
|
end
|
45
35
|
|
46
36
|
sig { params(output: String).returns(T::Array[Error]) }
|
@@ -66,9 +56,26 @@ module Spoom
|
|
66
56
|
|
67
57
|
private
|
68
58
|
|
59
|
+
sig { params(error_url_base: String).returns(Regexp) }
|
60
|
+
def error_line_match_regexp(error_url_base)
|
61
|
+
url = Regexp.escape(error_url_base)
|
62
|
+
%r{
|
63
|
+
^ # match beginning of line
|
64
|
+
(\S[^:]*) # capture filename as something that starts with a non-space character
|
65
|
+
# followed by anything that is not a colon character
|
66
|
+
: # match the filename - line number seperator
|
67
|
+
(\d+) # capture the line number
|
68
|
+
:\s # match the line number - error message separator
|
69
|
+
(.*) # capture the error message
|
70
|
+
\s#{url} # match the error code url prefix
|
71
|
+
(\d+) # capture the error code
|
72
|
+
$ # match end of line
|
73
|
+
}x
|
74
|
+
end
|
75
|
+
|
69
76
|
sig { params(line: String).returns(T.nilable(Error)) }
|
70
77
|
def match_error_line(line)
|
71
|
-
match = line.match(
|
78
|
+
match = line.match(@error_line_match_regex)
|
72
79
|
return unless match
|
73
80
|
|
74
81
|
file, line, message, code = match.captures
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
module Spoom
|
@@ -10,12 +10,17 @@ module Spoom
|
|
10
10
|
#
|
11
11
|
# The language server protocol always uses `"2.0"` as the `jsonrpc` version.
|
12
12
|
class Message
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
sig { returns(String) }
|
13
16
|
attr_reader :jsonrpc
|
14
17
|
|
18
|
+
sig { void }
|
15
19
|
def initialize
|
16
|
-
@jsonrpc =
|
20
|
+
@jsonrpc = T.let("2.0", String)
|
17
21
|
end
|
18
22
|
|
23
|
+
sig { returns(T::Hash[T.untyped, T.untyped]) }
|
19
24
|
def as_json
|
20
25
|
instance_variables.each_with_object({}) do |var, obj|
|
21
26
|
val = instance_variable_get(var)
|
@@ -23,8 +28,9 @@ module Spoom
|
|
23
28
|
end
|
24
29
|
end
|
25
30
|
|
31
|
+
sig { params(args: T.untyped).returns(String) }
|
26
32
|
def to_json(*args)
|
27
|
-
as_json.to_json(*args)
|
33
|
+
T.unsafe(as_json).to_json(*args)
|
28
34
|
end
|
29
35
|
end
|
30
36
|
|
@@ -32,8 +38,18 @@ module Spoom
|
|
32
38
|
#
|
33
39
|
# Every processed request must send a response back to the sender of the request.
|
34
40
|
class Request < Message
|
35
|
-
|
41
|
+
extend T::Sig
|
42
|
+
|
43
|
+
sig { returns(Integer) }
|
44
|
+
attr_reader :id
|
45
|
+
|
46
|
+
sig { returns(String) }
|
47
|
+
attr_reader :method
|
36
48
|
|
49
|
+
sig { returns(T::Hash[T.untyped, T.untyped]) }
|
50
|
+
attr_reader :params
|
51
|
+
|
52
|
+
sig { params(id: Integer, method: String, params: T::Hash[T.untyped, T.untyped]).void }
|
37
53
|
def initialize(id, method, params)
|
38
54
|
super()
|
39
55
|
@id = id
|
@@ -46,8 +62,15 @@ module Spoom
|
|
46
62
|
#
|
47
63
|
# A processed notification message must not send a response back. They work like events.
|
48
64
|
class Notification < Message
|
49
|
-
|
65
|
+
extend T::Sig
|
66
|
+
|
67
|
+
sig { returns(String) }
|
68
|
+
attr_reader :method
|
69
|
+
|
70
|
+
sig { returns(T::Hash[T.untyped, T.untyped]) }
|
71
|
+
attr_reader :params
|
50
72
|
|
73
|
+
sig { params(method: String, params: T::Hash[T.untyped, T.untyped]).void }
|
51
74
|
def initialize(method, params)
|
52
75
|
super()
|
53
76
|
@method = method
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
module Spoom
|
@@ -8,8 +8,15 @@ module Spoom
|
|
8
8
|
class BadHeaders < Error; end
|
9
9
|
|
10
10
|
class Diagnostics < Error
|
11
|
-
|
11
|
+
extend T::Sig
|
12
12
|
|
13
|
+
sig { returns(String) }
|
14
|
+
attr_reader :uri
|
15
|
+
|
16
|
+
sig { returns(T::Array[Diagnostic]) }
|
17
|
+
attr_reader :diagnostics
|
18
|
+
|
19
|
+
sig { params(json: T::Hash[T.untyped, T.untyped]).returns(Diagnostics) }
|
13
20
|
def self.from_json(json)
|
14
21
|
Diagnostics.new(
|
15
22
|
json['uri'],
|
@@ -17,6 +24,7 @@ module Spoom
|
|
17
24
|
)
|
18
25
|
end
|
19
26
|
|
27
|
+
sig { params(uri: String, diagnostics: T::Array[Diagnostic]).void }
|
20
28
|
def initialize(uri, diagnostics)
|
21
29
|
@uri = uri
|
22
30
|
@diagnostics = diagnostics
|
@@ -25,8 +33,18 @@ module Spoom
|
|
25
33
|
end
|
26
34
|
|
27
35
|
class ResponseError < Error
|
28
|
-
|
36
|
+
extend T::Sig
|
37
|
+
|
38
|
+
sig { returns(Integer) }
|
39
|
+
attr_reader :code
|
40
|
+
|
41
|
+
sig { returns(String) }
|
42
|
+
attr_reader :message
|
43
|
+
|
44
|
+
sig { returns(T::Hash[T.untyped, T.untyped]) }
|
45
|
+
attr_reader :data
|
29
46
|
|
47
|
+
sig { params(json: T::Hash[T.untyped, T.untyped]).returns(ResponseError) }
|
30
48
|
def self.from_json(json)
|
31
49
|
ResponseError.new(
|
32
50
|
json['code'],
|
@@ -35,6 +53,7 @@ module Spoom
|
|
35
53
|
)
|
36
54
|
end
|
37
55
|
|
56
|
+
sig { params(code: Integer, message: String, data: T::Hash[T.untyped, T.untyped]).void }
|
38
57
|
def initialize(code, message, data)
|
39
58
|
@code = code
|
40
59
|
@message = message
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require_relative "../../printer"
|
@@ -23,6 +23,7 @@ module Spoom
|
|
23
23
|
const :contents, String
|
24
24
|
const :range, T.nilable(Range)
|
25
25
|
|
26
|
+
sig { params(json: T::Hash[T.untyped, T.untyped]).returns(Hover) }
|
26
27
|
def self.from_json(json)
|
27
28
|
Hover.new(
|
28
29
|
contents: json['contents']['value'],
|
@@ -36,6 +37,7 @@ module Spoom
|
|
36
37
|
printer.print_object(range) if range
|
37
38
|
end
|
38
39
|
|
40
|
+
sig { returns(String) }
|
39
41
|
def to_s
|
40
42
|
"#{contents} (#{range})."
|
41
43
|
end
|
@@ -48,6 +50,7 @@ module Spoom
|
|
48
50
|
const :line, Integer
|
49
51
|
const :char, Integer
|
50
52
|
|
53
|
+
sig { params(json: T::Hash[T.untyped, T.untyped]).returns(Position) }
|
51
54
|
def self.from_json(json)
|
52
55
|
Position.new(
|
53
56
|
line: json['line'].to_i,
|
@@ -60,6 +63,7 @@ module Spoom
|
|
60
63
|
printer.print_colored("#{line}:#{char}", Color::LIGHT_BLACK)
|
61
64
|
end
|
62
65
|
|
66
|
+
sig { returns(String) }
|
63
67
|
def to_s
|
64
68
|
"#{line}:#{char}"
|
65
69
|
end
|
@@ -72,6 +76,7 @@ module Spoom
|
|
72
76
|
const :start, Position
|
73
77
|
const :end, Position
|
74
78
|
|
79
|
+
sig { params(json: T::Hash[T.untyped, T.untyped]).returns(Range) }
|
75
80
|
def self.from_json(json)
|
76
81
|
Range.new(
|
77
82
|
start: Position.from_json(json['start']),
|
@@ -86,6 +91,7 @@ module Spoom
|
|
86
91
|
printer.print_object(self.end)
|
87
92
|
end
|
88
93
|
|
94
|
+
sig { returns(String) }
|
89
95
|
def to_s
|
90
96
|
"#{start}-#{self.end}"
|
91
97
|
end
|
@@ -98,6 +104,7 @@ module Spoom
|
|
98
104
|
const :uri, String
|
99
105
|
const :range, LSP::Range
|
100
106
|
|
107
|
+
sig { params(json: T::Hash[T.untyped, T.untyped]).returns(Location) }
|
101
108
|
def self.from_json(json)
|
102
109
|
Location.new(
|
103
110
|
uri: json['uri'],
|
@@ -111,6 +118,7 @@ module Spoom
|
|
111
118
|
printer.print_object(range)
|
112
119
|
end
|
113
120
|
|
121
|
+
sig { returns(String) }
|
114
122
|
def to_s
|
115
123
|
"#{uri}:#{range}"
|
116
124
|
end
|
@@ -124,6 +132,7 @@ module Spoom
|
|
124
132
|
const :doc, Object # TODO
|
125
133
|
const :params, T::Array[T.untyped] # TODO
|
126
134
|
|
135
|
+
sig { params(json: T::Hash[T.untyped, T.untyped]).returns(SignatureHelp) }
|
127
136
|
def self.from_json(json)
|
128
137
|
SignatureHelp.new(
|
129
138
|
label: json['label'],
|
@@ -140,6 +149,7 @@ module Spoom
|
|
140
149
|
printer.print(")")
|
141
150
|
end
|
142
151
|
|
152
|
+
sig { returns(String) }
|
143
153
|
def to_s
|
144
154
|
"#{label}(#{params})."
|
145
155
|
end
|
@@ -154,6 +164,7 @@ module Spoom
|
|
154
164
|
const :message, String
|
155
165
|
const :informations, Object
|
156
166
|
|
167
|
+
sig { params(json: T::Hash[T.untyped, T.untyped]).returns(Diagnostic) }
|
157
168
|
def self.from_json(json)
|
158
169
|
Diagnostic.new(
|
159
170
|
range: Range.from_json(json['range']),
|
@@ -168,6 +179,7 @@ module Spoom
|
|
168
179
|
printer.print(to_s)
|
169
180
|
end
|
170
181
|
|
182
|
+
sig { returns(String) }
|
171
183
|
def to_s
|
172
184
|
"Error: #{message} (#{code})."
|
173
185
|
end
|
@@ -184,6 +196,7 @@ module Spoom
|
|
184
196
|
const :range, T.nilable(Range)
|
185
197
|
const :children, T::Array[DocumentSymbol]
|
186
198
|
|
199
|
+
sig { params(json: T::Hash[T.untyped, T.untyped]).returns(DocumentSymbol) }
|
187
200
|
def self.from_json(json)
|
188
201
|
DocumentSymbol.new(
|
189
202
|
name: json['name'],
|
@@ -221,16 +234,17 @@ module Spoom
|
|
221
234
|
# TODO: also display details?
|
222
235
|
end
|
223
236
|
|
237
|
+
sig { returns(String) }
|
224
238
|
def to_s
|
225
239
|
"#{name} (#{range})"
|
226
240
|
end
|
227
241
|
|
242
|
+
sig { returns(String) }
|
228
243
|
def kind_string
|
229
|
-
|
230
|
-
SYMBOL_KINDS[kind]
|
244
|
+
SYMBOL_KINDS[kind] || "<unknown:#{kind}>"
|
231
245
|
end
|
232
246
|
|
233
|
-
SYMBOL_KINDS = {
|
247
|
+
SYMBOL_KINDS = T.let({
|
234
248
|
1 => "file",
|
235
249
|
2 => "module",
|
236
250
|
3 => "namespace",
|
@@ -257,13 +271,17 @@ module Spoom
|
|
257
271
|
24 => "event",
|
258
272
|
25 => "operator",
|
259
273
|
26 => "type_parameter",
|
260
|
-
}
|
274
|
+
}, T::Hash[Integer, String])
|
261
275
|
end
|
262
276
|
|
263
277
|
class SymbolPrinter < Printer
|
264
278
|
extend T::Sig
|
265
279
|
|
266
|
-
|
280
|
+
sig { returns(T::Set[Integer]) }
|
281
|
+
attr_accessor :seen
|
282
|
+
|
283
|
+
sig { returns(T.nilable(String)) }
|
284
|
+
attr_accessor :prefix
|
267
285
|
|
268
286
|
sig do
|
269
287
|
params(
|
@@ -275,7 +293,7 @@ module Spoom
|
|
275
293
|
end
|
276
294
|
def initialize(out: $stdout, colors: true, indent_level: 0, prefix: nil)
|
277
295
|
super(out: out, colors: colors, indent_level: indent_level)
|
278
|
-
@seen = Set.new
|
296
|
+
@seen = T.let(Set.new, T::Set[Integer])
|
279
297
|
@out = out
|
280
298
|
@colors = colors
|
281
299
|
@indent_level = indent_level
|
@@ -295,6 +313,7 @@ module Spoom
|
|
295
313
|
|
296
314
|
sig { params(uri: String).returns(String) }
|
297
315
|
def clean_uri(uri)
|
316
|
+
prefix = self.prefix
|
298
317
|
return uri unless prefix
|
299
318
|
uri.delete_prefix(prefix)
|
300
319
|
end
|
data/lib/spoom/sorbet/lsp.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'open3'
|
@@ -11,24 +11,35 @@ require_relative 'lsp/errors'
|
|
11
11
|
module Spoom
|
12
12
|
module LSP
|
13
13
|
class Client
|
14
|
+
extend T::Sig
|
15
|
+
|
16
|
+
sig { params(sorbet_bin: String, sorbet_args: String, path: String).void }
|
14
17
|
def initialize(sorbet_bin, *sorbet_args, path: ".")
|
15
|
-
@id = 0
|
16
|
-
@
|
18
|
+
@id = T.let(0, Integer)
|
19
|
+
@open = T.let(false, T::Boolean)
|
20
|
+
io_in, io_out, io_err, _status = T.unsafe(Open3).popen3(sorbet_bin, *sorbet_args, chdir: path)
|
21
|
+
@in = T.let(io_in, IO)
|
22
|
+
@out = T.let(io_out, IO)
|
23
|
+
@err = T.let(io_err, IO)
|
17
24
|
end
|
18
25
|
|
26
|
+
sig { returns(Integer) }
|
19
27
|
def next_id
|
20
28
|
@id += 1
|
21
29
|
end
|
22
30
|
|
31
|
+
sig { params(json_string: String).void }
|
23
32
|
def send_raw(json_string)
|
24
33
|
@in.puts("Content-Length:#{json_string.length}\r\n\r\n#{json_string}")
|
25
34
|
end
|
26
35
|
|
36
|
+
sig { params(message: Message).returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
|
27
37
|
def send(message)
|
28
38
|
send_raw(message.to_json)
|
29
39
|
read if message.is_a?(Request)
|
30
40
|
end
|
31
41
|
|
42
|
+
sig { returns(T.nilable(String)) }
|
32
43
|
def read_raw
|
33
44
|
header = @out.gets
|
34
45
|
|
@@ -39,8 +50,12 @@ module Spoom
|
|
39
50
|
@out.read(len + 2) # +2 'cause of the final \r\n
|
40
51
|
end
|
41
52
|
|
53
|
+
sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
|
42
54
|
def read
|
43
|
-
|
55
|
+
raw_string = read_raw
|
56
|
+
return nil unless raw_string
|
57
|
+
|
58
|
+
json = JSON.parse(raw_string)
|
44
59
|
|
45
60
|
# Handle error in the LSP protocol
|
46
61
|
raise ResponseError.from_json(json['error']) if json['error']
|
@@ -53,6 +68,7 @@ module Spoom
|
|
53
68
|
|
54
69
|
# LSP requests
|
55
70
|
|
71
|
+
sig { params(workspace_path: String).void }
|
56
72
|
def open(workspace_path)
|
57
73
|
raise Error::AlreadyOpen, "Error: CLI already opened" if @open
|
58
74
|
send(Request.new(
|
@@ -68,6 +84,7 @@ module Spoom
|
|
68
84
|
@open = true
|
69
85
|
end
|
70
86
|
|
87
|
+
sig { params(uri: String, line: Integer, column: Integer).returns(T.nilable(Hover)) }
|
71
88
|
def hover(uri, line, column)
|
72
89
|
json = send(Request.new(
|
73
90
|
next_id,
|
@@ -82,10 +99,12 @@ module Spoom
|
|
82
99
|
},
|
83
100
|
}
|
84
101
|
))
|
85
|
-
|
102
|
+
|
103
|
+
return nil unless json && json['result']
|
86
104
|
Hover.from_json(json['result'])
|
87
105
|
end
|
88
106
|
|
107
|
+
sig { params(uri: String, line: Integer, column: Integer).returns(T::Array[SignatureHelp]) }
|
89
108
|
def signatures(uri, line, column)
|
90
109
|
json = send(Request.new(
|
91
110
|
next_id,
|
@@ -100,9 +119,12 @@ module Spoom
|
|
100
119
|
},
|
101
120
|
}
|
102
121
|
))
|
122
|
+
|
123
|
+
return [] unless json && json['result'] && json['result']['signatures']
|
103
124
|
json['result']['signatures'].map { |loc| SignatureHelp.from_json(loc) }
|
104
125
|
end
|
105
126
|
|
127
|
+
sig { params(uri: String, line: Integer, column: Integer).returns(T::Array[Location]) }
|
106
128
|
def definitions(uri, line, column)
|
107
129
|
json = send(Request.new(
|
108
130
|
next_id,
|
@@ -117,9 +139,12 @@ module Spoom
|
|
117
139
|
},
|
118
140
|
}
|
119
141
|
))
|
142
|
+
|
143
|
+
return [] unless json && json['result']
|
120
144
|
json['result'].map { |loc| Location.from_json(loc) }
|
121
145
|
end
|
122
146
|
|
147
|
+
sig { params(uri: String, line: Integer, column: Integer).returns(T::Array[Location]) }
|
123
148
|
def type_definitions(uri, line, column)
|
124
149
|
json = send(Request.new(
|
125
150
|
next_id,
|
@@ -134,9 +159,12 @@ module Spoom
|
|
134
159
|
},
|
135
160
|
}
|
136
161
|
))
|
162
|
+
|
163
|
+
return [] unless json && json['result']
|
137
164
|
json['result'].map { |loc| Location.from_json(loc) }
|
138
165
|
end
|
139
166
|
|
167
|
+
sig { params(uri: String, line: Integer, column: Integer, include_decl: T::Boolean).returns(T::Array[Location]) }
|
140
168
|
def references(uri, line, column, include_decl = true)
|
141
169
|
json = send(Request.new(
|
142
170
|
next_id,
|
@@ -154,9 +182,12 @@ module Spoom
|
|
154
182
|
},
|
155
183
|
}
|
156
184
|
))
|
185
|
+
|
186
|
+
return [] unless json && json['result']
|
157
187
|
json['result'].map { |loc| Location.from_json(loc) }
|
158
188
|
end
|
159
189
|
|
190
|
+
sig { params(query: String).returns(T::Array[DocumentSymbol]) }
|
160
191
|
def symbols(query)
|
161
192
|
json = send(Request.new(
|
162
193
|
next_id,
|
@@ -165,9 +196,12 @@ module Spoom
|
|
165
196
|
'query' => query,
|
166
197
|
}
|
167
198
|
))
|
199
|
+
|
200
|
+
return [] unless json && json['result']
|
168
201
|
json['result'].map { |loc| DocumentSymbol.from_json(loc) }
|
169
202
|
end
|
170
203
|
|
204
|
+
sig { params(uri: String).returns(T::Array[DocumentSymbol]) }
|
171
205
|
def document_symbols(uri)
|
172
206
|
json = send(Request.new(
|
173
207
|
next_id,
|
@@ -178,14 +212,18 @@ module Spoom
|
|
178
212
|
},
|
179
213
|
}
|
180
214
|
))
|
215
|
+
|
216
|
+
return [] unless json && json['result']
|
181
217
|
json['result'].map { |loc| DocumentSymbol.from_json(loc) }
|
182
218
|
end
|
183
219
|
|
220
|
+
sig { void }
|
184
221
|
def close
|
185
|
-
send(Request.new(next_id, "shutdown",
|
222
|
+
send(Request.new(next_id, "shutdown", {}))
|
186
223
|
@in.close
|
187
224
|
@out.close
|
188
225
|
@err.close
|
226
|
+
@open = false
|
189
227
|
end
|
190
228
|
end
|
191
229
|
end
|
data/lib/spoom/sorbet.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "spoom/sorbet/config"
|
@@ -12,8 +12,8 @@ require "open3"
|
|
12
12
|
module Spoom
|
13
13
|
module Sorbet
|
14
14
|
CONFIG_PATH = "sorbet/config"
|
15
|
-
GEM_PATH = Gem::Specification.find_by_name("sorbet-static").full_gem_path
|
16
|
-
BIN_PATH = (Pathname.new(GEM_PATH) / "libexec" / "sorbet").to_s
|
15
|
+
GEM_PATH = T.let(Gem::Specification.find_by_name("sorbet-static").full_gem_path, String)
|
16
|
+
BIN_PATH = T.let((Pathname.new(GEM_PATH) / "libexec" / "sorbet").to_s, String)
|
17
17
|
|
18
18
|
SEGFAULT_CODE = 139
|
19
19
|
|
@@ -26,7 +26,7 @@ module Spoom
|
|
26
26
|
path: String,
|
27
27
|
capture_err: T::Boolean,
|
28
28
|
sorbet_bin: T.nilable(String)
|
29
|
-
).returns(
|
29
|
+
).returns(ExecResult)
|
30
30
|
end
|
31
31
|
def srb(*arg, path: '.', capture_err: false, sorbet_bin: nil)
|
32
32
|
if sorbet_bin
|
@@ -34,7 +34,7 @@ module Spoom
|
|
34
34
|
else
|
35
35
|
arg.prepend("bundle", "exec", "srb")
|
36
36
|
end
|
37
|
-
|
37
|
+
Spoom.exec(*T.unsafe(arg), path: path, capture_err: capture_err)
|
38
38
|
end
|
39
39
|
|
40
40
|
sig do
|
@@ -43,11 +43,11 @@ module Spoom
|
|
43
43
|
path: String,
|
44
44
|
capture_err: T::Boolean,
|
45
45
|
sorbet_bin: T.nilable(String)
|
46
|
-
).returns(
|
46
|
+
).returns(ExecResult)
|
47
47
|
end
|
48
48
|
def srb_tc(*arg, path: '.', capture_err: false, sorbet_bin: nil)
|
49
49
|
arg.prepend("tc") unless sorbet_bin
|
50
|
-
T.unsafe(
|
50
|
+
srb(*T.unsafe(arg), path: path, capture_err: capture_err, sorbet_bin: sorbet_bin)
|
51
51
|
end
|
52
52
|
|
53
53
|
# List all files typechecked by Sorbet from its `config`
|
@@ -69,16 +69,16 @@ module Spoom
|
|
69
69
|
).returns(T.nilable(String))
|
70
70
|
end
|
71
71
|
def srb_version(*arg, path: '.', capture_err: false, sorbet_bin: nil)
|
72
|
-
|
72
|
+
result = T.let(T.unsafe(self).srb_tc(
|
73
73
|
"--no-config",
|
74
74
|
"--version",
|
75
75
|
*arg,
|
76
76
|
path: path,
|
77
77
|
capture_err: capture_err,
|
78
78
|
sorbet_bin: sorbet_bin
|
79
|
-
)
|
80
|
-
return nil unless
|
81
|
-
out.split(" ")[2]
|
79
|
+
), ExecResult)
|
80
|
+
return nil unless result.status
|
81
|
+
result.out.split(" ")[2]
|
82
82
|
end
|
83
83
|
|
84
84
|
sig do
|
@@ -62,9 +62,9 @@ module Spoom
|
|
62
62
|
# Actions
|
63
63
|
|
64
64
|
# Run `git init` in this project
|
65
|
-
sig { void }
|
66
|
-
def git_init
|
67
|
-
Spoom::Git.exec("git init -q", path: path)
|
65
|
+
sig { params(branch: String).void }
|
66
|
+
def git_init(branch: "main")
|
67
|
+
Spoom::Git.exec("git init -q -b #{branch}", path: path)
|
68
68
|
Spoom::Git.exec("git config user.name 'spoom-tests'", path: path)
|
69
69
|
Spoom::Git.exec("git config user.email 'spoom@shopify.com'", path: path)
|
70
70
|
end
|
@@ -77,21 +77,31 @@ module Spoom
|
|
77
77
|
end
|
78
78
|
|
79
79
|
# Run `bundle install` in this project
|
80
|
-
sig { returns(
|
80
|
+
sig { returns(ExecResult) }
|
81
81
|
def bundle_install
|
82
82
|
opts = {}
|
83
83
|
opts[:chdir] = path
|
84
84
|
out, err, status = Open3.capture3("bundle", "install", opts)
|
85
|
-
|
85
|
+
ExecResult.new(
|
86
|
+
out: out,
|
87
|
+
err: err,
|
88
|
+
status: T.must(status.success?),
|
89
|
+
exit_code: T.must(status.exitstatus)
|
90
|
+
)
|
86
91
|
end
|
87
92
|
|
88
93
|
# Run a command with `bundle exec` in this project
|
89
|
-
sig { params(cmd: String, args: String).returns(
|
94
|
+
sig { params(cmd: String, args: String).returns(ExecResult) }
|
90
95
|
def bundle_exec(cmd, *args)
|
91
96
|
opts = {}
|
92
97
|
opts[:chdir] = path
|
93
98
|
out, err, status = Open3.capture3(["bundle", "exec", cmd, *args].join(' '), opts)
|
94
|
-
|
99
|
+
ExecResult.new(
|
100
|
+
out: out,
|
101
|
+
err: err,
|
102
|
+
status: T.must(status.success?),
|
103
|
+
exit_code: T.must(status.exitstatus)
|
104
|
+
)
|
95
105
|
end
|
96
106
|
|
97
107
|
# Delete this project and its content
|
data/lib/spoom/timeline.rb
CHANGED
@@ -37,7 +37,7 @@ module Spoom
|
|
37
37
|
sig { params(dates: T::Array[Time]).returns(T::Array[String]) }
|
38
38
|
def commits_for_dates(dates)
|
39
39
|
dates.map do |t|
|
40
|
-
|
40
|
+
result = Spoom::Git.log(
|
41
41
|
"--since='#{t}'",
|
42
42
|
"--until='#{t.to_date.next_month}'",
|
43
43
|
"--format='format:%h'",
|
@@ -45,8 +45,8 @@ module Spoom
|
|
45
45
|
"-1",
|
46
46
|
path: @path,
|
47
47
|
)
|
48
|
-
next if out.empty?
|
49
|
-
out
|
48
|
+
next if result.out.empty?
|
49
|
+
result.out
|
50
50
|
end.compact.uniq
|
51
51
|
end
|
52
52
|
end
|
data/lib/spoom/version.rb
CHANGED
data/lib/spoom.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "sorbet-runtime"
|
@@ -7,24 +7,42 @@ require "pathname"
|
|
7
7
|
module Spoom
|
8
8
|
extend T::Sig
|
9
9
|
|
10
|
-
SPOOM_PATH = (Pathname.new(__FILE__) / ".." / "..").to_s
|
10
|
+
SPOOM_PATH = T.let((Pathname.new(__FILE__) / ".." / "..").to_s, String)
|
11
11
|
|
12
12
|
class Error < StandardError; end
|
13
13
|
|
14
|
+
class ExecResult < T::Struct
|
15
|
+
const :out, String
|
16
|
+
const :err, String
|
17
|
+
const :status, T::Boolean
|
18
|
+
const :exit_code, Integer
|
19
|
+
end
|
20
|
+
|
14
21
|
sig do
|
15
22
|
params(
|
16
23
|
cmd: String,
|
17
24
|
arg: String,
|
18
25
|
path: String,
|
19
26
|
capture_err: T::Boolean
|
20
|
-
).returns(
|
27
|
+
).returns(ExecResult)
|
21
28
|
end
|
22
29
|
def self.exec(cmd, *arg, path: '.', capture_err: false)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
30
|
+
if capture_err
|
31
|
+
stdout, stderr, status = T.unsafe(Open3).capture3([cmd, *arg].join(" "), chdir: path)
|
32
|
+
ExecResult.new(
|
33
|
+
out: stdout,
|
34
|
+
err: stderr,
|
35
|
+
status: status.success?,
|
36
|
+
exit_code: status.exitstatus
|
37
|
+
)
|
38
|
+
else
|
39
|
+
stdout, status = T.unsafe(Open3).capture2([cmd, *arg].join(" "), chdir: path)
|
40
|
+
ExecResult.new(
|
41
|
+
out: stdout,
|
42
|
+
err: "",
|
43
|
+
status: status.success?,
|
44
|
+
exit_code: status.exitstatus
|
45
|
+
)
|
28
46
|
end
|
29
47
|
end
|
30
48
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spoom
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexandre Terrasa
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-03-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|