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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +13 -2
  3. data/README.md +79 -25
  4. data/Rakefile +10 -14
  5. data/lib/tapioca/cli.rb +66 -80
  6. data/lib/tapioca/{generators/base.rb → commands/command.rb} +17 -10
  7. data/lib/tapioca/{generators → commands}/dsl.rb +59 -45
  8. data/lib/tapioca/{generators → commands}/gem.rb +93 -30
  9. data/lib/tapioca/{generators → commands}/init.rb +9 -13
  10. data/lib/tapioca/{generators → commands}/require.rb +8 -10
  11. data/lib/tapioca/commands/todo.rb +84 -0
  12. data/lib/tapioca/commands.rb +13 -0
  13. data/lib/tapioca/dsl/compiler.rb +185 -0
  14. data/lib/tapioca/{compilers/dsl → dsl/compilers}/aasm.rb +12 -9
  15. data/lib/tapioca/{compilers/dsl → dsl/compilers}/action_controller_helpers.rb +13 -20
  16. data/lib/tapioca/{compilers/dsl → dsl/compilers}/action_mailer.rb +10 -8
  17. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_job.rb +11 -9
  18. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_model_attributes.rb +32 -24
  19. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_model_secure_password.rb +10 -12
  20. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_associations.rb +29 -35
  21. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_columns.rb +26 -24
  22. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_enum.rb +14 -12
  23. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_fixtures.rb +10 -8
  24. data/lib/tapioca/dsl/compilers/active_record_relations.rb +712 -0
  25. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_scope.rb +21 -20
  26. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_typed_store.rb +12 -17
  27. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_resource.rb +10 -8
  28. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_storage.rb +11 -11
  29. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_support_concern.rb +19 -14
  30. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_support_current_attributes.rb +16 -21
  31. data/lib/tapioca/{compilers/dsl → dsl/compilers}/config.rb +10 -8
  32. data/lib/tapioca/{compilers/dsl → dsl/compilers}/frozen_record.rb +13 -11
  33. data/lib/tapioca/{compilers/dsl → dsl/compilers}/identity_cache.rb +28 -25
  34. data/lib/tapioca/{compilers/dsl → dsl/compilers}/mixed_in_class_attributes.rb +12 -10
  35. data/lib/tapioca/{compilers/dsl → dsl/compilers}/protobuf.rb +10 -8
  36. data/lib/tapioca/{compilers/dsl → dsl/compilers}/rails_generators.rb +13 -14
  37. data/lib/tapioca/{compilers/dsl → dsl/compilers}/sidekiq_worker.rb +14 -13
  38. data/lib/tapioca/{compilers/dsl → dsl/compilers}/smart_properties.rb +12 -13
  39. data/lib/tapioca/{compilers/dsl → dsl/compilers}/state_machines.rb +12 -10
  40. data/lib/tapioca/{compilers/dsl → dsl/compilers}/url_helpers.rb +16 -14
  41. data/lib/tapioca/dsl/compilers.rb +31 -0
  42. data/lib/tapioca/{compilers/dsl → dsl}/extensions/frozen_record.rb +2 -2
  43. data/lib/tapioca/dsl/helpers/active_record_column_type_helper.rb +114 -0
  44. data/lib/tapioca/dsl/helpers/active_record_constants_helper.rb +29 -0
  45. data/lib/tapioca/{compilers/dsl → dsl/helpers}/param_helper.rb +2 -2
  46. data/lib/tapioca/{compilers/dsl_compiler.rb → dsl/pipeline.rb} +41 -33
  47. data/lib/tapioca/gem/events.rb +120 -0
  48. data/lib/tapioca/gem/listeners/base.rb +48 -0
  49. data/lib/tapioca/gem/listeners/dynamic_mixins.rb +32 -0
  50. data/lib/tapioca/gem/listeners/methods.rb +183 -0
  51. data/lib/tapioca/gem/listeners/mixins.rb +101 -0
  52. data/lib/tapioca/gem/listeners/remove_empty_payload_scopes.rb +21 -0
  53. data/lib/tapioca/gem/listeners/sorbet_enums.rb +26 -0
  54. data/lib/tapioca/gem/listeners/sorbet_helpers.rb +29 -0
  55. data/lib/tapioca/gem/listeners/sorbet_props.rb +33 -0
  56. data/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb +23 -0
  57. data/lib/tapioca/gem/listeners/sorbet_signatures.rb +79 -0
  58. data/lib/tapioca/gem/listeners/sorbet_type_variables.rb +51 -0
  59. data/lib/tapioca/gem/listeners/subconstants.rb +37 -0
  60. data/lib/tapioca/gem/listeners/yard_doc.rb +96 -0
  61. data/lib/tapioca/gem/listeners.rb +16 -0
  62. data/lib/tapioca/gem/pipeline.rb +365 -0
  63. data/lib/tapioca/gemfile.rb +44 -20
  64. data/lib/tapioca/helpers/cli_helper.rb +16 -8
  65. data/lib/tapioca/helpers/config_helper.rb +113 -0
  66. data/lib/tapioca/helpers/rbi_helper.rb +17 -0
  67. data/lib/tapioca/helpers/shims_helper.rb +87 -0
  68. data/lib/tapioca/helpers/sorbet_helper.rb +57 -0
  69. data/lib/tapioca/helpers/test/dsl_compiler.rb +118 -0
  70. data/lib/tapioca/helpers/test/isolation.rb +1 -1
  71. data/lib/tapioca/helpers/test/template.rb +13 -2
  72. data/lib/tapioca/internal.rb +17 -10
  73. data/lib/tapioca/rbi_ext/model.rb +2 -48
  74. data/lib/tapioca/rbi_formatter.rb +37 -0
  75. data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +227 -0
  76. data/lib/tapioca/runtime/generic_type_registry.rb +166 -0
  77. data/lib/tapioca/runtime/loader.rb +123 -0
  78. data/lib/tapioca/runtime/reflection.rb +153 -0
  79. data/lib/tapioca/runtime/trackers/autoload.rb +72 -0
  80. data/lib/tapioca/runtime/trackers/constant_definition.rb +44 -0
  81. data/lib/tapioca/runtime/trackers/mixin.rb +80 -0
  82. data/lib/tapioca/runtime/trackers/required_ancestor.rb +50 -0
  83. data/lib/tapioca/{trackers.rb → runtime/trackers.rb} +4 -3
  84. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +110 -54
  85. data/lib/tapioca/sorbet_ext/name_patch.rb +7 -1
  86. data/lib/tapioca/{compilers → static}/requires_compiler.rb +5 -12
  87. data/lib/tapioca/static/symbol_loader.rb +83 -0
  88. data/lib/tapioca/static/symbol_table_parser.rb +63 -0
  89. data/lib/tapioca/version.rb +1 -1
  90. data/lib/tapioca.rb +2 -7
  91. metadata +82 -62
  92. data/lib/tapioca/compilers/dsl/active_record_relations.rb +0 -711
  93. data/lib/tapioca/compilers/dsl/base.rb +0 -179
  94. data/lib/tapioca/compilers/dsl/helper/active_record_constants.rb +0 -27
  95. data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +0 -198
  96. data/lib/tapioca/compilers/sorbet.rb +0 -59
  97. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +0 -780
  98. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +0 -90
  99. data/lib/tapioca/compilers/symbol_table_compiler.rb +0 -17
  100. data/lib/tapioca/compilers/todos_compiler.rb +0 -32
  101. data/lib/tapioca/generators/todo.rb +0 -76
  102. data/lib/tapioca/generators.rb +0 -9
  103. data/lib/tapioca/generic_type_registry.rb +0 -149
  104. data/lib/tapioca/helpers/active_record_column_type_helper.rb +0 -98
  105. data/lib/tapioca/loader.rb +0 -119
  106. data/lib/tapioca/reflection.rb +0 -151
  107. data/lib/tapioca/trackers/autoload.rb +0 -70
  108. data/lib/tapioca/trackers/constant_definition.rb +0 -42
  109. 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"