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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e58e5584ba33b3b678ddedb06972121af9dd4b9a8b7f1b6941f6bb939086107a
4
- data.tar.gz: c1cc668af1f42ace8318a47ac0b813815e4e6a9ee1d942593b1ff37b9b41cbc2
3
+ metadata.gz: 54a219cd3c4f0e2ea998e4aa5a81a7556c9aed944fdfe38f6aff1267c421336e
4
+ data.tar.gz: 10671ff002575fc844189cd4371c8f5c35960fc09c3335e7d3a9a234f89fd4ec
5
5
  SHA512:
6
- metadata.gz: f389c4b463b7bd1c4a275e844ed68d0d9e7279fefe4b1e9ed29a1c9238c25ad941aa494dee63c4f15dab288602a22efce3da6e7b08c795b1cb99fb236e2edf39
7
- data.tar.gz: 7071bc29a1d13087d0e0527832b7619af180b20375c006122d059c640a606a55edd68234e098cf2360f06c00aa165a7a673a4d009275707be830bebef947936a
6
+ metadata.gz: 3413d1409199b453275c3e41bfa79c4a4b88632d2917c7215eb46812768378a767f6eb9b6b79c3f2eeb7a99d14942bb63c3bdf0f4112cfd8be731ff76706dd06
7
+ data.tar.gz: 1ccd808b2ed86a945e44c6dc4a5a8bdc231b64eacaa70a3b6a4757b0e415911c9328d70ae148158751e36d320045e8f5d9dc23e4a08aa4305bceb36860f7f346
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  source "https://rubygems.org"
data/Rakefile CHANGED
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
data/exe/spoom CHANGED
@@ -1,5 +1,5 @@
1
1
  #! /usr/bin/env ruby
2
- # typed: true
2
+ # typed: strict
3
3
  # frozen_string_literal: true
4
4
 
5
5
  require_relative "../lib/spoom"
@@ -83,14 +83,16 @@ module Spoom
83
83
  exit(files_to_bump.empty?)
84
84
  end
85
85
 
86
- output, status, exit_code = Sorbet.srb_tc(
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(output)
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
- output, status, exit_code = T.unsafe(Spoom::Sorbet).srb_tc(
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(exit_code)
46
- say_error(output, status: nil, nl: false)
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
- output, status, exit_code = T.unsafe(Spoom::Sorbet).srb_tc(
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(output, status: nil, nl: false)
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(output)
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([String, String, T::Boolean]) }
12
+ sig { params(command: String, arg: String, path: String).returns(ExecResult) }
13
13
  def self.exec(command, *arg, path: '.')
14
- return "", "Error: `#{path}` is not a directory.", false unless File.directory?(path)
15
- opts = {}
16
- opts[:chdir] = path
17
- i, o, e, s = Open3.popen3(*T.unsafe([command, *T.unsafe(arg), opts]))
18
- out = o.read.to_s
19
- o.close
20
- err = e.read.to_s
21
- e.close
22
- i.close
23
- [out, err, s.value.success?]
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([String, String, T::Boolean]) }
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([String, String, T::Boolean]) }
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([String, String, T::Boolean]) }
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([String, String, T::Boolean]) }
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([String, String, T::Boolean]) }
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
- out, _, status = exec("git branch --show-current", path: path)
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
- out, _, status = show("--no-notes --no-patch --pretty=%at #{sha}", path: path)
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
- out, _, status = rev_parse("HEAD", path: path)
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).first.empty?
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
- res, _, status = Spoom::Git.log("--diff-filter=A --format='%h' -1 -- sorbet/config", path: path)
102
- return nil unless status
103
- res.strip!
104
- return nil if res.empty?
105
- res
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
- res, _, status = Spoom::Git.log("--diff-filter=D --format='%h' -1 -- sorbet/config", path: path)
112
- return nil unless status
113
- res.strip!
114
- return nil if res.empty?
115
- res
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
@@ -1,4 +1,4 @@
1
- # typed: true
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
- @current_error = nil
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(ERROR_LINE_MATCH_REGEX)
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: true
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 = '2.0'
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
- attr_reader :id, :method, :params
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
- attr_reader :method, :params
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: true
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
- attr_reader :uri, :diagnostics
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
- attr_reader :code, :message, :data
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: true
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
- return "<unknown:#{kind}>" unless SYMBOL_KINDS.key?(kind)
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
- attr_accessor :seen, :prefix
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
@@ -1,4 +1,4 @@
1
- # typed: true
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
- @in, @out, @err, @status = T.unsafe(Open3).popen3(sorbet_bin, *sorbet_args, chdir: path)
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
- json = JSON.parse(read_raw)
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
- return nil unless json['result']
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", nil))
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: true
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([String, T::Boolean, Integer])
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
- T.unsafe(Spoom).exec(*arg, path: path, capture_err: capture_err)
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([String, T::Boolean, Integer])
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(self).srb(*arg, path: path, capture_err: capture_err, sorbet_bin: sorbet_bin)
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
- out, res = T.unsafe(self).srb_tc(
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 res
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([T.nilable(String), T.nilable(String), T::Boolean]) }
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
- [out, err, T.must(status.success?)]
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([T.nilable(String), T.nilable(String), T::Boolean]) }
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
- [out, err, T.must(status.success?)]
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
@@ -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
- out, _, _ = Spoom::Git.log(
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
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Spoom
5
- VERSION = "1.1.6"
5
+ VERSION = "1.1.9"
6
6
  end
data/lib/spoom.rb CHANGED
@@ -1,4 +1,4 @@
1
- # typed: true
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([String, T::Boolean, Integer])
27
+ ).returns(ExecResult)
21
28
  end
22
29
  def self.exec(cmd, *arg, path: '.', capture_err: false)
23
- method = capture_err ? "popen2e" : "popen2"
24
- Open3.send(method, [cmd, *arg].join(" "), chdir: path) do |_, stdout, thread|
25
- out = stdout.read
26
- status = T.cast(thread.value, Process::Status)
27
- [out, status.success?, status.exitstatus]
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.6
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: 2021-12-08 00:00:00.000000000 Z
11
+ date: 2022-03-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler