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
@@ -1,780 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require "pathname"
5
-
6
- module Tapioca
7
- module Compilers
8
- module SymbolTable
9
- class SymbolGenerator
10
- extend(T::Sig)
11
- include(Reflection)
12
-
13
- IGNORED_SYMBOLS = T.let(["YAML", "MiniTest", "Mutex"], T::Array[String])
14
- IGNORED_COMMENTS = T.let([
15
- ":doc:",
16
- ":nodoc:",
17
- "typed:",
18
- "frozen_string_literal:",
19
- "encoding:",
20
- "warn_indent:",
21
- "shareable_constant_value:",
22
- "rubocop:",
23
- ], T::Array[String])
24
-
25
- sig { returns(Gemfile::GemSpec) }
26
- attr_reader :gem
27
-
28
- sig { returns(Integer) }
29
- attr_reader :indent
30
-
31
- sig { params(gem: Gemfile::GemSpec, indent: Integer, include_doc: T::Boolean).void }
32
- def initialize(gem, indent = 0, include_doc = false)
33
- @gem = gem
34
- @indent = indent
35
- @seen = T.let(Set.new, T::Set[String])
36
- @alias_namespace = T.let(Set.new, T::Set[String])
37
- @symbol_queue = T.let(symbols.sort.dup, T::Array[String])
38
- @symbols = T.let(nil, T.nilable(T::Set[String]))
39
- @include_doc = include_doc
40
-
41
- gem.parse_yard_docs if include_doc
42
- end
43
-
44
- sig { params(rbi: RBI::File).void }
45
- def generate(rbi)
46
- generate_from_symbol(rbi.root, T.must(@symbol_queue.shift)) until @symbol_queue.empty?
47
- end
48
-
49
- private
50
-
51
- sig { params(name: T.nilable(String)).void }
52
- def add_to_symbol_queue(name)
53
- @symbol_queue << name unless name.nil? || symbols.include?(name) || symbol_ignored?(name)
54
- end
55
-
56
- sig { returns(T::Set[String]) }
57
- def symbols
58
- @symbols ||= begin
59
- symbols = Tapioca::Compilers::SymbolTable::SymbolLoader.list_from_paths(gem.files)
60
- symbols.union(engine_symbols(symbols))
61
- end
62
- end
63
-
64
- sig { params(symbols: T::Set[String]).returns(T::Set[String]) }
65
- def engine_symbols(symbols)
66
- return Set.new unless Object.const_defined?("Rails::Engine")
67
-
68
- engine = descendants_of(Object.const_get("Rails::Engine"))
69
- .reject(&:abstract_railtie?)
70
- .find do |klass|
71
- name = name_of(klass)
72
- !name.nil? && symbols.include?(name)
73
- end
74
-
75
- return Set.new unless engine
76
-
77
- paths = engine.config.eager_load_paths.flat_map do |load_path|
78
- Pathname.glob("#{load_path}/**/*.rb")
79
- end
80
-
81
- Tapioca::Compilers::SymbolTable::SymbolLoader.list_from_paths(paths)
82
- rescue
83
- Set.new
84
- end
85
-
86
- sig { params(tree: RBI::Tree, symbol: String).void }
87
- def generate_from_symbol(tree, symbol)
88
- constant = constantize(symbol)
89
-
90
- return unless constant
91
-
92
- compile(tree, symbol, constant)
93
- end
94
-
95
- sig { params(tree: RBI::Tree, name: T.nilable(String), constant: BasicObject).void.checked(:never) }
96
- def compile(tree, name, constant)
97
- return unless constant
98
- return unless name
99
- return if name.strip.empty?
100
- return if name.start_with?("#<")
101
- return if name.downcase == name
102
- return if alias_namespaced?(name)
103
- return if seen?(name)
104
- return if T::Enum === constant # T::Enum instances are defined via `compile_enums`
105
-
106
- mark_seen(name)
107
- compile_constant(tree, name, constant)
108
- end
109
-
110
- sig { params(tree: RBI::Tree, name: String, constant: BasicObject).void.checked(:never) }
111
- def compile_constant(tree, name, constant)
112
- case constant
113
- when Module
114
- if name_of(constant) != name
115
- compile_alias(tree, name, constant)
116
- else
117
- compile_module(tree, name, constant)
118
- end
119
- else
120
- compile_object(tree, name, constant)
121
- end
122
- end
123
-
124
- sig { params(tree: RBI::Tree, name: String, constant: Module).void }
125
- def compile_alias(tree, name, constant)
126
- return if symbol_ignored?(name)
127
-
128
- target = name_of(constant)
129
- # If target has no name, let's make it an anonymous class or module with `Class.new` or `Module.new`
130
- target = "#{constant.class}.new" unless target
131
-
132
- add_to_alias_namespace(name)
133
-
134
- return if IGNORED_SYMBOLS.include?(name)
135
-
136
- tree << RBI::Const.new(name, target)
137
- end
138
-
139
- sig { params(tree: RBI::Tree, name: String, value: BasicObject).void.checked(:never) }
140
- def compile_object(tree, name, value)
141
- return if symbol_ignored?(name)
142
-
143
- klass = class_of(value)
144
- return if klass == TypeMember || klass == TypeTemplate
145
-
146
- klass_name = if klass == ObjectSpace::WeakMap
147
- # WeakMap is an implicit generic with one type variable
148
- "ObjectSpace::WeakMap[T.untyped]"
149
- elsif T::Generic === klass
150
- generic_name_of(klass)
151
- else
152
- name_of(klass)
153
- end
154
-
155
- comments = documentation_comments(name)
156
-
157
- if klass_name == "T::Private::Types::TypeAlias"
158
- type_alias = sanitize_signature_types(T.unsafe(value).aliased_type.to_s)
159
- constant = RBI::Const.new(name, "T.type_alias { #{type_alias} }", comments: comments)
160
- tree << constant
161
- return
162
- end
163
-
164
- return if klass_name&.start_with?("T::Types::", "T::Private::")
165
-
166
- type_name = klass_name || "T.untyped"
167
- constant = RBI::Const.new(name, "T.let(T.unsafe(nil), #{type_name})", comments: comments)
168
-
169
- tree << constant
170
- end
171
-
172
- sig { params(tree: RBI::Tree, name: String, constant: Module).void }
173
- def compile_module(tree, name, constant)
174
- return unless defined_in_gem?(constant, strict: false)
175
-
176
- comments = documentation_comments(name)
177
- scope =
178
- if constant.is_a?(Class)
179
- superclass = compile_superclass(constant)
180
- RBI::Class.new(name, superclass_name: superclass, comments: comments)
181
- else
182
- RBI::Module.new(name, comments: comments)
183
- end
184
-
185
- compile_body(scope, name, constant)
186
-
187
- return if symbol_ignored?(name) && scope.empty?
188
-
189
- tree << scope
190
- compile_subconstants(tree, name, constant)
191
- end
192
-
193
- sig { params(tree: RBI::Tree, name: String, constant: Module).void }
194
- def compile_body(tree, name, constant)
195
- # Compiling type variables must happen first to populate generic names
196
- compile_type_variables(tree, constant)
197
- compile_methods(tree, name, constant)
198
- compile_module_helpers(tree, constant)
199
- compile_mixins(tree, constant)
200
- compile_props(tree, constant)
201
- compile_enums(tree, constant)
202
- compile_dynamic_mixins(tree, constant)
203
- end
204
-
205
- sig { params(tree: RBI::Tree, constant: Module).void }
206
- def compile_dynamic_mixins(tree, constant)
207
- return if constant.is_a?(Class)
208
-
209
- mixin_compiler = DynamicMixinCompiler.new(constant)
210
- mixin_compiler.compile_class_attributes(tree)
211
- dynamic_extends, dynamic_includes = mixin_compiler.compile_mixes_in_class_methods(tree)
212
-
213
- (dynamic_includes + dynamic_extends).each do |mod|
214
- add_to_symbol_queue(name_of(mod))
215
- end
216
- end
217
-
218
- sig { params(tree: RBI::Tree, constant: Module).void }
219
- def compile_module_helpers(tree, constant)
220
- abstract_type = T::Private::Abstract::Data.get(constant, :abstract_type) ||
221
- T::Private::Abstract::Data.get(singleton_class_of(constant), :abstract_type)
222
-
223
- tree << RBI::Helper.new(abstract_type.to_s) if abstract_type
224
- tree << RBI::Helper.new("final") if T::Private::Final.final_module?(constant)
225
- tree << RBI::Helper.new("sealed") if T::Private::Sealed.sealed_module?(constant)
226
- end
227
-
228
- sig { params(tree: RBI::Tree, constant: Module).void }
229
- def compile_props(tree, constant)
230
- return unless T::Props::ClassMethods === constant
231
-
232
- constant.props.map do |name, prop|
233
- type = prop.fetch(:type_object, "T.untyped").to_s.gsub(".returns(<VOID>)", ".void")
234
-
235
- default = prop.key?(:default) ? "T.unsafe(nil)" : nil
236
- tree << if prop.fetch(:immutable, false)
237
- RBI::TStructConst.new(name.to_s, type, default: default)
238
- else
239
- RBI::TStructProp.new(name.to_s, type, default: default)
240
- end
241
- end
242
- end
243
-
244
- sig { params(tree: RBI::Tree, constant: Module).void }
245
- def compile_enums(tree, constant)
246
- return unless T::Enum > constant
247
-
248
- enums = T.unsafe(constant).values.map do |enum_type|
249
- enum_type.instance_variable_get(:@const_name).to_s
250
- end
251
-
252
- tree << RBI::TEnumBlock.new(enums)
253
- end
254
-
255
- sig { params(tree: RBI::Tree, name: String, constant: Module).void }
256
- def compile_subconstants(tree, name, constant)
257
- constants_of(constant).sort.uniq.map do |constant_name|
258
- symbol = (name == "Object" ? "" : name) + "::#{constant_name}"
259
- subconstant = constantize(symbol)
260
-
261
- # Don't compile modules of Object because Object::Foo == Foo
262
- # Don't compile modules of BasicObject because BasicObject::BasicObject == BasicObject
263
- next if (Object == constant || BasicObject == constant) && Module === subconstant
264
- next unless subconstant
265
-
266
- compile(tree, symbol, subconstant)
267
- end
268
- end
269
-
270
- sig { params(tree: RBI::Tree, constant: Module).void }
271
- def compile_type_variables(tree, constant)
272
- compile_type_variable_declarations(tree, constant)
273
-
274
- sclass = RBI::SingletonClass.new
275
- compile_type_variable_declarations(sclass, singleton_class_of(constant))
276
- tree << sclass if sclass.nodes.length > 1
277
- end
278
-
279
- sig { params(tree: RBI::Tree, constant: Module).void }
280
- def compile_type_variable_declarations(tree, constant)
281
- # Try to find the type variables defined on this constant, bail if we can't
282
- type_variables = GenericTypeRegistry.lookup_type_variables(constant)
283
- return unless type_variables
284
-
285
- # Create a map of subconstants (via their object ids) to their names.
286
- # We need this later when we want to lookup the name of the registered type
287
- # variable via the value of the type variable constant.
288
- subconstant_to_name_lookup = constants_of(constant)
289
- .each_with_object({}.compare_by_identity) do |constant_name, table|
290
- table[constantize(constant_name.to_s, namespace: constant)] = constant_name.to_s
291
- end
292
-
293
- # Map each type variable to its string representation.
294
- #
295
- # Each entry of `type_variables` maps an object_id to a String,
296
- # and the order they are inserted into the hash is the order they should be
297
- # defined in the source code.
298
- #
299
- # By looping over these entries and then getting the actual constant name
300
- # from the `subconstant_to_name_lookup` we defined above, gives us all the
301
- # information we need to serialize type variable definitions.
302
- type_variable_declarations = type_variables.map do |type_variable, serialized_type_variable|
303
- constant_name = subconstant_to_name_lookup[type_variable]
304
- type_variable.name = constant_name
305
- # Here, we know that constant_value will be an instance of
306
- # T::Types::CustomTypeVariable, which knows how to serialize
307
- # itself to a type_member/type_template
308
- tree << RBI::TypeMember.new(constant_name, serialized_type_variable)
309
- end
310
-
311
- return if type_variable_declarations.empty?
312
-
313
- tree << RBI::Extend.new("T::Generic")
314
- end
315
-
316
- sig { params(constant: Class).returns(T.nilable(String)) }
317
- def compile_superclass(constant)
318
- superclass = T.let(nil, T.nilable(Class)) # rubocop:disable Lint/UselessAssignment
319
-
320
- while (superclass = superclass_of(constant))
321
- constant_name = name_of(constant)
322
- constant = superclass
323
-
324
- # Some types have "themselves" as their superclass
325
- # which can happen via:
326
- #
327
- # class A < Numeric; end
328
- # A = Class.new(A)
329
- # A.superclass #=> A
330
- #
331
- # We compare names here to make sure we skip those
332
- # superclass instances and walk up the chain.
333
- #
334
- # The name comparison is against the name of the constant
335
- # resolved from the name of the superclass, since
336
- # this is also possible:
337
- #
338
- # B = Class.new
339
- # class A < B; end
340
- # B = A
341
- # A.superclass.name #=> "B"
342
- # B #=> A
343
- superclass_name = name_of(superclass)
344
- next unless superclass_name
345
-
346
- resolved_superclass = constantize(superclass_name)
347
- next unless Module === resolved_superclass
348
- next if name_of(resolved_superclass) == constant_name
349
-
350
- # We found a suitable superclass
351
- break
352
- end
353
-
354
- return if superclass == ::Object || superclass == ::Delegator
355
- return if superclass.nil?
356
-
357
- name = name_of(superclass)
358
- return if name.nil? || name.empty?
359
-
360
- add_to_symbol_queue(name)
361
-
362
- "::#{name}"
363
- end
364
-
365
- sig { params(tree: RBI::Tree, constant: Module).void }
366
- def compile_mixins(tree, constant)
367
- singleton_class = singleton_class_of(constant)
368
-
369
- interesting_ancestors = interesting_ancestors_of(constant)
370
- interesting_singleton_class_ancestors = interesting_ancestors_of(singleton_class)
371
-
372
- prepends = interesting_ancestors.take_while { |c| !are_equal?(constant, c) }
373
- includes = interesting_ancestors.drop(prepends.size + 1)
374
- extends = interesting_singleton_class_ancestors.reject do |mod|
375
- Module != class_of(mod) || are_equal?(mod, singleton_class)
376
- end
377
-
378
- add_mixins(tree, prepends.reverse, Trackers::Mixin::Type::Prepend)
379
- add_mixins(tree, includes.reverse, Trackers::Mixin::Type::Include)
380
- add_mixins(tree, extends.reverse, Trackers::Mixin::Type::Extend)
381
- end
382
-
383
- sig do
384
- params(
385
- tree: RBI::Tree,
386
- mods: T::Array[Module],
387
- mixin_type: Trackers::Mixin::Type
388
- ).void
389
- end
390
- def add_mixins(tree, mods, mixin_type)
391
- mods
392
- .select do |mod|
393
- name = name_of(mod)
394
-
395
- name && !name.start_with?("T::")
396
- end
397
- .map do |mod|
398
- add_to_symbol_queue(name_of(mod))
399
-
400
- qname = qualified_name_of(mod)
401
- case mixin_type
402
- # TODO: Sorbet currently does not handle prepend
403
- # properly for method resolution, so we generate an
404
- # include statement instead
405
- when Trackers::Mixin::Type::Include, Trackers::Mixin::Type::Prepend
406
- tree << RBI::Include.new(T.must(qname))
407
- when Trackers::Mixin::Type::Extend
408
- tree << RBI::Extend.new(T.must(qname))
409
- end
410
- end
411
- end
412
-
413
- sig { params(tree: RBI::Tree, name: String, constant: Module).void }
414
- def compile_methods(tree, name, constant)
415
- compile_method(
416
- tree,
417
- name,
418
- constant,
419
- initialize_method_for(constant)
420
- )
421
-
422
- compile_directly_owned_methods(tree, name, constant)
423
- compile_directly_owned_methods(tree, name, singleton_class_of(constant))
424
- end
425
-
426
- sig do
427
- params(
428
- tree: RBI::Tree,
429
- module_name: String,
430
- mod: Module,
431
- for_visibility: T::Array[Symbol]
432
- ).void
433
- end
434
- def compile_directly_owned_methods(tree, module_name, mod, for_visibility = [:public, :protected, :private])
435
- method_names_by_visibility(mod)
436
- .delete_if { |visibility, _method_list| !for_visibility.include?(visibility) }
437
- .each do |visibility, method_list|
438
- method_list.sort!.map do |name|
439
- next if name == :initialize
440
- vis = case visibility
441
- when :protected
442
- RBI::Protected.new
443
- when :private
444
- RBI::Private.new
445
- else
446
- RBI::Public.new
447
- end
448
- compile_method(tree, module_name, mod, mod.instance_method(name), vis)
449
- end
450
- end
451
- end
452
-
453
- sig { params(mod: Module).returns(T::Hash[Symbol, T::Array[Symbol]]) }
454
- def method_names_by_visibility(mod)
455
- {
456
- public: public_instance_methods_of(mod),
457
- protected: protected_instance_methods_of(mod),
458
- private: private_instance_methods_of(mod),
459
- }
460
- end
461
-
462
- sig { params(constant: Module, method_name: String).returns(T::Boolean) }
463
- def struct_method?(constant, method_name)
464
- return false unless T::Props::ClassMethods === constant
465
-
466
- constant
467
- .props
468
- .keys
469
- .include?(method_name.gsub(/=$/, "").to_sym)
470
- end
471
-
472
- sig do
473
- params(
474
- tree: RBI::Tree,
475
- symbol_name: String,
476
- constant: Module,
477
- method: T.nilable(UnboundMethod),
478
- visibility: RBI::Visibility
479
- ).void
480
- end
481
- def compile_method(tree, symbol_name, constant, method, visibility = RBI::Public.new)
482
- return unless method
483
- return unless method.owner == constant
484
- return if symbol_ignored?(symbol_name) && !method_in_gem?(method)
485
-
486
- signature = signature_of(method)
487
- method = T.let(signature.method, UnboundMethod) if signature
488
-
489
- method_name = method.name.to_s
490
- return unless valid_method_name?(method_name)
491
- return if struct_method?(constant, method_name)
492
- return if method_name.start_with?("__t_props_generated_")
493
-
494
- parameters = T.let(method.parameters, T::Array[[Symbol, T.nilable(Symbol)]])
495
-
496
- sanitized_parameters = parameters.each_with_index.map do |(type, name), index|
497
- fallback_arg_name = "_arg#{index}"
498
-
499
- unless name
500
- # For attr_writer methods, Sorbet signatures have the name
501
- # of the method (without the trailing = sign) as the name of
502
- # the only parameter. So, if the parameter does not have a name
503
- # then the replacement name should be the name of the method
504
- # (minus trailing =) if and only if there is a signature for the
505
- # method and the parameter is required and there is a single
506
- # parameter and the signature also defines a single parameter and
507
- # the name of the method ends with a = character.
508
- writer_method_with_sig = (
509
- signature && type == :req &&
510
- parameters.size == 1 &&
511
- signature.arg_types.size == 1 &&
512
- method_name[-1] == "="
513
- )
514
-
515
- name = if writer_method_with_sig
516
- T.must(method_name[0...-1]).to_sym
517
- else
518
- fallback_arg_name
519
- end
520
- end
521
-
522
- # Sanitize param names
523
- name = name.to_s.gsub(/[^a-zA-Z0-9_]/, fallback_arg_name)
524
-
525
- [type, name]
526
- end
527
-
528
- separator = constant.singleton_class? ? "." : "#"
529
- comments = documentation_comments("#{symbol_name}#{separator}#{method_name}")
530
- rbi_method = RBI::Method.new(
531
- method_name,
532
- is_singleton: constant.singleton_class?,
533
- visibility: visibility,
534
- comments: comments
535
- )
536
-
537
- rbi_method.sigs << compile_signature(signature, sanitized_parameters) if signature
538
-
539
- sanitized_parameters.each do |type, name|
540
- case type
541
- when :req
542
- rbi_method << RBI::Param.new(name)
543
- when :opt
544
- rbi_method << RBI::OptParam.new(name, "T.unsafe(nil)")
545
- when :rest
546
- rbi_method << RBI::RestParam.new(name)
547
- when :keyreq
548
- rbi_method << RBI::KwParam.new(name)
549
- when :key
550
- rbi_method << RBI::KwOptParam.new(name, "T.unsafe(nil)")
551
- when :keyrest
552
- rbi_method << RBI::KwRestParam.new(name)
553
- when :block
554
- rbi_method << RBI::BlockParam.new(name)
555
- end
556
- end
557
-
558
- tree << rbi_method
559
- end
560
-
561
- TYPE_PARAMETER_MATCHER = /T\.type_parameter\(:?([[:word:]]+)\)/
562
-
563
- sig { params(signature: T.untyped, parameters: T::Array[[Symbol, String]]).returns(RBI::Sig) }
564
- def compile_signature(signature, parameters)
565
- parameter_types = T.let(signature.arg_types.to_h, T::Hash[Symbol, T::Types::Base])
566
- parameter_types.merge!(signature.kwarg_types)
567
- parameter_types[signature.rest_name] = signature.rest_type if signature.has_rest
568
- parameter_types[signature.keyrest_name] = signature.keyrest_type if signature.has_keyrest
569
- parameter_types[signature.block_name] = signature.block_type if signature.block_name
570
-
571
- sig = RBI::Sig.new
572
-
573
- parameters.each do |_, name|
574
- type = sanitize_signature_types(parameter_types[name.to_sym].to_s)
575
- add_to_symbol_queue(type)
576
- sig << RBI::SigParam.new(name, type)
577
- end
578
-
579
- return_type = name_of_type(signature.return_type)
580
- sig.return_type = sanitize_signature_types(return_type)
581
- add_to_symbol_queue(sig.return_type)
582
-
583
- parameter_types.values.join(", ").scan(TYPE_PARAMETER_MATCHER).flatten.uniq.each do |k, _|
584
- sig.type_params << k
585
- end
586
-
587
- case signature.mode
588
- when "abstract"
589
- sig.is_abstract = true
590
- when "override"
591
- sig.is_override = true
592
- when "overridable_override"
593
- sig.is_overridable = true
594
- sig.is_override = true
595
- when "overridable"
596
- sig.is_overridable = true
597
- end
598
-
599
- sig
600
- end
601
-
602
- sig { params(sig_string: String).returns(String) }
603
- def sanitize_signature_types(sig_string)
604
- sig_string
605
- .gsub(".returns(<VOID>)", ".void")
606
- .gsub("<VOID>", "void")
607
- .gsub("<NOT-TYPED>", "T.untyped")
608
- .gsub(".params()", "")
609
- end
610
-
611
- sig { params(symbol_name: String).returns(T::Boolean) }
612
- def symbol_ignored?(symbol_name)
613
- SymbolLoader.ignore_symbol?(symbol_name)
614
- end
615
-
616
- SPECIAL_METHOD_NAMES = T.let([
617
- "!", "~", "+@", "**", "-@", "*", "/", "%", "+", "-", "<<", ">>", "&", "|", "^",
618
- "<", "<=", "=>", ">", ">=", "==", "===", "!=", "=~", "!~", "<=>", "[]", "[]=", "`"
619
- ], T::Array[String])
620
-
621
- sig { params(name: String).returns(T::Boolean) }
622
- def valid_method_name?(name)
623
- return true if SPECIAL_METHOD_NAMES.include?(name)
624
- !!name.match(/^[[:word:]]+[?!=]?$/)
625
- end
626
-
627
- sig { params(method: UnboundMethod).returns(T::Boolean) }
628
- def method_in_gem?(method)
629
- source_location = method.source_location&.first
630
- return false if source_location.nil?
631
-
632
- gem.contains_path?(source_location)
633
- end
634
-
635
- sig { params(constant: Module, strict: T::Boolean).returns(T::Boolean) }
636
- def defined_in_gem?(constant, strict: true)
637
- files = Set.new(get_file_candidates(constant))
638
- .merge(Tapioca::Trackers::ConstantDefinition.files_for(constant))
639
-
640
- return !strict if files.empty?
641
-
642
- files.any? do |file|
643
- gem.contains_path?(file)
644
- end
645
- end
646
-
647
- sig do
648
- params(
649
- mod: Module,
650
- mixin_type: Trackers::Mixin::Type,
651
- mixin_locations: T::Hash[Trackers::Mixin::Type, T::Hash[Module, T::Array[String]]]
652
- ).returns(T::Boolean)
653
- end
654
- def mixed_in_by_gem?(mod, mixin_type, mixin_locations)
655
- locations = mixin_locations.dig(mixin_type, mod)
656
- return true unless locations
657
- locations.any? { |location| gem.contains_path?(location) }
658
- end
659
-
660
- sig { params(constant: Module).returns(T::Array[String]) }
661
- def get_file_candidates(constant)
662
- wrapped_module = Pry::WrappedModule.new(constant)
663
-
664
- wrapped_module.candidates.map(&:file).to_a.compact
665
- rescue ArgumentError, NameError
666
- []
667
- end
668
-
669
- sig { params(name: String).void }
670
- def add_to_alias_namespace(name)
671
- @alias_namespace.add("#{name}::")
672
- end
673
-
674
- sig { params(name: String).returns(T::Boolean) }
675
- def alias_namespaced?(name)
676
- @alias_namespace.any? do |namespace|
677
- name.start_with?(namespace)
678
- end
679
- end
680
-
681
- sig { params(name: String).void }
682
- def mark_seen(name)
683
- @seen.add(name)
684
- end
685
-
686
- sig { params(name: String).returns(T::Boolean) }
687
- def seen?(name)
688
- @seen.include?(name)
689
- end
690
-
691
- sig { params(constant: Module).returns(T.nilable(UnboundMethod)) }
692
- def initialize_method_for(constant)
693
- constant.instance_method(:initialize)
694
- rescue
695
- nil
696
- end
697
-
698
- sig { params(constant: Module).returns(T::Array[Module]) }
699
- def interesting_ancestors_of(constant)
700
- inherited_ancestors_ids = Set.new(
701
- inherited_ancestors_of(constant).map { |mod| object_id_of(mod) }
702
- )
703
- # TODO: There is actually a bug here where this will drop modules that
704
- # may be included twice. For example:
705
- #
706
- # ```ruby
707
- # class Foo
708
- # prepend Kernel
709
- # end
710
- # ````
711
- # would give:
712
- # ```ruby
713
- # Foo.ancestors #=> [Kernel, Foo, Object, Kernel, BasicObject]
714
- # ````
715
- # but since we drop `Kernel` whenever we match it, we would miss
716
- # the `prepend Kernel` in the output.
717
- #
718
- # Instead, we should only drop the tail matches of the ancestors and
719
- # inherited ancestors, past the location of the constant itself.
720
- constant.ancestors.reject do |mod|
721
- inherited_ancestors_ids.include?(object_id_of(mod))
722
- end
723
- end
724
-
725
- sig { params(constant: Module).returns(T.nilable(String)) }
726
- def name_of(constant)
727
- name = name_of_proxy_target(constant, super(class_of(constant)))
728
- return name if name
729
- name = super(constant)
730
- return if name.nil?
731
- return unless are_equal?(constant, constantize(name, inherit: true))
732
- name = "Struct" if name =~ /^(::)?Struct::[^:]+$/
733
- name
734
- end
735
-
736
- sig { params(constant: T.all(Module, T::Generic)).returns(String) }
737
- def generic_name_of(constant)
738
- type_name = T.must(constant.name)
739
- return type_name if type_name =~ /\[.*\]$/
740
-
741
- type_variables = Tapioca::GenericTypeRegistry.lookup_type_variables(constant)
742
- return type_name unless type_variables
743
-
744
- type_variable_names = type_variables.map { "T.untyped" }.join(", ")
745
-
746
- "#{type_name}[#{type_variable_names}]"
747
- end
748
-
749
- sig { params(constant: Module, class_name: T.nilable(String)).returns(T.nilable(String)) }
750
- def name_of_proxy_target(constant, class_name)
751
- return unless class_name == "ActiveSupport::Deprecation::DeprecatedConstantProxy"
752
- # We are dealing with a ActiveSupport::Deprecation::DeprecatedConstantProxy
753
- # so try to get the name of the target class
754
- begin
755
- target = constant.__send__(:target)
756
- rescue NoMethodError
757
- return
758
- end
759
-
760
- name_of(target)
761
- end
762
-
763
- sig { params(name: String).returns(T::Array[RBI::Comment]) }
764
- def documentation_comments(name)
765
- return [] unless @include_doc
766
-
767
- yard_docs = YARD::Registry.at(name)
768
- return [] unless yard_docs
769
-
770
- docstring = yard_docs.docstring
771
- return [] if /(copyright|license)/i.match?(docstring)
772
-
773
- docstring.lines
774
- .reject { |line| IGNORED_COMMENTS.any? { |comment| line.include?(comment) } }
775
- .map! { |line| RBI::Comment.new(line) }
776
- end
777
- end
778
- end
779
- end
780
- end