spoom 1.0.4 → 1.0.5

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