tapioca 0.8.3 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +5 -2
  3. data/README.md +182 -36
  4. data/lib/tapioca/cli.rb +122 -65
  5. data/lib/tapioca/commands/annotations.rb +165 -33
  6. data/lib/tapioca/commands/check_shims.rb +91 -0
  7. data/lib/tapioca/commands/{init.rb → configure.rb} +1 -1
  8. data/lib/tapioca/commands/dsl.rb +1 -1
  9. data/lib/tapioca/commands/gem.rb +5 -5
  10. data/lib/tapioca/commands.rb +2 -1
  11. data/lib/tapioca/dsl/compiler.rb +1 -13
  12. data/lib/tapioca/dsl/compilers/active_model_attributes.rb +1 -1
  13. data/lib/tapioca/dsl/compilers/active_record_relations.rb +17 -0
  14. data/lib/tapioca/dsl/compilers/active_record_typed_store.rb +5 -4
  15. data/lib/tapioca/dsl/compilers/protobuf.rb +6 -0
  16. data/lib/tapioca/dsl/compilers.rb +0 -4
  17. data/lib/tapioca/dsl/helpers/active_record_column_type_helper.rb +2 -2
  18. data/lib/tapioca/dsl/pipeline.rb +0 -2
  19. data/lib/tapioca/dsl.rb +8 -0
  20. data/lib/tapioca/executor.rb +0 -3
  21. data/lib/tapioca/gem/events.rb +16 -2
  22. data/lib/tapioca/gem/listeners/base.rb +11 -0
  23. data/lib/tapioca/gem/listeners/dynamic_mixins.rb +5 -0
  24. data/lib/tapioca/gem/listeners/foreign_constants.rb +65 -0
  25. data/lib/tapioca/gem/listeners/methods.rb +6 -17
  26. data/lib/tapioca/gem/listeners/mixins.rb +31 -10
  27. data/lib/tapioca/gem/listeners/remove_empty_payload_scopes.rb +5 -0
  28. data/lib/tapioca/gem/listeners/sorbet_enums.rb +5 -0
  29. data/lib/tapioca/gem/listeners/sorbet_helpers.rb +5 -0
  30. data/lib/tapioca/gem/listeners/sorbet_props.rb +5 -0
  31. data/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb +5 -0
  32. data/lib/tapioca/gem/listeners/sorbet_signatures.rb +6 -1
  33. data/lib/tapioca/gem/listeners/sorbet_type_variables.rb +5 -0
  34. data/lib/tapioca/gem/listeners/subconstants.rb +5 -0
  35. data/lib/tapioca/gem/listeners/yard_doc.rb +5 -0
  36. data/lib/tapioca/gem/listeners.rb +1 -0
  37. data/lib/tapioca/gem/pipeline.rb +58 -15
  38. data/lib/tapioca/gem.rb +6 -0
  39. data/lib/tapioca/gemfile.rb +7 -6
  40. data/lib/tapioca/helpers/cli_helper.rb +8 -2
  41. data/lib/tapioca/helpers/config_helper.rb +0 -2
  42. data/lib/tapioca/helpers/env_helper.rb +16 -0
  43. data/lib/tapioca/helpers/rbi_files_helper.rb +252 -0
  44. data/lib/tapioca/helpers/rbi_helper.rb +74 -94
  45. data/lib/tapioca/helpers/sorbet_helper.rb +2 -3
  46. data/lib/tapioca/helpers/test/content.rb +0 -2
  47. data/lib/tapioca/helpers/test/template.rb +0 -2
  48. data/lib/tapioca/internal.rb +34 -12
  49. data/lib/tapioca/rbi_ext/model.rb +2 -15
  50. data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +18 -16
  51. data/lib/tapioca/runtime/reflection.rb +27 -0
  52. data/lib/tapioca/runtime/trackers/constant_definition.rb +17 -7
  53. data/lib/tapioca/runtime/trackers/mixin.rb +49 -14
  54. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +1 -4
  55. data/lib/tapioca/static/requires_compiler.rb +0 -2
  56. data/lib/tapioca/static/symbol_loader.rb +26 -30
  57. data/lib/tapioca/static/symbol_table_parser.rb +0 -3
  58. data/lib/tapioca/version.rb +1 -1
  59. data/lib/tapioca.rb +3 -0
  60. metadata +22 -7
  61. data/lib/tapioca/dsl/helpers/param_helper.rb +0 -55
  62. data/lib/tapioca/helpers/shims_helper.rb +0 -115
  63. data/lib/tapioca/helpers/signatures_helper.rb +0 -17
  64. data/lib/tapioca/helpers/type_variable_helper.rb +0 -43
@@ -1,14 +1,12 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "pathname"
5
-
6
4
  module Tapioca
7
5
  module Gem
8
6
  class Pipeline
9
7
  extend T::Sig
10
8
  include Runtime::Reflection
11
- include SignaturesHelper
9
+ include RBIHelper
12
10
 
13
11
  IGNORED_SYMBOLS = T.let(["YAML", "MiniTest", "Mutex"], T::Array[String])
14
12
 
@@ -25,8 +23,8 @@ module Tapioca
25
23
  @events = T.let([], T::Array[Gem::Event])
26
24
 
27
25
  @payload_symbols = T.let(Static::SymbolLoader.payload_symbols, T::Set[String])
28
- @bootstrap_symbols = T.let(Static::SymbolLoader.gem_symbols(@gem).union(Static::SymbolLoader.engine_symbols),
29
- T::Set[String])
26
+ @bootstrap_symbols = T.let(load_bootstrap_symbols(@gem), T::Set[String])
27
+
30
28
  @bootstrap_symbols.each { |symbol| push_symbol(symbol) }
31
29
 
32
30
  @node_listeners = T.let([], T::Array[Gem::Listeners::Base])
@@ -41,6 +39,7 @@ module Tapioca
41
39
  @node_listeners << Gem::Listeners::SorbetSignatures.new(self)
42
40
  @node_listeners << Gem::Listeners::Subconstants.new(self)
43
41
  @node_listeners << Gem::Listeners::YardDoc.new(self) if include_doc
42
+ @node_listeners << Gem::Listeners::ForeignConstants.new(self)
44
43
  @node_listeners << Gem::Listeners::RemoveEmptyPayloadScopes.new(self)
45
44
  end
46
45
 
@@ -60,16 +59,30 @@ module Tapioca
60
59
  @events << Gem::ConstantFound.new(symbol, constant)
61
60
  end
62
61
 
62
+ sig { params(symbol: String, constant: Module).void.checked(:never) }
63
+ def push_foreign_constant(symbol, constant)
64
+ @events << Gem::ForeignConstantFound.new(symbol, constant)
65
+ end
66
+
63
67
  sig { params(symbol: String, constant: Module, node: RBI::Const).void.checked(:never) }
64
68
  def push_const(symbol, constant, node)
65
69
  @events << Gem::ConstNodeAdded.new(symbol, constant, node)
66
70
  end
67
71
 
68
- sig { params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never) }
72
+ sig do
73
+ params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never)
74
+ end
69
75
  def push_scope(symbol, constant, node)
70
76
  @events << Gem::ScopeNodeAdded.new(symbol, constant, node)
71
77
  end
72
78
 
79
+ sig do
80
+ params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never)
81
+ end
82
+ def push_foreign_scope(symbol, constant, node)
83
+ @events << Gem::ForeignScopeNodeAdded.new(symbol, constant, node)
84
+ end
85
+
73
86
  sig do
74
87
  params(
75
88
  symbol: String,
@@ -114,6 +127,14 @@ module Tapioca
114
127
 
115
128
  private
116
129
 
130
+ sig { params(gem: Gemfile::GemSpec).returns(T::Set[String]) }
131
+ def load_bootstrap_symbols(gem)
132
+ engine_symbols = Static::SymbolLoader.engine_symbols(gem)
133
+ gem_symbols = Static::SymbolLoader.gem_symbols(gem)
134
+
135
+ gem_symbols.union(engine_symbols)
136
+ end
137
+
117
138
  sig { returns(Gem::Event) }
118
139
  def next_event
119
140
  T.must(@events.shift)
@@ -150,13 +171,14 @@ module Tapioca
150
171
  return if name.start_with?("#<")
151
172
  return if name.downcase == name
152
173
  return if alias_namespaced?(name)
153
- return if seen?(name)
154
174
 
155
- constant = event.constant
156
- return if T::Enum === constant # T::Enum instances are defined via `compile_enums`
175
+ return if T::Enum === event.constant # T::Enum instances are defined via `compile_enums`
157
176
 
158
- mark_seen(name)
159
- compile_constant(name, constant)
177
+ if event.is_a?(Gem::ForeignConstantFound)
178
+ compile_foreign_constant(name, event.constant)
179
+ else
180
+ compile_constant(name, event.constant)
181
+ end
160
182
  end
161
183
 
162
184
  sig { params(event: Gem::NodeAdded).void }
@@ -166,6 +188,11 @@ module Tapioca
166
188
 
167
189
  # Compile
168
190
 
191
+ sig { params(symbol: String, constant: Module).void }
192
+ def compile_foreign_constant(symbol, constant)
193
+ compile_module(symbol, constant, foreign_constant: true)
194
+ end
195
+
169
196
  sig { params(symbol: String, constant: BasicObject).void.checked(:never) }
170
197
  def compile_constant(symbol, constant)
171
198
  case constant
@@ -182,6 +209,10 @@ module Tapioca
182
209
 
183
210
  sig { params(name: String, constant: Module).void }
184
211
  def compile_alias(name, constant)
212
+ return if seen?(name)
213
+
214
+ mark_seen(name)
215
+
185
216
  return if symbol_in_payload?(name)
186
217
 
187
218
  target = name_of(constant)
@@ -199,6 +230,10 @@ module Tapioca
199
230
 
200
231
  sig { params(name: String, value: BasicObject).void.checked(:never) }
201
232
  def compile_object(name, value)
233
+ return if seen?(name)
234
+
235
+ mark_seen(name)
236
+
202
237
  return if symbol_in_payload?(name)
203
238
 
204
239
  klass = class_of(value)
@@ -228,10 +263,13 @@ module Tapioca
228
263
  @root << node
229
264
  end
230
265
 
231
- sig { params(name: String, constant: Module).void }
232
- def compile_module(name, constant)
233
- return unless defined_in_gem?(constant, strict: false)
266
+ sig { params(name: String, constant: Module, foreign_constant: T::Boolean).void }
267
+ def compile_module(name, constant, foreign_constant: false)
268
+ return unless defined_in_gem?(constant, strict: false) || foreign_constant
234
269
  return if Tapioca::TypeVariableModule === constant
270
+ return if seen?(name)
271
+
272
+ mark_seen(name)
235
273
 
236
274
  scope =
237
275
  if constant.is_a?(Class)
@@ -241,7 +279,12 @@ module Tapioca
241
279
  RBI::Module.new(name)
242
280
  end
243
281
 
244
- push_scope(name, constant, scope)
282
+ if foreign_constant
283
+ push_foreign_scope(name, constant, scope)
284
+ else
285
+ push_scope(name, constant, scope)
286
+ end
287
+
245
288
  @root << scope
246
289
  end
247
290
 
@@ -0,0 +1,6 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "tapioca/gem/events"
5
+ require "tapioca/gem/listeners"
6
+ require "tapioca/gem/pipeline"
@@ -1,10 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "bundler"
5
- require "logger"
6
- require "yard-sorbet"
7
-
8
4
  module Tapioca
9
5
  class Gemfile
10
6
  extend(T::Sig)
@@ -140,8 +136,13 @@ module Tapioca
140
136
  extend(T::Sig)
141
137
  include GemHelper
142
138
 
143
- IGNORED_GEMS = T.let(["sorbet", "sorbet-static", "sorbet-runtime", "sorbet-static-and-runtime"].freeze,
144
- T::Array[String])
139
+ IGNORED_GEMS = T.let(
140
+ [
141
+ "sorbet", "sorbet-static", "sorbet-runtime", "sorbet-static-and-runtime",
142
+ "debug", "fakefs",
143
+ ].freeze,
144
+ T::Array[String]
145
+ )
145
146
 
146
147
  sig { returns(String) }
147
148
  attr_reader :full_gem_path, :version
@@ -1,8 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "thor"
5
-
6
4
  module Tapioca
7
5
  module CliHelper
8
6
  extend T::Sig
@@ -30,5 +28,13 @@ module Tapioca
30
28
  rbi_formatter.max_line_length = options[:rbi_max_line_length]
31
29
  rbi_formatter
32
30
  end
31
+
32
+ sig { params(options: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
33
+ def netrc_file(options)
34
+ return nil if options[:auth]
35
+ return nil unless options[:netrc]
36
+
37
+ options[:netrc_file] || ENV["TAPIOCA_NETRC_FILE"] || File.join(ENV["HOME"].to_s, ".netrc")
38
+ end
33
39
  end
34
40
  end
@@ -1,8 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "yaml"
5
-
6
4
  module Tapioca
7
5
  module ConfigHelper
8
6
  extend T::Sig
@@ -0,0 +1,16 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module EnvHelper
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ requires_ancestor { Thor }
10
+
11
+ sig { params(options: T::Hash[Symbol, T.untyped]).void }
12
+ def set_environment(options) # rubocop:disable Naming/AccessorMethodName
13
+ ENV["RAILS_ENV"] = ENV["RACK_ENV"] = options[:environment]
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,252 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module RBIFilesHelper
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ requires_ancestor { Thor::Shell }
10
+ requires_ancestor { SorbetHelper }
11
+
12
+ sig { params(index: RBI::Index, dir: String).void }
13
+ def index_payload(index, dir)
14
+ return unless Dir.exist?(dir)
15
+
16
+ say("Loading Sorbet payload... ")
17
+ files = Dir.glob("#{dir}/**/*.rbi").sort
18
+ parse_and_index_files(index, files)
19
+ say(" Done", :green)
20
+ end
21
+
22
+ sig { params(index: RBI::Index, kind: String, file: String).void }
23
+ def index_rbi(index, kind, file)
24
+ return unless File.exist?(file)
25
+
26
+ say("Loading #{kind} RBIs from #{file}... ")
27
+ parse_and_index_file(index, file)
28
+ say(" Done", :green)
29
+ end
30
+
31
+ sig { params(index: RBI::Index, kind: String, dir: String).void }
32
+ def index_rbis(index, kind, dir)
33
+ return unless Dir.exist?(dir) && !Dir.empty?(dir)
34
+
35
+ say("Loading #{kind} RBIs from #{dir}... ")
36
+ files = Dir.glob("#{dir}/**/*.rbi").sort
37
+ parse_and_index_files(index, files)
38
+ say(" Done", :green)
39
+ end
40
+
41
+ sig do
42
+ params(
43
+ index: RBI::Index,
44
+ shim_rbi_dir: String,
45
+ todo_rbi_file: String
46
+ ).returns(T::Hash[String, T::Array[RBI::Node]])
47
+ end
48
+ def duplicated_nodes_from_index(index, shim_rbi_dir:, todo_rbi_file:)
49
+ duplicates = {}
50
+ say("Looking for duplicates... ")
51
+ index.keys.each do |key|
52
+ nodes = index[key]
53
+ next unless shims_or_todos_have_duplicates?(nodes, shim_rbi_dir: shim_rbi_dir, todo_rbi_file: todo_rbi_file)
54
+
55
+ duplicates[key] = nodes
56
+ end
57
+ say(" Done", :green)
58
+ duplicates
59
+ end
60
+
61
+ sig { params(loc: RBI::Loc, path_prefix: T.nilable(String)).returns(String) }
62
+ def location_to_payload_url(loc, path_prefix:)
63
+ return loc.to_s unless path_prefix
64
+
65
+ url = loc.file || ""
66
+ return loc.to_s unless url.start_with?(path_prefix)
67
+
68
+ url = url.sub(path_prefix, SorbetHelper::SORBET_PAYLOAD_URL)
69
+ url = "#{url}#L#{loc.begin_line}"
70
+ url
71
+ end
72
+
73
+ sig do
74
+ params(
75
+ command: String,
76
+ gem_dir: String,
77
+ dsl_dir: String,
78
+ auto_strictness: T::Boolean,
79
+ gems: T::Array[Gemfile::GemSpec],
80
+ compilers: T::Enumerable[Class]
81
+ ).void
82
+ end
83
+ def validate_rbi_files(command:, gem_dir:, dsl_dir:, auto_strictness:, gems: [], compilers: [])
84
+ error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
85
+
86
+ say("Checking generated RBI files... ")
87
+ res = sorbet(
88
+ "--no-config",
89
+ "--error-url-base=#{error_url_base}",
90
+ "--stop-after namer",
91
+ dsl_dir,
92
+ gem_dir
93
+ )
94
+ say(" Done", :green)
95
+
96
+ errors = Spoom::Sorbet::Errors::Parser.parse_string(res.err)
97
+
98
+ if errors.empty?
99
+ say(" No errors found\n\n", [:green, :bold])
100
+ return
101
+ end
102
+
103
+ parse_errors = errors.select { |error| error.code < 4000 }
104
+
105
+ if parse_errors.any?
106
+ say_error(<<~ERR, :red)
107
+
108
+ ##### INTERNAL ERROR #####
109
+
110
+ There are parse errors in the generated RBI files.
111
+
112
+ This seems related to a bug in Tapioca.
113
+ Please open an issue at https://github.com/Shopify/tapioca/issues/new with the following information:
114
+
115
+ Tapioca v#{Tapioca::VERSION}
116
+
117
+ Command:
118
+ #{command}
119
+
120
+ ERR
121
+
122
+ say_error(<<~ERR, :red) if gems.any?
123
+ Gems:
124
+ #{gems.map { |gem| " #{gem.name} (#{gem.version})" }.join("\n")}
125
+
126
+ ERR
127
+
128
+ say_error(<<~ERR, :red) if compilers.any?
129
+ Compilers:
130
+ #{compilers.map { |compiler| " #{compiler.name}" }.join("\n")}
131
+
132
+ ERR
133
+
134
+ say_error(<<~ERR, :red)
135
+ Errors:
136
+ #{parse_errors.map { |error| " #{error}" }.join("\n")}
137
+
138
+ ##########################
139
+
140
+ ERR
141
+ end
142
+
143
+ if auto_strictness
144
+ redef_errors = errors.select { |error| error.code == 4010 }
145
+ update_gem_rbis_strictnesses(redef_errors, gem_dir)
146
+ end
147
+
148
+ Kernel.exit(1) if parse_errors.any?
149
+ end
150
+
151
+ private
152
+
153
+ sig { params(index: RBI::Index, files: T::Array[String]).void }
154
+ def parse_and_index_files(index, files)
155
+ files.each do |file|
156
+ parse_and_index_file(index, file)
157
+ end
158
+ end
159
+
160
+ sig { params(index: RBI::Index, file: String).void }
161
+ def parse_and_index_file(index, file)
162
+ tree = RBI::Parser.parse_file(file)
163
+ index.visit(tree)
164
+ rescue RBI::ParseError => e
165
+ say_error("\nWarning: #{e} (#{e.location})", :yellow)
166
+ end
167
+
168
+ sig { params(nodes: T::Array[RBI::Node], shim_rbi_dir: String, todo_rbi_file: String).returns(T::Boolean) }
169
+ def shims_or_todos_have_duplicates?(nodes, shim_rbi_dir:, todo_rbi_file:)
170
+ return false if nodes.size == 1
171
+
172
+ shims_or_todos = extract_shims_and_todos(nodes, shim_rbi_dir: shim_rbi_dir, todo_rbi_file: todo_rbi_file)
173
+ return false if shims_or_todos.empty?
174
+
175
+ shims_or_todos_empty_scopes = extract_empty_scopes(shims_or_todos)
176
+ return true unless shims_or_todos_empty_scopes.empty?
177
+
178
+ props = extract_methods_and_attrs(shims_or_todos)
179
+ return false if props.empty?
180
+
181
+ shims_or_todos_with_sigs = extract_nodes_with_sigs(props)
182
+ shims_or_todos_with_sigs.each do |shim_or_todo|
183
+ shims_or_todos_sigs = shim_or_todo.sigs
184
+
185
+ extract_methods_and_attrs(nodes).each do |node|
186
+ next if node == shim_or_todo
187
+ return true if shims_or_todos_sigs.all? { |sig| node.sigs.include?(sig) }
188
+ end
189
+
190
+ return false
191
+ end
192
+
193
+ true
194
+ end
195
+
196
+ sig { params(nodes: T::Array[RBI::Node], shim_rbi_dir: String, todo_rbi_file: String).returns(T::Array[RBI::Node]) }
197
+ def extract_shims_and_todos(nodes, shim_rbi_dir:, todo_rbi_file:)
198
+ nodes.select do |node|
199
+ node.loc&.file&.start_with?(shim_rbi_dir) || node.loc&.file == todo_rbi_file
200
+ end
201
+ end
202
+
203
+ sig { params(nodes: T::Array[RBI::Node]).returns(T::Array[RBI::Scope]) }
204
+ def extract_empty_scopes(nodes)
205
+ T.cast(nodes.select { |node| node.is_a?(RBI::Scope) && node.empty? }, T::Array[RBI::Scope])
206
+ end
207
+
208
+ sig { params(nodes: T::Array[RBI::Node]).returns(T::Array[T.any(RBI::Method, RBI::Attr)]) }
209
+ def extract_methods_and_attrs(nodes)
210
+ T.cast(nodes.select do |node|
211
+ node.is_a?(RBI::Method) || node.is_a?(RBI::Attr)
212
+ end, T::Array[T.any(RBI::Method, RBI::Attr)])
213
+ end
214
+
215
+ sig { params(nodes: T::Array[T.any(RBI::Method, RBI::Attr)]).returns(T::Array[T.any(RBI::Method, RBI::Attr)]) }
216
+ def extract_nodes_with_sigs(nodes)
217
+ nodes.reject { |node| node.sigs.empty? }
218
+ end
219
+
220
+ sig { params(errors: T::Array[Spoom::Sorbet::Errors::Error], gem_dir: String).void }
221
+ def update_gem_rbis_strictnesses(errors, gem_dir)
222
+ files = []
223
+
224
+ errors.each do |error|
225
+ # Collect the file with error
226
+ files << error.file
227
+ error.more.each do |line|
228
+ # Also collect the conflicting definition file paths
229
+ next unless line.include?("Previous definition")
230
+
231
+ files << line.split(":").first&.strip
232
+ end
233
+ end
234
+
235
+ files
236
+ .uniq
237
+ .sort
238
+ .select { |file| file.start_with?(gem_dir) }
239
+ .each do |file|
240
+ Spoom::Sorbet::Sigils.change_sigil_in_file(file, "false")
241
+ say("\n Changed strictness of #{file} to `typed: false` (conflicting with DSL files)", [:yellow, :bold])
242
+ end
243
+
244
+ say("\n")
245
+ end
246
+
247
+ sig { params(path: String).returns(String) }
248
+ def gem_name_from_rbi_path(path)
249
+ T.must(File.basename(path, ".rbi").split("@").first)
250
+ end
251
+ end
252
+ end
@@ -4,121 +4,101 @@
4
4
  module Tapioca
5
5
  module RBIHelper
6
6
  extend T::Sig
7
- extend T::Helpers
7
+ include SorbetHelper
8
+ extend SorbetHelper
9
+ extend self
8
10
 
9
- requires_ancestor { Thor::Shell }
10
- requires_ancestor { SorbetHelper }
11
-
12
- sig do
13
- params(
14
- command: String,
15
- gem_dir: String,
16
- dsl_dir: String,
17
- auto_strictness: T::Boolean,
18
- gems: T::Array[Gemfile::GemSpec],
19
- compilers: T::Enumerable[Class]
20
- ).void
11
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
12
+ def create_param(name, type:)
13
+ create_typed_param(RBI::Param.new(name), type)
21
14
  end
22
- def validate_rbi_files(command:, gem_dir:, dsl_dir:, auto_strictness:, gems: [], compilers: [])
23
- error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
24
-
25
- say("Checking generated RBI files... ")
26
- res = sorbet(
27
- "--no-config",
28
- "--error-url-base=#{error_url_base}",
29
- "--stop-after namer",
30
- dsl_dir,
31
- gem_dir
32
- )
33
- say(" Done", :green)
34
-
35
- errors = Spoom::Sorbet::Errors::Parser.parse_string(res.err)
36
-
37
- if errors.empty?
38
- say(" No errors found\n\n", [:green, :bold])
39
- return
40
- end
41
-
42
- parse_errors = errors.select { |error| error.code < 4000 }
43
-
44
- if parse_errors.any?
45
- say_error(<<~ERR, :red)
46
-
47
- ##### INTERNAL ERROR #####
48
15
 
49
- There are parse errors in the generated RBI files.
50
-
51
- This seems related to a bug in Tapioca.
52
- Please open an issue at https://github.com/Shopify/tapioca/issues/new with the following information:
53
-
54
- Tapioca v#{Tapioca::VERSION}
55
-
56
- Command:
57
- #{command}
58
-
59
- ERR
60
-
61
- say_error(<<~ERR, :red) if gems.any?
62
- Gems:
63
- #{gems.map { |gem| " #{gem.name} (#{gem.version})" }.join("\n")}
16
+ sig { params(name: String, type: String, default: String).returns(RBI::TypedParam) }
17
+ def create_opt_param(name, type:, default:)
18
+ create_typed_param(RBI::OptParam.new(name, default), type)
19
+ end
64
20
 
65
- ERR
21
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
22
+ def create_rest_param(name, type:)
23
+ create_typed_param(RBI::RestParam.new(name), type)
24
+ end
66
25
 
67
- say_error(<<~ERR, :red) if compilers.any?
68
- Compilers:
69
- #{compilers.map { |compiler| " #{compiler.name}" }.join("\n")}
26
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
27
+ def create_kw_param(name, type:)
28
+ create_typed_param(RBI::KwParam.new(name), type)
29
+ end
70
30
 
71
- ERR
31
+ sig { params(name: String, type: String, default: String).returns(RBI::TypedParam) }
32
+ def create_kw_opt_param(name, type:, default:)
33
+ create_typed_param(RBI::KwOptParam.new(name, default), type)
34
+ end
72
35
 
73
- say_error(<<~ERR, :red)
74
- Errors:
75
- #{parse_errors.map { |error| " #{error}" }.join("\n")}
36
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
37
+ def create_kw_rest_param(name, type:)
38
+ create_typed_param(RBI::KwRestParam.new(name), type)
39
+ end
76
40
 
77
- ##########################
41
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
42
+ def create_block_param(name, type:)
43
+ create_typed_param(RBI::BlockParam.new(name), type)
44
+ end
78
45
 
79
- ERR
80
- end
46
+ sig { params(param: RBI::Param, type: String).returns(RBI::TypedParam) }
47
+ def create_typed_param(param, type)
48
+ RBI::TypedParam.new(param: param, type: sanitize_signature_types(type))
49
+ end
81
50
 
82
- if auto_strictness
83
- redef_errors = errors.select { |error| error.code == 4010 }
84
- update_gem_rbis_strictnesses(redef_errors, gem_dir)
85
- end
51
+ sig { params(sig_string: String).returns(String) }
52
+ def sanitize_signature_types(sig_string)
53
+ sig_string
54
+ .gsub(".returns(<VOID>)", ".void")
55
+ .gsub("<VOID>", "void")
56
+ .gsub("<NOT-TYPED>", "T.untyped")
57
+ .gsub(".params()", "")
58
+ end
86
59
 
87
- Kernel.exit(1) if parse_errors.any?
60
+ sig do
61
+ params(
62
+ type: String,
63
+ variance: Symbol,
64
+ fixed: T.nilable(String),
65
+ upper: T.nilable(String),
66
+ lower: T.nilable(String)
67
+ ).returns(String)
88
68
  end
69
+ def self.serialize_type_variable(type, variance, fixed, upper, lower)
70
+ variance = nil if variance == :invariant
89
71
 
90
- private
72
+ bounds = []
73
+ bounds << "fixed: #{fixed}" if fixed
74
+ bounds << "lower: #{lower}" if lower
75
+ bounds << "upper: #{upper}" if upper
91
76
 
92
- sig { params(errors: T::Array[Spoom::Sorbet::Errors::Error], gem_dir: String).void }
93
- def update_gem_rbis_strictnesses(errors, gem_dir)
94
- files = []
77
+ parameters = []
78
+ block = []
95
79
 
96
- errors.each do |error|
97
- # Collect the file with error
98
- files << error.file
99
- error.more.each do |line|
100
- # Also collect the conflicting definition file paths
101
- next unless line.include?("Previous definition")
80
+ parameters << ":#{variance}" if variance
102
81
 
103
- files << line.split(":").first&.strip
104
- end
82
+ if sorbet_supports?(:type_variable_block_syntax)
83
+ block = bounds
84
+ else
85
+ parameters.concat(bounds)
105
86
  end
106
87
 
107
- files
108
- .uniq
109
- .sort
110
- .select { |file| file.start_with?(gem_dir) }
111
- .each do |file|
112
- Spoom::Sorbet::Sigils.change_sigil_in_file(file, "false")
113
- say("\n Changed strictness of #{file} to `typed: false` (conflicting with DSL files)", [:yellow, :bold])
114
- end
88
+ serialized = type.dup
89
+ serialized << "(#{parameters.join(", ")})" unless parameters.empty?
90
+ serialized << " { { #{block.join(", ")} } }" unless block.empty?
91
+ serialized
92
+ end
115
93
 
116
- say("\n")
94
+ sig { params(name: String).returns(T::Boolean) }
95
+ def valid_method_name?(name)
96
+ !name.to_sym.inspect.start_with?(':"', ":@", ":$")
117
97
  end
118
98
 
119
- sig { params(path: String).returns(String) }
120
- def gem_name_from_rbi_path(path)
121
- T.must(File.basename(path, ".rbi").split("@").first)
99
+ sig { params(name: String).returns(T::Boolean) }
100
+ def valid_parameter_name?(name)
101
+ /^([[:lower:]]|_|[^[[:ascii:]]])([[:alnum:]]|_|[^[[:ascii:]]])*$/.match?(name)
122
102
  end
123
103
  end
124
104
  end