tapioca 0.6.4 → 0.7.2
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 +8 -2
- data/README.md +27 -15
- data/Rakefile +10 -14
- data/lib/tapioca/cli.rb +65 -80
- data/lib/tapioca/{generators/base.rb → commands/command.rb} +16 -9
- data/lib/tapioca/{generators → commands}/dsl.rb +59 -45
- data/lib/tapioca/{generators → commands}/gem.rb +93 -30
- data/lib/tapioca/{generators → commands}/init.rb +9 -13
- data/lib/tapioca/{generators → commands}/require.rb +8 -10
- data/lib/tapioca/commands/todo.rb +86 -0
- data/lib/tapioca/commands.rb +13 -0
- data/lib/tapioca/dsl/compiler.rb +185 -0
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/aasm.rb +12 -9
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/action_controller_helpers.rb +13 -20
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/action_mailer.rb +10 -8
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_job.rb +11 -9
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_model_attributes.rb +13 -11
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_model_secure_password.rb +10 -12
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_associations.rb +28 -34
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_columns.rb +18 -16
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_enum.rb +14 -12
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_fixtures.rb +12 -8
- data/lib/tapioca/dsl/compilers/active_record_relations.rb +712 -0
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_scope.rb +21 -20
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_typed_store.rb +11 -16
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_resource.rb +10 -8
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_storage.rb +14 -10
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_support_concern.rb +19 -14
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_support_current_attributes.rb +16 -21
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/config.rb +11 -9
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/frozen_record.rb +13 -11
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/identity_cache.rb +23 -22
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/mixed_in_class_attributes.rb +12 -10
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/protobuf.rb +22 -10
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/rails_generators.rb +12 -13
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/sidekiq_worker.rb +14 -13
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/smart_properties.rb +11 -9
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/state_machines.rb +12 -10
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/url_helpers.rb +20 -15
- data/lib/tapioca/dsl/compilers.rb +31 -0
- data/lib/tapioca/{compilers/dsl → dsl}/extensions/frozen_record.rb +2 -2
- data/lib/tapioca/dsl/helpers/active_record_column_type_helper.rb +114 -0
- data/lib/tapioca/dsl/helpers/active_record_constants_helper.rb +29 -0
- data/lib/tapioca/{compilers/dsl → dsl/helpers}/param_helper.rb +6 -3
- data/lib/tapioca/dsl/pipeline.rb +169 -0
- data/lib/tapioca/gem/events.rb +120 -0
- data/lib/tapioca/gem/listeners/base.rb +48 -0
- data/lib/tapioca/gem/listeners/dynamic_mixins.rb +32 -0
- data/lib/tapioca/gem/listeners/methods.rb +183 -0
- data/lib/tapioca/gem/listeners/mixins.rb +101 -0
- data/lib/tapioca/gem/listeners/remove_empty_payload_scopes.rb +21 -0
- data/lib/tapioca/gem/listeners/sorbet_enums.rb +26 -0
- data/lib/tapioca/gem/listeners/sorbet_helpers.rb +29 -0
- data/lib/tapioca/gem/listeners/sorbet_props.rb +33 -0
- data/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb +23 -0
- data/lib/tapioca/gem/listeners/sorbet_signatures.rb +79 -0
- data/lib/tapioca/gem/listeners/sorbet_type_variables.rb +51 -0
- data/lib/tapioca/gem/listeners/subconstants.rb +37 -0
- data/lib/tapioca/gem/listeners/yard_doc.rb +96 -0
- data/lib/tapioca/gem/listeners.rb +16 -0
- data/lib/tapioca/gem/pipeline.rb +365 -0
- data/lib/tapioca/helpers/cli_helper.rb +7 -0
- data/lib/tapioca/helpers/config_helper.rb +5 -8
- data/lib/tapioca/helpers/shims_helper.rb +87 -0
- data/lib/tapioca/helpers/signatures_helper.rb +17 -0
- data/lib/tapioca/helpers/sorbet_helper.rb +57 -0
- data/lib/tapioca/helpers/test/dsl_compiler.rb +118 -0
- data/lib/tapioca/helpers/test/isolation.rb +1 -1
- data/lib/tapioca/helpers/test/template.rb +13 -2
- data/lib/tapioca/helpers/type_variable_helper.rb +43 -0
- data/lib/tapioca/internal.rb +18 -10
- data/lib/tapioca/rbi_ext/model.rb +14 -50
- data/lib/tapioca/rbi_formatter.rb +37 -0
- data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +227 -0
- data/lib/tapioca/runtime/generic_type_registry.rb +168 -0
- data/lib/tapioca/runtime/loader.rb +123 -0
- data/lib/tapioca/runtime/reflection.rb +157 -0
- data/lib/tapioca/runtime/trackers/autoload.rb +72 -0
- data/lib/tapioca/runtime/trackers/constant_definition.rb +44 -0
- data/lib/tapioca/runtime/trackers/mixin.rb +80 -0
- data/lib/tapioca/runtime/trackers/required_ancestor.rb +50 -0
- data/lib/tapioca/{trackers.rb → runtime/trackers.rb} +4 -3
- data/lib/tapioca/sorbet_ext/generic_name_patch.rb +69 -34
- data/lib/tapioca/sorbet_ext/name_patch.rb +7 -1
- data/lib/tapioca/{compilers → static}/requires_compiler.rb +2 -2
- data/lib/tapioca/static/symbol_loader.rb +83 -0
- data/lib/tapioca/static/symbol_table_parser.rb +63 -0
- data/lib/tapioca/version.rb +1 -1
- data/lib/tapioca.rb +2 -7
- metadata +83 -62
- data/lib/tapioca/compilers/dsl/active_record_relations.rb +0 -720
- data/lib/tapioca/compilers/dsl/base.rb +0 -195
- data/lib/tapioca/compilers/dsl/helper/active_record_constants.rb +0 -27
- data/lib/tapioca/compilers/dsl_compiler.rb +0 -134
- data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +0 -223
- data/lib/tapioca/compilers/sorbet.rb +0 -59
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +0 -780
- data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +0 -90
- data/lib/tapioca/compilers/symbol_table_compiler.rb +0 -17
- data/lib/tapioca/compilers/todos_compiler.rb +0 -32
- data/lib/tapioca/generators/todo.rb +0 -76
- data/lib/tapioca/generators.rb +0 -9
- data/lib/tapioca/generic_type_registry.rb +0 -164
- data/lib/tapioca/helpers/active_record_column_type_helper.rb +0 -108
- data/lib/tapioca/loader.rb +0 -119
- data/lib/tapioca/reflection.rb +0 -151
- data/lib/tapioca/trackers/autoload.rb +0 -70
- data/lib/tapioca/trackers/constant_definition.rb +0 -42
- data/lib/tapioca/trackers/mixin.rb +0 -78
|
@@ -7,15 +7,18 @@ module Tapioca
|
|
|
7
7
|
module Helpers
|
|
8
8
|
module Test
|
|
9
9
|
module Template
|
|
10
|
-
include Kernel
|
|
11
10
|
extend T::Sig
|
|
11
|
+
extend T::Helpers
|
|
12
|
+
|
|
13
|
+
requires_ancestor { Kernel }
|
|
14
|
+
|
|
12
15
|
ERB_SUPPORTS_KVARGS = T.let(
|
|
13
16
|
::ERB.instance_method(:initialize).parameters.assoc(:key), T.nilable([Symbol, Symbol])
|
|
14
17
|
)
|
|
15
18
|
|
|
16
19
|
sig { params(selector: String).returns(T::Boolean) }
|
|
17
20
|
def ruby_version(selector)
|
|
18
|
-
Gem::Requirement.new(selector).satisfied_by?(Gem::Version.new(RUBY_VERSION))
|
|
21
|
+
::Gem::Requirement.new(selector).satisfied_by?(::Gem::Version.new(RUBY_VERSION))
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
sig { params(src: String).returns(String) }
|
|
@@ -28,6 +31,14 @@ module Tapioca
|
|
|
28
31
|
|
|
29
32
|
erb.result(binding)
|
|
30
33
|
end
|
|
34
|
+
|
|
35
|
+
sig { params(str: String, indent: Integer).returns(String) }
|
|
36
|
+
def indented(str, indent)
|
|
37
|
+
str.lines.map! do |line|
|
|
38
|
+
next line if line.chomp.empty?
|
|
39
|
+
(" " * indent) + line
|
|
40
|
+
end.join
|
|
41
|
+
end
|
|
31
42
|
end
|
|
32
43
|
end
|
|
33
44
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Tapioca
|
|
5
|
+
module TypeVariableHelper
|
|
6
|
+
extend T::Sig
|
|
7
|
+
extend SorbetHelper
|
|
8
|
+
|
|
9
|
+
sig do
|
|
10
|
+
params(
|
|
11
|
+
type: String,
|
|
12
|
+
variance: Symbol,
|
|
13
|
+
fixed: T.nilable(String),
|
|
14
|
+
upper: T.nilable(String),
|
|
15
|
+
lower: T.nilable(String)
|
|
16
|
+
).returns(String)
|
|
17
|
+
end
|
|
18
|
+
def self.serialize_type_variable(type, variance, fixed, upper, lower)
|
|
19
|
+
variance = nil if variance == :invariant
|
|
20
|
+
|
|
21
|
+
bounds = []
|
|
22
|
+
bounds << "fixed: #{fixed}" if fixed
|
|
23
|
+
bounds << "lower: #{lower}" if lower
|
|
24
|
+
bounds << "upper: #{upper}" if upper
|
|
25
|
+
|
|
26
|
+
parameters = []
|
|
27
|
+
block = []
|
|
28
|
+
|
|
29
|
+
parameters << ":#{variance}" if variance
|
|
30
|
+
|
|
31
|
+
if sorbet_supports?(:type_variable_block_syntax)
|
|
32
|
+
block = bounds
|
|
33
|
+
else
|
|
34
|
+
parameters.concat(bounds)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
serialized = type.dup
|
|
38
|
+
serialized << "(#{parameters.join(", ")})" unless parameters.empty?
|
|
39
|
+
serialized << " { { #{block.join(", ")} } }" unless block.empty?
|
|
40
|
+
serialized
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/tapioca/internal.rb
CHANGED
|
@@ -2,20 +2,28 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "tapioca"
|
|
5
|
-
require "tapioca/
|
|
5
|
+
require "tapioca/runtime/reflection"
|
|
6
|
+
require "tapioca/runtime/trackers"
|
|
7
|
+
require "tapioca/runtime/dynamic_mixin_compiler"
|
|
8
|
+
require "tapioca/runtime/loader"
|
|
9
|
+
require "tapioca/helpers/sorbet_helper"
|
|
10
|
+
require "tapioca/helpers/type_variable_helper"
|
|
6
11
|
require "tapioca/sorbet_ext/generic_name_patch"
|
|
7
12
|
require "tapioca/sorbet_ext/fixed_hash_patch"
|
|
8
|
-
require "tapioca/generic_type_registry"
|
|
13
|
+
require "tapioca/runtime/generic_type_registry"
|
|
9
14
|
require "tapioca/helpers/cli_helper"
|
|
10
15
|
require "tapioca/helpers/config_helper"
|
|
11
|
-
require "tapioca/
|
|
16
|
+
require "tapioca/helpers/signatures_helper"
|
|
17
|
+
require "tapioca/helpers/shims_helper"
|
|
18
|
+
require "tapioca/commands"
|
|
12
19
|
require "tapioca/cli"
|
|
13
20
|
require "tapioca/gemfile"
|
|
14
21
|
require "tapioca/executor"
|
|
15
|
-
require "tapioca/
|
|
16
|
-
require "tapioca/
|
|
17
|
-
require "tapioca/
|
|
18
|
-
require "tapioca/
|
|
19
|
-
require "tapioca/
|
|
20
|
-
require "tapioca/
|
|
21
|
-
require "tapioca/
|
|
22
|
+
require "tapioca/static/symbol_table_parser"
|
|
23
|
+
require "tapioca/static/symbol_loader"
|
|
24
|
+
require "tapioca/gem/events"
|
|
25
|
+
require "tapioca/gem/listeners"
|
|
26
|
+
require "tapioca/gem/pipeline"
|
|
27
|
+
require "tapioca/dsl/compiler"
|
|
28
|
+
require "tapioca/dsl/pipeline"
|
|
29
|
+
require "tapioca/static/requires_compiler"
|
|
@@ -1,59 +1,13 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "rbi"
|
|
5
|
-
|
|
6
4
|
module RBI
|
|
7
|
-
class File
|
|
8
|
-
extend T::Sig
|
|
9
|
-
|
|
10
|
-
sig { returns(String) }
|
|
11
|
-
def transformed_string
|
|
12
|
-
transform_rbi!
|
|
13
|
-
string
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
sig { void }
|
|
17
|
-
def transform_rbi!
|
|
18
|
-
root.nest_singleton_methods!
|
|
19
|
-
root.nest_non_public_methods!
|
|
20
|
-
root.group_nodes!
|
|
21
|
-
root.sort_nodes!
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
sig do
|
|
25
|
-
params(
|
|
26
|
-
command: String,
|
|
27
|
-
reason: T.nilable(String),
|
|
28
|
-
display_heading: T::Boolean
|
|
29
|
-
).void
|
|
30
|
-
end
|
|
31
|
-
def set_file_header(command, reason: nil, display_heading: true)
|
|
32
|
-
return unless display_heading
|
|
33
|
-
comments << RBI::Comment.new("DO NOT EDIT MANUALLY")
|
|
34
|
-
comments << RBI::Comment.new("This is an autogenerated file for #{reason}.") unless reason.nil?
|
|
35
|
-
comments << RBI::Comment.new("Please instead update this file by running `#{command}`.")
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
sig { void }
|
|
39
|
-
def set_empty_body_content
|
|
40
|
-
comments << RBI::BlankLine.new unless comments.empty?
|
|
41
|
-
comments << RBI::Comment.new("THIS IS AN EMPTY RBI FILE.")
|
|
42
|
-
comments << RBI::Comment.new("see https://github.com/Shopify/tapioca/wiki/Manual-Gem-Requires")
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
sig { returns(T::Boolean) }
|
|
46
|
-
def empty?
|
|
47
|
-
root.empty?
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
5
|
class Tree
|
|
52
6
|
extend T::Sig
|
|
53
7
|
|
|
54
8
|
sig { params(constant: ::Module, block: T.nilable(T.proc.params(scope: Scope).void)).void }
|
|
55
9
|
def create_path(constant, &block)
|
|
56
|
-
constant_name = Tapioca::Reflection.name_of(constant)
|
|
10
|
+
constant_name = Tapioca::Runtime::Reflection.name_of(constant)
|
|
57
11
|
raise "given constant does not have a name" unless constant_name
|
|
58
12
|
|
|
59
13
|
instance = ::Module.const_get(constant_name)
|
|
@@ -107,8 +61,18 @@ module RBI
|
|
|
107
61
|
create_node(RBI::MixesInClassMethods.new(name))
|
|
108
62
|
end
|
|
109
63
|
|
|
110
|
-
sig
|
|
111
|
-
|
|
64
|
+
sig do
|
|
65
|
+
params(
|
|
66
|
+
name: String,
|
|
67
|
+
type: String,
|
|
68
|
+
variance: Symbol,
|
|
69
|
+
fixed: T.nilable(String),
|
|
70
|
+
upper: T.nilable(String),
|
|
71
|
+
lower: T.nilable(String)
|
|
72
|
+
).void
|
|
73
|
+
end
|
|
74
|
+
def create_type_variable(name, type:, variance: :invariant, fixed: nil, upper: nil, lower: nil)
|
|
75
|
+
value = Tapioca::TypeVariableHelper.serialize_type_variable(type, variance, fixed, upper, lower)
|
|
112
76
|
create_node(RBI::TypeMember.new(name, value))
|
|
113
77
|
end
|
|
114
78
|
|
|
@@ -137,7 +101,7 @@ module RBI
|
|
|
137
101
|
|
|
138
102
|
SPECIAL_METHOD_NAMES = T.let(
|
|
139
103
|
["!", "~", "+@", "**", "-@", "*", "/", "%", "+", "-", "<<", ">>", "&", "|", "^", "<", "<=", "=>", ">", ">=",
|
|
140
|
-
"==", "===", "!=", "=~", "!~", "<=>", "[]", "[]=", "`"].freeze,
|
|
104
|
+
"==", "===", "!=", "=~", "!~", "<=>", "[]", "[]=", "`",].freeze,
|
|
141
105
|
T::Array[String]
|
|
142
106
|
)
|
|
143
107
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Tapioca
|
|
5
|
+
class RBIFormatter < RBI::Formatter
|
|
6
|
+
extend T::Sig
|
|
7
|
+
|
|
8
|
+
sig do
|
|
9
|
+
params(
|
|
10
|
+
file: RBI::File,
|
|
11
|
+
command: String,
|
|
12
|
+
reason: T.nilable(String)
|
|
13
|
+
).void
|
|
14
|
+
end
|
|
15
|
+
def write_header!(file, command, reason: nil)
|
|
16
|
+
file.comments << RBI::Comment.new("DO NOT EDIT MANUALLY")
|
|
17
|
+
file.comments << RBI::Comment.new("This is an autogenerated file for #{reason}.") unless reason.nil?
|
|
18
|
+
file.comments << RBI::Comment.new("Please instead update this file by running `#{command}`.")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
sig { params(file: RBI::File).void }
|
|
22
|
+
def write_empty_body_comment!(file)
|
|
23
|
+
file.comments << RBI::BlankLine.new unless file.comments.empty?
|
|
24
|
+
file.comments << RBI::Comment.new("THIS IS AN EMPTY RBI FILE.")
|
|
25
|
+
file.comments << RBI::Comment.new("see https://github.com/Shopify/tapioca/wiki/Manual-Gem-Requires")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
DEFAULT_RBI_FORMATTER = T.let(RBIFormatter.new(
|
|
30
|
+
add_sig_templates: false,
|
|
31
|
+
group_nodes: true,
|
|
32
|
+
max_line_length: nil,
|
|
33
|
+
nest_singleton_methods: true,
|
|
34
|
+
nest_non_public_methods: true,
|
|
35
|
+
sort_nodes: true
|
|
36
|
+
), RBIFormatter)
|
|
37
|
+
end
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Tapioca
|
|
5
|
+
module Runtime
|
|
6
|
+
class DynamicMixinCompiler
|
|
7
|
+
extend T::Sig
|
|
8
|
+
include Runtime::Reflection
|
|
9
|
+
|
|
10
|
+
sig { returns(T::Array[Module]) }
|
|
11
|
+
attr_reader :dynamic_extends, :dynamic_includes
|
|
12
|
+
|
|
13
|
+
sig { returns(T::Array[Symbol]) }
|
|
14
|
+
attr_reader :class_attribute_readers, :class_attribute_writers, :class_attribute_predicates
|
|
15
|
+
|
|
16
|
+
sig { returns(T::Array[Symbol]) }
|
|
17
|
+
attr_reader :instance_attribute_readers, :instance_attribute_writers, :instance_attribute_predicates
|
|
18
|
+
|
|
19
|
+
sig { params(constant: Module).void }
|
|
20
|
+
def initialize(constant)
|
|
21
|
+
@constant = constant
|
|
22
|
+
mixins_from_modules = {}.compare_by_identity
|
|
23
|
+
class_attribute_readers = T.let([], T::Array[Symbol])
|
|
24
|
+
class_attribute_writers = T.let([], T::Array[Symbol])
|
|
25
|
+
class_attribute_predicates = T.let([], T::Array[Symbol])
|
|
26
|
+
|
|
27
|
+
instance_attribute_readers = T.let([], T::Array[Symbol])
|
|
28
|
+
instance_attribute_writers = T.let([], T::Array[Symbol])
|
|
29
|
+
instance_attribute_predicates = T.let([], T::Array[Symbol])
|
|
30
|
+
|
|
31
|
+
Class.new do
|
|
32
|
+
# Override the `self.include` method
|
|
33
|
+
define_singleton_method(:include) do |mod|
|
|
34
|
+
# Take a snapshot of the list of singleton class ancestors
|
|
35
|
+
# before the actual include
|
|
36
|
+
before = singleton_class.ancestors
|
|
37
|
+
# Call the actual `include` method with the supplied module
|
|
38
|
+
super(mod).tap do
|
|
39
|
+
# Take a snapshot of the list of singleton class ancestors
|
|
40
|
+
# after the actual include
|
|
41
|
+
after = singleton_class.ancestors
|
|
42
|
+
# The difference is the modules that are added to the list
|
|
43
|
+
# of ancestors of the singleton class. Those are all the
|
|
44
|
+
# modules that were `extend`ed due to the `include` call.
|
|
45
|
+
#
|
|
46
|
+
# We record those modules on our lookup table keyed by
|
|
47
|
+
# the included module with the values being all the modules
|
|
48
|
+
# that that module pulls into the singleton class.
|
|
49
|
+
#
|
|
50
|
+
# We need to reverse the order, since the extend order should
|
|
51
|
+
# be the inverse of the ancestor order. That is, earlier
|
|
52
|
+
# extended modules would be later in the ancestor chain.
|
|
53
|
+
mixins_from_modules[mod] = (after - before).reverse!
|
|
54
|
+
end
|
|
55
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
56
|
+
# this is a best effort, bail if we can't perform this
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
define_singleton_method(:class_attribute) do |*attrs, **kwargs|
|
|
60
|
+
class_attribute_readers.concat(attrs)
|
|
61
|
+
class_attribute_writers.concat(attrs)
|
|
62
|
+
|
|
63
|
+
instance_predicate = kwargs.fetch(:instance_predicate, true)
|
|
64
|
+
instance_accessor = kwargs.fetch(:instance_accessor, true)
|
|
65
|
+
instance_reader = kwargs.fetch(:instance_reader, instance_accessor)
|
|
66
|
+
instance_writer = kwargs.fetch(:instance_writer, instance_accessor)
|
|
67
|
+
|
|
68
|
+
if instance_reader
|
|
69
|
+
instance_attribute_readers.concat(attrs)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if instance_writer
|
|
73
|
+
instance_attribute_writers.concat(attrs)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if instance_predicate
|
|
77
|
+
class_attribute_predicates.concat(attrs)
|
|
78
|
+
|
|
79
|
+
if instance_reader
|
|
80
|
+
instance_attribute_predicates.concat(attrs)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
super(*attrs, **kwargs) if defined?(super)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# rubocop:disable Style/MissingRespondToMissing
|
|
88
|
+
T::Sig::WithoutRuntime.sig { params(symbol: Symbol, args: T.untyped).returns(T.untyped) }
|
|
89
|
+
def method_missing(symbol, *args)
|
|
90
|
+
# We need this here so that we can handle any random instance
|
|
91
|
+
# method calls on the fake including class that may be done by
|
|
92
|
+
# the included module during the `self.included` hook.
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class << self
|
|
96
|
+
extend T::Sig
|
|
97
|
+
|
|
98
|
+
T::Sig::WithoutRuntime.sig { params(symbol: Symbol, args: T.untyped).returns(T.untyped) }
|
|
99
|
+
def method_missing(symbol, *args)
|
|
100
|
+
# Similarly, we need this here so that we can handle any
|
|
101
|
+
# random class method calls on the fake including class
|
|
102
|
+
# that may be done by the included module during the
|
|
103
|
+
# `self.included` hook.
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
# rubocop:enable Style/MissingRespondToMissing
|
|
107
|
+
end.include(constant)
|
|
108
|
+
|
|
109
|
+
# The value that corresponds to the original included constant
|
|
110
|
+
# is the list of all dynamically extended modules because of that
|
|
111
|
+
# constant. We grab that value by deleting the key for the original
|
|
112
|
+
# constant.
|
|
113
|
+
@dynamic_extends = T.let(mixins_from_modules.delete(constant) || [], T::Array[Module])
|
|
114
|
+
|
|
115
|
+
# Since we deleted the original constant from the list of keys, all
|
|
116
|
+
# the keys that remain are the ones that are dynamically included modules
|
|
117
|
+
# during the include of the original constant.
|
|
118
|
+
@dynamic_includes = T.let(mixins_from_modules.keys, T::Array[Module])
|
|
119
|
+
|
|
120
|
+
@class_attribute_readers = T.let(class_attribute_readers, T::Array[Symbol])
|
|
121
|
+
@class_attribute_writers = T.let(class_attribute_writers, T::Array[Symbol])
|
|
122
|
+
@class_attribute_predicates = T.let(class_attribute_predicates, T::Array[Symbol])
|
|
123
|
+
|
|
124
|
+
@instance_attribute_readers = T.let(instance_attribute_readers, T::Array[Symbol])
|
|
125
|
+
@instance_attribute_writers = T.let(instance_attribute_writers, T::Array[Symbol])
|
|
126
|
+
@instance_attribute_predicates = T.let(instance_attribute_predicates, T::Array[Symbol])
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
sig { returns(T::Boolean) }
|
|
130
|
+
def empty_attributes?
|
|
131
|
+
@class_attribute_readers.empty? && @class_attribute_writers.empty?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
sig { params(tree: RBI::Tree).void }
|
|
135
|
+
def compile_class_attributes(tree)
|
|
136
|
+
return if empty_attributes?
|
|
137
|
+
|
|
138
|
+
# Create a synthetic module to hold the generated class methods
|
|
139
|
+
tree << RBI::Module.new("GeneratedClassMethods") do |mod|
|
|
140
|
+
class_attribute_readers.each do |attribute|
|
|
141
|
+
mod << RBI::Method.new(attribute.to_s)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
class_attribute_writers.each do |attribute|
|
|
145
|
+
mod << RBI::Method.new("#{attribute}=") do |method|
|
|
146
|
+
method << RBI::Param.new("value")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
class_attribute_predicates.each do |attribute|
|
|
151
|
+
mod << RBI::Method.new("#{attribute}?")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Create a synthetic module to hold the generated instance methods
|
|
156
|
+
tree << RBI::Module.new("GeneratedInstanceMethods") do |mod|
|
|
157
|
+
instance_attribute_readers.each do |attribute|
|
|
158
|
+
mod << RBI::Method.new(attribute.to_s)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
instance_attribute_writers.each do |attribute|
|
|
162
|
+
mod << RBI::Method.new("#{attribute}=") do |method|
|
|
163
|
+
method << RBI::Param.new("value")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
instance_attribute_predicates.each do |attribute|
|
|
168
|
+
mod << RBI::Method.new("#{attribute}?")
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Add a mixes_in_class_methods and include for the generated modules
|
|
173
|
+
tree << RBI::MixesInClassMethods.new("GeneratedClassMethods")
|
|
174
|
+
tree << RBI::Include.new("GeneratedInstanceMethods")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
sig { params(tree: RBI::Tree).returns([T::Array[Module], T::Array[Module]]) }
|
|
178
|
+
def compile_mixes_in_class_methods(tree)
|
|
179
|
+
includes = dynamic_includes.map do |mod|
|
|
180
|
+
qname = qualified_name_of(mod)
|
|
181
|
+
|
|
182
|
+
next if qname.nil? || qname.empty?
|
|
183
|
+
next if filtered_mixin?(qname)
|
|
184
|
+
|
|
185
|
+
tree << RBI::Include.new(qname)
|
|
186
|
+
|
|
187
|
+
mod
|
|
188
|
+
end.compact
|
|
189
|
+
|
|
190
|
+
# If we can generate multiple mixes_in_class_methods, then we want to use all dynamic extends that are not the
|
|
191
|
+
# constant itself
|
|
192
|
+
mixed_in_class_methods = dynamic_extends.select do |mod|
|
|
193
|
+
mod != @constant && !module_included_by_another_dynamic_extend?(mod, dynamic_extends)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
return [[], []] if mixed_in_class_methods.empty?
|
|
197
|
+
|
|
198
|
+
mixed_in_class_methods.each do |mod|
|
|
199
|
+
qualified_name = qualified_name_of(mod)
|
|
200
|
+
|
|
201
|
+
next if qualified_name.nil? || qualified_name.empty?
|
|
202
|
+
next if filtered_mixin?(qualified_name)
|
|
203
|
+
|
|
204
|
+
tree << RBI::MixesInClassMethods.new(qualified_name)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
[mixed_in_class_methods, includes]
|
|
208
|
+
rescue
|
|
209
|
+
[[], []] # silence errors
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
sig { params(mod: Module, dynamic_extends: T::Array[Module]).returns(T::Boolean) }
|
|
213
|
+
def module_included_by_another_dynamic_extend?(mod, dynamic_extends)
|
|
214
|
+
dynamic_extends.any? do |dynamic_extend|
|
|
215
|
+
mod != dynamic_extend && ancestors_of(dynamic_extend).include?(mod)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
sig { params(qualified_mixin_name: String).returns(T::Boolean) }
|
|
220
|
+
def filtered_mixin?(qualified_mixin_name)
|
|
221
|
+
# filter T:: namespace mixins that aren't T::Props
|
|
222
|
+
# T::Props and subconstants have semantic value
|
|
223
|
+
qualified_mixin_name.start_with?("::T::") && !qualified_mixin_name.start_with?("::T::Props")
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Tapioca
|
|
5
|
+
module Runtime
|
|
6
|
+
# This class is responsible for storing and looking up information related to generic types.
|
|
7
|
+
#
|
|
8
|
+
# The class stores 2 different kinds of data, in two separate lookup tables:
|
|
9
|
+
# 1. a lookup of generic type instances by name: `@generic_instances`
|
|
10
|
+
# 2. a lookup of type variable serializer by constant and type variable
|
|
11
|
+
# instance: `@type_variables`
|
|
12
|
+
#
|
|
13
|
+
# By storing the above data, we can cheaply query each constant against this registry
|
|
14
|
+
# to see if it declares any generic type variables. This becomes a simple lookup in the
|
|
15
|
+
# `@type_variables` hash table with the given constant.
|
|
16
|
+
#
|
|
17
|
+
# If there is no entry, then we can cheaply know that we can skip generic type
|
|
18
|
+
# information generation for this type.
|
|
19
|
+
#
|
|
20
|
+
# On the other hand, if we get a result, then the result will be a hash of type
|
|
21
|
+
# variable to type variable serializers. This allows us to associate type variables
|
|
22
|
+
# to the constant names that represent them, easily.
|
|
23
|
+
module GenericTypeRegistry
|
|
24
|
+
@generic_instances = T.let(
|
|
25
|
+
{},
|
|
26
|
+
T::Hash[String, Module]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@type_variables = T.let(
|
|
30
|
+
{}.compare_by_identity,
|
|
31
|
+
T::Hash[Module, T::Array[TypeVariableModule]]
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
extend T::Sig
|
|
36
|
+
|
|
37
|
+
# This method is responsible for building the name of the instantiated concrete type
|
|
38
|
+
# and cloning the given constant so that we can return a type that is the same
|
|
39
|
+
# as the current type but is a different instance and has a different name method.
|
|
40
|
+
#
|
|
41
|
+
# We cache those cloned instances by their name in `@generic_instances`, so that
|
|
42
|
+
# we don't keep instantiating a new type every single time it is referenced.
|
|
43
|
+
# For example, `[Foo[Integer], Foo[Integer], Foo[Integer], Foo[String]]` will only
|
|
44
|
+
# result in 2 clones (1 for `Foo[Integer]` and another for `Foo[String]`) and
|
|
45
|
+
# 2 hash lookups (for the other two `Foo[Integer]`s).
|
|
46
|
+
#
|
|
47
|
+
# This method returns the created or cached clone of the constant.
|
|
48
|
+
sig { params(constant: T.untyped, types: T.untyped).returns(Module) }
|
|
49
|
+
def register_type(constant, types)
|
|
50
|
+
# Build the name of the instantiated generic type,
|
|
51
|
+
# something like `"Foo[X, Y, Z]"`
|
|
52
|
+
type_list = types.map { |type| T::Utils.coerce(type).name }.join(", ")
|
|
53
|
+
name = "#{Reflection.name_of(constant)}[#{type_list}]"
|
|
54
|
+
|
|
55
|
+
# Create a generic type with an overridden `name`
|
|
56
|
+
# method that returns the name we constructed above.
|
|
57
|
+
#
|
|
58
|
+
# Also, we try to memoize the generic type based on the name, so that
|
|
59
|
+
# we don't have to keep recreating them all the time.
|
|
60
|
+
@generic_instances[name] ||= create_generic_type(constant, name)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { params(instance: Object).returns(T::Boolean) }
|
|
64
|
+
def generic_type_instance?(instance)
|
|
65
|
+
@generic_instances.values.any? { |generic_type| generic_type === instance }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sig { params(constant: Module).returns(T.nilable(T::Array[TypeVariableModule])) }
|
|
69
|
+
def lookup_type_variables(constant)
|
|
70
|
+
@type_variables[constant]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# This method is called from intercepted calls to `type_member` and `type_template`.
|
|
74
|
+
# We get passed all the arguments to those methods, as well as the `T::Types::TypeVariable`
|
|
75
|
+
# instance generated by the Sorbet defined `type_member`/`type_template` call on `T::Generic`.
|
|
76
|
+
#
|
|
77
|
+
# This method creates a `String` with that data and stores it in the
|
|
78
|
+
# `@type_variables` lookup table, keyed by the `constant` and `type_variable`.
|
|
79
|
+
#
|
|
80
|
+
# Finally, the original `type_variable` is returned from this method, so that the caller
|
|
81
|
+
# can return it from the original methods as well.
|
|
82
|
+
sig do
|
|
83
|
+
params(
|
|
84
|
+
constant: T.untyped,
|
|
85
|
+
type_variable: TypeVariableModule,
|
|
86
|
+
).void
|
|
87
|
+
end
|
|
88
|
+
def register_type_variable(constant, type_variable)
|
|
89
|
+
type_variables = lookup_or_initialize_type_variables(constant)
|
|
90
|
+
|
|
91
|
+
type_variables << type_variable
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
sig { params(constant: Module, name: String).returns(Module) }
|
|
97
|
+
def create_generic_type(constant, name)
|
|
98
|
+
generic_type = case constant
|
|
99
|
+
when Class
|
|
100
|
+
# For classes, we want to create a subclass, so that an instance of
|
|
101
|
+
# the generic class `Foo[Bar]` is still a `Foo`. That is:
|
|
102
|
+
# `Foo[Bar].new.is_a?(Foo)` should be true, which isn't the case
|
|
103
|
+
# if we just clone the class. But subclassing works just fine.
|
|
104
|
+
create_safe_subclass(constant)
|
|
105
|
+
else
|
|
106
|
+
# This can only be a module and it is fine to just clone modules
|
|
107
|
+
# since they can't have instances and will not have `is_a?` relationships.
|
|
108
|
+
# Moreover, we never `include`/`extend` any generic modules into the
|
|
109
|
+
# ancestor tree, so this doesn't become a problem with checking the
|
|
110
|
+
# instance of a class being `is_a?` of a module type.
|
|
111
|
+
constant.clone
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Let's set the `name` and `to_s` methods to return the proper generic name
|
|
115
|
+
name_proc = -> { name }
|
|
116
|
+
generic_type.define_singleton_method(:name, name_proc)
|
|
117
|
+
generic_type.define_singleton_method(:to_s, name_proc)
|
|
118
|
+
|
|
119
|
+
# We need to define a `<=` method on the cloned constant, so that Sorbet
|
|
120
|
+
# can do covariance/contravariance checks on the type variables.
|
|
121
|
+
#
|
|
122
|
+
# Normally, we would be doing proper covariance/contravariance checks here, but
|
|
123
|
+
# that is not necessary, since we are not implementing a runtime type checker
|
|
124
|
+
# here. It is just enough for the checks to pass, so that we can serialize the
|
|
125
|
+
# signatures, assuming the sigs were well-formed.
|
|
126
|
+
#
|
|
127
|
+
# So we act like all subtype checks pass.
|
|
128
|
+
generic_type.define_singleton_method(:<=) { |_| true }
|
|
129
|
+
|
|
130
|
+
# Return the generic type we created
|
|
131
|
+
generic_type
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
sig { params(constant: Class).returns(Class) }
|
|
135
|
+
def create_safe_subclass(constant)
|
|
136
|
+
# Lookup the "inherited" class method
|
|
137
|
+
inherited_method = constant.method(:inherited)
|
|
138
|
+
# and the module that defines it
|
|
139
|
+
owner = inherited_method.owner
|
|
140
|
+
|
|
141
|
+
# If no one has overriden the inherited method yet, just subclass
|
|
142
|
+
return Class.new(constant) if Class == owner
|
|
143
|
+
|
|
144
|
+
begin
|
|
145
|
+
# Otherwise, some inherited method could be preventing us
|
|
146
|
+
# from creating subclasses, so let's override it and rescue
|
|
147
|
+
owner.send(:define_method, :inherited) do |s|
|
|
148
|
+
inherited_method.call(s)
|
|
149
|
+
rescue
|
|
150
|
+
# Ignoring errors
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# return a subclass
|
|
154
|
+
Class.new(constant)
|
|
155
|
+
ensure
|
|
156
|
+
# Reinstate the original inherited method back.
|
|
157
|
+
owner.send(:define_method, :inherited, inherited_method)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
sig { params(constant: Module).returns(T::Array[TypeVariableModule]) }
|
|
162
|
+
def lookup_or_initialize_type_variables(constant)
|
|
163
|
+
@type_variables[constant] ||= []
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|