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 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