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.
- checksums.yaml +4 -4
- data/README.md +87 -45
- data/lib/ruby_lsp/tapioca/run_gem_rbi_check.rb +1 -1
- data/lib/tapioca/cli.rb +10 -11
- data/lib/tapioca/commands/abstract_dsl.rb +71 -20
- data/lib/tapioca/commands/abstract_gem.rb +1 -0
- data/lib/tapioca/commands/annotations.rb +1 -1
- data/lib/tapioca/commands/dsl_generate.rb +0 -15
- data/lib/tapioca/commands/gem_generate.rb +20 -4
- data/lib/tapioca/dsl/compiler.rb +24 -10
- data/lib/tapioca/dsl/compilers/aasm.rb +24 -17
- data/lib/tapioca/dsl/compilers/active_job.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_model_attributes.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_fixtures.rb +17 -11
- data/lib/tapioca/dsl/compilers/active_record_relations.rb +2 -2
- data/lib/tapioca/dsl/compilers/config.rb +1 -1
- data/lib/tapioca/dsl/compilers/json_api_client_resource.rb +1 -1
- data/lib/tapioca/dsl/compilers/sidekiq_worker.rb +1 -1
- data/lib/tapioca/dsl/compilers/url_helpers.rb +312 -8
- data/lib/tapioca/dsl/compilers.rb +1 -1
- data/lib/tapioca/dsl/helpers/graphql_type_helper.rb +7 -2
- data/lib/tapioca/dsl/pipeline.rb +3 -0
- data/lib/tapioca/gem/events.rb +1 -1
- data/lib/tapioca/gem/listeners/documentation.rb +13 -8
- data/lib/tapioca/gem/listeners/methods.rb +79 -13
- data/lib/tapioca/gem/listeners/sorbet_enums.rb +1 -1
- data/lib/tapioca/gem/pipeline.rb +3 -3
- data/lib/tapioca/gemfile.rb +13 -0
- data/lib/tapioca/helpers/env_helper.rb +1 -1
- data/lib/tapioca/helpers/file_helper.rb +42 -0
- data/lib/tapioca/helpers/rbi_files_helper.rb +96 -5
- data/lib/tapioca/helpers/rbi_helper.rb +17 -3
- data/lib/tapioca/helpers/sorbet_helper.rb +1 -1
- data/lib/tapioca/internal.rb +1 -0
- data/lib/tapioca/loaders/gem.rb +33 -0
- data/lib/tapioca/loaders/loader.rb +0 -13
- data/lib/tapioca/rbi_ext/model.rb +16 -2
- data/lib/tapioca/rbs/bootsnap_cache.rb +52 -0
- data/lib/tapioca/rbs/rewriter.rb +32 -21
- data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +1 -2
- data/lib/tapioca/runtime/generic_type_registry.rb +27 -2
- data/lib/tapioca/runtime/reflection.rb +18 -1
- data/lib/tapioca/version.rb +1 -1
- data/lib/tapioca.rb +11 -10
- 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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
145
|
+
sanitized_parameters.each do |type, param_name, _sig_name|
|
|
141
146
|
case type
|
|
142
147
|
when :req
|
|
143
|
-
rbi_method << RBI::ReqParam.new(
|
|
148
|
+
rbi_method << RBI::ReqParam.new(param_name)
|
|
144
149
|
when :opt
|
|
145
|
-
rbi_method << RBI::OptParam.new(
|
|
150
|
+
rbi_method << RBI::OptParam.new(param_name, "T.unsafe(nil)")
|
|
146
151
|
when :rest
|
|
147
|
-
rbi_method << RBI::RestParam.new(
|
|
152
|
+
rbi_method << RBI::RestParam.new(param_name)
|
|
148
153
|
when :keyreq
|
|
149
|
-
rbi_method << RBI::KwParam.new(
|
|
154
|
+
rbi_method << RBI::KwParam.new(param_name)
|
|
150
155
|
when :key
|
|
151
|
-
rbi_method << RBI::KwOptParam.new(
|
|
156
|
+
rbi_method << RBI::KwOptParam.new(param_name, "T.unsafe(nil)")
|
|
152
157
|
when :keyrest
|
|
153
|
-
rbi_method << RBI::KwRestParam.new(
|
|
158
|
+
rbi_method << RBI::KwRestParam.new(param_name)
|
|
154
159
|
when :block
|
|
155
|
-
rbi_method << RBI::BlockParam.new(
|
|
160
|
+
rbi_method << RBI::BlockParam.new(param_name)
|
|
156
161
|
end
|
|
157
162
|
end
|
|
158
163
|
|
|
159
|
-
|
|
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
|
|
14
|
+
return unless T::Enum > event.constant
|
|
15
15
|
|
|
16
16
|
enum_block = RBI::TEnumBlock.new
|
|
17
17
|
|
data/lib/tapioca/gem/pipeline.rb
CHANGED
|
@@ -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)
|
|
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]?
|
|
359
|
+
superclass = nil #: Class[top]?
|
|
360
360
|
|
|
361
361
|
while (superclass = superclass_of(constant))
|
|
362
362
|
constant_name = name_of(constant)
|
data/lib/tapioca/gemfile.rb
CHANGED
|
@@ -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)
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 ""`
|
data/lib/tapioca/internal.rb
CHANGED
|
@@ -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
|
|
data/lib/tapioca/loaders/gem.rb
CHANGED
|
@@ -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:
|
|
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
|
data/lib/tapioca/rbs/rewriter.rb
CHANGED
|
@@ -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,
|
|
36
|
-
#
|
|
37
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|