tapioca 0.4.23 → 0.5.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 (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