spoom 1.0.5 → 1.1.0

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.
@@ -1,6 +1,7 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "fileutils"
4
5
  require "pathname"
5
6
  require "stringio"
6
7
 
@@ -10,16 +11,32 @@ module Spoom
10
11
  extend T::Sig
11
12
  include Thor::Shell
12
13
 
14
+ # Print `message` on `$stdout`
15
+ sig { params(message: String).void }
16
+ def say(message)
17
+ buffer = StringIO.new
18
+ buffer << highlight(message)
19
+ buffer << "\n" unless message.end_with?("\n")
20
+
21
+ $stdout.print(buffer.string)
22
+ $stdout.flush
23
+ end
24
+
13
25
  # Print `message` on `$stderr`
14
26
  #
15
27
  # The message is prefixed by a status (default: `Error`).
16
- sig { params(message: String, status: String).void }
17
- def say_error(message, status = "Error")
18
- status = set_color(status, :red)
19
-
28
+ sig do
29
+ params(
30
+ message: String,
31
+ status: T.nilable(String),
32
+ nl: T::Boolean
33
+ ).void
34
+ end
35
+ def say_error(message, status: "Error", nl: true)
20
36
  buffer = StringIO.new
21
- buffer << "#{status}: #{message}"
22
- buffer << "\n" unless message.end_with?("\n")
37
+ buffer << "#{red(status)}: " if status
38
+ buffer << highlight(message)
39
+ buffer << "\n" if nl && !message.end_with?("\n")
23
40
 
24
41
  $stderr.print(buffer.string)
25
42
  $stderr.flush
@@ -28,7 +45,7 @@ module Spoom
28
45
  # Is `spoom` ran inside a project with a `sorbet/config` file?
29
46
  sig { returns(T::Boolean) }
30
47
  def in_sorbet_project?
31
- File.file?(sorbet_config)
48
+ File.file?(sorbet_config_file)
32
49
  end
33
50
 
34
51
  # Enforce that `spoom` is ran inside a project with a `sorbet/config` file
@@ -37,7 +54,11 @@ module Spoom
37
54
  sig { void }
38
55
  def in_sorbet_project!
39
56
  unless in_sorbet_project?
40
- say_error("not in a Sorbet project (no sorbet/config)")
57
+ say_error(
58
+ "not in a Sorbet project (`#{sorbet_config_file}` not found)\n\n" \
59
+ "When running spoom from another path than the project's root, " \
60
+ "use `--path PATH` to specify the path to the root."
61
+ )
41
62
  Kernel.exit(1)
42
63
  end
43
64
  end
@@ -49,22 +70,80 @@ module Spoom
49
70
  end
50
71
 
51
72
  sig { returns(String) }
73
+ def sorbet_config_file
74
+ Pathname.new("#{exec_path}/#{Spoom::Sorbet::CONFIG_PATH}").cleanpath.to_s
75
+ end
76
+
77
+ sig { returns(Sorbet::Config) }
52
78
  def sorbet_config
53
- Pathname.new("#{exec_path}/#{Spoom::Config::SORBET_CONFIG}").cleanpath.to_s
79
+ Sorbet::Config.parse_file(sorbet_config_file)
54
80
  end
55
81
 
82
+ # Colors
83
+
84
+ # Color used to highlight expressions in backticks
85
+ HIGHLIGHT_COLOR = :blue
86
+
56
87
  # Is the `--color` option true?
57
88
  sig { returns(T::Boolean) }
58
89
  def color?
59
90
  T.unsafe(self).options[:color] # TODO: requires_ancestor
60
91
  end
61
92
 
93
+ sig { params(string: String).returns(String) }
94
+ def highlight(string)
95
+ return string unless color?
96
+
97
+ res = StringIO.new
98
+ word = StringIO.new
99
+ in_ticks = T.let(false, T::Boolean)
100
+ string.chars.each do |c|
101
+ if c == '`' && !in_ticks
102
+ in_ticks = true
103
+ elsif c == '`' && in_ticks
104
+ in_ticks = false
105
+ res << colorize(word.string, HIGHLIGHT_COLOR)
106
+ word = StringIO.new
107
+ elsif in_ticks
108
+ word << c
109
+ else
110
+ res << c
111
+ end
112
+ end
113
+ res.string
114
+ end
115
+
62
116
  # Colorize a string if `color?`
63
117
  sig { params(string: String, color: Symbol).returns(String) }
64
118
  def colorize(string, color)
65
119
  return string unless color?
66
120
  string.colorize(color)
67
121
  end
122
+
123
+ sig { params(string: String).returns(String) }
124
+ def blue(string)
125
+ colorize(string, :blue)
126
+ end
127
+
128
+ sig { params(string: String).returns(String) }
129
+ def gray(string)
130
+ colorize(string, :light_black)
131
+ end
132
+
133
+ sig { params(string: String).returns(String) }
134
+ def green(string)
135
+ colorize(string, :green)
136
+ end
137
+
138
+ sig { params(string: String).returns(String) }
139
+ def red(string)
140
+ colorize(string, :red)
141
+ end
142
+
143
+ sig { params(string: String).returns(String) }
144
+ def yellow(string)
145
+ colorize(string, :yellow)
146
+ end
68
147
  end
69
148
  end
70
149
  end
data/lib/spoom/cli/lsp.rb CHANGED
@@ -12,7 +12,7 @@ module Spoom
12
12
 
13
13
  default_task :show
14
14
 
15
- desc "interactive", "interactive LSP mode"
15
+ desc "interactive", "Interactive LSP mode"
16
16
  def show
17
17
  in_sorbet_project!
18
18
  lsp = lsp_client
@@ -20,7 +20,7 @@ module Spoom
20
20
  puts lsp
21
21
  end
22
22
 
23
- desc "list", "list all known symbols"
23
+ desc "list", "List all known symbols"
24
24
  # TODO: options, filter, limit, kind etc.. filter rbi
25
25
  def list
26
26
  run do |client|
@@ -28,82 +28,82 @@ module Spoom
28
28
  Dir["**/*.rb"].each do |file|
29
29
  res = client.document_symbols(to_uri(file))
30
30
  next if res.empty?
31
- puts "Symbols from `#{file}`:"
31
+ say("Symbols from `#{file}`:")
32
32
  printer.print_objects(res)
33
33
  end
34
34
  end
35
35
  end
36
36
 
37
- desc "hover", "request hover informations"
37
+ desc "hover", "Request hover informations"
38
38
  # TODO: options, filter, limit, kind etc.. filter rbi
39
39
  def hover(file, line, col)
40
40
  run do |client|
41
41
  res = client.hover(to_uri(file), line.to_i, col.to_i)
42
- say "Hovering `#{file}:#{line}:#{col}`:"
42
+ say("Hovering `#{file}:#{line}:#{col}`:")
43
43
  if res
44
44
  symbol_printer.print_object(res)
45
45
  else
46
- puts "<no data>"
46
+ say("<no data>")
47
47
  end
48
48
  end
49
49
  end
50
50
 
51
- desc "defs", "list definitions of a symbol"
51
+ desc "defs", "List definitions of a symbol"
52
52
  # TODO: options, filter, limit, kind etc.. filter rbi
53
53
  def defs(file, line, col)
54
54
  run do |client|
55
55
  res = client.definitions(to_uri(file), line.to_i, col.to_i)
56
- puts "Definitions for `#{file}:#{line}:#{col}`:"
56
+ say("Definitions for `#{file}:#{line}:#{col}`:")
57
57
  symbol_printer.print_list(res)
58
58
  end
59
59
  end
60
60
 
61
- desc "find", "find symbols matching a query"
61
+ desc "find", "Find symbols matching a query"
62
62
  # TODO: options, filter, limit, kind etc.. filter rbi
63
63
  def find(query)
64
64
  run do |client|
65
65
  res = client.symbols(query).reject { |symbol| symbol.location.uri.start_with?("https") }
66
- puts "Symbols matching `#{query}`:"
66
+ say("Symbols matching `#{query}`:")
67
67
  symbol_printer.print_objects(res)
68
68
  end
69
69
  end
70
70
 
71
- desc "symbols", "list symbols from a file"
71
+ desc "symbols", "List symbols from a file"
72
72
  # TODO: options, filter, limit, kind etc.. filter rbi
73
73
  def symbols(file)
74
74
  run do |client|
75
75
  res = client.document_symbols(to_uri(file))
76
- puts "Symbols from `#{file}`:"
76
+ say("Symbols from `#{file}`:")
77
77
  symbol_printer.print_objects(res)
78
78
  end
79
79
  end
80
80
 
81
- desc "refs", "list references to a symbol"
81
+ desc "refs", "List references to a symbol"
82
82
  # TODO: options, filter, limit, kind etc.. filter rbi
83
83
  def refs(file, line, col)
84
84
  run do |client|
85
85
  res = client.references(to_uri(file), line.to_i, col.to_i)
86
- puts "References to `#{file}:#{line}:#{col}`:"
86
+ say("References to `#{file}:#{line}:#{col}`:")
87
87
  symbol_printer.print_list(res)
88
88
  end
89
89
  end
90
90
 
91
- desc "sigs", "list signatures for a symbol"
91
+ desc "sigs", "List signatures for a symbol"
92
92
  # TODO: options, filter, limit, kind etc.. filter rbi
93
93
  def sigs(file, line, col)
94
94
  run do |client|
95
95
  res = client.signatures(to_uri(file), line.to_i, col.to_i)
96
- puts "Signature for `#{file}:#{line}:#{col}`:"
96
+ say("Signature for `#{file}:#{line}:#{col}`:")
97
97
  symbol_printer.print_list(res)
98
98
  end
99
99
  end
100
100
 
101
- desc "types", "display type of a symbol"
101
+ desc "types", "Display type of a symbol"
102
102
  # TODO: options, filter, limit, kind etc.. filter rbi
103
103
  def types(file, line, col)
104
104
  run do |client|
105
105
  res = client.type_definitions(to_uri(file), line.to_i, col.to_i)
106
- say "Type for `#{file}:#{line}:#{col}`:"
106
+ say("Type for `#{file}:#{line}:#{col}`:")
107
107
  symbol_printer.print_list(res)
108
108
  end
109
109
  end
@@ -113,7 +113,7 @@ module Spoom
113
113
  in_sorbet_project!
114
114
  path = exec_path
115
115
  client = Spoom::LSP::Client.new(
116
- Spoom::Config::SORBET_PATH,
116
+ Spoom::Sorbet::BIN_PATH,
117
117
  "--lsp",
118
118
  "--enable-all-experimental-lsp-features",
119
119
  "--disable-watchman",
@@ -137,7 +137,7 @@ module Spoom
137
137
  rescue Spoom::LSP::Error::Diagnostics => err
138
138
  say_error("Sorbet returned typechecking errors for `#{symbol_printer.clean_uri(err.uri)}`")
139
139
  err.diagnostics.each do |d|
140
- say_error("#{d.message} (#{d.code})", " #{d.range}")
140
+ say_error("#{d.message} (#{d.code})", status: " #{d.range}")
141
141
  end
142
142
  exit(1)
143
143
  rescue Spoom::LSP::Error::BadHeaders => err
data/lib/spoom/cli/run.rb CHANGED
@@ -8,59 +8,89 @@ module Spoom
8
8
 
9
9
  default_task :tc
10
10
 
11
- desc "tc", "run srb tc"
12
- option :limit, type: :numeric, aliases: :l
13
- option :code, type: :numeric, aliases: :c
14
- option :sort, type: :string, aliases: :s
15
- def tc
11
+ SORT_CODE = "code"
12
+ SORT_LOC = "loc"
13
+ SORT_ENUM = [SORT_CODE, SORT_LOC]
14
+
15
+ DEFAULT_FORMAT = "%C - %F:%L: %M"
16
+
17
+ desc "tc", "Run `srb tc`"
18
+ option :limit, type: :numeric, aliases: :l, desc: "Limit displayed errors"
19
+ option :code, type: :numeric, aliases: :c, desc: "Filter displayed errors by code"
20
+ option :sort, type: :string, aliases: :s, desc: "Sort errors", enum: SORT_ENUM, default: SORT_LOC
21
+ option :format, type: :string, aliases: :f, desc: "Format line output"
22
+ option :uniq, type: :boolean, aliases: :u, desc: "Remove duplicated lines"
23
+ option :count, type: :boolean, default: true, desc: "Show errors count"
24
+ option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
25
+ def tc(*arg)
16
26
  in_sorbet_project!
17
27
 
18
28
  path = exec_path
19
29
  limit = options[:limit]
20
30
  sort = options[:sort]
21
31
  code = options[:code]
22
- colors = options[:color]
32
+ uniq = options[:uniq]
33
+ format = options[:format]
34
+ count = options[:count]
35
+ sorbet = options[:sorbet]
23
36
 
24
37
  unless limit || code || sort
25
- return Spoom::Sorbet.srb_tc(path: path, capture_err: false).last
38
+ output, status = T.unsafe(Spoom::Sorbet).srb_tc(*arg, path: path, capture_err: false, sorbet_bin: sorbet)
39
+ say_error(output, status: nil, nl: false)
40
+ exit(status)
26
41
  end
27
42
 
28
- output, status = Spoom::Sorbet.srb_tc(path: path, capture_err: true)
43
+ output, status = T.unsafe(Spoom::Sorbet).srb_tc(*arg, path: path, capture_err: true, sorbet_bin: sorbet)
29
44
  if status
30
- $stderr.print(output)
31
- return 0
45
+ say_error(output, status: nil, nl: false)
46
+ exit(0)
32
47
  end
33
48
 
34
49
  errors = Spoom::Sorbet::Errors::Parser.parse_string(output)
35
50
  errors_count = errors.size
36
51
 
37
- errors = sort == "code" ? errors.sort_by { |e| [e.code, e.file, e.line, e.message] } : errors.sort
52
+ errors = case sort
53
+ when SORT_CODE
54
+ Spoom::Sorbet::Errors.sort_errors_by_code(errors)
55
+ when SORT_LOC
56
+ errors.sort
57
+ else
58
+ errors # preserve natural sort
59
+ end
60
+
38
61
  errors = errors.select { |e| e.code == code } if code
39
62
  errors = T.must(errors.slice(0, limit)) if limit
40
63
 
41
- errors.each do |e|
42
- code = colorize_code(e.code, colors)
43
- message = colorize_message(e.message, colors)
44
- $stderr.puts "#{code} - #{e.file}:#{e.line}: #{message}"
64
+ lines = errors.map { |e| format_error(e, format || DEFAULT_FORMAT) }
65
+ lines = lines.uniq if uniq
66
+
67
+ lines.each do |line|
68
+ say_error(line, status: nil)
45
69
  end
46
70
 
47
- if errors_count == errors.size
48
- $stderr.puts "Errors: #{errors_count}"
49
- else
50
- $stderr.puts "Errors: #{errors.size} shown, #{errors_count} total"
71
+ if count
72
+ if errors_count == errors.size
73
+ say_error("Errors: #{errors_count}", status: nil)
74
+ else
75
+ say_error("Errors: #{errors.size} shown, #{errors_count} total", status: nil)
76
+ end
51
77
  end
52
78
 
53
- 1
79
+ exit(1)
54
80
  end
55
81
 
56
82
  no_commands do
57
- def colorize_code(code, colors = true)
58
- return code.to_s unless colors
59
- code.to_s.light_black
83
+ def format_error(error, format)
84
+ line = format
85
+ line = line.gsub(/%C/, yellow(error.code.to_s))
86
+ line = line.gsub(/%F/, error.file)
87
+ line = line.gsub(/%L/, error.line.to_s)
88
+ line = line.gsub(/%M/, colorize_message(error.message))
89
+ line
60
90
  end
61
91
 
62
- def colorize_message(message, colors = true)
63
- return message unless colors
92
+ def colorize_message(message)
93
+ return message unless color?
64
94
 
65
95
  cyan = T.let(false, T::Boolean)
66
96
  word = StringIO.new
@@ -11,10 +11,23 @@ module Spoom
11
11
  module Coverage
12
12
  extend T::Sig
13
13
 
14
- sig { params(path: String).returns(Snapshot) }
15
- def self.snapshot(path: '.')
14
+ sig { params(path: String, rbi: T::Boolean, sorbet_bin: T.nilable(String)).returns(Snapshot) }
15
+ def self.snapshot(path: '.', rbi: true, sorbet_bin: nil)
16
+ config = sorbet_config(path: path)
17
+ config.allowed_extensions.push(".rb", ".rbi") if config.allowed_extensions.empty?
18
+
19
+ new_config = config.copy
20
+ new_config.allowed_extensions.reject! { |ext| !rbi && ext == ".rbi" }
21
+
22
+ metrics = Spoom::Sorbet.srb_metrics(
23
+ "--no-config",
24
+ new_config.options_string,
25
+ path: path,
26
+ capture_err: true,
27
+ sorbet_bin: sorbet_bin
28
+ )
29
+
16
30
  snapshot = Snapshot.new
17
- metrics = Spoom::Sorbet.srb_metrics(path: path, capture_err: true)
18
31
  return snapshot unless metrics
19
32
 
20
33
  sha = Spoom::Git.last_commit(path: path)
@@ -59,11 +72,14 @@ module Spoom
59
72
  )
60
73
  end
61
74
 
75
+ sig { params(path: String).returns(Sorbet::Config) }
76
+ def self.sorbet_config(path: ".")
77
+ Sorbet::Config.parse_file("#{path}/#{Spoom::Sorbet::CONFIG_PATH}")
78
+ end
79
+
62
80
  sig { params(path: String).returns(FileTree) }
63
81
  def self.sigils_tree(path: ".")
64
- config_file = "#{path}/#{Spoom::Config::SORBET_CONFIG}"
65
- return FileTree.new unless File.exist?(config_file)
66
- config = Sorbet::Config.parse_file(config_file)
82
+ config = sorbet_config(path: path)
67
83
  files = Sorbet.srb_files(config, path: path)
68
84
  files.select! { |file| file =~ /\.rb$/ }
69
85
  files.reject! { |file| file =~ %r{/test/} }
@@ -41,7 +41,7 @@ module Spoom
41
41
 
42
42
  abstract!
43
43
 
44
- TEMPLATE = T.let("#{Spoom::Config::SPOOM_PATH}/templates/page.erb", String)
44
+ TEMPLATE = T.let("#{Spoom::SPOOM_PATH}/templates/page.erb", String)
45
45
 
46
46
  sig { returns(String) }
47
47
  attr_reader :title
@@ -89,7 +89,7 @@ module Spoom
89
89
  class Card < Template
90
90
  extend T::Sig
91
91
 
92
- TEMPLATE = T.let("#{Spoom::Config::SPOOM_PATH}/templates/card.erb", String)
92
+ TEMPLATE = T.let("#{Spoom::SPOOM_PATH}/templates/card.erb", String)
93
93
 
94
94
  sig { returns(T.nilable(String)) }
95
95
  attr_reader :title, :body
@@ -123,7 +123,7 @@ module Spoom
123
123
  class Snapshot < Card
124
124
  extend T::Sig
125
125
 
126
- TEMPLATE = T.let("#{Spoom::Config::SPOOM_PATH}/templates/card_snapshot.erb", String)
126
+ TEMPLATE = T.let("#{Spoom::SPOOM_PATH}/templates/card_snapshot.erb", String)
127
127
 
128
128
  sig { returns(Coverage::Snapshot) }
129
129
  attr_reader :snapshot