tapioca 0.4.17 → 0.4.22
Sign up to get free protection for your applications and to get access to all the features.
- 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
|