tapioca 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +13 -2
- data/README.md +79 -25
- data/Rakefile +10 -14
- data/lib/tapioca/cli.rb +66 -80
- data/lib/tapioca/{generators/base.rb → commands/command.rb} +17 -10
- 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 +84 -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 +32 -24
- 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 +29 -35
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_columns.rb +26 -24
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_enum.rb +14 -12
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_fixtures.rb +10 -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 +12 -17
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_resource.rb +10 -8
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_storage.rb +11 -11
- 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 +10 -8
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/frozen_record.rb +13 -11
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/identity_cache.rb +28 -25
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/mixed_in_class_attributes.rb +12 -10
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/protobuf.rb +10 -8
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/rails_generators.rb +13 -14
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/sidekiq_worker.rb +14 -13
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/smart_properties.rb +12 -13
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/state_machines.rb +12 -10
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/url_helpers.rb +16 -14
- 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 +2 -2
- data/lib/tapioca/{compilers/dsl_compiler.rb → dsl/pipeline.rb} +41 -33
- 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/gemfile.rb +44 -20
- data/lib/tapioca/helpers/cli_helper.rb +16 -8
- data/lib/tapioca/helpers/config_helper.rb +113 -0
- data/lib/tapioca/helpers/rbi_helper.rb +17 -0
- data/lib/tapioca/helpers/shims_helper.rb +87 -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/internal.rb +17 -10
- data/lib/tapioca/rbi_ext/model.rb +2 -48
- 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 +166 -0
- data/lib/tapioca/runtime/loader.rb +123 -0
- data/lib/tapioca/runtime/reflection.rb +153 -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 +110 -54
- data/lib/tapioca/sorbet_ext/name_patch.rb +7 -1
- data/lib/tapioca/{compilers → static}/requires_compiler.rb +5 -12
- 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 +82 -62
- data/lib/tapioca/compilers/dsl/active_record_relations.rb +0 -711
- data/lib/tapioca/compilers/dsl/base.rb +0 -179
- data/lib/tapioca/compilers/dsl/helper/active_record_constants.rb +0 -27
- data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +0 -198
- 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 -149
- data/lib/tapioca/helpers/active_record_column_type_helper.rb +0 -98
- 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
|
@@ -0,0 +1,166 @@
|
|
|
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` method to return the proper generic name
|
|
115
|
+
generic_type.define_singleton_method(:name) { name }
|
|
116
|
+
|
|
117
|
+
# We need to define a `<=` method on the cloned constant, so that Sorbet
|
|
118
|
+
# can do covariance/contravariance checks on the type variables.
|
|
119
|
+
#
|
|
120
|
+
# Normally, we would be doing proper covariance/contravariance checks here, but
|
|
121
|
+
# that is not necessary, since we are not implementing a runtime type checker
|
|
122
|
+
# here. It is just enough for the checks to pass, so that we can serialize the
|
|
123
|
+
# signatures, assuming the sigs were well-formed.
|
|
124
|
+
#
|
|
125
|
+
# So we act like all subtype checks pass.
|
|
126
|
+
generic_type.define_singleton_method(:<=) { |_| true }
|
|
127
|
+
|
|
128
|
+
# Return the generic type we created
|
|
129
|
+
generic_type
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
sig { params(constant: Class).returns(Class) }
|
|
133
|
+
def create_safe_subclass(constant)
|
|
134
|
+
# Lookup the "inherited" class method
|
|
135
|
+
inherited_method = constant.method(:inherited)
|
|
136
|
+
# and the module that defines it
|
|
137
|
+
owner = inherited_method.owner
|
|
138
|
+
|
|
139
|
+
# If no one has overriden the inherited method yet, just subclass
|
|
140
|
+
return Class.new(constant) if Class == owner
|
|
141
|
+
|
|
142
|
+
begin
|
|
143
|
+
# Otherwise, some inherited method could be preventing us
|
|
144
|
+
# from creating subclasses, so let's override it and rescue
|
|
145
|
+
owner.send(:define_method, :inherited) do |s|
|
|
146
|
+
inherited_method.call(s)
|
|
147
|
+
rescue
|
|
148
|
+
# Ignoring errors
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# return a subclass
|
|
152
|
+
Class.new(constant)
|
|
153
|
+
ensure
|
|
154
|
+
# Reinstate the original inherited method back.
|
|
155
|
+
owner.send(:define_method, :inherited, inherited_method)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
sig { params(constant: Module).returns(T::Array[TypeVariableModule]) }
|
|
160
|
+
def lookup_or_initialize_type_variables(constant)
|
|
161
|
+
@type_variables[constant] ||= []
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Tapioca
|
|
5
|
+
module Runtime
|
|
6
|
+
class Loader
|
|
7
|
+
extend(T::Sig)
|
|
8
|
+
|
|
9
|
+
sig do
|
|
10
|
+
params(gemfile: Tapioca::Gemfile, initialize_file: T.nilable(String), require_file: T.nilable(String)).void
|
|
11
|
+
end
|
|
12
|
+
def load_bundle(gemfile, initialize_file, require_file)
|
|
13
|
+
require_helper(initialize_file)
|
|
14
|
+
|
|
15
|
+
load_rails_application
|
|
16
|
+
|
|
17
|
+
gemfile.require_bundle
|
|
18
|
+
|
|
19
|
+
require_helper(require_file)
|
|
20
|
+
|
|
21
|
+
load_rails_engines
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { params(environment_load: T::Boolean, eager_load: T::Boolean).void }
|
|
25
|
+
def load_rails_application(environment_load: false, eager_load: false)
|
|
26
|
+
return unless File.exist?("config/application.rb")
|
|
27
|
+
|
|
28
|
+
silence_deprecations
|
|
29
|
+
|
|
30
|
+
if environment_load
|
|
31
|
+
safe_require("./config/environment")
|
|
32
|
+
else
|
|
33
|
+
safe_require("./config/application")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
eager_load_rails_app if eager_load
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
sig { params(file: T.nilable(String)).void }
|
|
42
|
+
def require_helper(file)
|
|
43
|
+
return unless file
|
|
44
|
+
file = File.absolute_path(file)
|
|
45
|
+
return unless File.exist?(file)
|
|
46
|
+
|
|
47
|
+
require(file)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { returns(T::Array[T.untyped]) }
|
|
51
|
+
def rails_engines
|
|
52
|
+
return [] unless Object.const_defined?("Rails::Engine")
|
|
53
|
+
|
|
54
|
+
# We can use `Class#descendants` here, since we know Rails is loaded
|
|
55
|
+
Object.const_get("Rails::Engine").descendants.reject(&:abstract_railtie?)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
sig { params(path: String).void }
|
|
59
|
+
def safe_require(path)
|
|
60
|
+
require path
|
|
61
|
+
rescue LoadError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
sig { void }
|
|
66
|
+
def silence_deprecations
|
|
67
|
+
# Stop any ActiveSupport Deprecations from being reported
|
|
68
|
+
Object.const_get("ActiveSupport::Deprecation").silenced = true
|
|
69
|
+
rescue NameError
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig { void }
|
|
74
|
+
def eager_load_rails_app
|
|
75
|
+
rails = Object.const_get("Rails")
|
|
76
|
+
application = rails.application
|
|
77
|
+
|
|
78
|
+
if Object.const_defined?("ActiveSupport")
|
|
79
|
+
Object.const_get("ActiveSupport").run_load_hooks(
|
|
80
|
+
:before_eager_load,
|
|
81
|
+
application
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if Object.const_defined?("Zeitwerk::Loader")
|
|
86
|
+
zeitwerk_loader = Object.const_get("Zeitwerk::Loader")
|
|
87
|
+
zeitwerk_loader.eager_load_all
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if rails.respond_to?(:autoloaders) && rails.autoloaders.zeitwerk_enabled?
|
|
91
|
+
rails.autoloaders.each(&:eager_load)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
if application.config.respond_to?(:eager_load_namespaces)
|
|
95
|
+
application.config.eager_load_namespaces.each(&:eager_load!)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
sig { void }
|
|
100
|
+
def load_rails_engines
|
|
101
|
+
rails_engines.each do |engine|
|
|
102
|
+
errored_files = []
|
|
103
|
+
|
|
104
|
+
engine.config.eager_load_paths.each do |load_path|
|
|
105
|
+
Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
|
|
106
|
+
require(file)
|
|
107
|
+
rescue LoadError, StandardError
|
|
108
|
+
errored_files << file
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Try files that have errored one more time
|
|
113
|
+
# It might have been a load order problem
|
|
114
|
+
errored_files.each do |file|
|
|
115
|
+
require(file)
|
|
116
|
+
rescue LoadError, StandardError
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Tapioca
|
|
5
|
+
module Runtime
|
|
6
|
+
module Reflection
|
|
7
|
+
extend T::Sig
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
CLASS_METHOD = T.let(Kernel.instance_method(:class), UnboundMethod)
|
|
11
|
+
CONSTANTS_METHOD = T.let(Module.instance_method(:constants), UnboundMethod)
|
|
12
|
+
NAME_METHOD = T.let(Module.instance_method(:name), UnboundMethod)
|
|
13
|
+
SINGLETON_CLASS_METHOD = T.let(Object.instance_method(:singleton_class), UnboundMethod)
|
|
14
|
+
ANCESTORS_METHOD = T.let(Module.instance_method(:ancestors), UnboundMethod)
|
|
15
|
+
SUPERCLASS_METHOD = T.let(Class.instance_method(:superclass), UnboundMethod)
|
|
16
|
+
OBJECT_ID_METHOD = T.let(BasicObject.instance_method(:__id__), UnboundMethod)
|
|
17
|
+
EQUAL_METHOD = T.let(BasicObject.instance_method(:equal?), UnboundMethod)
|
|
18
|
+
PUBLIC_INSTANCE_METHODS_METHOD = T.let(Module.instance_method(:public_instance_methods), UnboundMethod)
|
|
19
|
+
PROTECTED_INSTANCE_METHODS_METHOD = T.let(Module.instance_method(:protected_instance_methods), UnboundMethod)
|
|
20
|
+
PRIVATE_INSTANCE_METHODS_METHOD = T.let(Module.instance_method(:private_instance_methods), UnboundMethod)
|
|
21
|
+
METHOD_METHOD = T.let(Kernel.instance_method(:method), UnboundMethod)
|
|
22
|
+
|
|
23
|
+
sig do
|
|
24
|
+
params(
|
|
25
|
+
symbol: String,
|
|
26
|
+
inherit: T::Boolean,
|
|
27
|
+
namespace: Module
|
|
28
|
+
).returns(BasicObject).checked(:never)
|
|
29
|
+
end
|
|
30
|
+
def constantize(symbol, inherit: false, namespace: Object)
|
|
31
|
+
namespace.const_get(symbol, inherit)
|
|
32
|
+
rescue NameError, LoadError, RuntimeError, ArgumentError, TypeError
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { params(object: BasicObject).returns(Class).checked(:never) }
|
|
37
|
+
def class_of(object)
|
|
38
|
+
CLASS_METHOD.bind(object).call
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { params(constant: Module).returns(T::Array[Symbol]) }
|
|
42
|
+
def constants_of(constant)
|
|
43
|
+
CONSTANTS_METHOD.bind(constant).call(false)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { params(constant: Module).returns(T.nilable(String)) }
|
|
47
|
+
def name_of(constant)
|
|
48
|
+
name = NAME_METHOD.bind(constant).call
|
|
49
|
+
name&.start_with?("#<") ? nil : name
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { params(constant: Module).returns(Class) }
|
|
53
|
+
def singleton_class_of(constant)
|
|
54
|
+
SINGLETON_CLASS_METHOD.bind(constant).call
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sig { params(constant: Module).returns(T::Array[Module]) }
|
|
58
|
+
def ancestors_of(constant)
|
|
59
|
+
ANCESTORS_METHOD.bind(constant).call
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { params(constant: Class).returns(T.nilable(Class)) }
|
|
63
|
+
def superclass_of(constant)
|
|
64
|
+
SUPERCLASS_METHOD.bind(constant).call
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { params(object: BasicObject).returns(Integer).checked(:never) }
|
|
68
|
+
def object_id_of(object)
|
|
69
|
+
OBJECT_ID_METHOD.bind(object).call
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
sig { params(object: BasicObject, other: BasicObject).returns(T::Boolean).checked(:never) }
|
|
73
|
+
def are_equal?(object, other)
|
|
74
|
+
EQUAL_METHOD.bind(object).call(other)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sig { params(constant: Module).returns(T::Array[Symbol]) }
|
|
78
|
+
def public_instance_methods_of(constant)
|
|
79
|
+
PUBLIC_INSTANCE_METHODS_METHOD.bind(constant).call
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
sig { params(constant: Module).returns(T::Array[Symbol]) }
|
|
83
|
+
def protected_instance_methods_of(constant)
|
|
84
|
+
PROTECTED_INSTANCE_METHODS_METHOD.bind(constant).call
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
sig { params(constant: Module).returns(T::Array[Symbol]) }
|
|
88
|
+
def private_instance_methods_of(constant)
|
|
89
|
+
PRIVATE_INSTANCE_METHODS_METHOD.bind(constant).call
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
sig { params(constant: Module).returns(T::Array[Module]) }
|
|
93
|
+
def inherited_ancestors_of(constant)
|
|
94
|
+
if Class === constant
|
|
95
|
+
ancestors_of(superclass_of(constant) || Object)
|
|
96
|
+
else
|
|
97
|
+
Module.ancestors
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
sig { params(constant: Module).returns(T.nilable(String)) }
|
|
102
|
+
def qualified_name_of(constant)
|
|
103
|
+
name = name_of(constant)
|
|
104
|
+
return if name.nil?
|
|
105
|
+
|
|
106
|
+
if name.start_with?("::")
|
|
107
|
+
name
|
|
108
|
+
else
|
|
109
|
+
"::#{name}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
sig { params(method: T.any(UnboundMethod, Method)).returns(T.untyped) }
|
|
114
|
+
def signature_of(method)
|
|
115
|
+
T::Utils.signature_for_method(method)
|
|
116
|
+
rescue LoadError, StandardError
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
sig { params(type: T::Types::Base).returns(String) }
|
|
121
|
+
def name_of_type(type)
|
|
122
|
+
type.to_s.gsub(/\bAttachedClass\b/, "T.attached_class")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
sig { params(constant: Module, method: Symbol).returns(Method) }
|
|
126
|
+
def method_of(constant, method)
|
|
127
|
+
METHOD_METHOD.bind(constant).call(method)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Returns an array with all classes that are < than the supplied class.
|
|
131
|
+
#
|
|
132
|
+
# class C; end
|
|
133
|
+
# descendants_of(C) # => []
|
|
134
|
+
#
|
|
135
|
+
# class B < C; end
|
|
136
|
+
# descendants_of(C) # => [B]
|
|
137
|
+
#
|
|
138
|
+
# class A < B; end
|
|
139
|
+
# descendants_of(C) # => [B, A]
|
|
140
|
+
#
|
|
141
|
+
# class D < C; end
|
|
142
|
+
# descendants_of(C) # => [B, A, D]
|
|
143
|
+
sig { type_parameters(:U).params(klass: T.type_parameter(:U)).returns(T::Array[T.type_parameter(:U)]) }
|
|
144
|
+
def descendants_of(klass)
|
|
145
|
+
result = ObjectSpace.each_object(klass.singleton_class).reject do |k|
|
|
146
|
+
T.cast(k, Module).singleton_class? || T.unsafe(k) == klass
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
T.unsafe(result)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Tapioca
|
|
5
|
+
module Runtime
|
|
6
|
+
module Trackers
|
|
7
|
+
module Autoload
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
NOOP_METHOD = ->(*_args, **_kwargs, &_block) {}
|
|
11
|
+
|
|
12
|
+
@constant_names_registered_for_autoload = T.let([], T::Array[String])
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
sig { void }
|
|
18
|
+
def eager_load_all!
|
|
19
|
+
with_disabled_exits do
|
|
20
|
+
until @constant_names_registered_for_autoload.empty?
|
|
21
|
+
# Grab the next constant name
|
|
22
|
+
constant_name = T.must(@constant_names_registered_for_autoload.shift)
|
|
23
|
+
# Trigger autoload by constantizing the registered name
|
|
24
|
+
Reflection.constantize(constant_name, inherit: true)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { params(constant_name: String).void }
|
|
30
|
+
def register(constant_name)
|
|
31
|
+
@constant_names_registered_for_autoload << constant_name
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sig do
|
|
35
|
+
type_parameters(:Result)
|
|
36
|
+
.params(block: T.proc.returns(T.type_parameter(:Result)))
|
|
37
|
+
.returns(T.type_parameter(:Result))
|
|
38
|
+
end
|
|
39
|
+
def with_disabled_exits(&block)
|
|
40
|
+
original_abort = Kernel.instance_method(:abort)
|
|
41
|
+
original_exit = Kernel.instance_method(:exit)
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
Kernel.define_method(:abort, NOOP_METHOD)
|
|
45
|
+
Kernel.define_method(:exit, NOOP_METHOD)
|
|
46
|
+
|
|
47
|
+
block.call
|
|
48
|
+
ensure
|
|
49
|
+
Kernel.define_method(:exit, original_exit)
|
|
50
|
+
Kernel.define_method(:abort, original_abort)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# We need to do the alias-method-chain dance since Bootsnap does the same,
|
|
60
|
+
# and prepended modules and alias-method-chain don't play well together.
|
|
61
|
+
#
|
|
62
|
+
# So, why does Bootsnap do alias-method-chain and not prepend? Glad you asked!
|
|
63
|
+
# That's because RubyGems does alias-method-chain for Kernel#require and such,
|
|
64
|
+
# so, if Bootsnap were to do prepend, it might end up breaking RubyGems.
|
|
65
|
+
class Module
|
|
66
|
+
alias_method(:autoload_without_tapioca, :autoload)
|
|
67
|
+
|
|
68
|
+
def autoload(const_name, path)
|
|
69
|
+
Tapioca::Runtime::Trackers::Autoload.register("#{self}::#{const_name}")
|
|
70
|
+
autoload_without_tapioca(const_name, path)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
module Tapioca
|
|
7
|
+
module Runtime
|
|
8
|
+
module Trackers
|
|
9
|
+
# Registers a TracePoint immediately upon load to track points at which
|
|
10
|
+
# classes and modules are opened for definition. This is used to track
|
|
11
|
+
# correspondence between classes/modules and files, as this information isn't
|
|
12
|
+
# available in the ruby runtime without extra accounting.
|
|
13
|
+
module ConstantDefinition
|
|
14
|
+
extend Reflection
|
|
15
|
+
|
|
16
|
+
@class_files = {}
|
|
17
|
+
|
|
18
|
+
# Immediately activated upon load. Observes class/module definition.
|
|
19
|
+
TracePoint.trace(:class) do |tp|
|
|
20
|
+
unless tp.self.singleton_class?
|
|
21
|
+
key = name_of(tp.self)
|
|
22
|
+
file = tp.path
|
|
23
|
+
if file == "(eval)"
|
|
24
|
+
file = T.must(caller_locations)
|
|
25
|
+
.drop_while { |loc| loc.path == "(eval)" }
|
|
26
|
+
.first&.path
|
|
27
|
+
end
|
|
28
|
+
@class_files[key] ||= Set.new
|
|
29
|
+
@class_files[key] << file
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns the files in which this class or module was opened. Doesn't know
|
|
34
|
+
# about situations where the class was opened prior to +require+ing,
|
|
35
|
+
# or where metaprogramming was used via +eval+, etc.
|
|
36
|
+
def self.files_for(klass)
|
|
37
|
+
name = String === klass ? klass : name_of(klass)
|
|
38
|
+
files = @class_files[name]
|
|
39
|
+
files || Set.new
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Tapioca
|
|
5
|
+
module Runtime
|
|
6
|
+
module Trackers
|
|
7
|
+
module Mixin
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
@mixin_map = {}.compare_by_identity
|
|
11
|
+
|
|
12
|
+
class Type < T::Enum
|
|
13
|
+
enums do
|
|
14
|
+
Prepend = new
|
|
15
|
+
Include = new
|
|
16
|
+
Extend = new
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig do
|
|
21
|
+
params(
|
|
22
|
+
constant: Module,
|
|
23
|
+
mod: Module,
|
|
24
|
+
mixin_type: Type,
|
|
25
|
+
locations: T.nilable(T::Array[Thread::Backtrace::Location])
|
|
26
|
+
).void
|
|
27
|
+
end
|
|
28
|
+
def self.register(constant, mod, mixin_type, locations)
|
|
29
|
+
locations ||= []
|
|
30
|
+
locations.map!(&:absolute_path).uniq!
|
|
31
|
+
locs = mixin_locations_for(constant)
|
|
32
|
+
locs.fetch(mixin_type).store(mod, T.cast(locations, T::Array[String]))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { params(constant: Module).returns(T::Hash[Type, T::Hash[Module, T::Array[String]]]) }
|
|
36
|
+
def self.mixin_locations_for(constant)
|
|
37
|
+
@mixin_map[constant] ||= {
|
|
38
|
+
Type::Prepend => {}.compare_by_identity,
|
|
39
|
+
Type::Include => {}.compare_by_identity,
|
|
40
|
+
Type::Extend => {}.compare_by_identity,
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class Module
|
|
49
|
+
prepend(Module.new do
|
|
50
|
+
def prepend_features(constant)
|
|
51
|
+
Tapioca::Runtime::Trackers::Mixin.register(
|
|
52
|
+
constant,
|
|
53
|
+
self,
|
|
54
|
+
Tapioca::Runtime::Trackers::Mixin::Type::Prepend,
|
|
55
|
+
caller_locations
|
|
56
|
+
)
|
|
57
|
+
super
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def append_features(constant)
|
|
61
|
+
Tapioca::Runtime::Trackers::Mixin.register(
|
|
62
|
+
constant,
|
|
63
|
+
self,
|
|
64
|
+
Tapioca::Runtime::Trackers::Mixin::Type::Include,
|
|
65
|
+
caller_locations
|
|
66
|
+
)
|
|
67
|
+
super
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def extend_object(obj)
|
|
71
|
+
Tapioca::Runtime::Trackers::Mixin.register(
|
|
72
|
+
obj,
|
|
73
|
+
self,
|
|
74
|
+
Tapioca::Runtime::Trackers::Mixin::Type::Extend,
|
|
75
|
+
caller_locations
|
|
76
|
+
) if Module === obj
|
|
77
|
+
super
|
|
78
|
+
end
|
|
79
|
+
end)
|
|
80
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Tapioca
|
|
5
|
+
module Runtime
|
|
6
|
+
module Trackers
|
|
7
|
+
module RequiredAncestor
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
@required_ancestors_map = {}.compare_by_identity
|
|
11
|
+
|
|
12
|
+
sig { params(requiring: T::Helpers, block: T.proc.returns(Module)).void }
|
|
13
|
+
def self.register(requiring, block)
|
|
14
|
+
ancestors = @required_ancestors_map[requiring] ||= []
|
|
15
|
+
ancestors << block
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { params(mod: Module).returns(T::Array[T.proc.returns(Module)]) }
|
|
19
|
+
def self.required_ancestors_blocks_by(mod)
|
|
20
|
+
@required_ancestors_map[mod] || []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sig { params(mod: Module).returns(T::Array[T.nilable(Module)]) }
|
|
24
|
+
def self.required_ancestors_by(mod)
|
|
25
|
+
blocks = required_ancestors_blocks_by(mod)
|
|
26
|
+
blocks.map do |block|
|
|
27
|
+
block.call
|
|
28
|
+
rescue NameError
|
|
29
|
+
# The ancestor required doesn't exist, let's return nil and let the compiler decide what to do.
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
module T
|
|
39
|
+
module Helpers
|
|
40
|
+
prepend(Module.new do
|
|
41
|
+
def requires_ancestor(&block)
|
|
42
|
+
# We can't directly call the block since the ancestor might not be loaded yet.
|
|
43
|
+
# We save the block in the map and will resolve it later.
|
|
44
|
+
Tapioca::Runtime::Trackers::RequiredAncestor.register(self, block)
|
|
45
|
+
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
end)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
# catch and filter those mixins as coming from Tapioca, we need
|
|
10
10
|
# the mixin tracker to be in place, before any mixin operations
|
|
11
11
|
# are performed.
|
|
12
|
-
require "tapioca/trackers/mixin"
|
|
13
|
-
require "tapioca/trackers/constant_definition"
|
|
14
|
-
require "tapioca/trackers/autoload"
|
|
12
|
+
require "tapioca/runtime/trackers/mixin"
|
|
13
|
+
require "tapioca/runtime/trackers/constant_definition"
|
|
14
|
+
require "tapioca/runtime/trackers/autoload"
|
|
15
|
+
require "tapioca/runtime/trackers/required_ancestor"
|