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