tapioca 0.4.17 → 0.4.22
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/Gemfile +13 -14
- 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_job.rb +71 -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/protobuf.rb +132 -16
- data/lib/tapioca/compilers/dsl/url_helpers.rb +3 -3
- data/lib/tapioca/compilers/requires_compiler.rb +1 -1
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +264 -301
- data/lib/tapioca/config.rb +1 -1
- data/lib/tapioca/config_builder.rb +7 -12
- data/lib/tapioca/core_ext/string.rb +18 -0
- data/lib/tapioca/gemfile.rb +1 -1
- data/lib/tapioca/generator.rb +144 -37
- data/lib/tapioca/generic_type_registry.rb +222 -0
- data/lib/tapioca/internal.rb +28 -0
- data/lib/tapioca/rbi/model.rb +405 -0
- data/lib/tapioca/rbi/printer.rb +410 -0
- data/lib/tapioca/rbi/rewriters/group_nodes.rb +106 -0
- data/lib/tapioca/rbi/rewriters/nest_non_public_methods.rb +65 -0
- data/lib/tapioca/rbi/rewriters/nest_singleton_methods.rb +42 -0
- data/lib/tapioca/rbi/rewriters/sort_nodes.rb +82 -0
- data/lib/tapioca/rbi/visitor.rb +21 -0
- 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 +17 -3
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,
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class String
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
sig { returns(String) }
|
8
|
+
def underscore
|
9
|
+
return self unless /[A-Z-]|::/.match?(self)
|
10
|
+
|
11
|
+
word = to_s.gsub("::", "/")
|
12
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
13
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
14
|
+
word.tr!("-", "_")
|
15
|
+
word.downcase!
|
16
|
+
word
|
17
|
+
end
|
18
|
+
end
|
data/lib/tapioca/gemfile.rb
CHANGED
data/lib/tapioca/generator.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
|
4
4
|
require 'pathname'
|
5
5
|
require 'thor'
|
6
|
+
require "tapioca/core_ext/string"
|
6
7
|
|
7
8
|
module Tapioca
|
8
9
|
class Generator < ::Thor::Shell::Color
|
@@ -63,7 +64,7 @@ module Tapioca
|
|
63
64
|
|
64
65
|
content = String.new
|
65
66
|
content << rbi_header(
|
66
|
-
|
67
|
+
"#{Config::DEFAULT_COMMAND} require",
|
67
68
|
reason: "explicit gem requires",
|
68
69
|
strictness: "false"
|
69
70
|
)
|
@@ -76,8 +77,8 @@ module Tapioca
|
|
76
77
|
say("Done", :green)
|
77
78
|
|
78
79
|
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
|
80
|
+
cmd = set_color("#{Config::DEFAULT_COMMAND} sync", :yellow, :bold)
|
81
|
+
say("Please review changes and commit them, then run `#{cmd}`.", [:green, :bold])
|
81
82
|
end
|
82
83
|
|
83
84
|
sig { void }
|
@@ -99,7 +100,7 @@ module Tapioca
|
|
99
100
|
|
100
101
|
content = String.new
|
101
102
|
content << rbi_header(
|
102
|
-
|
103
|
+
"#{Config::DEFAULT_COMMAND} todo",
|
103
104
|
reason: "unresolved constants",
|
104
105
|
strictness: "false"
|
105
106
|
)
|
@@ -116,16 +117,27 @@ module Tapioca
|
|
116
117
|
say("Please review changes and commit them.", [:green, :bold])
|
117
118
|
end
|
118
119
|
|
119
|
-
sig
|
120
|
-
|
120
|
+
sig do
|
121
|
+
params(
|
122
|
+
requested_constants: T::Array[String],
|
123
|
+
should_verify: T::Boolean,
|
124
|
+
quiet: T::Boolean
|
125
|
+
).void
|
126
|
+
end
|
127
|
+
def build_dsl(requested_constants, should_verify: false, quiet: false)
|
121
128
|
load_application(eager_load: requested_constants.empty?)
|
122
129
|
load_dsl_generators
|
123
130
|
|
124
|
-
|
125
|
-
|
126
|
-
|
131
|
+
if should_verify
|
132
|
+
say("Checking for out-of-date RBIs...")
|
133
|
+
else
|
134
|
+
say("Compiling DSL RBI files...")
|
135
|
+
end
|
127
136
|
say("")
|
128
137
|
|
138
|
+
outpath = should_verify ? Pathname.new(Dir.mktmpdir) : config.outpath
|
139
|
+
rbi_files_to_purge = existing_rbi_filenames(requested_constants)
|
140
|
+
|
129
141
|
compiler = Compilers::DslCompiler.new(
|
130
142
|
requested_constants: constantize(requested_constants),
|
131
143
|
requested_generators: config.generators,
|
@@ -135,24 +147,31 @@ module Tapioca
|
|
135
147
|
)
|
136
148
|
|
137
149
|
compiler.run do |constant, contents|
|
138
|
-
|
139
|
-
rbi_files_to_purge.delete(filename) if filename
|
140
|
-
end
|
150
|
+
constant_name = Module.instance_method(:name).bind(constant).call
|
141
151
|
|
142
|
-
|
143
|
-
|
144
|
-
|
152
|
+
filename = compile_dsl_rbi(
|
153
|
+
constant_name,
|
154
|
+
contents,
|
155
|
+
outpath: outpath,
|
156
|
+
quiet: should_verify || quiet
|
157
|
+
)
|
145
158
|
|
146
|
-
|
147
|
-
|
159
|
+
if filename
|
160
|
+
rbi_files_to_purge.delete(filename)
|
148
161
|
end
|
149
162
|
end
|
150
|
-
|
151
163
|
say("")
|
152
|
-
say("Done", :green)
|
153
164
|
|
154
|
-
|
155
|
-
|
165
|
+
if should_verify
|
166
|
+
perform_dsl_verification(outpath)
|
167
|
+
else
|
168
|
+
purge_stale_dsl_rbi_files(rbi_files_to_purge)
|
169
|
+
|
170
|
+
say("Done", :green)
|
171
|
+
|
172
|
+
say("All operations performed in working directory.", [:green, :bold])
|
173
|
+
say("Please review changes and commit them.", [:green, :bold])
|
174
|
+
end
|
156
175
|
end
|
157
176
|
|
158
177
|
sig { void }
|
@@ -218,7 +237,7 @@ module Tapioca
|
|
218
237
|
say_error("If you populated ", :yellow)
|
219
238
|
say_error("#{file} ", :bold, :blue)
|
220
239
|
say_error("with ", :yellow)
|
221
|
-
say_error("
|
240
|
+
say_error("`#{Config::DEFAULT_COMMAND} require`", :bold, :blue)
|
222
241
|
say_error("you should probably review it and remove the faulty line.", :yellow)
|
223
242
|
end
|
224
243
|
|
@@ -267,7 +286,7 @@ module Tapioca
|
|
267
286
|
def constantize(constant_names)
|
268
287
|
constant_map = constant_names.map do |name|
|
269
288
|
begin
|
270
|
-
[name, name
|
289
|
+
[name, Object.const_get(name)]
|
271
290
|
rescue NameError
|
272
291
|
[name, nil]
|
273
292
|
end
|
@@ -286,10 +305,10 @@ module Tapioca
|
|
286
305
|
constant_map.values
|
287
306
|
end
|
288
307
|
|
289
|
-
sig { params(requested_constants: T::Array[String]).returns(T::Set[Pathname]) }
|
290
|
-
def existing_rbi_filenames(requested_constants)
|
308
|
+
sig { params(requested_constants: T::Array[String], path: Pathname).returns(T::Set[Pathname]) }
|
309
|
+
def existing_rbi_filenames(requested_constants, path: config.outpath)
|
291
310
|
filenames = if requested_constants.empty?
|
292
|
-
Pathname.glob(
|
311
|
+
Pathname.glob(path / "**/*.rbi")
|
293
312
|
else
|
294
313
|
requested_constants.map do |constant_name|
|
295
314
|
dsl_rbi_filename(constant_name)
|
@@ -477,7 +496,7 @@ module Tapioca
|
|
477
496
|
rbi_body_content = compiler.compile(gem)
|
478
497
|
content = String.new
|
479
498
|
content << rbi_header(
|
480
|
-
|
499
|
+
"#{Config::DEFAULT_COMMAND} sync",
|
481
500
|
reason: "types exported from the `#{gem.name}` gem",
|
482
501
|
strictness: strictness
|
483
502
|
)
|
@@ -494,33 +513,121 @@ module Tapioca
|
|
494
513
|
end
|
495
514
|
File.write(filename.to_s, content)
|
496
515
|
|
497
|
-
Pathname.glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
|
516
|
+
T.unsafe(Pathname).glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
|
498
517
|
remove(file) unless file.basename.to_s == gem.rbi_file_name
|
499
518
|
end
|
500
519
|
end
|
501
520
|
|
502
|
-
sig
|
503
|
-
|
521
|
+
sig do
|
522
|
+
params(constant_name: String, contents: String, outpath: Pathname, quiet: T::Boolean)
|
523
|
+
.returns(T.nilable(Pathname))
|
524
|
+
end
|
525
|
+
def compile_dsl_rbi(constant_name, contents, outpath: config.outpath, quiet: false)
|
504
526
|
return if contents.nil?
|
505
527
|
|
506
|
-
command = format(config.generate_command, constant.name)
|
507
|
-
constant_name = Module.instance_method(:name).bind(constant).call
|
508
528
|
rbi_name = constant_name.underscore + ".rbi"
|
509
|
-
filename =
|
529
|
+
filename = outpath / rbi_name
|
510
530
|
|
511
531
|
out = String.new
|
512
532
|
out << rbi_header(
|
513
|
-
|
514
|
-
reason: "dynamic methods in `#{
|
533
|
+
"#{Config::DEFAULT_COMMAND} dsl #{constant_name}",
|
534
|
+
reason: "dynamic methods in `#{constant_name}`"
|
515
535
|
)
|
516
536
|
out << contents
|
517
537
|
|
518
538
|
FileUtils.mkdir_p(File.dirname(filename))
|
519
539
|
File.write(filename, out)
|
520
|
-
|
521
|
-
|
540
|
+
|
541
|
+
unless quiet
|
542
|
+
say("Wrote: ", [:green])
|
543
|
+
say(filename)
|
544
|
+
end
|
522
545
|
|
523
546
|
filename
|
524
547
|
end
|
548
|
+
|
549
|
+
sig { params(tmp_dir: Pathname).returns(T::Hash[String, Symbol]) }
|
550
|
+
def verify_dsl_rbi(tmp_dir:)
|
551
|
+
diff = {}
|
552
|
+
|
553
|
+
existing_rbis = rbi_files_in(config.outpath)
|
554
|
+
new_rbis = rbi_files_in(tmp_dir)
|
555
|
+
|
556
|
+
added_files = (new_rbis - existing_rbis)
|
557
|
+
|
558
|
+
added_files.each do |file|
|
559
|
+
diff[file] = :added
|
560
|
+
end
|
561
|
+
|
562
|
+
removed_files = (existing_rbis - new_rbis)
|
563
|
+
|
564
|
+
removed_files.each do |file|
|
565
|
+
diff[file] = :removed
|
566
|
+
end
|
567
|
+
|
568
|
+
common_files = (existing_rbis & new_rbis)
|
569
|
+
|
570
|
+
changed_files = common_files.map do |filename|
|
571
|
+
filename unless FileUtils.identical?(config.outpath / filename, tmp_dir / filename)
|
572
|
+
end.compact
|
573
|
+
|
574
|
+
changed_files.each do |file|
|
575
|
+
diff[file] = :changed
|
576
|
+
end
|
577
|
+
|
578
|
+
diff
|
579
|
+
end
|
580
|
+
|
581
|
+
sig { params(cause: Symbol, files: T::Array[String]).returns(String) }
|
582
|
+
def build_error_for_files(cause, files)
|
583
|
+
filenames = files.map do |file|
|
584
|
+
config.outpath / file
|
585
|
+
end.join("\n - ")
|
586
|
+
|
587
|
+
" File(s) #{cause}:\n - #{filenames}"
|
588
|
+
end
|
589
|
+
|
590
|
+
sig { params(path: Pathname).returns(T::Array[Pathname]) }
|
591
|
+
def rbi_files_in(path)
|
592
|
+
Pathname.glob(path / "**/*.rbi").map do |file|
|
593
|
+
file.relative_path_from(path)
|
594
|
+
end.sort
|
595
|
+
end
|
596
|
+
|
597
|
+
sig { params(dir: Pathname).void }
|
598
|
+
def perform_dsl_verification(dir)
|
599
|
+
diff = verify_dsl_rbi(tmp_dir: dir)
|
600
|
+
|
601
|
+
if diff.empty?
|
602
|
+
say("Nothing to do, all RBIs are up-to-date.")
|
603
|
+
else
|
604
|
+
say("RBI files are out-of-date. In your development environment, please run:", :green)
|
605
|
+
say(" `#{Config::DEFAULT_COMMAND} dsl`", [:green, :bold])
|
606
|
+
say("Once it is complete, be sure to commit and push any changes", :green)
|
607
|
+
|
608
|
+
say("")
|
609
|
+
|
610
|
+
say("Reason:", [:red])
|
611
|
+
diff.group_by(&:last).sort.each do |cause, diff_for_cause|
|
612
|
+
say(build_error_for_files(cause, diff_for_cause.map(&:first)))
|
613
|
+
end
|
614
|
+
|
615
|
+
exit(1)
|
616
|
+
end
|
617
|
+
ensure
|
618
|
+
FileUtils.remove_entry(dir)
|
619
|
+
end
|
620
|
+
|
621
|
+
sig { params(files: T::Set[Pathname]).void }
|
622
|
+
def purge_stale_dsl_rbi_files(files)
|
623
|
+
if files.any?
|
624
|
+
say("Removing stale RBI files...")
|
625
|
+
|
626
|
+
files.sort.each do |filename|
|
627
|
+
remove(filename)
|
628
|
+
end
|
629
|
+
say("")
|
630
|
+
end
|
631
|
+
end
|
525
632
|
end
|
526
633
|
end
|
@@ -0,0 +1,222 @@
|
|
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_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_safe_subclass(constant)
|
155
|
+
# Lookup the "inherited" class method
|
156
|
+
inherited_method = constant.method(:inherited)
|
157
|
+
# and the module that defines it
|
158
|
+
owner = inherited_method.owner
|
159
|
+
|
160
|
+
# If no one has overriden the inherited method yet, just subclass
|
161
|
+
return Class.new(constant) if Class == owner
|
162
|
+
|
163
|
+
begin
|
164
|
+
# Otherwise, some inherited method could be preventing us
|
165
|
+
# from creating subclasses, so let's override it and rescue
|
166
|
+
owner.send(:define_method, :inherited) do |s|
|
167
|
+
begin
|
168
|
+
inherited_method.call(s)
|
169
|
+
rescue
|
170
|
+
# Ignoring errors
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# return a subclass
|
175
|
+
Class.new(constant)
|
176
|
+
ensure
|
177
|
+
# Reinstate the original inherited method back.
|
178
|
+
owner.send(:define_method, :inherited, inherited_method)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
sig { params(constant: Module).returns(T::Hash[Integer, String]) }
|
183
|
+
def lookup_or_initialize_type_variables(constant)
|
184
|
+
@type_variables[object_id_of(constant)] ||= {}
|
185
|
+
end
|
186
|
+
|
187
|
+
sig do
|
188
|
+
params(
|
189
|
+
type_variable_type: Symbol,
|
190
|
+
variance: Symbol,
|
191
|
+
fixed: T.untyped,
|
192
|
+
lower: T.untyped,
|
193
|
+
upper: T.untyped
|
194
|
+
).returns(String)
|
195
|
+
end
|
196
|
+
def serialize_type_variable(type_variable_type, variance, fixed, lower, upper)
|
197
|
+
parts = []
|
198
|
+
parts << ":#{variance}" unless variance == :invariant
|
199
|
+
parts << "fixed: #{fixed}" if fixed
|
200
|
+
parts << "lower: #{lower}" unless lower == T.untyped
|
201
|
+
parts << "upper: #{upper}" unless upper == BasicObject
|
202
|
+
|
203
|
+
parameters = parts.join(", ")
|
204
|
+
|
205
|
+
serialized = T.let(type_variable_type.to_s, String)
|
206
|
+
serialized += "(#{parameters})" unless parameters.empty?
|
207
|
+
|
208
|
+
serialized
|
209
|
+
end
|
210
|
+
|
211
|
+
sig { params(constant: Module).returns(T.nilable(String)) }
|
212
|
+
def name_of(constant)
|
213
|
+
Module.instance_method(:name).bind(constant).call
|
214
|
+
end
|
215
|
+
|
216
|
+
sig { params(object: BasicObject).returns(Integer) }
|
217
|
+
def object_id_of(object)
|
218
|
+
Object.instance_method(:object_id).bind(object).call
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|