spoom 1.0.3 → 1.0.4

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