tapioca 0.19.2 → 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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +87 -45
  3. data/lib/ruby_lsp/tapioca/run_gem_rbi_check.rb +1 -1
  4. data/lib/tapioca/cli.rb +10 -11
  5. data/lib/tapioca/commands/abstract_dsl.rb +71 -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/dsl_generate.rb +0 -15
  9. data/lib/tapioca/commands/gem_generate.rb +20 -4
  10. data/lib/tapioca/dsl/compiler.rb +24 -10
  11. data/lib/tapioca/dsl/compilers/aasm.rb +24 -17
  12. data/lib/tapioca/dsl/compilers/active_job.rb +1 -1
  13. data/lib/tapioca/dsl/compilers/active_model_attributes.rb +1 -1
  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 +7 -2
  22. data/lib/tapioca/dsl/pipeline.rb +3 -0
  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 +1 -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 +32 -21
  40. data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +1 -2
  41. data/lib/tapioca/runtime/generic_type_registry.rb +27 -2
  42. data/lib/tapioca/runtime/reflection.rb +18 -1
  43. data/lib/tapioca/version.rb +1 -1
  44. data/lib/tapioca.rb +11 -10
  45. metadata +6 -4
@@ -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 ""`
@@ -54,6 +54,7 @@ require "tapioca/helpers/rbi_helper"
54
54
  require "tapioca/helpers/package_url"
55
55
  require "tapioca/helpers/cli_helper"
56
56
  require "tapioca/helpers/config_helper"
57
+ require "tapioca/helpers/file_helper"
57
58
  require "tapioca/helpers/rbi_files_helper"
58
59
  require "tapioca/helpers/env_helper"
59
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))
@@ -76,11 +76,10 @@ module RBI
76
76
  if !block || !parameters.empty? || return_type
77
77
  # If there is no block, and the params and return type have not been supplied, then
78
78
  # we create a single signature with the given parameters and return type
79
- params = parameters.map { |param| RBI::SigParam.new(param.param.name.to_s, param.type) }
80
79
  return_type ||= "T.untyped"
81
80
  type_params = Tapioca::RBIHelper.extract_type_parameters(parameters.map(&:type).append(return_type))
82
81
 
83
- sig = RBI::Sig.new(params: params, return_type: return_type, type_params: type_params)
82
+ sig = RBI::Sig.new(params: parameters.map(&:to_sig_param), return_type: return_type, type_params: type_params)
84
83
  sigs << sig
85
84
  end
86
85
 
@@ -117,5 +116,20 @@ module RBI
117
116
  class TypedParam < T::Struct
118
117
  const :param, RBI::Param
119
118
  const :type, String
119
+
120
+ #: -> RBI::SigParam
121
+ def to_sig_param
122
+ name = case param
123
+ when RestParam
124
+ param.anonymous? ? "*".inspect : param.name.to_s
125
+ when KwRestParam
126
+ param.anonymous? ? "**".inspect : param.name.to_s
127
+ when BlockParam
128
+ param.anonymous? ? "&".inspect : param.name.to_s
129
+ else
130
+ param.name.to_s
131
+ end
132
+ RBI::SigParam.new(name, type)
133
+ end
120
134
  end
121
135
  end
@@ -0,0 +1,52 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler"
5
+ require "digest"
6
+ require "fileutils"
7
+
8
+ module Tapioca
9
+ module RBS
10
+ # Prepares the Bootsnap iseq cache used for RBS rewrite output.
11
+ #
12
+ # RBS rewrite output can change when the lockfile changes, even if the
13
+ # source files are unchanged.
14
+ # To account for this, we store the current Gemfile.lock SHA256 in a
15
+ # `.gemfile-lock-digest` file.
16
+ # A digest mismatch deletes Bootsnap's cache payload and records the new
17
+ # digest, so this run rebuilds the cache from scratch.
18
+ module BootsnapCache
19
+ DIGEST_FILE = ".gemfile-lock-digest" #: String
20
+
21
+ class << self
22
+ #: (String) -> void
23
+ def prepare_for_setup(cache_dir)
24
+ digest = gemfile_lock_digest
25
+ return if digest_matches?(cache_dir, digest)
26
+
27
+ FileUtils.rm_rf(File.join(cache_dir, "bootsnap"))
28
+ FileUtils.mkdir_p(cache_dir)
29
+ File.write(digest_path(cache_dir), digest)
30
+ end
31
+
32
+ private
33
+
34
+ #: -> String
35
+ def gemfile_lock_digest
36
+ Digest::SHA256.file(Bundler.default_lockfile).hexdigest
37
+ end
38
+
39
+ #: (String, String) -> bool
40
+ def digest_matches?(cache_dir, digest)
41
+ path = digest_path(cache_dir)
42
+ File.file?(path) && File.read(path).chomp == digest
43
+ end
44
+
45
+ #: (String) -> String
46
+ def digest_path(cache_dir)
47
+ File.join(cache_dir, DIGEST_FILE)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -29,32 +29,43 @@ module Tapioca
29
29
  MSG
30
30
  end
31
31
  end
32
+
33
+ module BootsnapIntegration
34
+ class << self
35
+ extend T::Sig
36
+
37
+ sig { void }
38
+ def setup
39
+ require "bootsnap"
40
+ require "tapioca/rbs/bootsnap_cache"
41
+
42
+ cache_dir = ENV.fetch("TAPIOCA_BOOTSNAP_CACHE_DIR", File.join(Dir.pwd, "tmp/cache/bootsnap-tapioca-rbs"))
43
+ Tapioca::RBS::BootsnapCache.prepare_for_setup(cache_dir)
44
+
45
+ Bootsnap.setup(
46
+ cache_dir: cache_dir,
47
+ development_mode: true,
48
+ load_path_cache: true,
49
+ compile_cache_iseq: true,
50
+ compile_cache_yaml: true,
51
+ readonly: false,
52
+ revalidation: true,
53
+ )
54
+ Bootsnap.log_stats!
55
+
56
+ Bootsnap.singleton_class.prepend(Tapioca::RBS::BootsnapGuard)
57
+ end
58
+ end
59
+ end
32
60
  end
33
61
  end
34
62
 
35
- # When TAPIOCA_RBS_CACHE=1, set up bootsnap with a dedicated cache directory
36
- # and load require-hooks so the RBS-rewritten iseqs get cached. Subsequent
37
- # runs read the rewritten iseq directly and skip the rewrite.
38
- #
39
- # After our setup, BootsnapGuard is prepended so the host application can't
40
- # replace our cache directory.
63
+ # When TAPIOCA_RBS_CACHE=1, use a dedicated Bootsnap cache directory for
64
+ # RBS-rewritten iseqs. After setup, BootsnapGuard is prepended so the host
65
+ # application cannot replace Tapioca's cache directory.
41
66
  if ENV["TAPIOCA_RBS_CACHE"] == "1"
42
67
  begin
43
- require "bootsnap"
44
- # Respect BOOTSNAP_READONLY for consumers reading a pre-populated cache
45
- # (e.g. a CI prime step).
46
- readonly = !["0", "false", false].include?(ENV.fetch("BOOTSNAP_READONLY") { false })
47
- Bootsnap.setup(
48
- cache_dir: ENV.fetch("TAPIOCA_BOOTSNAP_CACHE_DIR", File.join(Dir.pwd, "tmp/cache/bootsnap-tapioca-rbs")),
49
- development_mode: true,
50
- load_path_cache: true,
51
- compile_cache_iseq: true,
52
- compile_cache_yaml: true,
53
- readonly: readonly,
54
- revalidation: true,
55
- )
56
- Bootsnap.log_stats!
57
- Bootsnap.singleton_class.prepend(Tapioca::RBS::BootsnapGuard)
68
+ Tapioca::RBS::BootsnapIntegration.setup
58
69
  rescue LoadError
59
70
  # Bootsnap is not in the bundle, skip iseq caching.
60
71
  end
@@ -53,7 +53,7 @@ module Tapioca
53
53
  mixins_from_modules[mod] = (after - before).reverse!
54
54
  end
55
55
  end
56
- rescue Exception # rubocop:disable Lint/RescueException
56
+ rescue Exception
57
57
  # this is a best effort, bail if we can't perform this
58
58
  end
59
59
 
@@ -85,7 +85,6 @@ module Tapioca
85
85
  super(*attrs, **kwargs) if defined?(super)
86
86
  end
87
87
 
88
- # rubocop:disable Style/MissingRespondToMissing
89
88
  T::Sig::WithoutRuntime.sig { params(symbol: Symbol, args: T.untyped).returns(T.untyped) }
90
89
  def method_missing(symbol, *args)
91
90
  # We need this here so that we can handle any random instance