spoom 1.0.5 → 1.1.0

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