tapioca 0.6.4 → 0.7.2

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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -2
  3. data/README.md +27 -15
  4. data/Rakefile +10 -14
  5. data/lib/tapioca/cli.rb +65 -80
  6. data/lib/tapioca/{generators/base.rb → commands/command.rb} +16 -9
  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 +86 -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 +13 -11
  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 +28 -34
  21. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_columns.rb +18 -16
  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 +12 -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 +11 -16
  27. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_resource.rb +10 -8
  28. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_storage.rb +14 -10
  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 +11 -9
  32. data/lib/tapioca/{compilers/dsl → dsl/compilers}/frozen_record.rb +13 -11
  33. data/lib/tapioca/{compilers/dsl → dsl/compilers}/identity_cache.rb +23 -22
  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 +22 -10
  36. data/lib/tapioca/{compilers/dsl → dsl/compilers}/rails_generators.rb +12 -13
  37. data/lib/tapioca/{compilers/dsl → dsl/compilers}/sidekiq_worker.rb +14 -13
  38. data/lib/tapioca/{compilers/dsl → dsl/compilers}/smart_properties.rb +11 -9
  39. data/lib/tapioca/{compilers/dsl → dsl/compilers}/state_machines.rb +12 -10
  40. data/lib/tapioca/{compilers/dsl → dsl/compilers}/url_helpers.rb +20 -15
  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 +6 -3
  46. data/lib/tapioca/dsl/pipeline.rb +169 -0
  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/helpers/cli_helper.rb +7 -0
  64. data/lib/tapioca/helpers/config_helper.rb +5 -8
  65. data/lib/tapioca/helpers/shims_helper.rb +87 -0
  66. data/lib/tapioca/helpers/signatures_helper.rb +17 -0
  67. data/lib/tapioca/helpers/sorbet_helper.rb +57 -0
  68. data/lib/tapioca/helpers/test/dsl_compiler.rb +118 -0
  69. data/lib/tapioca/helpers/test/isolation.rb +1 -1
  70. data/lib/tapioca/helpers/test/template.rb +13 -2
  71. data/lib/tapioca/helpers/type_variable_helper.rb +43 -0
  72. data/lib/tapioca/internal.rb +18 -10
  73. data/lib/tapioca/rbi_ext/model.rb +14 -50
  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 +168 -0
  77. data/lib/tapioca/runtime/loader.rb +123 -0
  78. data/lib/tapioca/runtime/reflection.rb +157 -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 +69 -34
  85. data/lib/tapioca/sorbet_ext/name_patch.rb +7 -1
  86. data/lib/tapioca/{compilers → static}/requires_compiler.rb +2 -2
  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 +83 -62
  92. data/lib/tapioca/compilers/dsl/active_record_relations.rb +0 -720
  93. data/lib/tapioca/compilers/dsl/base.rb +0 -195
  94. data/lib/tapioca/compilers/dsl/helper/active_record_constants.rb +0 -27
  95. data/lib/tapioca/compilers/dsl_compiler.rb +0 -134
  96. data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +0 -223
  97. data/lib/tapioca/compilers/sorbet.rb +0 -59
  98. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +0 -780
  99. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +0 -90
  100. data/lib/tapioca/compilers/symbol_table_compiler.rb +0 -17
  101. data/lib/tapioca/compilers/todos_compiler.rb +0 -32
  102. data/lib/tapioca/generators/todo.rb +0 -76
  103. data/lib/tapioca/generators.rb +0 -9
  104. data/lib/tapioca/generic_type_registry.rb +0 -164
  105. data/lib/tapioca/helpers/active_record_column_type_helper.rb +0 -108
  106. data/lib/tapioca/loader.rb +0 -119
  107. data/lib/tapioca/reflection.rb +0 -151
  108. data/lib/tapioca/trackers/autoload.rb +0 -70
  109. data/lib/tapioca/trackers/constant_definition.rb +0 -42
  110. data/lib/tapioca/trackers/mixin.rb +0 -78
@@ -0,0 +1,365 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "pathname"
5
+
6
+ module Tapioca
7
+ module Gem
8
+ class Pipeline
9
+ extend T::Sig
10
+ include Runtime::Reflection
11
+ include SignaturesHelper
12
+
13
+ IGNORED_SYMBOLS = T.let(["YAML", "MiniTest", "Mutex"], T::Array[String])
14
+
15
+ sig { returns(Gemfile::GemSpec) }
16
+ attr_reader :gem
17
+
18
+ sig { params(gem: Gemfile::GemSpec, include_doc: T::Boolean).void }
19
+ def initialize(gem, include_doc: false)
20
+ @root = T.let(RBI::Tree.new, RBI::Tree)
21
+ @gem = gem
22
+ @seen = T.let(Set.new, T::Set[String])
23
+ @alias_namespace = T.let(Set.new, T::Set[String])
24
+
25
+ @events = T.let([], T::Array[Gem::Event])
26
+
27
+ @payload_symbols = T.let(Static::SymbolLoader.payload_symbols, T::Set[String])
28
+ @bootstrap_symbols = T.let(Static::SymbolLoader.gem_symbols(@gem).union(Static::SymbolLoader.engine_symbols),
29
+ T::Set[String])
30
+ @bootstrap_symbols.each { |symbol| push_symbol(symbol) }
31
+
32
+ @node_listeners = T.let([], T::Array[Gem::Listeners::Base])
33
+ @node_listeners << Gem::Listeners::SorbetTypeVariables.new(self)
34
+ @node_listeners << Gem::Listeners::Mixins.new(self)
35
+ @node_listeners << Gem::Listeners::DynamicMixins.new(self)
36
+ @node_listeners << Gem::Listeners::Methods.new(self)
37
+ @node_listeners << Gem::Listeners::SorbetHelpers.new(self)
38
+ @node_listeners << Gem::Listeners::SorbetEnums.new(self)
39
+ @node_listeners << Gem::Listeners::SorbetProps.new(self)
40
+ @node_listeners << Gem::Listeners::SorbetRequiredAncestors.new(self)
41
+ @node_listeners << Gem::Listeners::SorbetSignatures.new(self)
42
+ @node_listeners << Gem::Listeners::Subconstants.new(self)
43
+ @node_listeners << Gem::Listeners::YardDoc.new(self) if include_doc
44
+ @node_listeners << Gem::Listeners::RemoveEmptyPayloadScopes.new(self)
45
+ end
46
+
47
+ sig { returns(RBI::Tree) }
48
+ def compile
49
+ dispatch(next_event) until @events.empty?
50
+ @root
51
+ end
52
+
53
+ sig { params(symbol: String).void }
54
+ def push_symbol(symbol)
55
+ @events << Gem::SymbolFound.new(symbol)
56
+ end
57
+
58
+ sig { params(symbol: String, constant: BasicObject).void.checked(:never) }
59
+ def push_constant(symbol, constant)
60
+ @events << Gem::ConstantFound.new(symbol, constant)
61
+ end
62
+
63
+ sig { params(symbol: String, constant: Module, node: RBI::Const).void.checked(:never) }
64
+ def push_const(symbol, constant, node)
65
+ @events << Gem::ConstNodeAdded.new(symbol, constant, node)
66
+ end
67
+
68
+ sig { params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never) }
69
+ def push_scope(symbol, constant, node)
70
+ @events << Gem::ScopeNodeAdded.new(symbol, constant, node)
71
+ end
72
+
73
+ sig do
74
+ params(
75
+ symbol: String,
76
+ constant: Module,
77
+ node: RBI::Method,
78
+ signature: T.untyped,
79
+ parameters: T::Array[[Symbol, String]]
80
+ ).void.checked(:never)
81
+ end
82
+ def push_method(symbol, constant, node, signature, parameters)
83
+ @events << Gem::MethodNodeAdded.new(symbol, constant, node, signature, parameters)
84
+ end
85
+
86
+ sig { params(symbol_name: String).returns(T::Boolean) }
87
+ def symbol_in_payload?(symbol_name)
88
+ symbol_name = symbol_name[2..-1] if symbol_name.start_with?("::")
89
+ return false unless symbol_name
90
+ @payload_symbols.include?(symbol_name)
91
+ end
92
+
93
+ sig { params(method: UnboundMethod).returns(T::Boolean) }
94
+ def method_in_gem?(method)
95
+ source_location = method.source_location&.first
96
+ return false if source_location.nil?
97
+
98
+ @gem.contains_path?(source_location)
99
+ end
100
+
101
+ sig { params(constant: Module).returns(T.nilable(String)) }
102
+ def name_of(constant)
103
+ name = name_of_proxy_target(constant, super(class_of(constant)))
104
+ return name if name
105
+ name = super(constant)
106
+ return if name.nil?
107
+ return unless are_equal?(constant, constantize(name, inherit: true))
108
+ name = "Struct" if name =~ /^(::)?Struct::[^:]+$/
109
+ name
110
+ end
111
+
112
+ private
113
+
114
+ sig { returns(Gem::Event) }
115
+ def next_event
116
+ T.must(@events.shift)
117
+ end
118
+
119
+ sig { params(event: Gem::Event).void }
120
+ def dispatch(event)
121
+ case event
122
+ when Gem::SymbolFound
123
+ on_symbol(event)
124
+ when Gem::ConstantFound
125
+ on_constant(event)
126
+ when Gem::NodeAdded
127
+ on_node(event)
128
+ else
129
+ raise "Unsupported event #{event.class}"
130
+ end
131
+ end
132
+
133
+ sig { params(event: Gem::SymbolFound).void }
134
+ def on_symbol(event)
135
+ symbol = event.symbol.delete_prefix("::")
136
+ return if symbol_in_payload?(symbol) && !@bootstrap_symbols.include?(symbol)
137
+
138
+ constant = constantize(symbol)
139
+ push_constant(symbol, constant) if constant
140
+ end
141
+
142
+ sig { params(event: Gem::ConstantFound).void.checked(:never) }
143
+ def on_constant(event)
144
+ name = event.symbol
145
+
146
+ return if name.strip.empty?
147
+ return if name.start_with?("#<")
148
+ return if name.downcase == name
149
+ return if alias_namespaced?(name)
150
+ return if seen?(name)
151
+
152
+ constant = event.constant
153
+ return if T::Enum === constant # T::Enum instances are defined via `compile_enums`
154
+
155
+ mark_seen(name)
156
+ compile_constant(name, constant)
157
+ end
158
+
159
+ sig { params(event: Gem::NodeAdded).void }
160
+ def on_node(event)
161
+ @node_listeners.each { |listener| listener.dispatch(event) }
162
+ end
163
+
164
+ # Compile
165
+
166
+ sig { params(symbol: String, constant: BasicObject).void.checked(:never) }
167
+ def compile_constant(symbol, constant)
168
+ case constant
169
+ when Module
170
+ if name_of(constant) != symbol
171
+ compile_alias(symbol, constant)
172
+ else
173
+ compile_module(symbol, constant)
174
+ end
175
+ else
176
+ compile_object(symbol, constant)
177
+ end
178
+ end
179
+
180
+ sig { params(name: String, constant: Module).void }
181
+ def compile_alias(name, constant)
182
+ return if symbol_in_payload?(name)
183
+
184
+ target = name_of(constant)
185
+ # If target has no name, let's make it an anonymous class or module with `Class.new` or `Module.new`
186
+ target = "#{constant.class}.new" unless target
187
+
188
+ add_to_alias_namespace(name)
189
+
190
+ return if IGNORED_SYMBOLS.include?(name)
191
+
192
+ node = RBI::Const.new(name, target)
193
+ push_const(name, constant, node)
194
+ @root << node
195
+ end
196
+
197
+ sig { params(name: String, value: BasicObject).void.checked(:never) }
198
+ def compile_object(name, value)
199
+ return if symbol_in_payload?(name)
200
+
201
+ klass = class_of(value)
202
+
203
+ klass_name = if klass == ObjectSpace::WeakMap
204
+ # WeakMap is an implicit generic with one type variable
205
+ "ObjectSpace::WeakMap[T.untyped]"
206
+ elsif T::Generic === klass
207
+ generic_name_of(klass)
208
+ else
209
+ name_of(klass)
210
+ end
211
+
212
+ if klass_name == "T::Private::Types::TypeAlias"
213
+ type_alias = sanitize_signature_types(T.unsafe(value).aliased_type.to_s)
214
+ node = RBI::Const.new(name, "T.type_alias { #{type_alias} }")
215
+ push_const(name, klass, node)
216
+ @root << node
217
+ return
218
+ end
219
+
220
+ return if klass_name&.start_with?("T::Types::", "T::Private::")
221
+
222
+ type_name = klass_name || "T.untyped"
223
+ node = RBI::Const.new(name, "T.let(T.unsafe(nil), #{type_name})")
224
+ push_const(name, klass, node)
225
+ @root << node
226
+ end
227
+
228
+ sig { params(name: String, constant: Module).void }
229
+ def compile_module(name, constant)
230
+ return unless defined_in_gem?(constant, strict: false)
231
+ return if Tapioca::TypeVariableModule === constant
232
+
233
+ scope =
234
+ if constant.is_a?(Class)
235
+ superclass = compile_superclass(constant)
236
+ RBI::Class.new(name, superclass_name: superclass)
237
+ else
238
+ RBI::Module.new(name)
239
+ end
240
+
241
+ push_scope(name, constant, scope)
242
+ @root << scope
243
+ end
244
+
245
+ sig { params(constant: Class).returns(T.nilable(String)) }
246
+ def compile_superclass(constant)
247
+ superclass = T.let(nil, T.nilable(Class)) # rubocop:disable Lint/UselessAssignment
248
+
249
+ while (superclass = superclass_of(constant))
250
+ constant_name = name_of(constant)
251
+ constant = superclass
252
+
253
+ # Some types have "themselves" as their superclass
254
+ # which can happen via:
255
+ #
256
+ # class A < Numeric; end
257
+ # A = Class.new(A)
258
+ # A.superclass #=> A
259
+ #
260
+ # We compare names here to make sure we skip those
261
+ # superclass instances and walk up the chain.
262
+ #
263
+ # The name comparison is against the name of the constant
264
+ # resolved from the name of the superclass, since
265
+ # this is also possible:
266
+ #
267
+ # B = Class.new
268
+ # class A < B; end
269
+ # B = A
270
+ # A.superclass.name #=> "B"
271
+ # B #=> A
272
+ superclass_name = name_of(superclass)
273
+ next unless superclass_name
274
+
275
+ resolved_superclass = constantize(superclass_name)
276
+ next unless Module === resolved_superclass
277
+ next if name_of(resolved_superclass) == constant_name
278
+
279
+ # We found a suitable superclass
280
+ break
281
+ end
282
+
283
+ return if superclass == ::Object || superclass == ::Delegator
284
+ return if superclass.nil?
285
+
286
+ name = name_of(superclass)
287
+ return if name.nil? || name.empty?
288
+
289
+ push_symbol(name)
290
+
291
+ "::#{name}"
292
+ end
293
+
294
+ sig { params(constant: Module, strict: T::Boolean).returns(T::Boolean) }
295
+ def defined_in_gem?(constant, strict: true)
296
+ files = Set.new(get_file_candidates(constant))
297
+ .merge(Runtime::Trackers::ConstantDefinition.files_for(constant))
298
+
299
+ return !strict if files.empty?
300
+
301
+ files.any? do |file|
302
+ @gem.contains_path?(file)
303
+ end
304
+ end
305
+
306
+ sig { params(constant: Module).returns(T::Array[String]) }
307
+ def get_file_candidates(constant)
308
+ wrapped_module = Pry::WrappedModule.new(constant)
309
+
310
+ wrapped_module.candidates.map(&:file).to_a.compact
311
+ rescue ArgumentError, NameError
312
+ []
313
+ end
314
+
315
+ sig { params(name: String).void }
316
+ def add_to_alias_namespace(name)
317
+ @alias_namespace.add("#{name}::")
318
+ end
319
+
320
+ sig { params(name: String).returns(T::Boolean) }
321
+ def alias_namespaced?(name)
322
+ @alias_namespace.any? do |namespace|
323
+ name.start_with?(namespace)
324
+ end
325
+ end
326
+
327
+ sig { params(name: String).void }
328
+ def mark_seen(name)
329
+ @seen.add(name)
330
+ end
331
+
332
+ sig { params(name: String).returns(T::Boolean) }
333
+ def seen?(name)
334
+ @seen.include?(name)
335
+ end
336
+
337
+ sig { params(constant: T.all(Module, T::Generic)).returns(String) }
338
+ def generic_name_of(constant)
339
+ type_name = T.must(constant.name)
340
+ return type_name if type_name =~ /\[.*\]$/
341
+
342
+ type_variables = Runtime::GenericTypeRegistry.lookup_type_variables(constant)
343
+ return type_name unless type_variables
344
+
345
+ type_variable_names = type_variables.map { "T.untyped" }.join(", ")
346
+
347
+ "#{type_name}[#{type_variable_names}]"
348
+ end
349
+
350
+ sig { params(constant: Module, class_name: T.nilable(String)).returns(T.nilable(String)) }
351
+ def name_of_proxy_target(constant, class_name)
352
+ return unless class_name == "ActiveSupport::Deprecation::DeprecatedConstantProxy"
353
+ # We are dealing with a ActiveSupport::Deprecation::DeprecatedConstantProxy
354
+ # so try to get the name of the target class
355
+ begin
356
+ target = constant.__send__(:target)
357
+ rescue NoMethodError
358
+ return
359
+ end
360
+
361
+ name_of(target)
362
+ end
363
+ end
364
+ end
365
+ end
@@ -23,5 +23,12 @@ module Tapioca
23
23
  super(message, color)
24
24
  end
25
25
  end
26
+
27
+ sig { params(options: T::Hash[Symbol, T.untyped]).returns(RBIFormatter) }
28
+ def rbi_formatter(options)
29
+ rbi_formatter = DEFAULT_RBI_FORMATTER
30
+ rbi_formatter.max_line_length = options[:rbi_max_line_length]
31
+ rbi_formatter
32
+ end
26
33
  end
27
34
  end
@@ -104,10 +104,8 @@ module Tapioca
104
104
  def validate_config_options(command_options, config_key, config_options)
105
105
  config_options.map do |config_option_key, config_option_value|
106
106
  command_option = command_options[config_option_key.to_sym]
107
-
108
- unless command_option
109
- next build_error("unknown option `#{config_option_key}` for key `#{config_key}`")
110
- end
107
+ error_msg = "unknown option `#{config_option_key}` for key `#{config_key}`"
108
+ next build_error(error_msg) unless command_option
111
109
 
112
110
  config_option_value_type = case config_option_value
113
111
  when FalseClass, TrueClass
@@ -124,10 +122,9 @@ module Tapioca
124
122
  :object
125
123
  end
126
124
 
127
- unless config_option_value_type == command_option.type
128
- next build_error("invalid value for option `#{config_option_key}` for key `#{config_key}` " \
129
- "- expected `#{command_option.type.capitalize}` but found #{config_option_value_type.capitalize}")
130
- end
125
+ error_msg = "invalid value for option `#{config_option_key}` for key `#{config_key}` - expected " \
126
+ "`#{command_option.type.capitalize}` but found #{config_option_value_type.capitalize}"
127
+ next build_error(error_msg) unless config_option_value_type == command_option.type
131
128
  end.compact
132
129
  end
133
130
 
@@ -0,0 +1,87 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module ShimsHelper
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ requires_ancestor { Thor::Shell }
10
+
11
+ sig { params(index: RBI::Index, kind: String, dir: String).void }
12
+ def index_rbis(index, kind, dir)
13
+ return unless Dir.exist?(dir) && !Dir.empty?(dir)
14
+
15
+ say("Loading #{kind} RBIs from #{dir}... ")
16
+ files = Dir.glob("#{dir}/**/*.rbi").sort
17
+
18
+ trees = files.map do |file|
19
+ RBI::Parser.parse_file(file)
20
+ rescue RBI::ParseError => e
21
+ say_error("\nWarning: #{e} (#{e.location})", :yellow)
22
+ end.compact
23
+
24
+ index.visit_all(trees)
25
+ say(" Done", :green)
26
+ end
27
+
28
+ sig { params(index: RBI::Index, shim_rbi_dir: String).returns(T::Hash[String, T::Array[RBI::Node]]) }
29
+ def duplicated_nodes_from_index(index, shim_rbi_dir)
30
+ duplicates = {}
31
+ say("Looking for duplicates... ")
32
+ index.keys.each do |key|
33
+ nodes = index[key]
34
+ next unless shims_have_duplicates?(nodes, shim_rbi_dir)
35
+ duplicates[key] = nodes
36
+ end
37
+ say(" Done", :green)
38
+ duplicates
39
+ end
40
+
41
+ private
42
+
43
+ sig { params(nodes: T::Array[RBI::Node], shim_rbi_dir: String).returns(T::Boolean) }
44
+ def shims_have_duplicates?(nodes, shim_rbi_dir)
45
+ return false if nodes.size == 1
46
+
47
+ shims = extract_shims(nodes, shim_rbi_dir)
48
+ return false if shims.empty?
49
+
50
+ props = extract_methods_and_attrs(shims)
51
+ return false if props.empty?
52
+
53
+ shims_with_sigs = extract_nodes_with_sigs(props)
54
+ shims_with_sigs.each do |shim|
55
+ shim_sigs = shim.sigs
56
+
57
+ extract_methods_and_attrs(nodes).each do |node|
58
+ next if node == shim
59
+ return true if shim_sigs.all? { |sig| node.sigs.include?(sig) }
60
+ end
61
+
62
+ return false
63
+ end
64
+
65
+ true
66
+ end
67
+
68
+ sig { params(nodes: T::Array[RBI::Node], shim_rbi_dir: String).returns(T::Array[RBI::Node]) }
69
+ def extract_shims(nodes, shim_rbi_dir)
70
+ nodes.select do |node|
71
+ node.loc&.file&.start_with?(shim_rbi_dir)
72
+ end
73
+ end
74
+
75
+ sig { params(nodes: T::Array[RBI::Node]).returns(T::Array[T.any(RBI::Method, RBI::Attr)]) }
76
+ def extract_methods_and_attrs(nodes)
77
+ T.cast(nodes.select do |node|
78
+ node.is_a?(RBI::Method) || node.is_a?(RBI::Attr)
79
+ end, T::Array[T.any(RBI::Method, RBI::Attr)])
80
+ end
81
+
82
+ sig { params(nodes: T::Array[T.any(RBI::Method, RBI::Attr)]).returns(T::Array[T.any(RBI::Method, RBI::Attr)]) }
83
+ def extract_nodes_with_sigs(nodes)
84
+ nodes.reject { |node| node.sigs.empty? }
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,17 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module SignaturesHelper
6
+ extend T::Sig
7
+
8
+ sig { params(sig_string: String).returns(String) }
9
+ def sanitize_signature_types(sig_string)
10
+ sig_string
11
+ .gsub(".returns(<VOID>)", ".void")
12
+ .gsub("<VOID>", "void")
13
+ .gsub("<NOT-TYPED>", "T.untyped")
14
+ .gsub(".params()", "")
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,57 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "pathname"
5
+ require "shellwords"
6
+
7
+ module Tapioca
8
+ module SorbetHelper
9
+ extend T::Sig
10
+
11
+ SORBET_GEM_SPEC = T.let(
12
+ ::Gem::Specification.find_by_name("sorbet-static"),
13
+ ::Gem::Specification
14
+ )
15
+
16
+ SORBET_BIN = T.let(
17
+ Pathname.new(SORBET_GEM_SPEC.full_gem_path) / "libexec" / "sorbet",
18
+ Pathname
19
+ )
20
+
21
+ SORBET_EXE_PATH_ENV_VAR = "TAPIOCA_SORBET_EXE"
22
+
23
+ FEATURE_REQUIREMENTS = T.let({
24
+ to_ary_nil_support: ::Gem::Requirement.new(">= 0.5.9220"), # https://github.com/sorbet/sorbet/pull/4706
25
+ type_variable_block_syntax: ::Gem::Requirement.new(">= 0.5.9892"), # https://github.com/sorbet/sorbet/pull/5639
26
+ }.freeze, T::Hash[Symbol, ::Gem::Requirement])
27
+
28
+ class CmdResult < T::Struct
29
+ const :out, String
30
+ const :err, String
31
+ const :status, T::Boolean
32
+ end
33
+
34
+ sig { params(sorbet_args: String).returns(CmdResult) }
35
+ def sorbet(*sorbet_args)
36
+ out, err, status = Open3.capture3([sorbet_path, *sorbet_args].join(" "))
37
+ CmdResult.new(out: out, err: err, status: status.success? || false)
38
+ end
39
+
40
+ sig { returns(String) }
41
+ def sorbet_path
42
+ sorbet_path = ENV.fetch(SORBET_EXE_PATH_ENV_VAR, SORBET_BIN)
43
+ sorbet_path = SORBET_BIN if sorbet_path.empty?
44
+ sorbet_path.to_s.shellescape
45
+ end
46
+
47
+ sig { params(feature: Symbol, version: T.nilable(::Gem::Version)).returns(T::Boolean) }
48
+ def sorbet_supports?(feature, version: nil)
49
+ version = SORBET_GEM_SPEC.version unless version
50
+ requirement = FEATURE_REQUIREMENTS[feature]
51
+
52
+ Kernel.raise "Invalid Sorbet feature #{feature}" unless requirement
53
+
54
+ requirement.satisfied_by?(version)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,118 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "tapioca/helpers/test/content"
5
+ require "tapioca/helpers/test/isolation"
6
+ require "tapioca/helpers/test/template"
7
+
8
+ module Tapioca
9
+ module Helpers
10
+ module Test
11
+ module DslCompiler
12
+ extend T::Sig
13
+ extend T::Helpers
14
+
15
+ include Isolation
16
+ include Content
17
+ include Template
18
+
19
+ requires_ancestor { Kernel }
20
+
21
+ sig { params(compiler_class: T.class_of(Tapioca::Dsl::Compiler)).void }
22
+ def use_dsl_compiler(compiler_class)
23
+ @context = T.let(CompilerContext.new(compiler_class), T.nilable(CompilerContext))
24
+ end
25
+
26
+ sig { params(compiler_classes: T.class_of(Tapioca::Dsl::Compiler)).void }
27
+ def activate_other_dsl_compilers(*compiler_classes)
28
+ context.activate_other_dsl_compilers(compiler_classes)
29
+ end
30
+
31
+ sig { params(constant_name: T.any(Symbol, String)).returns(String) }
32
+ def rbi_for(constant_name)
33
+ context.rbi_for(constant_name)
34
+ end
35
+
36
+ sig { returns(T::Array[String]) }
37
+ def gathered_constants
38
+ context.gathered_constants
39
+ end
40
+
41
+ sig { returns(T::Array[String]) }
42
+ def generated_errors
43
+ context.errors
44
+ end
45
+
46
+ sig { returns(CompilerContext) }
47
+ def context
48
+ raise "Please call `use_dsl_compiler` before" unless @context
49
+ @context
50
+ end
51
+
52
+ class CompilerContext
53
+ extend T::Sig
54
+
55
+ sig { returns(T.class_of(Tapioca::Dsl::Compiler)) }
56
+ attr_reader :compiler_class
57
+
58
+ sig { returns(T::Array[T.class_of(Tapioca::Dsl::Compiler)]) }
59
+ attr_reader :other_compiler_classes
60
+
61
+ sig { params(compiler_class: T.class_of(Tapioca::Dsl::Compiler)).void }
62
+ def initialize(compiler_class)
63
+ @compiler_class = compiler_class
64
+ @other_compiler_classes = T.let([], T::Array[T.class_of(Tapioca::Dsl::Compiler)])
65
+ @pipeline = T.let(nil, T.nilable(Tapioca::Dsl::Pipeline))
66
+ @errors = T.let([], T::Array[String])
67
+ end
68
+
69
+ sig { params(compiler_classes: T::Array[T.class_of(Tapioca::Dsl::Compiler)]).void }
70
+ def activate_other_dsl_compilers(compiler_classes)
71
+ @other_compiler_classes = compiler_classes
72
+ end
73
+
74
+ sig { returns(T::Array[T.class_of(Tapioca::Dsl::Compiler)]) }
75
+ def activated_compiler_classes
76
+ [compiler_class, *other_compiler_classes]
77
+ end
78
+
79
+ sig { returns(T::Array[String]) }
80
+ def gathered_constants
81
+ compiler_class.processable_constants.map(&:name).compact.sort
82
+ end
83
+
84
+ sig { params(constant_name: T.any(Symbol, String)).returns(String) }
85
+ def rbi_for(constant_name)
86
+ # Make sure this is a constant that we can handle.
87
+ unless gathered_constants.include?(constant_name.to_s)
88
+ raise "`#{constant_name}` is not processable by the `#{compiler_class}` compiler."
89
+ end
90
+
91
+ file = RBI::File.new(strictness: "strong")
92
+ constant = Object.const_get(constant_name)
93
+
94
+ compiler = compiler_class.new(pipeline, file.root, constant)
95
+ compiler.decorate
96
+
97
+ Tapioca::DEFAULT_RBI_FORMATTER.print_file(file)
98
+ end
99
+
100
+ sig { returns(T::Array[String]) }
101
+ def errors
102
+ pipeline.errors
103
+ end
104
+
105
+ private
106
+
107
+ sig { returns(Tapioca::Dsl::Pipeline) }
108
+ def pipeline
109
+ @pipeline ||= Tapioca::Dsl::Pipeline.new(
110
+ requested_constants: [],
111
+ requested_compilers: activated_compiler_classes
112
+ )
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -100,7 +100,7 @@ module Tapioca
100
100
  load_path_args << File.expand_path(p)
101
101
  end
102
102
 
103
- child = IO.popen([env, Gem.ruby, *load_path_args, $PROGRAM_NAME, *ORIG_ARGV, test_opts])
103
+ child = IO.popen([env, ::Gem.ruby, *load_path_args, $PROGRAM_NAME, *ORIG_ARGV, test_opts])
104
104
 
105
105
  begin
106
106
  Process.wait(child.pid)