tapioca 0.4.15 → 0.4.20
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 +4 -2
- data/exe/tapioca +17 -2
- data/lib/tapioca.rb +1 -27
- data/lib/tapioca/cli.rb +1 -108
- data/lib/tapioca/cli/main.rb +146 -0
- data/lib/tapioca/compilers/dsl/active_record_columns.rb +4 -4
- data/lib/tapioca/compilers/dsl/active_record_scope.rb +1 -1
- data/lib/tapioca/compilers/dsl/base.rb +1 -1
- data/lib/tapioca/compilers/dsl/url_helpers.rb +3 -3
- data/lib/tapioca/compilers/sorbet.rb +4 -1
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +145 -39
- data/lib/tapioca/config.rb +1 -1
- data/lib/tapioca/config_builder.rb +7 -12
- data/lib/tapioca/generator.rb +183 -28
- data/lib/tapioca/generic_type_registry.rb +219 -0
- data/lib/tapioca/internal.rb +21 -0
- data/lib/tapioca/loader.rb +13 -2
- data/lib/tapioca/sorbet_ext/generic_name_patch.rb +66 -0
- data/lib/tapioca/sorbet_ext/name_patch.rb +16 -0
- data/lib/tapioca/version.rb +1 -1
- metadata +7 -2
data/lib/tapioca/config.rb
CHANGED
@@ -8,7 +8,6 @@ module Tapioca
|
|
8
8
|
const(:outdir, String)
|
9
9
|
const(:prerequire, T.nilable(String))
|
10
10
|
const(:postrequire, String)
|
11
|
-
const(:generate_command, String)
|
12
11
|
const(:exclude, T::Array[String])
|
13
12
|
const(:typed_overrides, T::Hash[String, String])
|
14
13
|
const(:todos_path, String)
|
@@ -27,6 +26,7 @@ module Tapioca
|
|
27
26
|
TAPIOCA_PATH = T.let("#{SORBET_PATH}/tapioca", String)
|
28
27
|
TAPIOCA_CONFIG = T.let("#{TAPIOCA_PATH}/config.yml", String)
|
29
28
|
|
29
|
+
DEFAULT_COMMAND = T.let("bin/tapioca", String)
|
30
30
|
DEFAULT_POSTREQUIRE = T.let("#{TAPIOCA_PATH}/require.rb", String)
|
31
31
|
DEFAULT_RBIDIR = T.let("#{SORBET_PATH}/rbi", String)
|
32
32
|
DEFAULT_DSLDIR = T.let("#{DEFAULT_RBIDIR}/dsl", String)
|
@@ -10,9 +10,13 @@ module Tapioca
|
|
10
10
|
|
11
11
|
sig { params(command: Symbol, options: T::Hash[String, T.untyped]).returns(Config) }
|
12
12
|
def from_options(command, options)
|
13
|
-
|
14
|
-
|
15
|
-
)
|
13
|
+
merged_options = merge_options(default_options(command), config_options, options)
|
14
|
+
|
15
|
+
puts(<<~MSG) if merged_options.include?("generate_command")
|
16
|
+
DEPRECATION: The `-c` and `--cmd` flags will be removed in a future release.
|
17
|
+
MSG
|
18
|
+
|
19
|
+
Config.from_hash(merged_options)
|
16
20
|
end
|
17
21
|
|
18
22
|
private
|
@@ -40,14 +44,6 @@ module Tapioca
|
|
40
44
|
DEFAULT_OPTIONS.merge("outdir" => default_outdir)
|
41
45
|
end
|
42
46
|
|
43
|
-
sig { returns(String) }
|
44
|
-
def default_command
|
45
|
-
command = File.basename($PROGRAM_NAME)
|
46
|
-
args = ARGV.join(" ")
|
47
|
-
|
48
|
-
"#{command} #{args}".strip
|
49
|
-
end
|
50
|
-
|
51
47
|
sig { params(options: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
|
52
48
|
def merge_options(*options)
|
53
49
|
options.each_with_object({}) do |option, result|
|
@@ -65,7 +61,6 @@ module Tapioca
|
|
65
61
|
DEFAULT_OPTIONS = T.let({
|
66
62
|
"postrequire" => Config::DEFAULT_POSTREQUIRE,
|
67
63
|
"outdir" => nil,
|
68
|
-
"generate_command" => default_command,
|
69
64
|
"exclude" => [],
|
70
65
|
"typed_overrides" => Config::DEFAULT_OVERRIDES,
|
71
66
|
"todos_path" => Config::DEFAULT_TODOSPATH,
|
data/lib/tapioca/generator.rb
CHANGED
@@ -63,7 +63,7 @@ module Tapioca
|
|
63
63
|
|
64
64
|
content = String.new
|
65
65
|
content << rbi_header(
|
66
|
-
|
66
|
+
"#{Config::DEFAULT_COMMAND} require",
|
67
67
|
reason: "explicit gem requires",
|
68
68
|
strictness: "false"
|
69
69
|
)
|
@@ -76,8 +76,8 @@ module Tapioca
|
|
76
76
|
say("Done", :green)
|
77
77
|
|
78
78
|
say("All requires from this application have been written to #{name}.", [:green, :bold])
|
79
|
-
cmd = set_color("
|
80
|
-
say("Please review changes and commit them, then run
|
79
|
+
cmd = set_color("#{Config::DEFAULT_COMMAND} sync", :yellow, :bold)
|
80
|
+
say("Please review changes and commit them, then run `#{cmd}`.", [:green, :bold])
|
81
81
|
end
|
82
82
|
|
83
83
|
sig { void }
|
@@ -99,7 +99,7 @@ module Tapioca
|
|
99
99
|
|
100
100
|
content = String.new
|
101
101
|
content << rbi_header(
|
102
|
-
|
102
|
+
"#{Config::DEFAULT_COMMAND} todo",
|
103
103
|
reason: "unresolved constants",
|
104
104
|
strictness: "false"
|
105
105
|
)
|
@@ -116,14 +116,27 @@ module Tapioca
|
|
116
116
|
say("Please review changes and commit them.", [:green, :bold])
|
117
117
|
end
|
118
118
|
|
119
|
-
sig
|
120
|
-
|
119
|
+
sig do
|
120
|
+
params(
|
121
|
+
requested_constants: T::Array[String],
|
122
|
+
should_verify: T::Boolean,
|
123
|
+
quiet: T::Boolean
|
124
|
+
).void
|
125
|
+
end
|
126
|
+
def build_dsl(requested_constants, should_verify: false, quiet: false)
|
121
127
|
load_application(eager_load: requested_constants.empty?)
|
122
128
|
load_dsl_generators
|
123
129
|
|
124
|
-
|
130
|
+
if should_verify
|
131
|
+
say("Checking for out-of-date RBIs...")
|
132
|
+
else
|
133
|
+
say("Compiling DSL RBI files...")
|
134
|
+
end
|
125
135
|
say("")
|
126
136
|
|
137
|
+
outpath = should_verify ? Pathname.new(Dir.mktmpdir) : config.outpath
|
138
|
+
rbi_files_to_purge = existing_rbi_filenames(requested_constants)
|
139
|
+
|
127
140
|
compiler = Compilers::DslCompiler.new(
|
128
141
|
requested_constants: constantize(requested_constants),
|
129
142
|
requested_generators: config.generators,
|
@@ -132,15 +145,35 @@ module Tapioca
|
|
132
145
|
}
|
133
146
|
)
|
134
147
|
|
148
|
+
constant_lookup = {}
|
149
|
+
|
135
150
|
compiler.run do |constant, contents|
|
136
|
-
|
151
|
+
constant_name = Module.instance_method(:name).bind(constant).call
|
152
|
+
|
153
|
+
filename = compile_dsl_rbi(
|
154
|
+
constant_name,
|
155
|
+
contents,
|
156
|
+
outpath: outpath,
|
157
|
+
quiet: should_verify || quiet
|
158
|
+
)
|
159
|
+
|
160
|
+
if filename
|
161
|
+
rbi_files_to_purge.delete(filename)
|
162
|
+
constant_lookup[filename.relative_path_from(outpath)] = constant_name
|
163
|
+
end
|
137
164
|
end
|
138
|
-
|
139
165
|
say("")
|
140
|
-
say("Done", :green)
|
141
166
|
|
142
|
-
|
143
|
-
|
167
|
+
if should_verify
|
168
|
+
perform_dsl_verification(outpath, constant_lookup)
|
169
|
+
else
|
170
|
+
purge_stale_dsl_rbi_files(rbi_files_to_purge)
|
171
|
+
|
172
|
+
say("Done", :green)
|
173
|
+
|
174
|
+
say("All operations performed in working directory.", [:green, :bold])
|
175
|
+
say("Please review changes and commit them.", [:green, :bold])
|
176
|
+
end
|
144
177
|
end
|
145
178
|
|
146
179
|
sig { void }
|
@@ -206,7 +239,7 @@ module Tapioca
|
|
206
239
|
say_error("If you populated ", :yellow)
|
207
240
|
say_error("#{file} ", :bold, :blue)
|
208
241
|
say_error("with ", :yellow)
|
209
|
-
say_error("
|
242
|
+
say_error("`#{Config::DEFAULT_COMMAND} require`", :bold, :blue)
|
210
243
|
say_error("you should probably review it and remove the faulty line.", :yellow)
|
211
244
|
end
|
212
245
|
|
@@ -253,13 +286,38 @@ module Tapioca
|
|
253
286
|
|
254
287
|
sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) }
|
255
288
|
def constantize(constant_names)
|
256
|
-
constant_names.map do |name|
|
289
|
+
constant_map = constant_names.map do |name|
|
257
290
|
begin
|
258
|
-
name.constantize
|
291
|
+
[name, name.constantize]
|
259
292
|
rescue NameError
|
260
|
-
nil
|
293
|
+
[name, nil]
|
261
294
|
end
|
262
|
-
end.
|
295
|
+
end.to_h
|
296
|
+
|
297
|
+
unprocessable_constants = constant_map.select { |_, v| v.nil? }
|
298
|
+
unless unprocessable_constants.empty?
|
299
|
+
unprocessable_constants.each do |name, _|
|
300
|
+
say("Error: Cannot find constant '#{name}'", :red)
|
301
|
+
remove(dsl_rbi_filename(name))
|
302
|
+
end
|
303
|
+
|
304
|
+
exit(1)
|
305
|
+
end
|
306
|
+
|
307
|
+
constant_map.values
|
308
|
+
end
|
309
|
+
|
310
|
+
sig { params(requested_constants: T::Array[String], path: Pathname).returns(T::Set[Pathname]) }
|
311
|
+
def existing_rbi_filenames(requested_constants, path: config.outpath)
|
312
|
+
filenames = if requested_constants.empty?
|
313
|
+
Pathname.glob(path / "**/*.rbi")
|
314
|
+
else
|
315
|
+
requested_constants.map do |constant_name|
|
316
|
+
dsl_rbi_filename(constant_name)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
filenames.to_set
|
263
321
|
end
|
264
322
|
|
265
323
|
sig { returns(T::Hash[String, String]) }
|
@@ -277,6 +335,11 @@ module Tapioca
|
|
277
335
|
.to_h
|
278
336
|
end
|
279
337
|
|
338
|
+
sig { params(constant_name: String).returns(Pathname) }
|
339
|
+
def dsl_rbi_filename(constant_name)
|
340
|
+
config.outpath / "#{constant_name.underscore}.rbi"
|
341
|
+
end
|
342
|
+
|
280
343
|
sig { params(gem_name: String, version: String).returns(Pathname) }
|
281
344
|
def gem_rbi_filename(gem_name, version)
|
282
345
|
config.outpath / "#{gem_name}@#{version}.rbi"
|
@@ -316,6 +379,7 @@ module Tapioca
|
|
316
379
|
|
317
380
|
sig { params(filename: Pathname).void }
|
318
381
|
def remove(filename)
|
382
|
+
return unless filename.exist?
|
319
383
|
say("-- Removing: #{filename}")
|
320
384
|
filename.unlink
|
321
385
|
end
|
@@ -434,7 +498,7 @@ module Tapioca
|
|
434
498
|
rbi_body_content = compiler.compile(gem)
|
435
499
|
content = String.new
|
436
500
|
content << rbi_header(
|
437
|
-
|
501
|
+
"#{Config::DEFAULT_COMMAND} sync",
|
438
502
|
reason: "types exported from the `#{gem.name}` gem",
|
439
503
|
strictness: strictness
|
440
504
|
)
|
@@ -451,31 +515,122 @@ module Tapioca
|
|
451
515
|
end
|
452
516
|
File.write(filename.to_s, content)
|
453
517
|
|
454
|
-
Pathname.glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
|
518
|
+
T.unsafe(Pathname).glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
|
455
519
|
remove(file) unless file.basename.to_s == gem.rbi_file_name
|
456
520
|
end
|
457
521
|
end
|
458
522
|
|
459
|
-
sig
|
460
|
-
|
523
|
+
sig do
|
524
|
+
params(constant_name: String, contents: String, outpath: Pathname, quiet: T::Boolean)
|
525
|
+
.returns(T.nilable(Pathname))
|
526
|
+
end
|
527
|
+
def compile_dsl_rbi(constant_name, contents, outpath: config.outpath, quiet: false)
|
461
528
|
return if contents.nil?
|
462
529
|
|
463
|
-
command = format(config.generate_command, constant.name)
|
464
|
-
constant_name = Module.instance_method(:name).bind(constant).call
|
465
530
|
rbi_name = constant_name.underscore + ".rbi"
|
466
|
-
filename =
|
531
|
+
filename = outpath / rbi_name
|
467
532
|
|
468
533
|
out = String.new
|
469
534
|
out << rbi_header(
|
470
|
-
|
471
|
-
reason: "dynamic methods in `#{
|
535
|
+
"#{Config::DEFAULT_COMMAND} dsl #{constant_name}",
|
536
|
+
reason: "dynamic methods in `#{constant_name}`"
|
472
537
|
)
|
473
538
|
out << contents
|
474
539
|
|
475
540
|
FileUtils.mkdir_p(File.dirname(filename))
|
476
541
|
File.write(filename, out)
|
477
|
-
|
478
|
-
|
542
|
+
|
543
|
+
unless quiet
|
544
|
+
say("Wrote: ", [:green])
|
545
|
+
say(filename)
|
546
|
+
end
|
547
|
+
|
548
|
+
filename
|
549
|
+
end
|
550
|
+
|
551
|
+
sig { params(tmp_dir: Pathname).returns(T::Hash[String, Symbol]) }
|
552
|
+
def verify_dsl_rbi(tmp_dir:)
|
553
|
+
diff = {}
|
554
|
+
|
555
|
+
existing_rbis = rbi_files_in(config.outpath)
|
556
|
+
new_rbis = rbi_files_in(tmp_dir)
|
557
|
+
|
558
|
+
added_files = (new_rbis - existing_rbis)
|
559
|
+
|
560
|
+
added_files.each do |file|
|
561
|
+
diff[file] = :added
|
562
|
+
end
|
563
|
+
|
564
|
+
removed_files = (existing_rbis - new_rbis)
|
565
|
+
|
566
|
+
removed_files.each do |file|
|
567
|
+
diff[file] = :removed
|
568
|
+
end
|
569
|
+
|
570
|
+
common_files = (existing_rbis & new_rbis)
|
571
|
+
|
572
|
+
changed_files = common_files.map do |filename|
|
573
|
+
filename unless FileUtils.identical?(config.outpath / filename, tmp_dir / filename)
|
574
|
+
end.compact
|
575
|
+
|
576
|
+
changed_files.each do |file|
|
577
|
+
diff[file] = :changed
|
578
|
+
end
|
579
|
+
|
580
|
+
diff
|
581
|
+
end
|
582
|
+
|
583
|
+
sig { params(cause: Symbol, files: T::Array[String]).returns(String) }
|
584
|
+
def build_error_for_files(cause, files)
|
585
|
+
filenames = files.map do |file|
|
586
|
+
config.outpath / file
|
587
|
+
end.join("\n - ")
|
588
|
+
|
589
|
+
" File(s) #{cause}:\n - #{filenames}"
|
590
|
+
end
|
591
|
+
|
592
|
+
sig { params(path: Pathname).returns(T::Array[Pathname]) }
|
593
|
+
def rbi_files_in(path)
|
594
|
+
Pathname.glob(path / "**/*.rbi").map do |file|
|
595
|
+
file.relative_path_from(path)
|
596
|
+
end.sort
|
597
|
+
end
|
598
|
+
|
599
|
+
sig { params(dir: Pathname, constant_lookup: T::Hash[String, String]).void }
|
600
|
+
def perform_dsl_verification(dir, constant_lookup)
|
601
|
+
diff = verify_dsl_rbi(tmp_dir: dir)
|
602
|
+
|
603
|
+
if diff.empty?
|
604
|
+
say("Nothing to do, all RBIs are up-to-date.")
|
605
|
+
else
|
606
|
+
constants = T.unsafe(constant_lookup).values_at(*diff.keys).join(" ")
|
607
|
+
|
608
|
+
say("RBI files are out-of-date, please run:")
|
609
|
+
say(" `#{Config::DEFAULT_COMMAND} dsl #{constants}`")
|
610
|
+
|
611
|
+
say("")
|
612
|
+
|
613
|
+
say("Reason:", [:red])
|
614
|
+
diff.group_by(&:last).sort.each do |cause, diff_for_cause|
|
615
|
+
say(build_error_for_files(cause, diff_for_cause.map(&:first)))
|
616
|
+
end
|
617
|
+
|
618
|
+
exit(1)
|
619
|
+
end
|
620
|
+
ensure
|
621
|
+
FileUtils.remove_entry(dir)
|
622
|
+
end
|
623
|
+
|
624
|
+
sig { params(files: T::Set[Pathname]).void }
|
625
|
+
def purge_stale_dsl_rbi_files(files)
|
626
|
+
if files.any?
|
627
|
+
say("Removing stale RBI files...")
|
628
|
+
|
629
|
+
files.sort.each do |filename|
|
630
|
+
remove(filename)
|
631
|
+
end
|
632
|
+
say("")
|
633
|
+
end
|
479
634
|
end
|
480
635
|
end
|
481
636
|
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Tapioca
|
5
|
+
# This class is responsible for storing and looking up information related to generic types.
|
6
|
+
#
|
7
|
+
# The class stores 2 different kinds of data, in two separate lookup tables:
|
8
|
+
# 1. a lookup of generic type instances by name: `@generic_instances`
|
9
|
+
# 2. a lookup of type variable serializer by constant and type variable
|
10
|
+
# instance: `@type_variables`
|
11
|
+
#
|
12
|
+
# By storing the above data, we can cheaply query each constant against this registry
|
13
|
+
# to see if it declares any generic type variables. This becomes a simple lookup in the
|
14
|
+
# `@type_variables` hash table with the given constant.
|
15
|
+
#
|
16
|
+
# If there is no entry, then we can cheaply know that we can skip generic type
|
17
|
+
# information generation for this type.
|
18
|
+
#
|
19
|
+
# On the other hand, if we get a result, then the result will be a hash of type
|
20
|
+
# variable to type variable serializers. This allows us to associate type variables
|
21
|
+
# to the constant names that represent them, easily.
|
22
|
+
module GenericTypeRegistry
|
23
|
+
@generic_instances = T.let(
|
24
|
+
{},
|
25
|
+
T::Hash[String, Module]
|
26
|
+
)
|
27
|
+
|
28
|
+
@type_variables = T.let(
|
29
|
+
{},
|
30
|
+
T::Hash[Integer, T::Hash[Integer, String]]
|
31
|
+
)
|
32
|
+
|
33
|
+
class << self
|
34
|
+
extend T::Sig
|
35
|
+
|
36
|
+
# This method is responsible for building the name of the instantiated concrete type
|
37
|
+
# and cloning the given constant so that we can return a type that is the same
|
38
|
+
# as the current type but is a different instance and has a different name method.
|
39
|
+
#
|
40
|
+
# We cache those cloned instances by their name in `@generic_instances`, so that
|
41
|
+
# we don't keep instantiating a new type every single time it is referenced.
|
42
|
+
# For example, `[Foo[Integer], Foo[Integer], Foo[Integer], Foo[String]]` will only
|
43
|
+
# result in 2 clones (1 for `Foo[Integer]` and another for `Foo[String]`) and
|
44
|
+
# 2 hash lookups (for the other two `Foo[Integer]`s).
|
45
|
+
#
|
46
|
+
# This method returns the created or cached clone of the constant.
|
47
|
+
sig { params(constant: T.untyped, types: T.untyped).returns(Module) }
|
48
|
+
def register_type(constant, types)
|
49
|
+
# Build the name of the instantiated generic type,
|
50
|
+
# something like `"Foo[X, Y, Z]"`
|
51
|
+
type_list = types.map { |type| T::Utils.coerce(type).name }.join(", ")
|
52
|
+
name = "#{name_of(constant)}[#{type_list}]"
|
53
|
+
|
54
|
+
# Create a generic type with an overridden `name`
|
55
|
+
# method that returns the name we constructed above.
|
56
|
+
#
|
57
|
+
# Also, we try to memoize the generic type based on the name, so that
|
58
|
+
# we don't have to keep recreating them all the time.
|
59
|
+
@generic_instances[name] ||= create_generic_type(constant, name)
|
60
|
+
end
|
61
|
+
|
62
|
+
sig do
|
63
|
+
params(
|
64
|
+
constant: T.untyped,
|
65
|
+
type_member: T::Types::TypeVariable,
|
66
|
+
fixed: T.untyped,
|
67
|
+
lower: T.untyped,
|
68
|
+
upper: T.untyped
|
69
|
+
).void
|
70
|
+
end
|
71
|
+
def register_type_member(constant, type_member, fixed, lower, upper)
|
72
|
+
register_type_variable(constant, :type_member, type_member, fixed, lower, upper)
|
73
|
+
end
|
74
|
+
|
75
|
+
sig do
|
76
|
+
params(
|
77
|
+
constant: T.untyped,
|
78
|
+
type_template: T::Types::TypeVariable,
|
79
|
+
fixed: T.untyped,
|
80
|
+
lower: T.untyped,
|
81
|
+
upper: T.untyped
|
82
|
+
).void
|
83
|
+
end
|
84
|
+
def register_type_template(constant, type_template, fixed, lower, upper)
|
85
|
+
register_type_variable(constant, :type_template, type_template, fixed, lower, upper)
|
86
|
+
end
|
87
|
+
|
88
|
+
sig { params(constant: Module).returns(T.nilable(T::Hash[Integer, String])) }
|
89
|
+
def lookup_type_variables(constant)
|
90
|
+
@type_variables[object_id_of(constant)]
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
sig { params(constant: Module, name: String).returns(Module) }
|
96
|
+
def create_generic_type(constant, name)
|
97
|
+
generic_type = case constant
|
98
|
+
when Class
|
99
|
+
# For classes, we want to create a subclass, so that an instance of
|
100
|
+
# the generic class `Foo[Bar]` is still a `Foo`. That is:
|
101
|
+
# `Foo[Bar].new.is_a?(Foo)` should be true, which isn't the case
|
102
|
+
# if we just clone the class. But subclassing works just fine.
|
103
|
+
create_sealed_safe_subclass(constant)
|
104
|
+
else
|
105
|
+
# This can only be a module and it is fine to just clone modules
|
106
|
+
# since they can't have instances and will not have `is_a?` relationships.
|
107
|
+
# Moreover, we never `include`/`extend` any generic modules into the
|
108
|
+
# ancestor tree, so this doesn't become a problem with checking the
|
109
|
+
# instance of a class being `is_a?` of a module type.
|
110
|
+
constant.clone
|
111
|
+
end
|
112
|
+
|
113
|
+
# Let's set the `name` method to return the proper generic name
|
114
|
+
generic_type.define_singleton_method(:name) { name }
|
115
|
+
|
116
|
+
# Return the generic type we created
|
117
|
+
generic_type
|
118
|
+
end
|
119
|
+
|
120
|
+
# This method is called from intercepted calls to `type_member` and `type_template`.
|
121
|
+
# We get passed all the arguments to those methods, as well as the `T::Types::TypeVariable`
|
122
|
+
# instance generated by the Sorbet defined `type_member`/`type_template` call on `T::Generic`.
|
123
|
+
#
|
124
|
+
# This method creates a `String` with that data and stores it in the
|
125
|
+
# `@type_variables` lookup table, keyed by the `constant` and `type_variable`.
|
126
|
+
#
|
127
|
+
# Finally, the original `type_variable` is returned from this method, so that the caller
|
128
|
+
# can return it from the original methods as well.
|
129
|
+
sig do
|
130
|
+
params(
|
131
|
+
constant: T.untyped,
|
132
|
+
type_variable_type: T.enum([:type_member, :type_template]),
|
133
|
+
type_variable: T::Types::TypeVariable,
|
134
|
+
fixed: T.untyped,
|
135
|
+
lower: T.untyped,
|
136
|
+
upper: T.untyped
|
137
|
+
).void
|
138
|
+
end
|
139
|
+
# rubocop:disable Metrics/ParameterLists
|
140
|
+
def register_type_variable(constant, type_variable_type, type_variable, fixed, lower, upper)
|
141
|
+
# rubocop:enable Metrics/ParameterLists
|
142
|
+
type_variables = lookup_or_initialize_type_variables(constant)
|
143
|
+
|
144
|
+
type_variables[object_id_of(type_variable)] = serialize_type_variable(
|
145
|
+
type_variable_type,
|
146
|
+
type_variable.variance,
|
147
|
+
fixed,
|
148
|
+
lower,
|
149
|
+
upper
|
150
|
+
)
|
151
|
+
end
|
152
|
+
|
153
|
+
sig { params(constant: Class).returns(Class) }
|
154
|
+
def create_sealed_safe_subclass(constant)
|
155
|
+
# If the constant is not sealed let's just bail early.
|
156
|
+
# We just return a subclass of the constant.
|
157
|
+
return Class.new(constant) unless T::Private::Sealed.sealed_module?(constant)
|
158
|
+
|
159
|
+
# Since sealed classes can normally not be subclassed, we need to trick
|
160
|
+
# sealed classes into thinking that the generic type we are
|
161
|
+
# creating by subclassing is actually safe for sealed types.
|
162
|
+
#
|
163
|
+
# Get the filename the sealed class was declared in
|
164
|
+
decl_file = constant.instance_variable_get(:@sorbet_sealed_module_decl_file)
|
165
|
+
begin
|
166
|
+
# Clear the current declaration filename on the class
|
167
|
+
constant.remove_instance_variable(:@sorbet_sealed_module_decl_file)
|
168
|
+
# Make this file be the declaration filename so that Sorbet runtime
|
169
|
+
# does not shout at us for an invalid subclassing.
|
170
|
+
T.cast(constant, T::Helpers).sealed!
|
171
|
+
# return a subclass
|
172
|
+
Class.new(constant)
|
173
|
+
ensure
|
174
|
+
# Reinstate the original declaration filename
|
175
|
+
constant.instance_variable_set(:@sorbet_sealed_module_decl_file, decl_file)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
sig { params(constant: Module).returns(T::Hash[Integer, String]) }
|
180
|
+
def lookup_or_initialize_type_variables(constant)
|
181
|
+
@type_variables[object_id_of(constant)] ||= {}
|
182
|
+
end
|
183
|
+
|
184
|
+
sig do
|
185
|
+
params(
|
186
|
+
type_variable_type: Symbol,
|
187
|
+
variance: Symbol,
|
188
|
+
fixed: T.untyped,
|
189
|
+
lower: T.untyped,
|
190
|
+
upper: T.untyped
|
191
|
+
).returns(String)
|
192
|
+
end
|
193
|
+
def serialize_type_variable(type_variable_type, variance, fixed, lower, upper)
|
194
|
+
parts = []
|
195
|
+
parts << ":#{variance}" unless variance == :invariant
|
196
|
+
parts << "fixed: #{fixed}" if fixed
|
197
|
+
parts << "lower: #{lower}" unless lower == T.untyped
|
198
|
+
parts << "upper: #{upper}" unless upper == BasicObject
|
199
|
+
|
200
|
+
parameters = parts.join(", ")
|
201
|
+
|
202
|
+
serialized = T.let(type_variable_type.to_s, String)
|
203
|
+
serialized += "(#{parameters})" unless parameters.empty?
|
204
|
+
|
205
|
+
serialized
|
206
|
+
end
|
207
|
+
|
208
|
+
sig { params(constant: Module).returns(T.nilable(String)) }
|
209
|
+
def name_of(constant)
|
210
|
+
Module.instance_method(:name).bind(constant).call
|
211
|
+
end
|
212
|
+
|
213
|
+
sig { params(object: BasicObject).returns(Integer) }
|
214
|
+
def object_id_of(object)
|
215
|
+
Object.instance_method(:object_id).bind(object).call
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|