tapioca 0.19.1 → 0.20.0

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -0
  3. data/lib/ruby_lsp/tapioca/run_gem_rbi_check.rb +1 -1
  4. data/lib/tapioca/cli.rb +9 -0
  5. data/lib/tapioca/commands/abstract_dsl.rb +76 -20
  6. data/lib/tapioca/commands/abstract_gem.rb +1 -0
  7. data/lib/tapioca/commands/annotations.rb +1 -1
  8. data/lib/tapioca/commands/gem_generate.rb +20 -4
  9. data/lib/tapioca/dsl/compiler.rb +24 -10
  10. data/lib/tapioca/dsl/compilers/aasm.rb +24 -17
  11. data/lib/tapioca/dsl/compilers/active_job.rb +1 -1
  12. data/lib/tapioca/dsl/compilers/active_model_attributes.rb +1 -1
  13. data/lib/tapioca/dsl/compilers/active_record_delegated_types.rb +64 -16
  14. data/lib/tapioca/dsl/compilers/active_record_fixtures.rb +17 -11
  15. data/lib/tapioca/dsl/compilers/active_record_relations.rb +2 -2
  16. data/lib/tapioca/dsl/compilers/config.rb +1 -1
  17. data/lib/tapioca/dsl/compilers/json_api_client_resource.rb +1 -1
  18. data/lib/tapioca/dsl/compilers/sidekiq_worker.rb +1 -1
  19. data/lib/tapioca/dsl/compilers/url_helpers.rb +312 -8
  20. data/lib/tapioca/dsl/compilers.rb +1 -1
  21. data/lib/tapioca/dsl/helpers/graphql_type_helper.rb +10 -3
  22. data/lib/tapioca/dsl/pipeline.rb +6 -27
  23. data/lib/tapioca/gem/events.rb +1 -1
  24. data/lib/tapioca/gem/listeners/documentation.rb +13 -8
  25. data/lib/tapioca/gem/listeners/methods.rb +79 -13
  26. data/lib/tapioca/gem/listeners/sorbet_enums.rb +1 -1
  27. data/lib/tapioca/gem/pipeline.rb +3 -3
  28. data/lib/tapioca/gemfile.rb +13 -0
  29. data/lib/tapioca/helpers/env_helper.rb +1 -1
  30. data/lib/tapioca/helpers/file_helper.rb +42 -0
  31. data/lib/tapioca/helpers/rbi_files_helper.rb +96 -5
  32. data/lib/tapioca/helpers/rbi_helper.rb +17 -3
  33. data/lib/tapioca/helpers/sorbet_helper.rb +1 -1
  34. data/lib/tapioca/internal.rb +2 -0
  35. data/lib/tapioca/loaders/gem.rb +33 -0
  36. data/lib/tapioca/loaders/loader.rb +0 -13
  37. data/lib/tapioca/rbi_ext/model.rb +16 -2
  38. data/lib/tapioca/rbs/bootsnap_cache.rb +52 -0
  39. data/lib/tapioca/rbs/rewriter.rb +91 -25
  40. data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +1 -2
  41. data/lib/tapioca/runtime/generic_type_registry.rb +38 -14
  42. data/lib/tapioca/runtime/reflection.rb +18 -1
  43. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +0 -20
  44. data/lib/tapioca/sorbet_ext/generic_type_patch.rb +48 -0
  45. data/lib/tapioca/version.rb +1 -1
  46. data/lib/tapioca.rb +11 -10
  47. metadata +9 -6
@@ -80,7 +80,9 @@ module Tapioca
80
80
  signature = Runtime::Reflection.signature_of(method)
81
81
  return_type = signature&.return_type
82
82
 
83
- valid_return_type?(return_type) ? return_type.to_s : "T.untyped"
83
+ # Wrap as non-nilable for required arguments. `coerce_input` supports both
84
+ # required and optional; optional arguments are re-wrapped below based on `type.non_null?`
85
+ valid_return_type?(return_type) ? (T::Utils.unwrap_nilable(return_type) || return_type).to_s : "T.untyped"
84
86
  when GraphQL::Schema::InputObject.singleton_class
85
87
  type_for_constant(unwrapped_type)
86
88
  when Module
@@ -89,15 +91,20 @@ module Tapioca
89
91
  "T.untyped"
90
92
  end
91
93
 
94
+ prepared = false
92
95
  if prepare_method
93
96
  prepare_signature = Runtime::Reflection.signature_of(prepare_method)
94
97
  prepare_return_type = prepare_signature&.return_type
95
98
  if valid_return_type?(prepare_return_type)
96
99
  parsed_type = prepare_return_type&.to_s
100
+ prepared = true
97
101
  end
98
102
  end
99
103
 
100
- if type.list?
104
+ # A `prepare` method receives (and returns) the argument's fully coerced value, list
105
+ # wrapper included, so its return type already accounts for `type.list?` and must not be
106
+ # wrapped again.
107
+ if type.list? && !prepared
101
108
  parsed_type = "T::Array[#{parsed_type}]"
102
109
  end
103
110
 
@@ -112,7 +119,7 @@ module Tapioca
112
119
 
113
120
  #: (Module[top] constant) -> String
114
121
  def type_for_constant(constant)
115
- if constant.instance_methods.include?(:prepare)
122
+ if constant.method_defined?(:prepare)
116
123
  prepare_method = constant.instance_method(:prepare)
117
124
 
118
125
  prepare_signature = Runtime::Reflection.signature_of(prepare_method)
@@ -64,6 +64,9 @@ module Tapioca
64
64
 
65
65
  # It's OK if there are no constants to process if we received a valid file/path.
66
66
  if constants_to_process.empty? && requested_paths.none? { |p| File.exist?(p) }
67
+ # When running within the add-on, return early so this expected case is not logged as an error
68
+ return [] if @lsp_addon
69
+
67
70
  report_error(<<~ERROR)
68
71
  No classes/modules can be matched for RBI generation.
69
72
  Please check that the requested classes/modules include processable DSL methods.
@@ -138,7 +141,7 @@ module Tapioca
138
141
  active_compilers.each do |compiler|
139
142
  constants.merge(compiler.processable_constants)
140
143
  end
141
- constants = filter_anonymous_and_reloaded_constants(constants)
144
+ constants = filter_anonymous_constants(constants)
142
145
  constants -= skipped_constants
143
146
 
144
147
  unless requested_constants.empty? && requested_paths.empty?
@@ -154,32 +157,8 @@ module Tapioca
154
157
  end
155
158
 
156
159
  #: (Set[Module[top]] constants) -> Set[Module[top]]
157
- def filter_anonymous_and_reloaded_constants(constants)
158
- # Group constants by their names
159
- constants_by_name = constants
160
- .group_by { |c| Runtime::Reflection.name_of(c) }
161
- .select { |name, _| !name.nil? }
162
-
163
- constants_by_name = T.cast(constants_by_name, T::Hash[String, T::Array[T::Module[T.anything]]])
164
-
165
- # Find the constants that have been reloaded
166
- reloaded_constants = constants_by_name.select { |_, constants| constants.size > 1 }.keys
167
-
168
- unless reloaded_constants.empty? || @lsp_addon
169
- reloaded_constant_names = reloaded_constants.map { |name| "`#{name}`" }.join(", ")
170
-
171
- $stderr.puts("WARNING: Multiple constants with the same name: #{reloaded_constant_names}")
172
- $stderr.puts("Make sure some object is not holding onto these constants during an app reload.")
173
- end
174
-
175
- # Look up all the constants back from their names. The resulting constant set will be the
176
- # set of constants that are actually in memory with those names.
177
- filtered_constants = constants_by_name
178
- .keys
179
- .map { |name| T.cast(Runtime::Reflection.constantize(name), T::Module[T.anything]) }
180
- .select { |mod| Runtime::Reflection.constant_defined?(mod) }
181
-
182
- Set.new.compare_by_identity.merge(filtered_constants)
160
+ def filter_anonymous_constants(constants)
161
+ constants.keep_if { |constant| Runtime::Reflection.name_of(constant) }
183
162
  end
184
163
 
185
164
  #: (Module[top] constant) -> RBI::File?
@@ -109,7 +109,7 @@ module Tapioca
109
109
  #| untyped signature,
110
110
  #| Array[[Symbol, String]] parameters
111
111
  #| ) -> void
112
- def initialize(symbol, constant, method, node, signature, parameters) # rubocop:disable Metrics/ParameterLists
112
+ def initialize(symbol, constant, method, node, signature, parameters)
113
113
  super(symbol, constant)
114
114
  @node = node
115
115
  @method = method
@@ -15,7 +15,7 @@ module Tapioca
15
15
  "shareable_constant_value:",
16
16
  "rubocop:",
17
17
  "@requires_ancestor:",
18
- ] #: Array[String]
18
+ ].freeze #: Array[String]
19
19
 
20
20
  #: (Pipeline pipeline, Rubydex::Graph gem_graph) -> void
21
21
  def initialize(pipeline, gem_graph)
@@ -69,13 +69,11 @@ module Tapioca
69
69
  end
70
70
  return [] unless declaration
71
71
 
72
- comments = declaration.definitions.flat_map(&:comments)
73
- comments.uniq!
74
- return [] if comments.empty?
75
-
76
- lines = comments
77
- .map { |comment| comment.string.gsub(/^#+ ?/, "") }
78
- .reject { |line| IGNORED_COMMENTS.any? { |comment| line.include?(comment) } || rbs_comment?(line) }
72
+ # Definitions often share a comment block, such as a license header, so de-duplicate them
73
+ lines = declaration.definitions
74
+ .map { |definition| comment_lines(definition) }
75
+ .uniq
76
+ .flatten
79
77
 
80
78
  # Strip leading and trailing blank lines, matching YARD's behavior
81
79
  lines = lines.reverse_each.drop_while(&:empty?).reverse_each.drop_while(&:empty?)
@@ -83,6 +81,13 @@ module Tapioca
83
81
  lines.map! { |line| RBI::Comment.new(line) }
84
82
  end
85
83
 
84
+ #: (Rubydex::Definition definition) -> Array[String]
85
+ def comment_lines(definition)
86
+ lines = definition.comments.map { |comment| comment.string.gsub(/^#+ ?/, "") }
87
+ lines.reject! { |line| IGNORED_COMMENTS.any? { |comment| line.include?(comment) } || rbs_comment?(line) }
88
+ lines
89
+ end
90
+
86
91
  # @override
87
92
  #: (NodeAdded event) -> bool
88
93
  def ignore?(event)
@@ -69,6 +69,7 @@ module Tapioca
69
69
 
70
70
  begin
71
71
  signature = signature_of!(method)
72
+ signature ||= inferred_attr_writer_signature(method, constant)
72
73
  method = signature.method if signature #: UnboundMethod
73
74
 
74
75
  case @pipeline.method_definition_in_gem(method.name, constant)
@@ -101,7 +102,7 @@ module Tapioca
101
102
  sanitized_parameters = parameters.each_with_index.map do |(type, name), index|
102
103
  fallback_arg_name = "_arg#{index}"
103
104
 
104
- name = if name
105
+ sig_name = if name
105
106
  name.to_s
106
107
  else
107
108
  # For attr_writer methods, Sorbet signatures have the name
@@ -125,10 +126,14 @@ module Tapioca
125
126
  end
126
127
  end
127
128
 
128
- # Sanitize param names
129
- name = fallback_arg_name unless valid_parameter_name?(name)
129
+ # Sanitize param names, except for anonymous splat, keyword splat,
130
+ # and block parameters. Ruby reflects those as `:*`, `:**`, and `:&`,
131
+ # and Sorbet signatures use the same names to store their types.
132
+ is_anonymous_parameter = anonymous_parameter_name?(type, sig_name)
133
+ sig_name = fallback_arg_name unless is_anonymous_parameter || valid_parameter_name?(sig_name)
134
+ param_name = is_anonymous_parameter ? nil : sig_name
130
135
 
131
- [type, name]
136
+ [type, param_name, sig_name]
132
137
  end
133
138
 
134
139
  rbi_method = RBI::Method.new(
@@ -137,26 +142,27 @@ module Tapioca
137
142
  visibility: visibility,
138
143
  )
139
144
 
140
- sanitized_parameters.each do |type, name|
145
+ sanitized_parameters.each do |type, param_name, _sig_name|
141
146
  case type
142
147
  when :req
143
- rbi_method << RBI::ReqParam.new(name)
148
+ rbi_method << RBI::ReqParam.new(param_name)
144
149
  when :opt
145
- rbi_method << RBI::OptParam.new(name, "T.unsafe(nil)")
150
+ rbi_method << RBI::OptParam.new(param_name, "T.unsafe(nil)")
146
151
  when :rest
147
- rbi_method << RBI::RestParam.new(name)
152
+ rbi_method << RBI::RestParam.new(param_name)
148
153
  when :keyreq
149
- rbi_method << RBI::KwParam.new(name)
154
+ rbi_method << RBI::KwParam.new(param_name)
150
155
  when :key
151
- rbi_method << RBI::KwOptParam.new(name, "T.unsafe(nil)")
156
+ rbi_method << RBI::KwOptParam.new(param_name, "T.unsafe(nil)")
152
157
  when :keyrest
153
- rbi_method << RBI::KwRestParam.new(name)
158
+ rbi_method << RBI::KwRestParam.new(param_name)
154
159
  when :block
155
- rbi_method << RBI::BlockParam.new(name)
160
+ rbi_method << RBI::BlockParam.new(param_name)
156
161
  end
157
162
  end
158
163
 
159
- @pipeline.push_method(symbol_name, constant, method, rbi_method, signature, sanitized_parameters)
164
+ parameters_for_signature = sanitized_parameters.map { |type, _param_name, sig_name| [type, sig_name] }
165
+ @pipeline.push_method(symbol_name, constant, method, rbi_method, signature, parameters_for_signature)
160
166
  tree << rbi_method
161
167
  end
162
168
 
@@ -192,6 +198,66 @@ module Tapioca
192
198
  }
193
199
  end
194
200
 
201
+ #: (UnboundMethod method, Module[top] constant) -> untyped
202
+ def inferred_attr_writer_signature(method, constant)
203
+ reader_method = attr_reader_for_writer(method, constant)
204
+ return unless reader_method
205
+
206
+ reader_signature = signature_of(reader_method)
207
+ return unless reader_signature
208
+
209
+ build_attr_writer_signature(method, reader_method, reader_signature)
210
+ end
211
+
212
+ #: (UnboundMethod method, Module[top] constant) -> UnboundMethod?
213
+ def attr_reader_for_writer(method, constant)
214
+ method_name = method.name.to_s
215
+ return unless method_name.end_with?("=")
216
+ return unless method.parameters == [[:req]]
217
+
218
+ reader_method = T.let(constant.instance_method(method_name.delete_suffix("=").to_sym), UnboundMethod)
219
+ reader_method = original_method(reader_method)
220
+ return unless same_source_location?(method, reader_method)
221
+ return unless method_owned_by_constant?(reader_method, constant)
222
+
223
+ reader_method
224
+ rescue NameError
225
+ nil
226
+ end
227
+
228
+ #: (UnboundMethod writer_method, UnboundMethod reader_method, untyped reader_signature) -> untyped
229
+ def build_attr_writer_signature(writer_method, reader_method, reader_signature)
230
+ return unless reader_signature.arg_types.empty?
231
+ return unless reader_signature.kwarg_types.empty?
232
+ return if reader_signature.rest_type
233
+ return if reader_signature.keyrest_type
234
+ return if reader_signature.block_type
235
+
236
+ T::Private::Methods::Signature.new(
237
+ method: writer_method,
238
+ method_name: writer_method.name,
239
+ raw_arg_types: { reader_method.name => reader_signature.return_type },
240
+ raw_return_type: reader_signature.return_type,
241
+ bind: nil,
242
+ mode: reader_signature.mode,
243
+ check_level: reader_signature.check_level,
244
+ on_failure: reader_signature.on_failure,
245
+ override_allow_incompatible: reader_signature.override_allow_incompatible,
246
+ defined_raw: reader_signature.defined_raw,
247
+ )
248
+ end
249
+
250
+ #: (UnboundMethod method) -> UnboundMethod
251
+ def original_method(method)
252
+ T.let(signature_of(method)&.method || method, UnboundMethod)
253
+ end
254
+
255
+ #: (UnboundMethod method, UnboundMethod other_method) -> bool
256
+ def same_source_location?(method, other_method)
257
+ source_location = method.source_location
258
+ !!source_location && source_location == other_method.source_location
259
+ end
260
+
195
261
  #: (Module[top] constant, String method_name) -> bool
196
262
  def struct_method?(constant, method_name)
197
263
  return false unless T::Props::ClassMethods === constant
@@ -11,7 +11,7 @@ module Tapioca
11
11
  #: (ScopeNodeAdded event) -> void
12
12
  def on_scope(event)
13
13
  constant = event.constant
14
- return unless T::Enum > event.constant # rubocop:disable Style/InvertibleUnlessCondition
14
+ return unless T::Enum > event.constant
15
15
 
16
16
  enum_block = RBI::TEnumBlock.new
17
17
 
@@ -7,7 +7,7 @@ module Tapioca
7
7
  include Runtime::Reflection
8
8
  include RBIHelper
9
9
 
10
- IGNORED_SYMBOLS = ["YAML", "MiniTest", "Mutex"] #: Array[String]
10
+ IGNORED_SYMBOLS = ["YAML", "MiniTest", "Mutex"].freeze #: Array[String]
11
11
 
12
12
  #: Gemfile::GemSpec
13
13
  attr_reader :gem
@@ -100,7 +100,7 @@ module Tapioca
100
100
  #| untyped signature,
101
101
  #| Array[[Symbol, String]] parameters
102
102
  #| ) -> void
103
- def push_method(symbol, constant, method, node, signature, parameters) # rubocop:disable Metrics/ParameterLists
103
+ def push_method(symbol, constant, method, node, signature, parameters)
104
104
  @events << Gem::MethodNodeAdded.new(symbol, constant, method, node, signature, parameters)
105
105
  end
106
106
 
@@ -356,7 +356,7 @@ module Tapioca
356
356
 
357
357
  #: (Class[top] constant) -> String?
358
358
  def compile_superclass(constant)
359
- superclass = nil #: Class[top]? # rubocop:disable Lint/UselessAssignment
359
+ superclass = nil #: Class[top]?
360
360
 
361
361
  while (superclass = superclass_of(constant))
362
362
  constant_name = name_of(constant)
@@ -48,8 +48,21 @@ module Tapioca
48
48
  end
49
49
  end
50
50
 
51
+ #: (String path) -> bool
52
+ def excluded_gem_path?(path)
53
+ excluded_gem_specs.any? { |spec| spec.contains_path?(path) }
54
+ end
55
+
51
56
  private
52
57
 
58
+ #: -> Array[GemSpec]
59
+ def excluded_gem_specs
60
+ @excluded_gem_specs ||= @excluded_gems.filter_map do |name|
61
+ spec = ::Gem.loaded_specs[name]
62
+ GemSpec.new(spec) if spec
63
+ end #: Array[GemSpec]?
64
+ end
65
+
53
66
  #: File
54
67
  attr_reader(:gemfile, :lockfile)
55
68
 
@@ -5,7 +5,7 @@ module Tapioca
5
5
  # @requires_ancestor: Thor
6
6
  module EnvHelper
7
7
  #: (Hash[Symbol, untyped] options) -> void
8
- def set_environment(options) # rubocop:disable Naming/AccessorMethodName
8
+ def set_environment(options)
9
9
  ENV["RAILS_ENV"] = ENV["RACK_ENV"] = options[:environment]
10
10
  ENV["RUBY_DEBUG_LAZY"] = "1"
11
11
  end
@@ -0,0 +1,42 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "open3"
5
+
6
+ module Tapioca
7
+ module FileHelper
8
+ #: (Pathname filename, Pathname | String old_path, Pathname | String new_path) -> String?
9
+ def file_diff(filename, old_path, new_path)
10
+ filename = filename.to_s
11
+ stdout, stderr, status = Open3.capture3(
12
+ "diff",
13
+ "-u",
14
+ "--label=Current #{filename}",
15
+ old_path.to_s,
16
+ "--label=Expected #{filename} (After running `bin/tapioca dsl`)",
17
+ new_path.to_s,
18
+ )
19
+
20
+ unless [0, 1].include?(status.exitstatus)
21
+ error_msg("Failed to create #{filename} diff. #{stderr.chomp}")
22
+ return
23
+ end
24
+
25
+ stdout
26
+ rescue SystemCallError => e
27
+ error_msg("Failed to create #{filename} diff. #{e.message}")
28
+ nil
29
+ end
30
+
31
+ private
32
+
33
+ RED = "\e[31m" #: String
34
+ CLEAR = "\e[0m" #: String
35
+
36
+ #: (String message) -> void
37
+ def error_msg(message)
38
+ message = "#{RED}#{message}#{CLEAR}"
39
+ Kernel.warn(message)
40
+ end
41
+ end
42
+ end
@@ -78,7 +78,7 @@ module Tapioca
78
78
  res = sorbet(
79
79
  "--no-config",
80
80
  "--error-url-base=#{error_url_base}",
81
- "--stop-after namer",
81
+ "--stop-after resolver",
82
82
  dsl_dir,
83
83
  gem_dir,
84
84
  )
@@ -129,12 +129,16 @@ module Tapioca
129
129
  ERR
130
130
  end
131
131
 
132
- if auto_strictness
133
- redef_errors = errors.select { |error| error.code == 4010 }
134
- update_gem_rbis_strictnesses(redef_errors, gem_dir)
135
- end
132
+ handled_errors = apply_validation_fixes(errors, gem_dir: gem_dir, auto_strictness: auto_strictness)
136
133
 
137
134
  Kernel.raise Tapioca::Error, error_messages.join("\n") if parse_errors.any?
135
+
136
+ unhandled_errors = errors - handled_errors
137
+ unhandled_errors.reject! { |error| ignored_validation_error?(error, gem_dir: gem_dir, dsl_dir: dsl_dir) }
138
+
139
+ if unhandled_errors.empty?
140
+ say(" No errors found\n\n", [:green, :bold])
141
+ end
138
142
  end
139
143
 
140
144
  private
@@ -258,6 +262,93 @@ module Tapioca
258
262
  )
259
263
  end
260
264
 
265
+ SUPPRESS_PAYLOAD_SUPERCLASS_REDEFINITION_FLAG =
266
+ "--suppress-payload-superclass-redefinition-for" #: String
267
+
268
+ #: (Array[Spoom::Sorbet::Errors::Error] errors) -> void
269
+ def update_sorbet_config_for_payload_superclass_redefinitions(errors)
270
+ errors
271
+ .filter_map { |error| payload_superclass_constant_from_error(error) }
272
+ .uniq
273
+ .each { |constant| add_payload_superclass_suppression_to_config(constant) }
274
+ end
275
+
276
+ #: (Spoom::Sorbet::Errors::Error error) -> String?
277
+ def payload_superclass_constant_from_error(error)
278
+ error.more.each do |line|
279
+ if line =~ /--suppress-payload-superclass-redefinition-for=([^\s`]+)/
280
+ return T.must(Regexp.last_match(1))
281
+ end
282
+ end
283
+
284
+ nil
285
+ end
286
+
287
+ #: (
288
+ #| Array[Spoom::Sorbet::Errors::Error] errors,
289
+ #| gem_dir: String,
290
+ #| auto_strictness: bool,
291
+ #| ) -> Array[Spoom::Sorbet::Errors::Error]
292
+ def apply_validation_fixes(errors, gem_dir:, auto_strictness:)
293
+ handled_errors = [] #: Array[Spoom::Sorbet::Errors::Error]
294
+
295
+ if auto_strictness
296
+ redef_errors = errors.select { |error| error.code == 4010 }
297
+ update_gem_rbis_strictnesses(redef_errors, gem_dir) if redef_errors.any?
298
+ handled_errors.concat(redef_errors)
299
+ end
300
+
301
+ # Automatically fix payload superclass redefinition errors.
302
+ payload_superclass_errors = errors.select { |error| payload_superclass_error?(error) }
303
+ if payload_superclass_errors.any?
304
+ update_sorbet_config_for_payload_superclass_redefinitions(payload_superclass_errors)
305
+ end
306
+ handled_errors.concat(payload_superclass_errors)
307
+
308
+ handled_errors
309
+ end
310
+
311
+ #: (Spoom::Sorbet::Errors::Error error) -> bool
312
+ def payload_superclass_error?(error)
313
+ error.more.any? { |line| line.include?(SUPPRESS_PAYLOAD_SUPERCLASS_REDEFINITION_FLAG) }
314
+ end
315
+
316
+ #: (Spoom::Sorbet::Errors::Error error, gem_dir: String, dsl_dir: String) -> bool
317
+ def ignored_validation_error?(error, gem_dir:, dsl_dir:)
318
+ return false if Dir.exist?(gem_dir) && !Dir.glob("#{gem_dir}/**/*.rbi").empty?
319
+
320
+ [5002, 5067].include?(error.code) && T.must(error.file).start_with?(dsl_dir)
321
+ end
322
+
323
+ #: (String constant) -> void
324
+ def add_payload_superclass_suppression_to_config(constant)
325
+ flag = "#{SUPPRESS_PAYLOAD_SUPERCLASS_REDEFINITION_FLAG}=#{constant}"
326
+ config_path = Tapioca::SORBET_CONFIG_FILE
327
+ config = File.exist?(config_path) ? File.read(config_path) : ""
328
+ flag_already_present = config.lines(chomp: true).include?(flag)
329
+
330
+ if flag_already_present
331
+ say(
332
+ "\n Payload superclass of `#{constant}` was redefined; `#{flag}` is already in sorbet/config\n",
333
+ [:yellow, :bold],
334
+ )
335
+ return
336
+ end
337
+
338
+ FileUtils.mkdir_p(File.dirname(config_path))
339
+ if config.empty?
340
+ File.write(config_path, "#{flag}\n")
341
+ else
342
+ suffix = config.end_with?("\n") ? "" : "\n"
343
+ File.write(config_path, "#{config}#{suffix}#{flag}\n")
344
+ end
345
+ say(
346
+ "\n Added `#{flag}` to sorbet/config (payload superclass of `#{constant}` was redefined)",
347
+ [:yellow, :bold],
348
+ )
349
+ say("\n")
350
+ end
351
+
261
352
  #: (Array[Spoom::Sorbet::Errors::Error] errors, String gem_dir) -> void
262
353
  def update_gem_rbis_strictnesses(errors, gem_dir)
263
354
  files = []
@@ -37,7 +37,7 @@ module Tapioca
37
37
  create_typed_param(RBI::OptParam.new(name, default), type)
38
38
  end
39
39
 
40
- #: (String name, type: String) -> RBI::TypedParam
40
+ #: (String? name, type: String) -> RBI::TypedParam
41
41
  def create_rest_param(name, type:)
42
42
  create_typed_param(RBI::RestParam.new(name), type)
43
43
  end
@@ -52,12 +52,12 @@ module Tapioca
52
52
  create_typed_param(RBI::KwOptParam.new(name, default), type)
53
53
  end
54
54
 
55
- #: (String name, type: String) -> RBI::TypedParam
55
+ #: (String? name, type: String) -> RBI::TypedParam
56
56
  def create_kw_rest_param(name, type:)
57
57
  create_typed_param(RBI::KwRestParam.new(name), type)
58
58
  end
59
59
 
60
- #: (String name, type: String) -> RBI::TypedParam
60
+ #: (String? name, type: String) -> RBI::TypedParam
61
61
  def create_block_param(name, type:)
62
62
  create_typed_param(RBI::BlockParam.new(name), type)
63
63
  end
@@ -110,5 +110,19 @@ module Tapioca
110
110
  def valid_parameter_name?(name)
111
111
  Prism.parse_success?("def sentinel_method_name(#{name}:); end")
112
112
  end
113
+
114
+ #: (Symbol type, String name) -> bool
115
+ def anonymous_parameter_name?(type, name)
116
+ case type
117
+ when :rest
118
+ name == "*"
119
+ when :keyrest
120
+ name == "**"
121
+ when :block
122
+ name == "&"
123
+ else
124
+ false
125
+ end
126
+ end
113
127
  end
114
128
  end
@@ -24,7 +24,7 @@ module Tapioca
24
24
 
25
25
  #: (String, rbi_mode: bool) { (String stderr) -> void } -> void
26
26
  def sorbet_syntax_check!(source, rbi_mode:, &on_failure)
27
- quoted_source = "\"#{source}\""
27
+ quoted_source = source.shellescape
28
28
 
29
29
  result = if rbi_mode
30
30
  # --e-rbi cannot be used on its own, so we pass a dummy value like `-e ""`
@@ -12,6 +12,7 @@ require "tapioca/runtime/dynamic_mixin_compiler"
12
12
  require "tapioca/sorbet_ext/backcompat_patches"
13
13
  require "tapioca/sorbet_ext/name_patch"
14
14
  require "tapioca/sorbet_ext/generic_name_patch"
15
+ require "tapioca/sorbet_ext/generic_type_patch"
15
16
  require "tapioca/sorbet_ext/proc_bind_patch"
16
17
  require "tapioca/sorbet_ext/void_patch"
17
18
  require "tapioca/runtime/generic_type_registry"
@@ -53,6 +54,7 @@ require "tapioca/helpers/rbi_helper"
53
54
  require "tapioca/helpers/package_url"
54
55
  require "tapioca/helpers/cli_helper"
55
56
  require "tapioca/helpers/config_helper"
57
+ require "tapioca/helpers/file_helper"
56
58
  require "tapioca/helpers/rbi_files_helper"
57
59
  require "tapioca/helpers/env_helper"
58
60
 
@@ -49,6 +49,39 @@ module Tapioca
49
49
  @halt_upon_load_error = halt_upon_load_error
50
50
  end
51
51
 
52
+ #: -> void
53
+ def load_gem_extensions
54
+ say("Loading gem extension classes... ")
55
+
56
+ # Extensions are loaded before the bundle is required so that they can patch the gems
57
+ # they apply to as those gems are being loaded.
58
+ Dir.glob("#{Tapioca::TAPIOCA_DIR}/gem/extensions/**/*.rb").each do |extension|
59
+ require File.expand_path(extension)
60
+ end
61
+
62
+ ::Gem.find_files("tapioca/gem/extensions/*.rb").each do |extension|
63
+ next if @bundle.excluded_gem_path?(extension)
64
+
65
+ require File.expand_path(extension)
66
+ end
67
+
68
+ say("Done", :green)
69
+ end
70
+
71
+ #: (Tapioca::Gemfile gemfile, String? initialize_file, String? require_file, bool halt_upon_load_error) -> void
72
+ def load_bundle(gemfile, initialize_file, require_file, halt_upon_load_error)
73
+ require_helper(initialize_file)
74
+ load_gem_extensions
75
+
76
+ load_rails_application(halt_upon_load_error: halt_upon_load_error)
77
+
78
+ gemfile.require_bundle
79
+
80
+ require_helper(require_file)
81
+
82
+ load_rails_engines
83
+ end
84
+
52
85
  #: -> void
53
86
  def require_gem_file
54
87
  say("Requiring all gems to prepare for compiling... ")
@@ -15,19 +15,6 @@ module Tapioca
15
15
 
16
16
  private
17
17
 
18
- #: (Tapioca::Gemfile gemfile, String? initialize_file, String? require_file, bool halt_upon_load_error) -> void
19
- def load_bundle(gemfile, initialize_file, require_file, halt_upon_load_error)
20
- require_helper(initialize_file)
21
-
22
- load_rails_application(halt_upon_load_error: halt_upon_load_error)
23
-
24
- gemfile.require_bundle
25
-
26
- require_helper(require_file)
27
-
28
- load_rails_engines
29
- end
30
-
31
18
  #: (?environment_load: bool, ?eager_load: bool, ?app_root: String, ?halt_upon_load_error: bool) -> void
32
19
  def load_rails_application(environment_load: false, eager_load: false, app_root: ".", halt_upon_load_error: true)
33
20
  return unless File.exist?(File.expand_path("config/application.rb", app_root))