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
@@ -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 RBIHelper
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
@@ -99,6 +99,9 @@ module Tapioca
99
99
  sig { returns(String) }
100
100
  attr_reader :full_gem_path, :version
101
101
 
102
+ sig { returns(T::Array[Pathname]) }
103
+ attr_reader :files
104
+
102
105
  sig { params(spec: Spec).void }
103
106
  def initialize(spec)
104
107
  @spec = T.let(spec, Tapioca::Gemfile::Spec)
@@ -106,6 +109,7 @@ module Tapioca
106
109
  @full_gem_path = T.let(real_gem_path, String)
107
110
  @version = T.let(version_string, String)
108
111
  @exported_rbi_files = T.let(nil, T.nilable(T::Array[String]))
112
+ @files = T.let(collect_files, T::Array[Pathname])
109
113
  end
110
114
 
111
115
  sig { params(gemfile_dir: String).returns(T::Boolean) }
@@ -113,21 +117,6 @@ module Tapioca
113
117
  gem_ignored? || gem_in_app_dir?(gemfile_dir)
114
118
  end
115
119
 
116
- sig { returns(T::Array[Pathname]) }
117
- def files
118
- if default_gem?
119
- # `Bundler::RemoteSpecification` delegates missing methods to
120
- # `Gem::Specification`, so `files` actually always exists on spec.
121
- T.unsafe(@spec).files.map do |file|
122
- ruby_lib_dir.join(file)
123
- end
124
- else
125
- @spec.full_require_paths.flat_map do |path|
126
- Pathname.glob((Pathname.new(path) / "**/*.rb").to_s)
127
- end
128
- end
129
- end
130
-
131
120
  sig { returns(String) }
132
121
  def name
133
122
  @spec.name
@@ -154,7 +143,7 @@ module Tapioca
154
143
 
155
144
  sig { returns(T::Array[String]) }
156
145
  def exported_rbi_files
157
- @exported_rbi_files ||= Dir.glob("#{full_gem_path}/rbi/**/*.rbi")
146
+ @exported_rbi_files ||= Dir.glob("#{full_gem_path}/rbi/**/*.rbi").sort
158
147
  end
159
148
 
160
149
  sig { returns(T::Boolean) }
@@ -176,14 +165,49 @@ module Tapioca
176
165
 
177
166
  private
178
167
 
179
- sig { returns(T::Boolean) }
168
+ sig { returns(T::Array[Pathname]) }
169
+ def collect_files
170
+ if default_gem?
171
+ # `Bundler::RemoteSpecification` delegates missing methods to
172
+ # `Gem::Specification`, so `files` actually always exists on spec.
173
+ T.unsafe(@spec).files.map do |file|
174
+ resolve_to_ruby_lib_dir(file)
175
+ end
176
+ else
177
+ @spec.full_require_paths.flat_map do |path|
178
+ Pathname.glob((Pathname.new(path) / "**/*.rb").to_s)
179
+ end
180
+ end
181
+ end
182
+
183
+ sig { returns(T.nilable(T::Boolean)) }
180
184
  def default_gem?
181
185
  @spec.respond_to?(:default_gem?) && @spec.default_gem?
182
186
  end
183
187
 
184
- sig { returns(Pathname) }
185
- def ruby_lib_dir
186
- Pathname.new(RbConfig::CONFIG["rubylibdir"])
188
+ sig { returns(Regexp) }
189
+ def require_paths_prefix_matcher
190
+ @require_paths_prefix_matcher = T.let(@require_paths_prefix_matcher, T.nilable(Regexp))
191
+
192
+ @require_paths_prefix_matcher ||= begin
193
+ require_paths = T.unsafe(@spec).require_paths
194
+ prefix_matchers = require_paths.map { |rp| Regexp.new("^#{rp}/") }
195
+ Regexp.union(prefix_matchers)
196
+ end
197
+ end
198
+
199
+ sig { params(file: String).returns(Pathname) }
200
+ def resolve_to_ruby_lib_dir(file)
201
+ # We want to match require prefixes but fallback to an empty match
202
+ # if none of the require prefixes actually match. This is so that
203
+ # we can always replace the match with the Ruby lib directory and
204
+ # we would have properly resolved the file under the Ruby lib dir.
205
+ prefix_matcher = Regexp.union(require_paths_prefix_matcher, //)
206
+
207
+ ruby_lib_dir = RbConfig::CONFIG["rubylibdir"]
208
+ file = file.sub(prefix_matcher, "#{ruby_lib_dir}/")
209
+
210
+ Pathname.new(file).expand_path
187
211
  end
188
212
 
189
213
  sig { returns(String) }
@@ -12,15 +12,23 @@ module Tapioca
12
12
 
13
13
  sig { params(message: String, color: T.any(Symbol, T::Array[Symbol])).void }
14
14
  def say_error(message = "", *color)
15
- force_new_line = (message.to_s !~ /( |\t)\Z/)
16
- # NOTE: This is a hack. We're no longer subclassing from Thor::Shell::Color
17
- # so we no longer have access to the prepare_message call.
18
- # We should update this to remove this.
19
- buffer = shell.send(:prepare_message, *T.unsafe([message, *T.unsafe(color)]))
20
- buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
15
+ # Thor has its own `say_error` now, but it has two problems:
16
+ # 1. it adds the padding around all the messages, even if they continue on
17
+ # the same line, and
18
+ # 2. it accepts a last parameter which breaks the ability to pass color values
19
+ # as splats.
20
+ #
21
+ # So we implement our own version here to work around those problems.
22
+ shell.indent(-shell.padding) do
23
+ super(message, color)
24
+ end
25
+ end
21
26
 
22
- $stderr.print(buffer)
23
- $stderr.flush
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
24
32
  end
25
33
  end
26
34
  end
@@ -6,6 +6,9 @@ require "yaml"
6
6
  module Tapioca
7
7
  module ConfigHelper
8
8
  extend T::Sig
9
+ extend T::Helpers
10
+
11
+ requires_ancestor { Thor }
9
12
 
10
13
  sig { returns(String) }
11
14
  attr_reader :command_name
@@ -60,9 +63,119 @@ module Tapioca
60
63
  config = YAML.load_file(config_file, fallback: {})
61
64
  end
62
65
 
66
+ validate_config!(config_file, config)
67
+
63
68
  Thor::CoreExt::HashWithIndifferentAccess.new(config[command_name] || {})
64
69
  end
65
70
 
71
+ sig { params(config_file: String, config: T::Hash[T.untyped, T.untyped]).void }
72
+ def validate_config!(config_file, config)
73
+ # To ensure that this is not re-entered, we mark during validation
74
+ return if @validating_config
75
+ @validating_config = T.let(true, T.nilable(T::Boolean))
76
+
77
+ commands = T.cast(self, Thor).class.commands
78
+
79
+ errors = config.flat_map do |config_key, config_options|
80
+ command = commands[config_key.to_s]
81
+
82
+ unless command
83
+ next build_error("unknown key `#{config_key}`")
84
+ end
85
+
86
+ validate_config_options(command.options, config_key, config_options || {})
87
+ end.compact
88
+
89
+ unless errors.empty?
90
+ print_errors(config_file, errors)
91
+ exit(1)
92
+ end
93
+ ensure
94
+ @validating_config = false
95
+ end
96
+
97
+ sig do
98
+ params(
99
+ command_options: T::Hash[Symbol, Thor::Option],
100
+ config_key: String,
101
+ config_options: T::Hash[T.untyped, T.untyped]
102
+ ).returns(T::Array[ConfigError])
103
+ end
104
+ def validate_config_options(command_options, config_key, config_options)
105
+ config_options.map do |config_option_key, config_option_value|
106
+ command_option = command_options[config_option_key.to_sym]
107
+ error_msg = "unknown option `#{config_option_key}` for key `#{config_key}`"
108
+ next build_error(error_msg) unless command_option
109
+
110
+ config_option_value_type = case config_option_value
111
+ when FalseClass, TrueClass
112
+ :boolean
113
+ when Numeric
114
+ :numeric
115
+ when Hash
116
+ :hash
117
+ when Array
118
+ :array
119
+ when String
120
+ :string
121
+ else
122
+ :object
123
+ end
124
+
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
128
+ end.compact
129
+ end
130
+
131
+ class ConfigErrorMessagePart < T::Struct
132
+ const :message, String
133
+ const :colors, T::Array[Symbol]
134
+ end
135
+
136
+ class ConfigError < T::Struct
137
+ const :message_parts, T::Array[ConfigErrorMessagePart]
138
+ end
139
+
140
+ sig { params(msg: String).returns(ConfigError) }
141
+ def build_error(msg)
142
+ parts = msg.split(/(`[^`]+` ?)/)
143
+
144
+ message_parts = parts.map do |part|
145
+ match = part.match(/`([^`]+)`( ?)/)
146
+
147
+ if match
148
+ ConfigErrorMessagePart.new(
149
+ message: "#{match[1]}#{match[2]}",
150
+ colors: [:bold, :blue]
151
+ )
152
+ else
153
+ ConfigErrorMessagePart.new(
154
+ message: part,
155
+ colors: [:yellow]
156
+ )
157
+ end
158
+ end
159
+
160
+ ConfigError.new(
161
+ message_parts: message_parts
162
+ )
163
+ end
164
+
165
+ sig { params(config_file: String, errors: T::Array[ConfigError]).void }
166
+ def print_errors(config_file, errors)
167
+ say_error("\nConfiguration file ", :red)
168
+ say_error("#{config_file} ", :blue, :bold)
169
+ say_error("has the following errors:\n\n", :red)
170
+
171
+ errors.each do |error|
172
+ say_error("- ")
173
+ error.message_parts.each do |part|
174
+ T.unsafe(self).say_error(part.message, *part.colors)
175
+ end
176
+ end
177
+ end
178
+
66
179
  sig do
67
180
  params(options: T.nilable(Thor::CoreExt::HashWithIndifferentAccess))
68
181
  .returns(Thor::CoreExt::HashWithIndifferentAccess)
@@ -0,0 +1,17 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module RBIHelper
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