tapioca 0.4.23 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +14 -14
  3. data/README.md +2 -2
  4. data/Rakefile +5 -7
  5. data/exe/tapioca +2 -2
  6. data/lib/tapioca/cli.rb +256 -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_record_associations.rb +33 -54
  13. data/lib/tapioca/compilers/dsl/active_record_columns.rb +10 -105
  14. data/lib/tapioca/compilers/dsl/active_record_enum.rb +8 -10
  15. data/lib/tapioca/compilers/dsl/active_record_scope.rb +7 -10
  16. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +5 -8
  17. data/lib/tapioca/compilers/dsl/active_resource.rb +9 -37
  18. data/lib/tapioca/compilers/dsl/active_storage.rb +98 -0
  19. data/lib/tapioca/compilers/dsl/active_support_concern.rb +108 -0
  20. data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +13 -8
  21. data/lib/tapioca/compilers/dsl/base.rb +96 -82
  22. data/lib/tapioca/compilers/dsl/config.rb +111 -0
  23. data/lib/tapioca/compilers/dsl/frozen_record.rb +5 -7
  24. data/lib/tapioca/compilers/dsl/identity_cache.rb +66 -29
  25. data/lib/tapioca/compilers/dsl/protobuf.rb +19 -69
  26. data/lib/tapioca/compilers/dsl/sidekiq_worker.rb +25 -12
  27. data/lib/tapioca/compilers/dsl/smart_properties.rb +19 -31
  28. data/lib/tapioca/compilers/dsl/state_machines.rb +56 -78
  29. data/lib/tapioca/compilers/dsl/url_helpers.rb +7 -10
  30. data/lib/tapioca/compilers/dsl_compiler.rb +22 -38
  31. data/lib/tapioca/compilers/requires_compiler.rb +2 -2
  32. data/lib/tapioca/compilers/sorbet.rb +26 -5
  33. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +139 -154
  34. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +4 -4
  35. data/lib/tapioca/compilers/symbol_table_compiler.rb +1 -1
  36. data/lib/tapioca/compilers/todos_compiler.rb +1 -1
  37. data/lib/tapioca/config.rb +2 -0
  38. data/lib/tapioca/config_builder.rb +4 -2
  39. data/lib/tapioca/constant_locator.rb +6 -8
  40. data/lib/tapioca/gemfile.rb +26 -19
  41. data/lib/tapioca/generator.rb +127 -43
  42. data/lib/tapioca/generic_type_registry.rb +25 -98
  43. data/lib/tapioca/helpers/active_record_column_type_helper.rb +98 -0
  44. data/lib/tapioca/internal.rb +1 -9
  45. data/lib/tapioca/loader.rb +14 -48
  46. data/lib/tapioca/rbi_ext/model.rb +122 -0
  47. data/lib/tapioca/reflection.rb +131 -0
  48. data/lib/tapioca/sorbet_ext/fixed_hash_patch.rb +1 -1
  49. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +72 -4
  50. data/lib/tapioca/sorbet_ext/name_patch.rb +1 -1
  51. data/lib/tapioca/version.rb +1 -1
  52. data/lib/tapioca.rb +2 -0
  53. metadata +35 -23
  54. data/lib/tapioca/cli/main.rb +0 -146
  55. data/lib/tapioca/core_ext/class.rb +0 -28
  56. data/lib/tapioca/core_ext/string.rb +0 -18
  57. data/lib/tapioca/rbi/model.rb +0 -405
  58. data/lib/tapioca/rbi/printer.rb +0 -410
  59. data/lib/tapioca/rbi/rewriters/group_nodes.rb +0 -106
  60. data/lib/tapioca/rbi/rewriters/nest_non_public_methods.rb +0 -65
  61. data/lib/tapioca/rbi/rewriters/nest_singleton_methods.rb +0 -42
  62. data/lib/tapioca/rbi/rewriters/sort_nodes.rb +0 -82
  63. 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
@@ -4,22 +4,14 @@
4
4
  require "tapioca"
5
5
  require "tapioca/loader"
6
6
  require "tapioca/constant_locator"
7
- require "tapioca/generic_type_registry"
8
7
  require "tapioca/sorbet_ext/generic_name_patch"
9
8
  require "tapioca/sorbet_ext/fixed_hash_patch"
9
+ require "tapioca/generic_type_registry"
10
10
  require "tapioca/config"
11
11
  require "tapioca/config_builder"
12
12
  require "tapioca/generator"
13
13
  require "tapioca/cli"
14
- require "tapioca/cli/main"
15
14
  require "tapioca/gemfile"
16
- require "tapioca/rbi/model"
17
- require "tapioca/rbi/visitor"
18
- require "tapioca/rbi/rewriters/nest_singleton_methods"
19
- require "tapioca/rbi/rewriters/nest_non_public_methods"
20
- require "tapioca/rbi/rewriters/group_nodes"
21
- require "tapioca/rbi/rewriters/sort_nodes"
22
- require "tapioca/rbi/printer"
23
15
  require "tapioca/compilers/sorbet"
24
16
  require "tapioca/compilers/requires_compiler"
25
17
  require "tapioca/compilers/symbol_table_compiler"
@@ -5,19 +5,13 @@ 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
- load_rails
18
- load_rake
12
+ gemfile.require_bundle
19
13
 
20
- require_bundle
14
+ load_rails_application
21
15
 
22
16
  require_helper(require_file)
23
17
 
@@ -25,14 +19,11 @@ module Tapioca
25
19
  end
26
20
 
27
21
  sig { params(environment_load: T::Boolean, eager_load: T::Boolean).void }
28
- def load_rails(environment_load: false, eager_load: false)
22
+ def load_rails_application(environment_load: false, eager_load: false)
29
23
  return unless File.exist?("config/application.rb")
30
24
 
31
- safe_require("rails")
32
-
33
25
  silence_deprecations
34
26
 
35
- safe_require("rails/generators/test_case")
36
27
  if environment_load
37
28
  safe_require("./config/environment")
38
29
  else
@@ -44,9 +35,6 @@ module Tapioca
44
35
 
45
36
  private
46
37
 
47
- sig { returns(Tapioca::Gemfile) }
48
- attr_reader :gemfile
49
-
50
38
  sig { params(file: T.nilable(String)).void }
51
39
  def require_helper(file)
52
40
  return unless file
@@ -56,25 +44,12 @@ module Tapioca
56
44
  require(file)
57
45
  end
58
46
 
59
- sig { void }
60
- def require_bundle
61
- gemfile.require
62
- end
63
-
64
47
  sig { returns(T::Array[T.untyped]) }
65
48
  def rails_engines
66
- engines = []
67
-
68
- return engines unless Object.const_defined?("Rails::Engine")
69
-
70
- base = Object.const_get("Rails::Engine")
71
- ObjectSpace.each_object(base.singleton_class) do |k|
72
- k = T.cast(k, Class)
73
- next if k.singleton_class?
74
- engines.unshift(k) unless k == base
75
- end
49
+ return [] unless Object.const_defined?("Rails::Engine")
76
50
 
77
- 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?)
78
53
  end
79
54
 
80
55
  sig { params(path: String).void }
@@ -84,11 +59,6 @@ module Tapioca
84
59
  nil
85
60
  end
86
61
 
87
- sig { void }
88
- def load_rake
89
- safe_require("rake")
90
- end
91
-
92
62
  sig { void }
93
63
  def silence_deprecations
94
64
  # Stop any ActiveSupport Deprecations from being reported
@@ -130,22 +100,18 @@ module Tapioca
130
100
 
131
101
  engine.config.eager_load_paths.each do |load_path|
132
102
  Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
133
- begin
134
- require(file)
135
- rescue LoadError, StandardError
136
- errored_files << file
137
- end
103
+ require(file)
104
+ rescue LoadError, StandardError
105
+ errored_files << file
138
106
  end
139
107
  end
140
108
 
141
109
  # Try files that have errored one more time
142
110
  # It might have been a load order problem
143
111
  errored_files.each do |file|
144
- begin
145
- require(file)
146
- rescue LoadError, StandardError
147
- nil
148
- end
112
+ require(file)
113
+ rescue LoadError, StandardError
114
+ nil
149
115
  end
150
116
  end
151
117
  end
@@ -0,0 +1,122 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "rbi"
5
+
6
+ module RBI
7
+ class Tree
8
+ extend T::Sig
9
+
10
+ sig { params(constant: ::Module, block: T.nilable(T.proc.params(scope: Scope).void)).void }
11
+ def create_path(constant, &block)
12
+ constant_name = Tapioca::Reflection.name_of(constant)
13
+ raise "given constant does not have a name" unless constant_name
14
+
15
+ instance = ::Module.const_get(constant_name)
16
+ case instance
17
+ when ::Class
18
+ create_class(constant.to_s, &block)
19
+ when ::Module
20
+ create_module(constant.to_s, &block)
21
+ else
22
+ raise "unexpected type: #{constant_name} is a #{instance.class}"
23
+ end
24
+ end
25
+
26
+ sig { params(name: String, block: T.nilable(T.proc.params(scope: Scope).void)).void }
27
+ def create_module(name, &block)
28
+ node = create_node(RBI::Module.new(name))
29
+ block&.call(T.cast(node, RBI::Scope))
30
+ end
31
+
32
+ sig do
33
+ params(
34
+ name: String,
35
+ superclass_name: T.nilable(String),
36
+ block: T.nilable(T.proc.params(scope: RBI::Scope).void)
37
+ ).void
38
+ end
39
+ def create_class(name, superclass_name: nil, &block)
40
+ node = create_node(RBI::Class.new(name, superclass_name: superclass_name))
41
+ block&.call(T.cast(node, RBI::Scope))
42
+ end
43
+
44
+ sig { params(name: String, value: String).void }
45
+ def create_constant(name, value:)
46
+ create_node(RBI::Const.new(name, value))
47
+ end
48
+
49
+ sig { params(name: String).void }
50
+ def create_include(name)
51
+ create_node(RBI::Include.new(name))
52
+ end
53
+
54
+ sig { params(name: String).void }
55
+ def create_extend(name)
56
+ create_node(RBI::Extend.new(name))
57
+ end
58
+
59
+ sig { params(name: String).void }
60
+ def create_mixes_in_class_methods(name)
61
+ create_node(RBI::MixesInClassMethods.new(name))
62
+ end
63
+
64
+ sig { params(name: String, value: String).void }
65
+ def create_type_member(name, value: "type_member")
66
+ create_node(RBI::TypeMember.new(name, value))
67
+ end
68
+
69
+ sig do
70
+ params(
71
+ name: String,
72
+ parameters: T::Array[TypedParam],
73
+ return_type: String,
74
+ class_method: T::Boolean
75
+ ).void
76
+ end
77
+ def create_method(name, parameters: [], return_type: "T.untyped", class_method: false)
78
+ return unless valid_method_name?(name)
79
+
80
+ sig = RBI::Sig.new(return_type: return_type)
81
+ method = RBI::Method.new(name, sigs: [sig], is_singleton: class_method)
82
+ parameters.each do |param|
83
+ method << param.param
84
+ sig << RBI::SigParam.new(param.param.name, param.type)
85
+ end
86
+ self << method
87
+ end
88
+
89
+ private
90
+
91
+ SPECIAL_METHOD_NAMES = T.let(
92
+ ["!", "~", "+@", "**", "-@", "*", "/", "%", "+", "-", "<<", ">>", "&", "|", "^", "<", "<=", "=>", ">", ">=",
93
+ "==", "===", "!=", "=~", "!~", "<=>", "[]", "[]=", "`"].freeze,
94
+ T::Array[String]
95
+ )
96
+
97
+ sig { params(name: String).returns(T::Boolean) }
98
+ def valid_method_name?(name)
99
+ return true if SPECIAL_METHOD_NAMES.include?(name)
100
+ !!name.match(/^[a-zA-Z_][[:word:]]*[?!=]?$/)
101
+ end
102
+
103
+ sig { returns(T::Hash[String, RBI::Node]) }
104
+ def nodes_cache
105
+ T.must(@nodes_cache ||= T.let({}, T.nilable(T::Hash[String, Node])))
106
+ end
107
+
108
+ sig { params(node: RBI::Node).returns(RBI::Node) }
109
+ def create_node(node)
110
+ cached = nodes_cache[node.to_s]
111
+ return cached if cached
112
+ nodes_cache[node.to_s] = node
113
+ self << node
114
+ node
115
+ end
116
+ end
117
+
118
+ class TypedParam < T::Struct
119
+ const :param, RBI::Param
120
+ const :type, String
121
+ end
122
+ end