spoom 1.0.3 → 1.0.4

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: 3f6a3db37efef69beb451a63043e45e10e8c49cb6c86c774fdc8e01a18aee858
4
- data.tar.gz: de6e4f8e97f5802b3843ccde507510d4de081a98f4fdd3b7bf6da62d1b177dcb
3
+ metadata.gz: d851bf66dbfb8e1d316b27466fde8ee205e55e68b9b489c3f8f234c03e1763d2
4
+ data.tar.gz: '068972f0ebb9393400dcbf68837be0a58114cc55560438fd329be3b54acfcaaf'
5
5
  SHA512:
6
- metadata.gz: 74189496b4e9a126ba649fa529f3061c03e334f01c715b571dc12920f97219ea79e6d7f25811d073fc6daa2fa6db04aeb12030fa119fefe94cb822964710e658
7
- data.tar.gz: bcefd877fc949636d850a660e389bd0f395ef3fb38b6eed178f2b09b0bc63685202dff5628ac6f39a54597471e4ea8004356971db9d578a0e07fdde986d05a8e
6
+ metadata.gz: 8315d9c7de9beaf2aa9a5d6963cde78ec6e9e7949659bac85a0ff719080755808fb5a854c2a03f6dcab8134c17bc8d90d145d53d32a3f1632d83a2c6a21ebfaa
7
+ data.tar.gz: 88fee999fa79caf40dd776ce5253a1337a5acc2507653a2c09adfb3e4723b6a8a06033d34c8f299b3a7a663ef01c2fa4a8243d98fd9f104e9c77ad69f9836ff7
data/Gemfile CHANGED
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  source "https://rubygems.org"
@@ -6,4 +7,7 @@ gemspec
6
7
 
7
8
  group(:development) do
8
9
  gem('rubocop-shopify', require: false)
10
+ gem('rubocop-sorbet', require: false)
11
+ gem('byebug')
12
+ gem('pry-byebug')
9
13
  end
data/Rakefile CHANGED
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
  require "bundler/gem_tasks"
3
4
  require "rake/testtask"
data/exe/spoom CHANGED
@@ -0,0 +1,7 @@
1
+ #! /usr/bin/env ruby
2
+ # typed: true
3
+ # frozen_string_literal: true
4
+
5
+ require_relative "../lib/spoom"
6
+
7
+ Spoom::Cli::Main.start(ARGV)
@@ -1,6 +1,13 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require "sorbet-runtime"
4
5
 
6
+ module Spoom
7
+ class Error < StandardError; end
8
+ end
9
+
10
+ require "spoom/cli"
11
+ require "spoom/config"
12
+ require "spoom/sorbet"
5
13
  require "spoom/version"
6
- require "spoom/sorbet/config"
@@ -0,0 +1,33 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "thor"
5
+
6
+ require_relative "cli/commands/config"
7
+ require_relative "cli/commands/lsp"
8
+ require_relative "cli/commands/run"
9
+
10
+ module Spoom
11
+ module Cli
12
+ class Main < Thor
13
+ extend T::Sig
14
+
15
+ class_option :no_color, desc: "Don't use colors", type: :boolean
16
+
17
+ desc "config", "manage Sorbet config"
18
+ subcommand "config", Spoom::Cli::Commands::Config
19
+
20
+ desc "lsp", "send LSP requests to Sorbet"
21
+ subcommand "lsp", Spoom::Cli::Commands::LSP
22
+
23
+ desc "tc", "run Sorbet and parses its output"
24
+ subcommand "tc", Spoom::Cli::Commands::Run
25
+
26
+ # Utils
27
+
28
+ def self.exit_on_failure?
29
+ true
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'stringio'
5
+
6
+ module Spoom
7
+ module Cli
8
+ module Commands
9
+ class Base < Thor
10
+ no_commands do
11
+ def say_error(message, status = "Error")
12
+ status = set_color(status, :red)
13
+
14
+ buffer = StringIO.new
15
+ buffer << "#{status}: #{message}"
16
+ buffer << "\n" unless message.end_with?("\n")
17
+
18
+ $stderr.print(buffer.string)
19
+ $stderr.flush
20
+ end
21
+
22
+ def in_sorbet_project?
23
+ File.file?(Spoom::Config::SORBET_CONFIG)
24
+ end
25
+
26
+ def in_sorbet_project!
27
+ unless in_sorbet_project?
28
+ say_error("not in a Sorbet project (no sorbet/config)")
29
+ exit(1)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,67 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "base"
5
+ require_relative "../../sorbet/config"
6
+
7
+ module Spoom
8
+ module Cli
9
+ module Commands
10
+ class Config < Base
11
+ default_task :show
12
+
13
+ desc "show", "show Sorbet config"
14
+ def show
15
+ in_sorbet_project!
16
+ config = Spoom::Sorbet::Config.parse_file(Spoom::Config::SORBET_CONFIG)
17
+
18
+ say("Found Sorbet config at `#{Spoom::Config::SORBET_CONFIG}`.")
19
+
20
+ say("\nPaths typechecked:")
21
+ if config.paths.empty?
22
+ say(" * (default: .)")
23
+ else
24
+ config.paths.each do |path|
25
+ say(" * #{path}")
26
+ end
27
+ end
28
+
29
+ say("\nPaths ignored:")
30
+ if config.ignore.empty?
31
+ say(" * (default: none)")
32
+ else
33
+ config.ignore.each do |path|
34
+ say(" * #{path}")
35
+ end
36
+ end
37
+
38
+ say("\nAllowed extensions:")
39
+ if config.allowed_extensions.empty?
40
+ say(" * .rb (default)")
41
+ say(" * .rbi (default)")
42
+ else
43
+ config.allowed_extensions.each do |ext|
44
+ say(" * #{ext}")
45
+ end
46
+ end
47
+ end
48
+
49
+ desc "files", "show files matching Sorbet config"
50
+ def files
51
+ in_sorbet_project!
52
+ config = Spoom::Sorbet::Config.parse_file(Spoom::Config::SORBET_CONFIG)
53
+ files = Spoom::Sorbet.srb_files(config)
54
+
55
+ say("Files matching `#{Spoom::Config::SORBET_CONFIG}`:")
56
+ if files.empty?
57
+ say(" NONE")
58
+ else
59
+ files.each do |path|
60
+ say(" * #{path}")
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,156 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'shellwords'
5
+
6
+ require_relative "base"
7
+ require_relative "../symbol_printer"
8
+ require_relative "../../sorbet/lsp"
9
+
10
+ module Spoom
11
+ module Cli
12
+ module Commands
13
+ class LSP < Base
14
+ default_task :show
15
+
16
+ desc "interactive", "interactive LSP mode"
17
+ def show
18
+ in_sorbet_project!
19
+ lsp = lsp_client
20
+ # TODO: run interactive mode
21
+ puts lsp
22
+ end
23
+
24
+ desc "list", "list all known symbols"
25
+ # TODO: options, filter, limit, kind etc.. filter rbi
26
+ def list
27
+ run do |client|
28
+ Dir["**/*.rb"].each do |file|
29
+ res = client.document_symbols(file.to_uri)
30
+ next if res.empty?
31
+ puts "Symbols from `#{file}`:"
32
+ SymbolPrinter.print_object(res, options["no_color"])
33
+ end
34
+ end
35
+ end
36
+
37
+ desc "hover", "request hover informations"
38
+ # TODO: options, filter, limit, kind etc.. filter rbi
39
+ def hover(file, line, col)
40
+ run do |client|
41
+ res = client.hover(file.to_uri, line.to_i, col.to_i)
42
+ say "Hovering `#{file}:#{line}:#{col}`:"
43
+ if res
44
+ SymbolPrinter.print_object(res, options["no_color"])
45
+ else
46
+ puts "<no data>"
47
+ end
48
+ end
49
+ end
50
+
51
+ desc "defs", "list definitions of a symbol"
52
+ # TODO: options, filter, limit, kind etc.. filter rbi
53
+ def defs(file, line, col)
54
+ run do |client|
55
+ res = client.definitions(file.to_uri, line.to_i, col.to_i)
56
+ puts "Definitions for `#{file}:#{line}:#{col}`:"
57
+ SymbolPrinter.print_list(res, options["no_color"])
58
+ end
59
+ end
60
+
61
+ desc "find", "find symbols matching a query"
62
+ # TODO: options, filter, limit, kind etc.. filter rbi
63
+ def find(query)
64
+ run do |client|
65
+ res = client.symbols(query)
66
+ puts "Symbols matching `#{query}`:"
67
+ printer = SymbolPrinter.new(2, options["no_color"])
68
+ res.each do |symbol|
69
+ next if symbol.location.uri.start_with?("https")
70
+ printer.visit(symbol)
71
+ end
72
+ end
73
+ end
74
+
75
+ desc "symbols", "list symbols from a file"
76
+ # TODO: options, filter, limit, kind etc.. filter rbi
77
+ def symbols(file)
78
+ run do |client|
79
+ res = client.document_symbols(file.to_uri)
80
+ puts "Symbols from `#{file}`:"
81
+ SymbolPrinter.print_object(res, options["no_color"])
82
+ end
83
+ end
84
+
85
+ desc "refs", "list references to a symbol"
86
+ # TODO: options, filter, limit, kind etc.. filter rbi
87
+ def refs(file, line, col)
88
+ run do |client|
89
+ res = client.references(file.to_uri, line.to_i, col.to_i)
90
+ puts "References to `#{file}:#{line}:#{col}`:"
91
+ SymbolPrinter.print_list(res, options["no_color"])
92
+ end
93
+ end
94
+
95
+ desc "sigs", "list signatures for a symbol"
96
+ # TODO: options, filter, limit, kind etc.. filter rbi
97
+ def sigs(file, line, col)
98
+ run do |client|
99
+ res = client.signatures(file.to_uri, line.to_i, col.to_i)
100
+ puts "Signature for `#{file}:#{line}:#{col}`:"
101
+ SymbolPrinter.print_list(res, options["no_color"])
102
+ end
103
+ end
104
+
105
+ desc "types", "display type of a symbol"
106
+ # TODO: options, filter, limit, kind etc.. filter rbi
107
+ def types(file, line, col)
108
+ run do |client|
109
+ res = client.type_definitions(file.to_uri, line.to_i, col.to_i)
110
+ say "Type for `#{file}:#{line}:#{col}`:"
111
+ SymbolPrinter.print_list(res, options["no_color"])
112
+ end
113
+ end
114
+
115
+ no_commands do
116
+ def lsp_client
117
+ in_sorbet_project!
118
+ client = Spoom::LSP::Client.new(
119
+ Spoom::Config::SORBET_PATH,
120
+ "--lsp",
121
+ "--enable-all-experimental-lsp-features",
122
+ "--disable-watchman",
123
+ )
124
+ client.open(Spoom::Config::WORKSPACE_PATH)
125
+ client
126
+ end
127
+
128
+ def run(&block)
129
+ client = lsp_client
130
+ block.call(client)
131
+ rescue Spoom::LSP::Error::Diagnostics => err
132
+ say_error("Sorbet returned typechecking errors for `#{err.uri.from_uri}`")
133
+ err.diagnostics.each do |d|
134
+ say_error("#{d.message} (#{d.code})", " #{d.range}")
135
+ end
136
+ exit(1)
137
+ rescue Spoom::LSP::Error::BadHeaders => err
138
+ say_error("Sorbet didn't answer correctly (#{err.message})")
139
+ exit(1)
140
+ rescue Spoom::LSP::Error => err
141
+ say_error(err.message)
142
+ exit(1)
143
+ ensure
144
+ begin
145
+ client&.close
146
+ rescue
147
+ # We can't do much if Sorbet refuse to close.
148
+ # We kill the parent process and let the child be killed.
149
+ exit(1)
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,92 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "base"
5
+
6
+ module Spoom
7
+ module Cli
8
+ module Commands
9
+ class Run < Base
10
+ default_task :tc
11
+
12
+ desc "tc", "run srb tc"
13
+ option :limit, type: :numeric, aliases: :l
14
+ option :code, type: :numeric, aliases: :c
15
+ option :sort, type: :string, aliases: :s
16
+ def tc
17
+ in_sorbet_project!
18
+
19
+ limit = options[:limit]
20
+ sort = options[:sort]
21
+ code = options[:code]
22
+ colors = !options[:no_color]
23
+
24
+ unless limit || code || sort
25
+ return Spoom::Sorbet.srb_tc(capture_err: false).last
26
+ end
27
+
28
+ output, status = Spoom::Sorbet.srb_tc(capture_err: true)
29
+ if status
30
+ $stderr.print(output)
31
+ return 0
32
+ end
33
+
34
+ errors = Spoom::Sorbet::Errors::Parser.parse_string(output)
35
+ errors_count = errors.size
36
+
37
+ errors = sort == "code" ? errors.sort_by { |e| [e.code, e.file, e.line, e.message] } : errors.sort
38
+ errors = errors.select { |e| e.code == code } if code
39
+ errors = T.must(errors.slice(0, limit)) if limit
40
+
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}"
45
+ end
46
+
47
+ if errors_count == errors.size
48
+ $stderr.puts "Errors: #{errors_count}"
49
+ else
50
+ $stderr.puts "Errors: #{errors.size} shown, #{errors_count} total"
51
+ end
52
+
53
+ 1
54
+ end
55
+
56
+ desc "metrics", "run srb tc and display metrics"
57
+ def metrics
58
+ in_sorbet_project!
59
+
60
+ metrics = Spoom::Sorbet.srb_metrics(capture_err: false)
61
+ if metrics
62
+ metrics.show
63
+ else
64
+ puts "no data"
65
+ end
66
+ end
67
+
68
+ no_commands do
69
+ def colorize_code(code, colors = true)
70
+ return code.to_s unless colors
71
+ code.to_s.light_black
72
+ end
73
+
74
+ def colorize_message(message, colors = true)
75
+ return message unless colors
76
+
77
+ cyan = T.let(false, T::Boolean)
78
+ word = StringIO.new
79
+ message.chars.each do |c|
80
+ if c == '`'
81
+ cyan = !cyan
82
+ next
83
+ end
84
+ word << (cyan ? c.cyan : c.red)
85
+ end
86
+ word.string
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,71 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "colorize"
5
+
6
+ module Spoom
7
+ module Cli
8
+ class SymbolPrinter
9
+ attr_accessor :seen, :no_color
10
+
11
+ def self.print_list(list, no_color)
12
+ printer = SymbolPrinter.new(2, no_color)
13
+ list.each do |item|
14
+ printer.print(" * ")
15
+ printer.visit(item)
16
+ printer.printn
17
+ end
18
+ end
19
+
20
+ def self.print_object(object, no_color)
21
+ printer = SymbolPrinter.new(2, no_color)
22
+ printer.visit(object)
23
+ end
24
+
25
+ def initialize(default_indent, no_color = false)
26
+ @seen = Set.new
27
+ @current_indent = default_indent
28
+ @no_color = no_color
29
+ String.disable_colorization = no_color
30
+ end
31
+
32
+ def indent
33
+ @current_indent += 2
34
+ end
35
+
36
+ def dedent
37
+ @current_indent -= 2
38
+ end
39
+
40
+ def print(string)
41
+ Kernel.print(string)
42
+ end
43
+
44
+ def printn
45
+ print("\n")
46
+ end
47
+
48
+ def printt
49
+ print(" " * @current_indent)
50
+ end
51
+
52
+ def visit(object)
53
+ if object.is_a?(Array)
54
+ object.each { |e| visit(e) }
55
+ else
56
+ object.accept_printer(self)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ class String
64
+ def to_uri
65
+ "file://" + File.join(Spoom::Config::WORKSPACE_PATH, self)
66
+ end
67
+
68
+ def from_uri
69
+ sub("file://#{Spoom::Config::WORKSPACE_PATH}", "")
70
+ end
71
+ end