spoom 1.0.4 → 1.0.5

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.
@@ -0,0 +1,98 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "time"
5
+
6
+ module Spoom
7
+ # Execute git commands
8
+ module Git
9
+ extend T::Sig
10
+
11
+ # Execute a `command`
12
+ sig { params(command: String, arg: String, path: String).returns([String, String, T::Boolean]) }
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
+ _, 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
+ [out, err, T.cast(s.value, Process::Status).success?]
23
+ end
24
+
25
+ # Git commands
26
+
27
+ sig { params(arg: String, path: String).returns([String, String, T::Boolean]) }
28
+ def self.checkout(*arg, path: ".")
29
+ exec("git checkout -q #{arg.join(' ')}", path: path)
30
+ end
31
+
32
+ sig { params(arg: String, path: String).returns([String, String, T::Boolean]) }
33
+ def self.diff(*arg, path: ".")
34
+ exec("git diff #{arg.join(' ')}", path: path)
35
+ end
36
+
37
+ sig { params(arg: String, path: String).returns([String, String, T::Boolean]) }
38
+ def self.log(*arg, path: ".")
39
+ exec("git log #{arg.join(' ')}", path: path)
40
+ end
41
+
42
+ sig { params(arg: String, path: String).returns([String, String, T::Boolean]) }
43
+ def self.rev_parse(*arg, path: ".")
44
+ exec("git rev-parse --short #{arg.join(' ')}", path: path)
45
+ end
46
+
47
+ sig { params(arg: String, path: String).returns([String, String, T::Boolean]) }
48
+ def self.show(*arg, path: ".")
49
+ exec("git show #{arg.join(' ')}", path: path)
50
+ end
51
+
52
+ # Utils
53
+
54
+ # Get the commit epoch timestamp for a `sha`
55
+ sig { params(sha: String, path: String).returns(T.nilable(Integer)) }
56
+ def self.commit_timestamp(sha, path: ".")
57
+ out, _, status = show("--no-notes --no-patch --pretty=%at #{sha}", path: path)
58
+ return nil unless status
59
+ out.strip.to_i
60
+ end
61
+
62
+ # Get the commit Time for a `sha`
63
+ sig { params(sha: String, path: String).returns(T.nilable(Time)) }
64
+ def self.commit_time(sha, path: ".")
65
+ timestamp = commit_timestamp(sha, path: path)
66
+ return nil unless timestamp
67
+ epoch_to_time(timestamp.to_s)
68
+ end
69
+
70
+ # Get the last commit sha
71
+ sig { params(path: String).returns(T.nilable(String)) }
72
+ def self.last_commit(path: ".")
73
+ out, _, status = rev_parse("HEAD", path: path)
74
+ return nil unless status
75
+ out.strip
76
+ end
77
+
78
+ # Translate a git epoch timestamp into a Time
79
+ sig { params(timestamp: String).returns(Time) }
80
+ def self.epoch_to_time(timestamp)
81
+ Time.strptime(timestamp, "%s")
82
+ end
83
+
84
+ # Is there uncommited changes in `path`?
85
+ sig { params(path: String).returns(T::Boolean) }
86
+ def self.workdir_clean?(path: ".")
87
+ diff("HEAD", path: path).first.empty?
88
+ end
89
+
90
+ # Get the hash of the commit introducing the `sorbet/config` file
91
+ sig { params(path: String).returns(T.nilable(String)) }
92
+ def self.sorbet_intro_commit(path: ".")
93
+ res, _, status = Spoom::Git.log("--diff-filter=A --format='%h' -1 -- sorbet/config", path: path)
94
+ return nil unless status
95
+ res.strip
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,81 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "colorize"
5
+ require "stringio"
6
+
7
+ module Spoom
8
+ class Printer
9
+ extend T::Sig
10
+ extend T::Helpers
11
+
12
+ abstract!
13
+
14
+ sig { returns(T.any(IO, StringIO)) }
15
+ attr_accessor :out
16
+
17
+ sig { params(out: T.any(IO, StringIO), colors: T::Boolean, indent_level: Integer).void }
18
+ def initialize(out: $stdout, colors: true, indent_level: 0)
19
+ @out = out
20
+ @colors = colors
21
+ @indent_level = indent_level
22
+ end
23
+
24
+ # Increase indent level
25
+ sig { void }
26
+ def indent
27
+ @indent_level += 2
28
+ end
29
+
30
+ # Decrease indent level
31
+ sig { void }
32
+ def dedent
33
+ @indent_level -= 2
34
+ end
35
+
36
+ # Print `string` into `out`
37
+ sig { params(string: T.nilable(String)).void }
38
+ def print(string)
39
+ return unless string
40
+ @out.print(string)
41
+ end
42
+
43
+ # Print `string` colored with `color` into `out`
44
+ #
45
+ # Does not use colors unless `@colors`.
46
+ sig { params(string: T.nilable(String), color: Symbol, colors: Symbol).void }
47
+ def print_colored(string, color, *colors)
48
+ return unless string
49
+ string = colorize(string, color)
50
+ colors.each { |c| string = colorize(string, c) }
51
+ @out.print(string)
52
+ end
53
+
54
+ # Print a new line into `out`
55
+ sig { void }
56
+ def printn
57
+ print("\n")
58
+ end
59
+
60
+ # Print `string` with indent and newline
61
+ sig { params(string: T.nilable(String)).void }
62
+ def printl(string)
63
+ return unless string
64
+ printt
65
+ print(string)
66
+ printn
67
+ end
68
+
69
+ # Print an indent space into `out`
70
+ sig { void }
71
+ def printt
72
+ print(" " * @indent_level)
73
+ end
74
+
75
+ # Colorize `string` with color if `@colors`
76
+ sig { params(string: String, color: Symbol).returns(String) }
77
+ def colorize(string, color)
78
+ @colors ? string.colorize(color) : string
79
+ end
80
+ end
81
+ end
@@ -5,6 +5,7 @@ require "spoom/sorbet/config"
5
5
  require "spoom/sorbet/errors"
6
6
  require "spoom/sorbet/lsp"
7
7
  require "spoom/sorbet/metrics"
8
+ require "spoom/sorbet/sigils"
8
9
 
9
10
  require "open3"
10
11
 
@@ -54,17 +55,29 @@ module Spoom
54
55
  out.split(" ")[2]
55
56
  end
56
57
 
57
- sig { params(arg: String, path: String, capture_err: T::Boolean).returns(T.nilable(Metrics)) }
58
+ sig { params(arg: String, path: String, capture_err: T::Boolean).returns(T.nilable(T::Hash[String, Integer])) }
58
59
  def self.srb_metrics(*arg, path: '.', capture_err: false)
59
60
  metrics_file = "metrics.tmp"
60
61
  metrics_path = "#{path}/#{metrics_file}"
61
62
  srb_tc(*T.unsafe(["--metrics-file=#{metrics_file}", *arg]), path: path, capture_err: capture_err)
62
63
  if File.exist?(metrics_path)
63
- metrics = Spoom::Sorbet::Metrics.parse_file(metrics_path)
64
+ metrics = Spoom::Sorbet::MetricsParser.parse_file(metrics_path)
64
65
  File.delete(metrics_path)
65
66
  return metrics
66
67
  end
67
68
  nil
68
69
  end
70
+
71
+ # Get `gem` version from the `Gemfile.lock` content
72
+ #
73
+ # Returns `nil` if `gem` cannot be found in the Gemfile.
74
+ sig { params(gem: String, path: String).returns(T.nilable(String)) }
75
+ def self.version_from_gemfile_lock(gem: 'sorbet', path: '.')
76
+ gemfile_path = "#{path}/Gemfile.lock"
77
+ return nil unless File.exist?(gemfile_path)
78
+ content = File.read(gemfile_path).match(/^ #{gem} \(.*(\d+\.\d+\.\d+).*\)/)
79
+ return nil unless content
80
+ content[1]
81
+ end
69
82
  end
70
83
  end
@@ -16,6 +16,19 @@ module Spoom
16
16
  "or set SORBET_SILENCE_DEV_MESSAGE=1 in your shell environment.",
17
17
  ]
18
18
 
19
+ ERROR_LINE_MATCH_REGEX = %r{
20
+ ^ # match beginning of line
21
+ (\S[^:]*) # capture filename as something that starts with a non-space character
22
+ # followed by anything that is not a colon character
23
+ : # match the filename - line number seperator
24
+ (\d+) # capture the line number
25
+ :\s # match the line number - error message separator
26
+ (.*) # capture the error message
27
+ \shttps://srb.help/ # match the error code url prefix
28
+ (\d+) # capture the error code
29
+ $ # match end of line
30
+ }x.freeze
31
+
19
32
  sig { params(output: String).returns(T::Array[Error]) }
20
33
  def self.parse_string(output)
21
34
  parser = Spoom::Sorbet::Errors::Parser.new
@@ -37,9 +50,9 @@ module Spoom
37
50
 
38
51
  next if line == "\n"
39
52
 
40
- if leading_spaces(line) == 0
53
+ if (error = match_error_line(line))
41
54
  close_error if @current_error
42
- open_error(line)
55
+ open_error(error)
43
56
  next
44
57
  end
45
58
 
@@ -51,15 +64,19 @@ module Spoom
51
64
 
52
65
  private
53
66
 
54
- sig { params(line: String).returns(T.nilable(Integer)) }
55
- def leading_spaces(line)
56
- line.index(/[^ ]/)
67
+ sig { params(line: String).returns(T.nilable(Error)) }
68
+ def match_error_line(line)
69
+ match = line.match(ERROR_LINE_MATCH_REGEX)
70
+ return unless match
71
+
72
+ file, line, message, code = match.captures
73
+ Error.new(file, line&.to_i, message, code&.to_i)
57
74
  end
58
75
 
59
- sig { params(line: String).void }
60
- def open_error(line)
76
+ sig { params(error: Error).void }
77
+ def open_error(error)
61
78
  raise "Error: Already parsing an error!" if @current_error
62
- @current_error = Error.from_error_line(line)
79
+ @current_error = error
63
80
  end
64
81
 
65
82
  sig { void }
@@ -106,13 +123,6 @@ module Spoom
106
123
  @more = more
107
124
  end
108
125
 
109
- sig { params(line: String).returns(Error) }
110
- def self.from_error_line(line)
111
- file, line, rest = line.split(/: ?/, 3)
112
- message, code = rest&.split(%r{ https://srb\.help/}, 2)
113
- Error.new(file, line&.to_i, message, code&.to_i)
114
- end
115
-
116
126
  sig { params(other: T.untyped).returns(Integer) }
117
127
  def <=>(other)
118
128
  return 0 unless other.is_a?(Error)
@@ -11,10 +11,12 @@ require_relative 'lsp/errors'
11
11
  module Spoom
12
12
  module LSP
13
13
  class Client
14
- def initialize(sorbet_cmd, *sorbet_args)
14
+ def initialize(sorbet_cmd, *sorbet_args, path: ".")
15
15
  @id = 0
16
16
  Bundler.with_clean_env do
17
- @in, @out, @err, @status = Open3.popen3([sorbet_cmd, *sorbet_args].join(" "))
17
+ opts = {}
18
+ opts[:chdir] = path
19
+ @in, @out, @err, @status = Open3.popen3([sorbet_cmd, *sorbet_args].join(" "), opts)
18
20
  end
19
21
  end
20
22
 
@@ -1,9 +1,24 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative "../../printer"
5
+
4
6
  module Spoom
5
7
  module LSP
8
+ module PrintableSymbol
9
+ extend T::Sig
10
+ extend T::Helpers
11
+
12
+ interface!
13
+
14
+ sig { abstract.params(printer: SymbolPrinter).void }
15
+ def accept_printer(printer); end
16
+ end
17
+
6
18
  class Hover < T::Struct
19
+ extend T::Sig
20
+ include PrintableSymbol
21
+
7
22
  const :contents, String
8
23
  const :range, T.nilable(Range)
9
24
 
@@ -14,9 +29,10 @@ module Spoom
14
29
  )
15
30
  end
16
31
 
32
+ sig { override.params(printer: SymbolPrinter).void }
17
33
  def accept_printer(printer)
18
34
  printer.print("#{contents}\n")
19
- printer.visit(range) if range
35
+ printer.print_object(range) if range
20
36
  end
21
37
 
22
38
  def to_s
@@ -25,6 +41,9 @@ module Spoom
25
41
  end
26
42
 
27
43
  class Position < T::Struct
44
+ extend T::Sig
45
+ include PrintableSymbol
46
+
28
47
  const :line, Integer
29
48
  const :char, Integer
30
49
 
@@ -35,8 +54,9 @@ module Spoom
35
54
  )
36
55
  end
37
56
 
57
+ sig { override.params(printer: SymbolPrinter).void }
38
58
  def accept_printer(printer)
39
- printer.print("#{line}:#{char}".light_black)
59
+ printer.print_colored("#{line}:#{char}", :light_black)
40
60
  end
41
61
 
42
62
  def to_s
@@ -45,6 +65,9 @@ module Spoom
45
65
  end
46
66
 
47
67
  class Range < T::Struct
68
+ extend T::Sig
69
+ include PrintableSymbol
70
+
48
71
  const :start, Position
49
72
  const :end, Position
50
73
 
@@ -55,10 +78,11 @@ module Spoom
55
78
  )
56
79
  end
57
80
 
81
+ sig { override.params(printer: SymbolPrinter).void }
58
82
  def accept_printer(printer)
59
- printer.visit(start)
60
- printer.print("-".light_black)
61
- printer.visit(self.end)
83
+ printer.print_object(start)
84
+ printer.print_colored("-", :light_black)
85
+ printer.print_object(self.end)
62
86
  end
63
87
 
64
88
  def to_s
@@ -67,6 +91,9 @@ module Spoom
67
91
  end
68
92
 
69
93
  class Location < T::Struct
94
+ extend T::Sig
95
+ include PrintableSymbol
96
+
70
97
  const :uri, String
71
98
  const :range, LSP::Range
72
99
 
@@ -77,17 +104,21 @@ module Spoom
77
104
  )
78
105
  end
79
106
 
107
+ sig { override.params(printer: SymbolPrinter).void }
80
108
  def accept_printer(printer)
81
- printer.print("#{uri.from_uri}:".light_black)
82
- printer.visit(range)
109
+ printer.print_colored("#{printer.clean_uri(uri)}:", :light_black)
110
+ printer.print_object(range)
83
111
  end
84
112
 
85
113
  def to_s
86
- "#{uri}:#{range})."
114
+ "#{uri}:#{range}"
87
115
  end
88
116
  end
89
117
 
90
118
  class SignatureHelp < T::Struct
119
+ extend T::Sig
120
+ include PrintableSymbol
121
+
91
122
  const :label, T.nilable(String)
92
123
  const :doc, Object # TODO
93
124
  const :params, T::Array[T.untyped] # TODO
@@ -100,6 +131,7 @@ module Spoom
100
131
  )
101
132
  end
102
133
 
134
+ sig { override.params(printer: SymbolPrinter).void }
103
135
  def accept_printer(printer)
104
136
  printer.print(label)
105
137
  printer.print("(")
@@ -113,6 +145,9 @@ module Spoom
113
145
  end
114
146
 
115
147
  class Diagnostic < T::Struct
148
+ extend T::Sig
149
+ include PrintableSymbol
150
+
116
151
  const :range, LSP::Range
117
152
  const :code, Integer
118
153
  const :message, String
@@ -127,12 +162,20 @@ module Spoom
127
162
  )
128
163
  end
129
164
 
165
+ sig { override.params(printer: SymbolPrinter).void }
166
+ def accept_printer(printer)
167
+ printer.print(to_s)
168
+ end
169
+
130
170
  def to_s
131
171
  "Error: #{message} (#{code})."
132
172
  end
133
173
  end
134
174
 
135
175
  class DocumentSymbol < T::Struct
176
+ extend T::Sig
177
+ include PrintableSymbol
178
+
136
179
  const :name, String
137
180
  const :detail, T.nilable(String)
138
181
  const :kind, Integer
@@ -151,6 +194,7 @@ module Spoom
151
194
  )
152
195
  end
153
196
 
197
+ sig { override.params(printer: SymbolPrinter).void }
154
198
  def accept_printer(printer)
155
199
  h = serialize.hash
156
200
  return if printer.seen.include?(h)
@@ -159,18 +203,18 @@ module Spoom
159
203
  printer.printt
160
204
  printer.print(kind_string)
161
205
  printer.print(' ')
162
- printer.print(name.blue.bold)
163
- printer.print(' ('.light_black)
206
+ printer.print_colored(name, :blue, :bold)
207
+ printer.print_colored(' (', :light_black)
164
208
  if range
165
- printer.visit(range)
209
+ printer.print_object(range)
166
210
  elsif location
167
- printer.visit(location)
211
+ printer.print_object(location)
168
212
  end
169
- printer.print(')'.light_black)
213
+ printer.print_colored(')', :light_black)
170
214
  printer.printn
171
215
  unless children.empty?
172
216
  printer.indent
173
- printer.visit(children)
217
+ printer.print_objects(children)
174
218
  printer.dedent
175
219
  end
176
220
  # TODO: also display details?
@@ -214,5 +258,55 @@ module Spoom
214
258
  26 => "type_parameter",
215
259
  }
216
260
  end
261
+
262
+ class SymbolPrinter < Printer
263
+ extend T::Sig
264
+
265
+ attr_accessor :seen, :prefix
266
+
267
+ sig do
268
+ params(
269
+ out: T.any(IO, StringIO),
270
+ colors: T::Boolean,
271
+ indent_level: Integer,
272
+ prefix: T.nilable(String)
273
+ ).void
274
+ end
275
+ def initialize(out: $stdout, colors: true, indent_level: 0, prefix: nil)
276
+ super(out: out, colors: colors, indent_level: indent_level)
277
+ @seen = Set.new
278
+ @out = out
279
+ @colors = colors
280
+ @indent_level = indent_level
281
+ @prefix = prefix
282
+ end
283
+
284
+ sig { params(object: T.nilable(PrintableSymbol)).void }
285
+ def print_object(object)
286
+ return unless object
287
+ object.accept_printer(self)
288
+ end
289
+
290
+ sig { params(objects: T::Array[PrintableSymbol]).void }
291
+ def print_objects(objects)
292
+ objects.each { |object| print_object(object) }
293
+ end
294
+
295
+ sig { params(uri: String).returns(String) }
296
+ def clean_uri(uri)
297
+ return uri unless prefix
298
+ uri.delete_prefix(prefix)
299
+ end
300
+
301
+ sig { params(objects: T::Array[PrintableSymbol]).void }
302
+ def print_list(objects)
303
+ objects.each do |object|
304
+ printt
305
+ print "* "
306
+ print_object(object)
307
+ printn
308
+ end
309
+ end
310
+ end
217
311
  end
218
312
  end