spoom 1.5.0 → 1.7.2

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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -0
  3. data/lib/spoom/backtrace_filter/minitest.rb +3 -4
  4. data/lib/spoom/cli/deadcode.rb +1 -2
  5. data/lib/spoom/cli/helper.rb +41 -31
  6. data/lib/spoom/cli/srb/assertions.rb +48 -0
  7. data/lib/spoom/cli/srb/bump.rb +1 -2
  8. data/lib/spoom/cli/srb/coverage.rb +1 -1
  9. data/lib/spoom/cli/srb/metrics.rb +68 -0
  10. data/lib/spoom/cli/srb/sigs.rb +209 -0
  11. data/lib/spoom/cli/srb/tc.rb +16 -1
  12. data/lib/spoom/cli/srb.rb +16 -4
  13. data/lib/spoom/cli.rb +1 -2
  14. data/lib/spoom/colors.rb +2 -6
  15. data/lib/spoom/context/bundle.rb +8 -9
  16. data/lib/spoom/context/exec.rb +3 -6
  17. data/lib/spoom/context/file_system.rb +12 -19
  18. data/lib/spoom/context/git.rb +14 -19
  19. data/lib/spoom/context/sorbet.rb +14 -27
  20. data/lib/spoom/context.rb +4 -8
  21. data/lib/spoom/counters.rb +22 -0
  22. data/lib/spoom/coverage/d3/base.rb +6 -8
  23. data/lib/spoom/coverage/d3/circle_map.rb +6 -16
  24. data/lib/spoom/coverage/d3/pie.rb +14 -19
  25. data/lib/spoom/coverage/d3/timeline.rb +46 -47
  26. data/lib/spoom/coverage/d3.rb +2 -4
  27. data/lib/spoom/coverage/report.rb +41 -79
  28. data/lib/spoom/coverage/snapshot.rb +8 -14
  29. data/lib/spoom/coverage.rb +3 -5
  30. data/lib/spoom/deadcode/definition.rb +12 -14
  31. data/lib/spoom/deadcode/erb.rb +10 -8
  32. data/lib/spoom/deadcode/index.rb +21 -25
  33. data/lib/spoom/deadcode/indexer.rb +5 -6
  34. data/lib/spoom/deadcode/plugins/action_mailer.rb +2 -3
  35. data/lib/spoom/deadcode/plugins/action_mailer_preview.rb +2 -3
  36. data/lib/spoom/deadcode/plugins/actionpack.rb +19 -22
  37. data/lib/spoom/deadcode/plugins/active_model.rb +2 -3
  38. data/lib/spoom/deadcode/plugins/active_record.rb +62 -53
  39. data/lib/spoom/deadcode/plugins/active_support.rb +3 -2
  40. data/lib/spoom/deadcode/plugins/base.rb +29 -32
  41. data/lib/spoom/deadcode/plugins/graphql.rb +2 -3
  42. data/lib/spoom/deadcode/plugins/minitest.rb +4 -4
  43. data/lib/spoom/deadcode/plugins/namespaces.rb +5 -5
  44. data/lib/spoom/deadcode/plugins/rails.rb +5 -5
  45. data/lib/spoom/deadcode/plugins/rubocop.rb +5 -5
  46. data/lib/spoom/deadcode/plugins/ruby.rb +3 -4
  47. data/lib/spoom/deadcode/plugins/sorbet.rb +12 -6
  48. data/lib/spoom/deadcode/plugins/thor.rb +2 -3
  49. data/lib/spoom/deadcode/plugins.rb +23 -31
  50. data/lib/spoom/deadcode/remover.rb +58 -79
  51. data/lib/spoom/deadcode/send.rb +2 -8
  52. data/lib/spoom/file_collector.rb +11 -19
  53. data/lib/spoom/file_tree.rb +36 -51
  54. data/lib/spoom/location.rb +9 -20
  55. data/lib/spoom/model/builder.rb +54 -17
  56. data/lib/spoom/model/model.rb +71 -74
  57. data/lib/spoom/model/namespace_visitor.rb +4 -3
  58. data/lib/spoom/model/reference.rb +4 -8
  59. data/lib/spoom/model/references_visitor.rb +50 -30
  60. data/lib/spoom/parse.rb +4 -4
  61. data/lib/spoom/poset.rb +22 -24
  62. data/lib/spoom/printer.rb +10 -13
  63. data/lib/spoom/rbs.rb +77 -0
  64. data/lib/spoom/sorbet/config.rb +17 -24
  65. data/lib/spoom/sorbet/errors.rb +87 -45
  66. data/lib/spoom/sorbet/lsp/base.rb +10 -16
  67. data/lib/spoom/sorbet/lsp/errors.rb +8 -16
  68. data/lib/spoom/sorbet/lsp/structures.rb +65 -91
  69. data/lib/spoom/sorbet/lsp.rb +20 -22
  70. data/lib/spoom/sorbet/metrics/code_metrics_visitor.rb +236 -0
  71. data/lib/spoom/sorbet/metrics/metrics_file_parser.rb +34 -0
  72. data/lib/spoom/sorbet/metrics.rb +2 -32
  73. data/lib/spoom/sorbet/sigils.rb +16 -23
  74. data/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb +242 -0
  75. data/lib/spoom/sorbet/translate/sorbet_assertions_to_rbs_comments.rb +123 -0
  76. data/lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb +293 -0
  77. data/lib/spoom/sorbet/translate/strip_sorbet_sigs.rb +23 -0
  78. data/lib/spoom/sorbet/translate/translator.rb +71 -0
  79. data/lib/spoom/sorbet/translate.rb +49 -0
  80. data/lib/spoom/sorbet.rb +6 -12
  81. data/lib/spoom/source/rewriter.rb +167 -0
  82. data/lib/spoom/source.rb +4 -0
  83. data/lib/spoom/timeline.rb +4 -6
  84. data/lib/spoom/version.rb +1 -1
  85. data/lib/spoom/visitor.rb +298 -151
  86. data/lib/spoom.rb +4 -3
  87. data/rbi/spoom.rbi +3567 -0
  88. metadata +62 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff274fd4bbf10a58317e281a8e570908fc9827ff74799cb2380d3b90f032240e
4
- data.tar.gz: 16d95b8b1311eade324cbd6086b7f4fd2a30a760478582571cc6f88768a593d5
3
+ metadata.gz: 59987238a3b2bae115f02d1da2a7e2a4cd6b3532d816bbe80d9153fa7819f989
4
+ data.tar.gz: d7e6bccfa046b7c457cb3e5d3d4383141243949021bd584b22cde0db0cc165ef
5
5
  SHA512:
6
- metadata.gz: 90b22051252521cf38c3ed26a4e728526e57e190f0f1cd8aca1f2574f9f6219f608f28905ab4fb6dca1d54e04834182e470f9da779d664f205d269fb6ac4c0e4
7
- data.tar.gz: 3aa88be0f3edb8f2cc32d89e76ff83833f295204d3739424c6ea35dbe48db4df41a325771f840451b6bea1ed5e329ee2efaefd2c503a76ee483590f751a3017a
6
+ metadata.gz: b2f53478b7fdee59bfa7c579c3686b6d6c388db8737584c02f613daa2c4d42c0be2c1baaa39c50e90137697f62d7b8bb04e9fdbb458defdfa7813be3b6cbbd36
7
+ data.tar.gz: 57b26d457d2235bedaff15c832accc34a9c5ffcb89876306394f12b74a87ce03964d0309cd62e213e9954607150968607227436ebeb522a84f27782f7cc8cac8
data/README.md CHANGED
@@ -221,6 +221,20 @@ Count the number of type-checking errors if all files were bumped to true:
221
221
  $ spoom srb bump --count-errors --dry
222
222
  ```
223
223
 
224
+ #### Translate sigs between RBI and RBS
225
+
226
+ Translate all file sigs from RBI to RBS:
227
+
228
+ ```
229
+ $ spoom srb sigs translate
230
+ ```
231
+
232
+ Translate one file's sigs from RBS to RBI:
233
+
234
+ ```
235
+ $ spoom srb sigs translate --from rbs --to rbi /path/to/file.rb
236
+ ```
237
+
224
238
  #### Interact with Sorbet LSP mode
225
239
 
226
240
  **Experimental**
@@ -6,11 +6,10 @@ require "minitest"
6
6
  module Spoom
7
7
  module BacktraceFilter
8
8
  class Minitest < ::Minitest::BacktraceFilter
9
- extend T::Sig
9
+ SORBET_PATHS = Gem.loaded_specs["sorbet-runtime"].full_require_paths.freeze #: Array[String]
10
10
 
11
- SORBET_PATHS = T.let(Gem.loaded_specs["sorbet-runtime"].full_require_paths.freeze, T::Array[String])
12
-
13
- sig { override.params(bt: T.nilable(T::Array[String])).returns(T::Array[String]) }
11
+ # @override
12
+ #: (Array[String]? bt) -> Array[String]
14
13
  def filter(bt)
15
14
  super.select do |line|
16
15
  SORBET_PATHS.none? { |path| line.include?(path) }
@@ -6,7 +6,6 @@ require_relative "../deadcode"
6
6
  module Spoom
7
7
  module Cli
8
8
  class Deadcode < Thor
9
- extend T::Sig
10
9
  include Helper
11
10
 
12
11
  default_task :deadcode
@@ -48,7 +47,7 @@ module Spoom
48
47
  default: "name",
49
48
  enum: ["name", "location"],
50
49
  desc: "Sort the output by name or location"
51
- sig { params(paths: String).void }
50
+ #: (*String paths) -> void
52
51
  def deadcode(*paths)
53
52
  context = self.context
54
53
 
@@ -8,7 +8,6 @@ require "stringio"
8
8
  module Spoom
9
9
  module Cli
10
10
  module Helper
11
- extend T::Sig
12
11
  extend T::Helpers
13
12
 
14
13
  include Colorize
@@ -16,7 +15,7 @@ module Spoom
16
15
  requires_ancestor { Thor }
17
16
 
18
17
  # Print `message` on `$stdout`
19
- sig { params(message: String).void }
18
+ #: (String message) -> void
20
19
  def say(message)
21
20
  buffer = StringIO.new
22
21
  buffer << highlight(message)
@@ -29,13 +28,7 @@ module Spoom
29
28
  # Print `message` on `$stderr`
30
29
  #
31
30
  # The message is prefixed by a status (default: `Error`).
32
- sig do
33
- params(
34
- message: String,
35
- status: T.nilable(String),
36
- nl: T::Boolean,
37
- ).void
38
- end
31
+ #: (String message, ?status: String?, ?nl: bool) -> void
39
32
  def say_error(message, status: "Error", nl: true)
40
33
  buffer = StringIO.new
41
34
  buffer << "#{red(status)}: " if status
@@ -49,13 +42,7 @@ module Spoom
49
42
  # Print `message` on `$stderr`
50
43
  #
51
44
  # The message is prefixed by a status (default: `Warning`).
52
- sig do
53
- params(
54
- message: String,
55
- status: T.nilable(String),
56
- nl: T::Boolean,
57
- ).void
58
- end
45
+ #: (String message, ?status: String?, ?nl: bool) -> void
59
46
  def say_warning(message, status: "Warning", nl: true)
60
47
  buffer = StringIO.new
61
48
  buffer << "#{yellow(status)}: " if status
@@ -67,13 +54,13 @@ module Spoom
67
54
  end
68
55
 
69
56
  # Returns the context at `--path` (by default the current working directory)
70
- sig { returns(Context) }
57
+ #: -> Context
71
58
  def context
72
- @context ||= T.let(Context.new(exec_path), T.nilable(Context))
59
+ @context ||= Context.new(exec_path) #: Context?
73
60
  end
74
61
 
75
62
  # Raise if `spoom` is not ran inside a context with a `sorbet/config` file
76
- sig { returns(Context) }
63
+ #: -> Context
77
64
  def context_requiring_sorbet!
78
65
  context = self.context
79
66
  unless context.has_sorbet_config?
@@ -88,29 +75,52 @@ module Spoom
88
75
  end
89
76
 
90
77
  # Return the path specified through `--path`
91
- sig { returns(String) }
78
+ #: -> String
92
79
  def exec_path
93
80
  options[:path]
94
81
  end
95
82
 
83
+ # Collect files from `paths`, defaulting to `exec_path`
84
+ #: (Array[String] paths, ?include_rbi_files: bool) -> Array[String]
85
+ def collect_files(paths, include_rbi_files: false)
86
+ paths << exec_path if paths.empty?
87
+
88
+ files = paths.flat_map do |path|
89
+ if File.file?(path)
90
+ path
91
+ else
92
+ exts = ["rb"]
93
+ exts << "rbi" if include_rbi_files
94
+ Dir.glob("#{path}/**/*.{#{exts.join(",")}}")
95
+ end
96
+ end
97
+
98
+ if files.empty?
99
+ say_error("No files found")
100
+ exit(1)
101
+ end
102
+
103
+ files
104
+ end
105
+
96
106
  # Colors
97
107
 
98
108
  # Color used to highlight expressions in backticks
99
- HIGHLIGHT_COLOR = T.let(Spoom::Color::BLUE, Spoom::Color)
109
+ HIGHLIGHT_COLOR = Spoom::Color::BLUE #: Spoom::Color
100
110
 
101
111
  # Is the `--color` option true?
102
- sig { returns(T::Boolean) }
112
+ #: -> bool
103
113
  def color?
104
114
  options[:color]
105
115
  end
106
116
 
107
- sig { params(string: String).returns(String) }
117
+ #: (String string) -> String
108
118
  def highlight(string)
109
119
  return string unless color?
110
120
 
111
121
  res = StringIO.new
112
122
  word = StringIO.new
113
- in_ticks = T.let(false, T::Boolean)
123
+ in_ticks = false #: bool
114
124
  string.chars.each do |c|
115
125
  if c == "`" && !in_ticks
116
126
  in_ticks = true
@@ -128,39 +138,39 @@ module Spoom
128
138
  end
129
139
 
130
140
  # Colorize a string if `color?`
131
- sig { params(string: String, color: Color).returns(String) }
141
+ #: (String string, *Color color) -> String
132
142
  def colorize(string, *color)
133
143
  return string unless color?
134
144
 
135
145
  T.unsafe(self).set_color(string, *color)
136
146
  end
137
147
 
138
- sig { params(string: String).returns(String) }
148
+ #: (String string) -> String
139
149
  def blue(string)
140
150
  colorize(string, Color::BLUE)
141
151
  end
142
152
 
143
- sig { params(string: String).returns(String) }
153
+ #: (String string) -> String
144
154
  def cyan(string)
145
155
  colorize(string, Color::CYAN)
146
156
  end
147
157
 
148
- sig { params(string: String).returns(String) }
158
+ #: (String string) -> String
149
159
  def gray(string)
150
160
  colorize(string, Color::LIGHT_BLACK)
151
161
  end
152
162
 
153
- sig { params(string: String).returns(String) }
163
+ #: (String string) -> String
154
164
  def green(string)
155
165
  colorize(string, Color::GREEN)
156
166
  end
157
167
 
158
- sig { params(string: String).returns(String) }
168
+ #: (String string) -> String
159
169
  def red(string)
160
170
  colorize(string, Color::RED)
161
171
  end
162
172
 
163
- sig { params(string: String).returns(String) }
173
+ #: (String string) -> String
164
174
  def yellow(string)
165
175
  colorize(string, Color::YELLOW)
166
176
  end
@@ -0,0 +1,48 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Cli
6
+ module Srb
7
+ class Assertions < Thor
8
+ include Helper
9
+
10
+ desc "translate", "Translate type assertions from/to RBI and RBS"
11
+ option :from, type: :string, aliases: :f, desc: "From format", enum: ["rbi"], default: "rbi"
12
+ option :to, type: :string, aliases: :t, desc: "To format", enum: ["rbs"], default: "rbs"
13
+ def translate(*paths)
14
+ from = options[:from]
15
+ to = options[:to]
16
+ files = collect_files(paths)
17
+
18
+ say("Translating type assertions from `#{from}` to `#{to}` " \
19
+ "in `#{files.size}` file#{files.size == 1 ? "" : "s"}...\n\n")
20
+
21
+ transformed_files = transform_files(files) do |file, contents|
22
+ Spoom::Sorbet::Translate.sorbet_assertions_to_rbs_comments(contents, file: file)
23
+ end
24
+
25
+ say("Translated type assertions in `#{transformed_files}` file#{transformed_files == 1 ? "" : "s"}.")
26
+ end
27
+
28
+ no_commands do
29
+ def transform_files(files, &block)
30
+ transformed_count = 0
31
+
32
+ files.each do |file|
33
+ contents = File.read(file)
34
+ contents = block.call(file, contents)
35
+ File.write(file, contents)
36
+ transformed_count += 1
37
+ rescue Spoom::ParseError => error
38
+ say_warning("Can't parse #{file}: #{error.message}")
39
+ next
40
+ end
41
+
42
+ transformed_count
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -8,7 +8,6 @@ module Spoom
8
8
  module Cli
9
9
  module Srb
10
10
  class Bump < Thor
11
- extend T::Sig
12
11
  include Helper
13
12
 
14
13
  default_task :bump
@@ -46,7 +45,7 @@ module Spoom
46
45
  default: false,
47
46
  desc: "Count the number of errors if all files were bumped"
48
47
  option :sorbet_options, type: :string, default: "", desc: "Pass options to Sorbet"
49
- sig { params(directory: String).void }
48
+ #: (?String directory) -> void
50
49
  def bump(directory = ".")
51
50
  context = context_requiring_sorbet!
52
51
  from = options[:from]
@@ -89,7 +89,7 @@ module Spoom
89
89
 
90
90
  context.git_checkout!(ref: commit.sha)
91
91
 
92
- snapshot = T.let(nil, T.nilable(Spoom::Coverage::Snapshot))
92
+ snapshot = nil #: Spoom::Coverage::Snapshot?
93
93
  if options[:bundle_install]
94
94
  Bundler.with_unbundled_env do
95
95
  next unless bundle_install(path, commit.sha)
@@ -0,0 +1,68 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Cli
6
+ module Srb
7
+ class Metrics < Thor
8
+ include Helper
9
+
10
+ default_task :show
11
+
12
+ desc "show", "Show metrics about Sorbet usage"
13
+ option :dump, type: :boolean, default: false
14
+ def show(*paths)
15
+ files = collect_files(paths)
16
+ metrics = Spoom::Sorbet::Metrics.collect_code_metrics(files)
17
+
18
+ if options[:dump]
19
+ metrics.sort_by { |key, _value| key }.each do |key, value|
20
+ puts "#{key} #{value}"
21
+ end
22
+
23
+ return
24
+ end
25
+
26
+ say("Files: `#{files.size}`")
27
+
28
+ ["classes", "modules", "singleton_classes"].each do |key|
29
+ value = metrics[key]
30
+ next if value == 0
31
+
32
+ say("\n#{key.capitalize}: `#{value}`")
33
+ ["with_srb_type_params", "with_rbs_type_params"].each do |subkey|
34
+ say(" * #{subkey.gsub("_", " ")}: `#{metrics["#{key}_#{subkey}"]}`")
35
+ end
36
+ end
37
+
38
+ ["methods", "accessors"].each do |key|
39
+ value = metrics[key]
40
+ next if value == 0
41
+
42
+ say("\n#{key.capitalize}: `#{value}`")
43
+ ["without_sig", "with_srb_sig", "with_rbs_sig"].each do |subkey|
44
+ say(" * #{subkey.gsub("_", " ")}: `#{metrics["#{key}_#{subkey}"]}`")
45
+ end
46
+ end
47
+
48
+ say("\nT. calls: `#{metrics["T_calls"]}`")
49
+ metrics
50
+ .select { |key, _value| key.start_with?("T.") }
51
+ .sort_by { |_key, value| -value }
52
+ .each do |key, value|
53
+ say(" * #{key}: `#{value}`")
54
+ end
55
+
56
+ say("\nRBS Assertions: `#{metrics["rbs_assertions"]}`")
57
+ metrics
58
+ .reject { |key, _value| key == "rbs_assertions" }
59
+ .select { |key, _value| key.start_with?("rbs_") }
60
+ .sort_by { |_key, value| -value }
61
+ .each do |key, value|
62
+ say(" * #{key}: `#{value}`")
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,209 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Cli
6
+ module Srb
7
+ class Sigs < Thor
8
+ include Helper
9
+
10
+ desc "translate", "Translate signatures from/to RBI and RBS"
11
+ option :from, type: :string, aliases: :f, desc: "From format", enum: ["rbi", "rbs"], default: "rbi"
12
+ option :to, type: :string, aliases: :t, desc: "To format", enum: ["rbi", "rbs"], default: "rbs"
13
+ option :positional_names,
14
+ type: :boolean,
15
+ aliases: :p,
16
+ desc: "Use positional names when translating from RBI to RBS",
17
+ default: true
18
+ option :include_rbi_files, type: :boolean, desc: "Include RBI files", default: false
19
+ def translate(*paths)
20
+ from = options[:from]
21
+ to = options[:to]
22
+
23
+ if from == to
24
+ say_error("Can't translate signatures from `#{from}` to `#{to}`")
25
+ exit(1)
26
+ end
27
+
28
+ files = collect_files(paths, include_rbi_files: options[:include_rbi_files])
29
+
30
+ say("Translating signatures from `#{from}` to `#{to}` " \
31
+ "in `#{files.size}` file#{files.size == 1 ? "" : "s"}...\n\n")
32
+
33
+ case from
34
+ when "rbi"
35
+ transformed_files = transform_files(files) do |file, contents|
36
+ Spoom::Sorbet::Translate.sorbet_sigs_to_rbs_comments(contents, file: file, positional_names: options[:positional_names])
37
+ end
38
+ when "rbs"
39
+ transformed_files = transform_files(files) do |file, contents|
40
+ Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigs(contents, file: file)
41
+ end
42
+ end
43
+
44
+ say("Translated signatures in `#{transformed_files}` file#{transformed_files == 1 ? "" : "s"}.")
45
+ end
46
+
47
+ desc "strip", "Strip all the signatures from the files"
48
+ def strip(*paths)
49
+ files = collect_files(paths)
50
+
51
+ say("Stripping signatures from `#{files.size}` file#{files.size == 1 ? "" : "s"}...\n\n")
52
+
53
+ transformed_files = transform_files(files) do |file, contents|
54
+ Spoom::Sorbet::Translate.strip_sorbet_sigs(contents, file: file)
55
+ end
56
+
57
+ say("Stripped signatures from `#{transformed_files}` file#{transformed_files == 1 ? "" : "s"}.")
58
+ end
59
+
60
+ # Extract signatures from gem's files and save them to the output file
61
+ #
62
+ # This command will use Tapioca to generate a `.rbi` file that contains the signatures of all the files listed
63
+ # in the gemspec.
64
+ desc "export", "Export gem files signatures"
65
+ option :gemspec, type: :string, desc: "Path to the gemspec file", optional: true, default: nil
66
+ option :check_sync, type: :boolean, desc: "Check the generated RBI is up to date", default: false
67
+ def export(output_path = nil)
68
+ gemspec = options[:gemspec]
69
+
70
+ unless gemspec
71
+ say("Locating gemspec file...")
72
+ gemspec = Dir.glob("*.gemspec").first
73
+ unless gemspec
74
+ say_error("No gemspec file found")
75
+ exit(1)
76
+ end
77
+ say("Using `#{gemspec}` as gemspec file")
78
+ end
79
+
80
+ spec = Gem::Specification.load(gemspec)
81
+
82
+ # First, we copy the files to a temporary directory so we can rewrite them without messing with the
83
+ # original ones.
84
+ say("Copying files to a temporary directory...")
85
+ copy_context = Spoom::Context.mktmp!
86
+ FileUtils.cp_r(
87
+ ["Gemfile", "Gemfile.lock", gemspec, "lib/"],
88
+ copy_context.absolute_path,
89
+ )
90
+
91
+ # Then, we transform the copied files to translate all the RBS signatures into RBI signatures.
92
+ say("Translating signatures from RBS to RBI...")
93
+ files = collect_files([copy_context.absolute_path])
94
+ transform_files(files) do |file, contents|
95
+ Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigs(contents, file: file)
96
+ end
97
+
98
+ # We need to inject `extend T::Sig` to be sure all the classes can run the `sig{}` blocks.
99
+ # For this we find the entry point of the gem and inject the `extend T::Sig` line at the top of the file.
100
+ entry_point = "lib/#{spec.name}.rb"
101
+ unless copy_context.file?(entry_point)
102
+ say_error("No entry point found at `#{entry_point}`")
103
+ exit(1)
104
+ end
105
+
106
+ say("Injecting `extend T::Sig` to `#{entry_point}`...")
107
+ copy_context.write!(entry_point, <<~RB)
108
+ require "sorbet-runtime"
109
+
110
+ class Module; include T::Sig; end
111
+ extend T::Sig
112
+
113
+ #{copy_context.read(entry_point)}
114
+ RB
115
+
116
+ # Now we create a new context to import our modified gem and run tapioca
117
+ say("Running Tapioca...")
118
+ tapioca_context = Spoom::Context.mktmp!
119
+ tapioca_context.write!("Gemfile", <<~RB)
120
+ source "https://rubygems.org"
121
+
122
+ gem "tapioca"
123
+ gem "#{spec.name}", path: "#{copy_context.absolute_path}"
124
+ RB
125
+ exec(tapioca_context, "bundle install")
126
+ exec(tapioca_context, "bundle exec tapioca gem #{spec.name} --no-doc --no-loc --no-file-header")
127
+
128
+ rbi_path = tapioca_context.glob("sorbet/rbi/gems/#{spec.name}@*.rbi").first
129
+ unless rbi_path && tapioca_context.file?(rbi_path)
130
+ say_error("No RBI file found at `sorbet/rbi/gems/#{spec.name}@*.rbi`")
131
+ exit(1)
132
+ end
133
+
134
+ tapioca_context.write!(rbi_path, tapioca_context.read(rbi_path).gsub(/^# typed: true/, <<~RB.rstrip))
135
+ # typed: true
136
+
137
+ # DO NOT EDIT MANUALLY
138
+ # This is an autogenerated file for types exported from the `#{spec.name}` gem.
139
+ # Please instead update this file by running `spoom srb sigs export`.
140
+ RB
141
+
142
+ output_path ||= "rbi/#{spec.name}.rbi"
143
+ generated_path = tapioca_context.absolute_path_to(rbi_path)
144
+
145
+ if options[:check_sync]
146
+ # If the check option is set, we just compare the generated RBI with the one in the gem.
147
+ # If they are different, we exit with a non-zero exit code.
148
+ unless system("diff -u -L 'generated' -L 'current' #{generated_path} #{output_path} >&2")
149
+ say_error(<<~ERR, status: "\nError")
150
+ The RBI file at `#{output_path}` is not up to date
151
+
152
+ Please run `spoom srb sigs export` to update it.
153
+ ERR
154
+ exit(1)
155
+ end
156
+
157
+ say("The RBI file at `#{output_path}` is up to date")
158
+ exit(0)
159
+ else
160
+ output_dir = File.dirname(output_path)
161
+ FileUtils.rm_rf(output_dir)
162
+ FileUtils.mkdir_p(output_dir)
163
+ FileUtils.cp(generated_path, output_path)
164
+
165
+ say("Exported signatures to `#{output_path}`")
166
+ end
167
+ ensure
168
+ copy_context&.destroy!
169
+ tapioca_context&.destroy!
170
+ end
171
+
172
+ no_commands do
173
+ def transform_files(files, &block)
174
+ transformed_count = 0
175
+
176
+ files.each do |file|
177
+ contents = File.read(file)
178
+ first_line = contents.lines.first
179
+
180
+ if first_line&.start_with?("# encoding:")
181
+ encoding = T.must(first_line).gsub(/^#\s*encoding:\s*/, "").strip
182
+ contents = contents.force_encoding(encoding)
183
+ end
184
+
185
+ contents = block.call(file, contents)
186
+ File.write(file, contents)
187
+ transformed_count += 1
188
+ rescue RBI::Error => error
189
+ say_warning("Can't parse #{file}: #{error.message}")
190
+ next
191
+ end
192
+
193
+ transformed_count
194
+ end
195
+
196
+ def exec(context, command)
197
+ res = context.exec(command)
198
+
199
+ unless res.status
200
+ $stderr.puts "Error: #{command} failed"
201
+ $stderr.puts res.err
202
+ exit(1)
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -22,6 +22,7 @@ module Spoom
22
22
  option :format, type: :string, aliases: :f, desc: "Format line output"
23
23
  option :uniq, type: :boolean, aliases: :u, desc: "Remove duplicated lines"
24
24
  option :count, type: :boolean, default: true, desc: "Show errors count"
25
+ option :junit_output_path, type: :string, desc: "Output failures to XML file formatted for JUnit"
25
26
  option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
26
27
  option :sorbet_options, type: :string, default: "", desc: "Pass options to Sorbet"
27
28
  def tc(*paths_to_select)
@@ -32,6 +33,7 @@ module Spoom
32
33
  uniq = options[:uniq]
33
34
  format = options[:format]
34
35
  count = options[:count]
36
+ junit_output_path = options[:junit_output_path]
35
37
  sorbet = options[:sorbet]
36
38
 
37
39
  unless limit || code || sort
@@ -55,6 +57,12 @@ module Spoom
55
57
 
56
58
  if result.status
57
59
  say_error(result.err, status: nil, nl: false)
60
+ if junit_output_path
61
+ doc = Spoom::Sorbet::Errors.to_junit_xml([])
62
+ file = File.open(junit_output_path, "w")
63
+ doc.write(output: file, indent: 2)
64
+ file.close
65
+ end
58
66
  exit(0)
59
67
  end
60
68
 
@@ -94,6 +102,13 @@ module Spoom
94
102
  say_error(line, status: nil)
95
103
  end
96
104
 
105
+ if junit_output_path
106
+ doc = Spoom::Sorbet::Errors.to_junit_xml(errors)
107
+ file = File.open(junit_output_path, "w")
108
+ doc.write(output: file, indent: 2)
109
+ file.close
110
+ end
111
+
97
112
  if count
98
113
  if errors_count == errors.size
99
114
  say_error("Errors: #{errors_count}", status: nil)
@@ -132,7 +147,7 @@ module Spoom
132
147
  def colorize_message(message)
133
148
  return message unless color?
134
149
 
135
- cyan = T.let(false, T::Boolean)
150
+ cyan = false #: bool
136
151
  word = StringIO.new
137
152
  message.chars.each do |c|
138
153
  if c == "`"
data/lib/spoom/cli/srb.rb CHANGED
@@ -1,23 +1,35 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative "srb/assertions"
4
5
  require_relative "srb/bump"
5
6
  require_relative "srb/coverage"
6
7
  require_relative "srb/lsp"
8
+ require_relative "srb/metrics"
9
+ require_relative "srb/sigs"
7
10
  require_relative "srb/tc"
8
11
 
9
12
  module Spoom
10
13
  module Cli
11
14
  module Srb
12
15
  class Main < Thor
13
- desc "lsp", "Send LSP requests to Sorbet"
14
- subcommand "lsp", Spoom::Cli::Srb::LSP
16
+ desc "assertions", "Translate type assertions from/to RBI and RBS"
17
+ subcommand "assertions", Spoom::Cli::Srb::Assertions
18
+
19
+ desc "bump", "Change Sorbet sigils from one strictness to another when no errors"
20
+ subcommand "bump", Spoom::Cli::Srb::Bump
15
21
 
16
22
  desc "coverage", "Collect metrics related to Sorbet coverage"
17
23
  subcommand "coverage", Spoom::Cli::Srb::Coverage
18
24
 
19
- desc "bump", "Change Sorbet sigils from one strictness to another when no errors"
20
- subcommand "bump", Spoom::Cli::Srb::Bump
25
+ desc "lsp", "Send LSP requests to Sorbet"
26
+ subcommand "lsp", Spoom::Cli::Srb::LSP
27
+
28
+ desc "metrics", "Collect metrics about Sorbet usage"
29
+ subcommand "metrics", Spoom::Cli::Srb::Metrics
30
+
31
+ desc "sigs", "Translate signatures from/to RBI and RBS"
32
+ subcommand "sigs", Spoom::Cli::Srb::Sigs
21
33
 
22
34
  desc "tc", "Run typechecking with advanced options"
23
35
  subcommand "tc", Spoom::Cli::Srb::Tc