tapioca 0.4.27 → 0.5.3

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +15 -15
  3. data/README.md +2 -2
  4. data/Rakefile +5 -7
  5. data/exe/tapioca +2 -2
  6. data/lib/tapioca/cli.rb +172 -2
  7. data/lib/tapioca/compilers/dsl/aasm.rb +122 -0
  8. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +52 -12
  9. data/lib/tapioca/compilers/dsl/action_mailer.rb +6 -9
  10. data/lib/tapioca/compilers/dsl/active_job.rb +8 -12
  11. data/lib/tapioca/compilers/dsl/active_model_attributes.rb +131 -0
  12. data/lib/tapioca/compilers/dsl/active_model_secure_password.rb +101 -0
  13. data/lib/tapioca/compilers/dsl/active_record_associations.rb +33 -54
  14. data/lib/tapioca/compilers/dsl/active_record_columns.rb +10 -105
  15. data/lib/tapioca/compilers/dsl/active_record_enum.rb +8 -10
  16. data/lib/tapioca/compilers/dsl/active_record_fixtures.rb +86 -0
  17. data/lib/tapioca/compilers/dsl/active_record_scope.rb +7 -10
  18. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +5 -8
  19. data/lib/tapioca/compilers/dsl/active_resource.rb +9 -37
  20. data/lib/tapioca/compilers/dsl/active_storage.rb +98 -0
  21. data/lib/tapioca/compilers/dsl/active_support_concern.rb +106 -0
  22. data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +13 -8
  23. data/lib/tapioca/compilers/dsl/base.rb +108 -82
  24. data/lib/tapioca/compilers/dsl/config.rb +111 -0
  25. data/lib/tapioca/compilers/dsl/frozen_record.rb +5 -7
  26. data/lib/tapioca/compilers/dsl/identity_cache.rb +66 -29
  27. data/lib/tapioca/compilers/dsl/mixed_in_class_attributes.rb +74 -0
  28. data/lib/tapioca/compilers/dsl/protobuf.rb +19 -69
  29. data/lib/tapioca/compilers/dsl/sidekiq_worker.rb +25 -12
  30. data/lib/tapioca/compilers/dsl/smart_properties.rb +21 -33
  31. data/lib/tapioca/compilers/dsl/state_machines.rb +56 -78
  32. data/lib/tapioca/compilers/dsl/url_helpers.rb +7 -10
  33. data/lib/tapioca/compilers/dsl_compiler.rb +25 -40
  34. data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +198 -0
  35. data/lib/tapioca/compilers/requires_compiler.rb +2 -2
  36. data/lib/tapioca/compilers/sorbet.rb +25 -5
  37. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +122 -206
  38. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +4 -4
  39. data/lib/tapioca/compilers/symbol_table_compiler.rb +5 -11
  40. data/lib/tapioca/compilers/todos_compiler.rb +1 -1
  41. data/lib/tapioca/config.rb +3 -0
  42. data/lib/tapioca/config_builder.rb +5 -2
  43. data/lib/tapioca/constant_locator.rb +6 -8
  44. data/lib/tapioca/gemfile.rb +14 -11
  45. data/lib/tapioca/generators/base.rb +61 -0
  46. data/lib/tapioca/generators/dsl.rb +362 -0
  47. data/lib/tapioca/generators/gem.rb +345 -0
  48. data/lib/tapioca/generators/init.rb +79 -0
  49. data/lib/tapioca/generators/require.rb +52 -0
  50. data/lib/tapioca/generators/todo.rb +76 -0
  51. data/lib/tapioca/generators.rb +9 -0
  52. data/lib/tapioca/generic_type_registry.rb +25 -98
  53. data/lib/tapioca/helpers/active_record_column_type_helper.rb +98 -0
  54. data/lib/tapioca/internal.rb +2 -10
  55. data/lib/tapioca/loader.rb +11 -31
  56. data/lib/tapioca/rbi_ext/model.rb +166 -0
  57. data/lib/tapioca/reflection.rb +138 -0
  58. data/lib/tapioca/sorbet_ext/fixed_hash_patch.rb +1 -1
  59. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +72 -4
  60. data/lib/tapioca/sorbet_ext/name_patch.rb +1 -1
  61. data/lib/tapioca/version.rb +1 -1
  62. data/lib/tapioca.rb +3 -0
  63. metadata +45 -23
  64. data/lib/tapioca/cli/main.rb +0 -146
  65. data/lib/tapioca/core_ext/class.rb +0 -28
  66. data/lib/tapioca/core_ext/string.rb +0 -18
  67. data/lib/tapioca/generator.rb +0 -633
  68. data/lib/tapioca/rbi/model.rb +0 -405
  69. data/lib/tapioca/rbi/printer.rb +0 -410
  70. data/lib/tapioca/rbi/rewriters/group_nodes.rb +0 -106
  71. data/lib/tapioca/rbi/rewriters/nest_non_public_methods.rb +0 -65
  72. data/lib/tapioca/rbi/rewriters/nest_singleton_methods.rb +0 -42
  73. data/lib/tapioca/rbi/rewriters/sort_nodes.rb +0 -86
  74. data/lib/tapioca/rbi/visitor.rb +0 -21
@@ -20,14 +20,15 @@ module Tapioca
20
20
  # variable to type variable serializers. This allows us to associate type variables
21
21
  # to the constant names that represent them, easily.
22
22
  module GenericTypeRegistry
23
+ TypeVariable = T.type_alias { T.any(TypeMember, TypeTemplate) }
23
24
  @generic_instances = T.let(
24
25
  {},
25
26
  T::Hash[String, Module]
26
27
  )
27
28
 
28
29
  @type_variables = T.let(
29
- {},
30
- T::Hash[Integer, T::Hash[Integer, String]]
30
+ {}.compare_by_identity,
31
+ T::Hash[Module, T::Hash[TypeVariable, String]]
31
32
  )
32
33
 
33
34
  class << self
@@ -49,7 +50,7 @@ module Tapioca
49
50
  # Build the name of the instantiated generic type,
50
51
  # something like `"Foo[X, Y, Z]"`
51
52
  type_list = types.map { |type| T::Utils.coerce(type).name }.join(", ")
52
- name = "#{name_of(constant)}[#{type_list}]"
53
+ name = "#{Reflection.name_of(constant)}[#{type_list}]"
53
54
 
54
55
  # Create a generic type with an overridden `name`
55
56
  # method that returns the name we constructed above.
@@ -59,35 +60,30 @@ module Tapioca
59
60
  @generic_instances[name] ||= create_generic_type(constant, name)
60
61
  end
61
62
 
62
- sig do
63
- params(
64
- constant: T.untyped,
65
- type_member: T::Types::TypeVariable,
66
- fixed: T.untyped,
67
- lower: T.untyped,
68
- upper: T.untyped
69
- ).void
70
- end
71
- def register_type_member(constant, type_member, fixed, lower, upper)
72
- register_type_variable(constant, :type_member, type_member, fixed, lower, upper)
63
+ sig { params(constant: Module).returns(T.nilable(T::Hash[TypeVariable, String])) }
64
+ def lookup_type_variables(constant)
65
+ @type_variables[constant]
73
66
  end
74
67
 
68
+ # This method is called from intercepted calls to `type_member` and `type_template`.
69
+ # We get passed all the arguments to those methods, as well as the `T::Types::TypeVariable`
70
+ # instance generated by the Sorbet defined `type_member`/`type_template` call on `T::Generic`.
71
+ #
72
+ # This method creates a `String` with that data and stores it in the
73
+ # `@type_variables` lookup table, keyed by the `constant` and `type_variable`.
74
+ #
75
+ # Finally, the original `type_variable` is returned from this method, so that the caller
76
+ # can return it from the original methods as well.
75
77
  sig do
76
78
  params(
77
79
  constant: T.untyped,
78
- type_template: T::Types::TypeVariable,
79
- fixed: T.untyped,
80
- lower: T.untyped,
81
- upper: T.untyped
80
+ type_variable: TypeVariable,
82
81
  ).void
83
82
  end
84
- def register_type_template(constant, type_template, fixed, lower, upper)
85
- register_type_variable(constant, :type_template, type_template, fixed, lower, upper)
86
- end
83
+ def register_type_variable(constant, type_variable)
84
+ type_variables = lookup_or_initialize_type_variables(constant)
87
85
 
88
- sig { params(constant: Module).returns(T.nilable(T::Hash[Integer, String])) }
89
- def lookup_type_variables(constant)
90
- @type_variables[object_id_of(constant)]
86
+ type_variables[type_variable] = type_variable.serialize
91
87
  end
92
88
 
93
89
  private
@@ -117,39 +113,6 @@ module Tapioca
117
113
  generic_type
118
114
  end
119
115
 
120
- # This method is called from intercepted calls to `type_member` and `type_template`.
121
- # We get passed all the arguments to those methods, as well as the `T::Types::TypeVariable`
122
- # instance generated by the Sorbet defined `type_member`/`type_template` call on `T::Generic`.
123
- #
124
- # This method creates a `String` with that data and stores it in the
125
- # `@type_variables` lookup table, keyed by the `constant` and `type_variable`.
126
- #
127
- # Finally, the original `type_variable` is returned from this method, so that the caller
128
- # can return it from the original methods as well.
129
- sig do
130
- params(
131
- constant: T.untyped,
132
- type_variable_type: T.enum([:type_member, :type_template]),
133
- type_variable: T::Types::TypeVariable,
134
- fixed: T.untyped,
135
- lower: T.untyped,
136
- upper: T.untyped
137
- ).void
138
- end
139
- # rubocop:disable Metrics/ParameterLists
140
- def register_type_variable(constant, type_variable_type, type_variable, fixed, lower, upper)
141
- # rubocop:enable Metrics/ParameterLists
142
- type_variables = lookup_or_initialize_type_variables(constant)
143
-
144
- type_variables[object_id_of(type_variable)] = serialize_type_variable(
145
- type_variable_type,
146
- type_variable.variance,
147
- fixed,
148
- lower,
149
- upper
150
- )
151
- end
152
-
153
116
  sig { params(constant: Class).returns(Class) }
154
117
  def create_safe_subclass(constant)
155
118
  # Lookup the "inherited" class method
@@ -164,11 +127,9 @@ module Tapioca
164
127
  # Otherwise, some inherited method could be preventing us
165
128
  # from creating subclasses, so let's override it and rescue
166
129
  owner.send(:define_method, :inherited) do |s|
167
- begin
168
- inherited_method.call(s)
169
- rescue
170
- # Ignoring errors
171
- end
130
+ inherited_method.call(s)
131
+ rescue
132
+ # Ignoring errors
172
133
  end
173
134
 
174
135
  # return a subclass
@@ -179,43 +140,9 @@ module Tapioca
179
140
  end
180
141
  end
181
142
 
182
- sig { params(constant: Module).returns(T::Hash[Integer, String]) }
143
+ sig { params(constant: Module).returns(T::Hash[TypeVariable, String]) }
183
144
  def lookup_or_initialize_type_variables(constant)
184
- @type_variables[object_id_of(constant)] ||= {}
185
- end
186
-
187
- sig do
188
- params(
189
- type_variable_type: Symbol,
190
- variance: Symbol,
191
- fixed: T.untyped,
192
- lower: T.untyped,
193
- upper: T.untyped
194
- ).returns(String)
195
- end
196
- def serialize_type_variable(type_variable_type, variance, fixed, lower, upper)
197
- parts = []
198
- parts << ":#{variance}" unless variance == :invariant
199
- parts << "fixed: #{fixed}" if fixed
200
- parts << "lower: #{lower}" unless lower == T.untyped
201
- parts << "upper: #{upper}" unless upper == BasicObject
202
-
203
- parameters = parts.join(", ")
204
-
205
- serialized = T.let(type_variable_type.to_s, String)
206
- serialized += "(#{parameters})" unless parameters.empty?
207
-
208
- serialized
209
- end
210
-
211
- sig { params(constant: Module).returns(T.nilable(String)) }
212
- def name_of(constant)
213
- Module.instance_method(:name).bind(constant).call
214
- end
215
-
216
- sig { params(object: BasicObject).returns(Integer) }
217
- def object_id_of(object)
218
- Object.instance_method(:object_id).bind(object).call
145
+ @type_variables[constant] ||= {}.compare_by_identity
219
146
  end
220
147
  end
221
148
  end
@@ -0,0 +1,98 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ class ActiveRecordColumnTypeHelper
5
+ extend T::Sig
6
+
7
+ sig { params(constant: T.class_of(ActiveRecord::Base)).void }
8
+ def initialize(constant)
9
+ @constant = constant
10
+ end
11
+
12
+ sig { params(column_name: String).returns([String, String]) }
13
+ def type_for(column_name)
14
+ return ["T.untyped", "T.untyped"] if do_not_generate_strong_types?(@constant)
15
+
16
+ column_type = @constant.attribute_types[column_name]
17
+
18
+ getter_type =
19
+ case column_type
20
+ when defined?(MoneyColumn) && MoneyColumn::ActiveRecordType
21
+ "::Money"
22
+ when ActiveRecord::Type::Integer
23
+ "::Integer"
24
+ when ActiveRecord::Type::String
25
+ "::String"
26
+ when ActiveRecord::Type::Date
27
+ "::Date"
28
+ when ActiveRecord::Type::Decimal
29
+ "::BigDecimal"
30
+ when ActiveRecord::Type::Float
31
+ "::Float"
32
+ when ActiveRecord::Type::Boolean
33
+ "T::Boolean"
34
+ when ActiveRecord::Type::DateTime, ActiveRecord::Type::Time
35
+ "::DateTime"
36
+ when ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter
37
+ "::ActiveSupport::TimeWithZone"
38
+ else
39
+ handle_unknown_type(column_type)
40
+ end
41
+
42
+ column = @constant.columns_hash[column_name]
43
+ setter_type = getter_type
44
+
45
+ if column&.null
46
+ return ["T.nilable(#{getter_type})", "T.nilable(#{setter_type})"]
47
+ end
48
+
49
+ if column_name == @constant.primary_key ||
50
+ column_name == "created_at" ||
51
+ column_name == "updated_at"
52
+ getter_type = "T.nilable(#{getter_type})"
53
+ end
54
+
55
+ [getter_type, setter_type]
56
+ end
57
+
58
+ private
59
+
60
+ sig { params(constant: Module).returns(T::Boolean) }
61
+ def do_not_generate_strong_types?(constant)
62
+ Object.const_defined?(:StrongTypeGeneration) &&
63
+ !(constant.singleton_class < Object.const_get(:StrongTypeGeneration))
64
+ end
65
+
66
+ sig { params(column_type: Object).returns(String) }
67
+ def handle_unknown_type(column_type)
68
+ return "T.untyped" unless ActiveModel::Type::Value === column_type
69
+
70
+ lookup_return_type_of_method(column_type, :deserialize) ||
71
+ lookup_return_type_of_method(column_type, :cast) ||
72
+ lookup_arg_type_of_method(column_type, :serialize) ||
73
+ "T.untyped"
74
+ end
75
+
76
+ sig { params(column_type: ActiveModel::Type::Value, method: Symbol).returns(T.nilable(String)) }
77
+ def lookup_return_type_of_method(column_type, method)
78
+ signature = T::Private::Methods.signature_for_method(column_type.method(method))
79
+ return unless signature
80
+
81
+ return_type = signature.return_type
82
+ return if return_type == T::Private::Types::Void || return_type == T::Private::Types::NotTyped
83
+
84
+ return_type.to_s
85
+ end
86
+
87
+ sig { params(column_type: ActiveModel::Type::Value, method: Symbol).returns(T.nilable(String)) }
88
+ def lookup_arg_type_of_method(column_type, method)
89
+ signature = T::Private::Methods.signature_for_method(column_type.method(method))
90
+ return unless signature
91
+
92
+ # Arg types is an array [name, type] entries, so we desctructure the type of
93
+ # first argument to get the first argument type
94
+ _, first_argument_type = signature.arg_types.first
95
+
96
+ first_argument_type.to_s
97
+ end
98
+ end
@@ -3,22 +3,14 @@
3
3
 
4
4
  require "tapioca"
5
5
  require "tapioca/loader"
6
- require "tapioca/generic_type_registry"
7
6
  require "tapioca/sorbet_ext/generic_name_patch"
8
7
  require "tapioca/sorbet_ext/fixed_hash_patch"
8
+ require "tapioca/generic_type_registry"
9
9
  require "tapioca/config"
10
10
  require "tapioca/config_builder"
11
- require "tapioca/generator"
11
+ require "tapioca/generators"
12
12
  require "tapioca/cli"
13
- require "tapioca/cli/main"
14
13
  require "tapioca/gemfile"
15
- require "tapioca/rbi/model"
16
- require "tapioca/rbi/visitor"
17
- require "tapioca/rbi/rewriters/nest_singleton_methods"
18
- require "tapioca/rbi/rewriters/nest_non_public_methods"
19
- require "tapioca/rbi/rewriters/group_nodes"
20
- require "tapioca/rbi/rewriters/sort_nodes"
21
- require "tapioca/rbi/printer"
22
14
  require "tapioca/compilers/sorbet"
23
15
  require "tapioca/compilers/requires_compiler"
24
16
  require "tapioca/compilers/symbol_table_compiler"
@@ -5,13 +5,8 @@ module Tapioca
5
5
  class Loader
6
6
  extend(T::Sig)
7
7
 
8
- sig { params(gemfile: Tapioca::Gemfile).void }
9
- def initialize(gemfile)
10
- @gemfile = T.let(gemfile, Tapioca::Gemfile)
11
- end
12
-
13
- sig { params(initialize_file: T.nilable(String), require_file: T.nilable(String)).void }
14
- def load_bundle(initialize_file, require_file)
8
+ sig { params(gemfile: Tapioca::Gemfile, initialize_file: T.nilable(String), require_file: T.nilable(String)).void }
9
+ def load_bundle(gemfile, initialize_file, require_file)
15
10
  require_helper(initialize_file)
16
11
 
17
12
  load_rails_application
@@ -40,9 +35,6 @@ module Tapioca
40
35
 
41
36
  private
42
37
 
43
- sig { returns(Tapioca::Gemfile) }
44
- attr_reader :gemfile
45
-
46
38
  sig { params(file: T.nilable(String)).void }
47
39
  def require_helper(file)
48
40
  return unless file
@@ -54,18 +46,10 @@ module Tapioca
54
46
 
55
47
  sig { returns(T::Array[T.untyped]) }
56
48
  def rails_engines
57
- engines = []
58
-
59
- return engines unless Object.const_defined?("Rails::Engine")
60
-
61
- base = Object.const_get("Rails::Engine")
62
- ObjectSpace.each_object(base.singleton_class) do |k|
63
- k = T.cast(k, Class)
64
- next if k.singleton_class?
65
- engines.unshift(k) unless k == base
66
- end
49
+ return [] unless Object.const_defined?("Rails::Engine")
67
50
 
68
- engines.reject(&:abstract_railtie?)
51
+ # We can use `Class#descendants` here, since we know Rails is loaded
52
+ Object.const_get("Rails::Engine").descendants.reject(&:abstract_railtie?)
69
53
  end
70
54
 
71
55
  sig { params(path: String).void }
@@ -116,22 +100,18 @@ module Tapioca
116
100
 
117
101
  engine.config.eager_load_paths.each do |load_path|
118
102
  Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
119
- begin
120
- require(file)
121
- rescue LoadError, StandardError
122
- errored_files << file
123
- end
103
+ require(file)
104
+ rescue LoadError, StandardError
105
+ errored_files << file
124
106
  end
125
107
  end
126
108
 
127
109
  # Try files that have errored one more time
128
110
  # It might have been a load order problem
129
111
  errored_files.each do |file|
130
- begin
131
- require(file)
132
- rescue LoadError, StandardError
133
- nil
134
- end
112
+ require(file)
113
+ rescue LoadError, StandardError
114
+ nil
135
115
  end
136
116
  end
137
117
  end
@@ -0,0 +1,166 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "rbi"
5
+
6
+ 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::EmptyComment.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
+ class Tree
52
+ extend T::Sig
53
+
54
+ sig { params(constant: ::Module, block: T.nilable(T.proc.params(scope: Scope).void)).void }
55
+ def create_path(constant, &block)
56
+ constant_name = Tapioca::Reflection.name_of(constant)
57
+ raise "given constant does not have a name" unless constant_name
58
+
59
+ instance = ::Module.const_get(constant_name)
60
+ case instance
61
+ when ::Class
62
+ create_class(constant.to_s, &block)
63
+ when ::Module
64
+ create_module(constant.to_s, &block)
65
+ else
66
+ raise "unexpected type: #{constant_name} is a #{instance.class}"
67
+ end
68
+ end
69
+
70
+ sig { params(name: String, block: T.nilable(T.proc.params(scope: Scope).void)).void }
71
+ def create_module(name, &block)
72
+ node = create_node(RBI::Module.new(name))
73
+ block&.call(T.cast(node, RBI::Scope))
74
+ end
75
+
76
+ sig do
77
+ params(
78
+ name: String,
79
+ superclass_name: T.nilable(String),
80
+ block: T.nilable(T.proc.params(scope: RBI::Scope).void)
81
+ ).void
82
+ end
83
+ def create_class(name, superclass_name: nil, &block)
84
+ node = create_node(RBI::Class.new(name, superclass_name: superclass_name))
85
+ block&.call(T.cast(node, RBI::Scope))
86
+ end
87
+
88
+ sig { params(name: String, value: String).void }
89
+ def create_constant(name, value:)
90
+ create_node(RBI::Const.new(name, value))
91
+ end
92
+
93
+ sig { params(name: String).void }
94
+ def create_include(name)
95
+ create_node(RBI::Include.new(name))
96
+ end
97
+
98
+ sig { params(name: String).void }
99
+ def create_extend(name)
100
+ create_node(RBI::Extend.new(name))
101
+ end
102
+
103
+ sig { params(name: String).void }
104
+ def create_mixes_in_class_methods(name)
105
+ create_node(RBI::MixesInClassMethods.new(name))
106
+ end
107
+
108
+ sig { params(name: String, value: String).void }
109
+ def create_type_member(name, value: "type_member")
110
+ create_node(RBI::TypeMember.new(name, value))
111
+ end
112
+
113
+ sig do
114
+ params(
115
+ name: String,
116
+ parameters: T::Array[TypedParam],
117
+ return_type: String,
118
+ class_method: T::Boolean
119
+ ).void
120
+ end
121
+ def create_method(name, parameters: [], return_type: "T.untyped", class_method: false)
122
+ return unless valid_method_name?(name)
123
+
124
+ sig = RBI::Sig.new(return_type: return_type)
125
+ method = RBI::Method.new(name, sigs: [sig], is_singleton: class_method)
126
+ parameters.each do |param|
127
+ method << param.param
128
+ sig << RBI::SigParam.new(param.param.name, param.type)
129
+ end
130
+ self << method
131
+ end
132
+
133
+ private
134
+
135
+ SPECIAL_METHOD_NAMES = T.let(
136
+ ["!", "~", "+@", "**", "-@", "*", "/", "%", "+", "-", "<<", ">>", "&", "|", "^", "<", "<=", "=>", ">", ">=",
137
+ "==", "===", "!=", "=~", "!~", "<=>", "[]", "[]=", "`"].freeze,
138
+ T::Array[String]
139
+ )
140
+
141
+ sig { params(name: String).returns(T::Boolean) }
142
+ def valid_method_name?(name)
143
+ return true if SPECIAL_METHOD_NAMES.include?(name)
144
+ !!name.match(/^[a-zA-Z_][[:word:]]*[?!=]?$/)
145
+ end
146
+
147
+ sig { returns(T::Hash[String, RBI::Node]) }
148
+ def nodes_cache
149
+ T.must(@nodes_cache ||= T.let({}, T.nilable(T::Hash[String, Node])))
150
+ end
151
+
152
+ sig { params(node: RBI::Node).returns(RBI::Node) }
153
+ def create_node(node)
154
+ cached = nodes_cache[node.to_s]
155
+ return cached if cached
156
+ nodes_cache[node.to_s] = node
157
+ self << node
158
+ node
159
+ end
160
+ end
161
+
162
+ class TypedParam < T::Struct
163
+ const :param, RBI::Param
164
+ const :type, String
165
+ end
166
+ end
@@ -0,0 +1,138 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Reflection
6
+ extend T::Sig
7
+ extend self
8
+
9
+ CLASS_METHOD = T.let(Kernel.instance_method(:class), UnboundMethod)
10
+ CONSTANTS_METHOD = T.let(Module.instance_method(:constants), UnboundMethod)
11
+ NAME_METHOD = T.let(Module.instance_method(:name), UnboundMethod)
12
+ SINGLETON_CLASS_METHOD = T.let(Object.instance_method(:singleton_class), UnboundMethod)
13
+ ANCESTORS_METHOD = T.let(Module.instance_method(:ancestors), UnboundMethod)
14
+ SUPERCLASS_METHOD = T.let(Class.instance_method(:superclass), UnboundMethod)
15
+ OBJECT_ID_METHOD = T.let(BasicObject.instance_method(:__id__), UnboundMethod)
16
+ EQUAL_METHOD = T.let(BasicObject.instance_method(:equal?), UnboundMethod)
17
+ PUBLIC_INSTANCE_METHODS_METHOD = T.let(Module.instance_method(:public_instance_methods), UnboundMethod)
18
+ PROTECTED_INSTANCE_METHODS_METHOD = T.let(Module.instance_method(:protected_instance_methods), UnboundMethod)
19
+ PRIVATE_INSTANCE_METHODS_METHOD = T.let(Module.instance_method(:private_instance_methods), UnboundMethod)
20
+ METHOD_METHOD = T.let(Kernel.instance_method(:method), UnboundMethod)
21
+
22
+ sig { params(object: BasicObject).returns(Class).checked(:never) }
23
+ def class_of(object)
24
+ CLASS_METHOD.bind(object).call
25
+ end
26
+
27
+ sig { params(constant: Module).returns(T::Array[Symbol]) }
28
+ def constants_of(constant)
29
+ CONSTANTS_METHOD.bind(constant).call(false)
30
+ end
31
+
32
+ sig { params(constant: Module).returns(T.nilable(String)) }
33
+ def name_of(constant)
34
+ name = NAME_METHOD.bind(constant).call
35
+ name&.start_with?("#<") ? nil : name
36
+ end
37
+
38
+ sig { params(constant: Module).returns(Class) }
39
+ def singleton_class_of(constant)
40
+ SINGLETON_CLASS_METHOD.bind(constant).call
41
+ end
42
+
43
+ sig { params(constant: Module).returns(T::Array[Module]) }
44
+ def ancestors_of(constant)
45
+ ANCESTORS_METHOD.bind(constant).call
46
+ end
47
+
48
+ sig { params(constant: Class).returns(T.nilable(Class)) }
49
+ def superclass_of(constant)
50
+ SUPERCLASS_METHOD.bind(constant).call
51
+ end
52
+
53
+ sig { params(object: BasicObject).returns(Integer).checked(:never) }
54
+ def object_id_of(object)
55
+ OBJECT_ID_METHOD.bind(object).call
56
+ end
57
+
58
+ sig { params(object: BasicObject, other: BasicObject).returns(T::Boolean).checked(:never) }
59
+ def are_equal?(object, other)
60
+ EQUAL_METHOD.bind(object).call(other)
61
+ end
62
+
63
+ sig { params(constant: Module).returns(T::Array[Symbol]) }
64
+ def public_instance_methods_of(constant)
65
+ PUBLIC_INSTANCE_METHODS_METHOD.bind(constant).call
66
+ end
67
+
68
+ sig { params(constant: Module).returns(T::Array[Symbol]) }
69
+ def protected_instance_methods_of(constant)
70
+ PROTECTED_INSTANCE_METHODS_METHOD.bind(constant).call
71
+ end
72
+
73
+ sig { params(constant: Module).returns(T::Array[Symbol]) }
74
+ def private_instance_methods_of(constant)
75
+ PRIVATE_INSTANCE_METHODS_METHOD.bind(constant).call
76
+ end
77
+
78
+ sig { params(constant: Module).returns(T::Array[Module]) }
79
+ def inherited_ancestors_of(constant)
80
+ if Class === constant
81
+ ancestors_of(superclass_of(constant) || Object)
82
+ else
83
+ Module.ancestors
84
+ end
85
+ end
86
+
87
+ sig { params(constant: Module).returns(T.nilable(String)) }
88
+ def qualified_name_of(constant)
89
+ name = name_of(constant)
90
+ return if name.nil?
91
+
92
+ if name.start_with?("::")
93
+ name
94
+ else
95
+ "::#{name}"
96
+ end
97
+ end
98
+
99
+ sig { params(method: T.any(UnboundMethod, Method)).returns(T.untyped) }
100
+ def signature_of(method)
101
+ T::Private::Methods.signature_for_method(method)
102
+ rescue LoadError, StandardError
103
+ nil
104
+ end
105
+
106
+ sig { params(type: T::Types::Base).returns(String) }
107
+ def name_of_type(type)
108
+ type.to_s.gsub(/\bAttachedClass\b/, "T.attached_class")
109
+ end
110
+
111
+ sig { params(constant: Module, method: Symbol).returns(Method) }
112
+ def method_of(constant, method)
113
+ METHOD_METHOD.bind(constant).call(method)
114
+ end
115
+
116
+ # Returns an array with all classes that are < than the supplied class.
117
+ #
118
+ # class C; end
119
+ # descendants_of(C) # => []
120
+ #
121
+ # class B < C; end
122
+ # descendants_of(C) # => [B]
123
+ #
124
+ # class A < B; end
125
+ # descendants_of(C) # => [B, A]
126
+ #
127
+ # class D < C; end
128
+ # descendants_of(C) # => [B, A, D]
129
+ sig { type_parameters(:U).params(klass: T.type_parameter(:U)).returns(T::Array[T.type_parameter(:U)]) }
130
+ def descendants_of(klass)
131
+ result = ObjectSpace.each_object(klass.singleton_class).reject do |k|
132
+ T.cast(k, Module).singleton_class? || T.unsafe(k) == klass
133
+ end
134
+
135
+ T.unsafe(result)
136
+ end
137
+ end
138
+ end
@@ -13,7 +13,7 @@ module T
13
13
  end
14
14
  end
15
15
 
16
- "{#{entries.join(', ')}}"
16
+ "{#{entries.join(", ")}}"
17
17
  end
18
18
  end
19
19
  end