tapioca 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/README.md +491 -73
  4. data/lib/tapioca/cli.rb +40 -3
  5. data/lib/tapioca/commands/annotations.rb +154 -0
  6. data/lib/tapioca/commands/dsl.rb +20 -1
  7. data/lib/tapioca/commands/gem.rb +17 -57
  8. data/lib/tapioca/commands/init.rb +1 -0
  9. data/lib/tapioca/commands/todo.rb +4 -2
  10. data/lib/tapioca/commands.rb +1 -0
  11. data/lib/tapioca/dsl/compiler.rb +2 -2
  12. data/lib/tapioca/dsl/compilers/aasm.rb +1 -1
  13. data/lib/tapioca/dsl/compilers/action_controller_helpers.rb +1 -1
  14. data/lib/tapioca/dsl/compilers/action_mailer.rb +1 -1
  15. data/lib/tapioca/dsl/compilers/active_job.rb +1 -1
  16. data/lib/tapioca/dsl/compilers/active_model_attributes.rb +1 -1
  17. data/lib/tapioca/dsl/compilers/active_model_secure_password.rb +1 -1
  18. data/lib/tapioca/dsl/compilers/active_record_associations.rb +1 -1
  19. data/lib/tapioca/dsl/compilers/active_record_columns.rb +1 -1
  20. data/lib/tapioca/dsl/compilers/active_record_enum.rb +1 -1
  21. data/lib/tapioca/dsl/compilers/active_record_fixtures.rb +3 -1
  22. data/lib/tapioca/dsl/compilers/active_record_relations.rb +8 -8
  23. data/lib/tapioca/dsl/compilers/active_record_scope.rb +1 -1
  24. data/lib/tapioca/dsl/compilers/active_record_typed_store.rb +1 -1
  25. data/lib/tapioca/dsl/compilers/active_resource.rb +1 -1
  26. data/lib/tapioca/dsl/compilers/active_storage.rb +6 -2
  27. data/lib/tapioca/dsl/compilers/active_support_concern.rb +1 -1
  28. data/lib/tapioca/dsl/compilers/active_support_current_attributes.rb +1 -1
  29. data/lib/tapioca/dsl/compilers/config.rb +2 -2
  30. data/lib/tapioca/dsl/compilers/frozen_record.rb +1 -1
  31. data/lib/tapioca/dsl/compilers/identity_cache.rb +1 -1
  32. data/lib/tapioca/dsl/compilers/mixed_in_class_attributes.rb +1 -1
  33. data/lib/tapioca/dsl/compilers/protobuf.rb +27 -3
  34. data/lib/tapioca/dsl/compilers/rails_generators.rb +1 -1
  35. data/lib/tapioca/dsl/compilers/sidekiq_worker.rb +1 -1
  36. data/lib/tapioca/dsl/compilers/smart_properties.rb +1 -1
  37. data/lib/tapioca/dsl/compilers/state_machines.rb +1 -1
  38. data/lib/tapioca/dsl/compilers/url_helpers.rb +5 -2
  39. data/lib/tapioca/dsl/helpers/param_helper.rb +4 -1
  40. data/lib/tapioca/dsl/pipeline.rb +32 -1
  41. data/lib/tapioca/dsl.rb +6 -0
  42. data/lib/tapioca/executor.rb +4 -46
  43. data/lib/tapioca/gem/listeners/methods.rb +26 -1
  44. data/lib/tapioca/gem/listeners/sorbet_props.rb +1 -1
  45. data/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb +1 -0
  46. data/lib/tapioca/gem/listeners/sorbet_signatures.rb +1 -1
  47. data/lib/tapioca/gem/pipeline.rb +5 -1
  48. data/lib/tapioca/gemfile.rb +50 -3
  49. data/lib/tapioca/helpers/config_helper.rb +13 -0
  50. data/lib/tapioca/helpers/rbi_helper.rb +114 -7
  51. data/lib/tapioca/helpers/shims_helper.rb +36 -8
  52. data/lib/tapioca/helpers/signatures_helper.rb +17 -0
  53. data/lib/tapioca/helpers/sorbet_helper.rb +5 -11
  54. data/lib/tapioca/helpers/test/content.rb +1 -0
  55. data/lib/tapioca/helpers/test/dsl_compiler.rb +1 -0
  56. data/lib/tapioca/helpers/test/template.rb +1 -0
  57. data/lib/tapioca/helpers/type_variable_helper.rb +43 -0
  58. data/lib/tapioca/internal.rb +4 -1
  59. data/lib/tapioca/rbi_ext/model.rb +14 -2
  60. data/lib/tapioca/repo_index.rb +41 -0
  61. data/lib/tapioca/runtime/generic_type_registry.rb +4 -2
  62. data/lib/tapioca/runtime/loader.rb +3 -0
  63. data/lib/tapioca/runtime/reflection.rb +17 -13
  64. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +38 -21
  65. data/lib/tapioca/static/symbol_table_parser.rb +2 -0
  66. data/lib/tapioca/version.rb +1 -1
  67. data/lib/tapioca.rb +5 -0
  68. metadata +26 -21
@@ -46,8 +46,12 @@ module Tapioca
46
46
  class ActiveStorage < Compiler
47
47
  extend T::Sig
48
48
 
49
- ConstantType = type_member(fixed: T.all(Module,
50
- ::ActiveStorage::Reflection::ActiveRecordExtensions::ClassMethods))
49
+ ConstantType = type_member do
50
+ {
51
+ fixed: T.all(Module,
52
+ ::ActiveStorage::Reflection::ActiveRecordExtensions::ClassMethods),
53
+ }
54
+ end
51
55
 
52
56
  sig { override.void }
53
57
  def decorate
@@ -44,7 +44,7 @@ module Tapioca
44
44
  class ActiveSupportConcern < Compiler
45
45
  extend T::Sig
46
46
 
47
- ConstantType = type_member(fixed: Module)
47
+ ConstantType = type_member { { fixed: Module } }
48
48
 
49
49
  sig { override.void }
50
50
  def decorate
@@ -62,7 +62,7 @@ module Tapioca
62
62
  class ActiveSupportCurrentAttributes < Compiler
63
63
  extend T::Sig
64
64
 
65
- ConstantType = type_member(fixed: T.class_of(::ActiveSupport::CurrentAttributes))
65
+ ConstantType = type_member { { fixed: T.class_of(::ActiveSupport::CurrentAttributes) } }
66
66
 
67
67
  sig { override.void }
68
68
  def decorate
@@ -49,7 +49,7 @@ module Tapioca
49
49
 
50
50
  CONFIG_OPTIONS_SUFFIX = "ConfigOptions"
51
51
 
52
- ConstantType = type_member(fixed: Module)
52
+ ConstantType = type_member { { fixed: Module } }
53
53
 
54
54
  sig { override.void }
55
55
  def decorate
@@ -77,7 +77,7 @@ module Tapioca
77
77
  # enumerates the entries, we don't make any assumptions about their
78
78
  # types.
79
79
  mod.create_extend("T::Generic")
80
- mod.create_type_member("Elem", value: "type_member(fixed: T.untyped)")
80
+ mod.create_type_variable("Elem", type: "type_member", fixed: "T.untyped")
81
81
 
82
82
  method_names.each do |method_name|
83
83
  # Create getter method
@@ -65,7 +65,7 @@ module Tapioca
65
65
  class FrozenRecord < Compiler
66
66
  extend T::Sig
67
67
 
68
- ConstantType = type_member(fixed: T.class_of(::FrozenRecord::Base))
68
+ ConstantType = type_member { { fixed: T.class_of(::FrozenRecord::Base) } }
69
69
 
70
70
  sig { override.void }
71
71
  def decorate
@@ -69,7 +69,7 @@ module Tapioca
69
69
  T.proc.params(type: T.any(Module, String)).returns(String)
70
70
  )
71
71
 
72
- ConstantType = type_member(fixed: T.class_of(::ActiveRecord::Base))
72
+ ConstantType = type_member { { fixed: T.class_of(::ActiveRecord::Base) } }
73
73
 
74
74
  sig { override.void }
75
75
  def decorate
@@ -51,7 +51,7 @@ module Tapioca
51
51
  class MixedInClassAttributes < Compiler
52
52
  extend T::Sig
53
53
 
54
- ConstantType = type_member(fixed: Module)
54
+ ConstantType = type_member { { fixed: Module } }
55
55
 
56
56
  sig { override.void }
57
57
  def decorate
@@ -70,7 +70,9 @@ module Tapioca
70
70
 
71
71
  extend T::Sig
72
72
 
73
- ConstantType = type_member(fixed: Module)
73
+ ConstantType = type_member { { fixed: Module } }
74
+
75
+ FIELD_RE = /^[a-z_][a-zA-Z0-9_]*$/
74
76
 
75
77
  sig { override.void }
76
78
  def decorate
@@ -81,6 +83,7 @@ module Tapioca
81
83
  create_type_members(klass, "Key", "Value")
82
84
  else
83
85
  descriptor = T.let(T.unsafe(constant).descriptor, Google::Protobuf::Descriptor)
86
+ descriptor.each_oneof { |oneof| create_oneof_method(klass, oneof) }
84
87
  fields = descriptor.map { |desc| create_descriptor_method(klass, desc) }
85
88
  fields.sort_by!(&:name)
86
89
 
@@ -88,7 +91,15 @@ module Tapioca
88
91
  create_kw_opt_param(field.name, type: field.init_type, default: field.default)
89
92
  end
90
93
 
91
- klass.create_method("initialize", parameters: parameters, return_type: "void")
94
+ if fields.all? { |field| FIELD_RE.match?(field.name) }
95
+ klass.create_method("initialize", parameters: parameters, return_type: "void")
96
+ else
97
+ # One of the fields has an incorrect name for a named parameter so creating the default initialize for
98
+ # it would create a RBI with a syntax error.
99
+ # The workaround is to create an initialize that takes a **kwargs instead.
100
+ kwargs_parameter = create_kw_rest_param("fields", type: "T.untyped")
101
+ klass.create_method("initialize", parameters: [kwargs_parameter], return_type: "void")
102
+ end
92
103
  end
93
104
  end
94
105
  end
@@ -107,7 +118,7 @@ module Tapioca
107
118
  klass.create_extend("T::Generic")
108
119
 
109
120
  names.each do |name|
110
- klass.create_type_member(name)
121
+ klass.create_type_variable(name, type: "type_member")
111
122
  end
112
123
  end
113
124
 
@@ -206,6 +217,19 @@ module Tapioca
206
217
 
207
218
  field
208
219
  end
220
+
221
+ sig do
222
+ params(
223
+ klass: RBI::Scope,
224
+ desc: Google::Protobuf::OneofDescriptor
225
+ ).void
226
+ end
227
+ def create_oneof_method(klass, desc)
228
+ klass.create_method(
229
+ desc.name,
230
+ return_type: "T.nilable(Symbol)"
231
+ )
232
+ end
209
233
  end
210
234
  end
211
235
  end
@@ -46,7 +46,7 @@ module Tapioca
46
46
  Regexp
47
47
  )
48
48
 
49
- ConstantType = type_member(fixed: T.class_of(::Rails::Generators::Base))
49
+ ConstantType = type_member { { fixed: T.class_of(::Rails::Generators::Base) } }
50
50
 
51
51
  sig { override.void }
52
52
  def decorate
@@ -43,7 +43,7 @@ module Tapioca
43
43
  class SidekiqWorker < Compiler
44
44
  extend T::Sig
45
45
 
46
- ConstantType = type_member(fixed: T.class_of(::Sidekiq::Worker))
46
+ ConstantType = type_member { { fixed: T.class_of(::Sidekiq::Worker) } }
47
47
 
48
48
  sig { override.void }
49
49
  def decorate
@@ -63,7 +63,7 @@ module Tapioca
63
63
  class SmartProperties < Compiler
64
64
  extend T::Sig
65
65
 
66
- ConstantType = type_member(fixed: T.class_of(::SmartProperties))
66
+ ConstantType = type_member { { fixed: T.class_of(::SmartProperties) } }
67
67
 
68
68
  sig { override.void }
69
69
  def decorate
@@ -118,7 +118,7 @@ module Tapioca
118
118
  class StateMachines < Compiler
119
119
  extend T::Sig
120
120
 
121
- ConstantType = type_member(fixed: T.all(Module, ::StateMachines::ClassMethods))
121
+ ConstantType = type_member { { fixed: T.all(Module, ::StateMachines::ClassMethods) } }
122
122
 
123
123
  sig { override.void }
124
124
  def decorate
@@ -87,7 +87,7 @@ module Tapioca
87
87
  class UrlHelpers < Compiler
88
88
  extend T::Sig
89
89
 
90
- ConstantType = type_member(fixed: Module)
90
+ ConstantType = type_member { { fixed: Module } }
91
91
 
92
92
  sig { override.void }
93
93
  def decorate
@@ -109,6 +109,8 @@ module Tapioca
109
109
 
110
110
  sig { override.returns(T::Enumerable[Module]) }
111
111
  def self.gather_constants
112
+ return [] unless Rails.application
113
+
112
114
  Object.const_set(:GeneratedUrlHelpersModule, Rails.application.routes.named_routes.url_helpers_module)
113
115
  Object.const_set(:GeneratedPathHelpersModule, Rails.application.routes.named_routes.path_helpers_module)
114
116
 
@@ -160,7 +162,8 @@ module Tapioca
160
162
  superclass_ancestors = ancestors_of(superclass) if superclass
161
163
  end
162
164
 
163
- (ancestors_of(mod) - superclass_ancestors).any? { |ancestor| helper == ancestor }
165
+ ancestors = Set.new.compare_by_identity.merge(ancestors_of(mod)).subtract(superclass_ancestors)
166
+ ancestors.any? { |ancestor| helper == ancestor }
164
167
  end
165
168
  end
166
169
  end
@@ -1,11 +1,14 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "tapioca/helpers/signatures_helper"
5
+
4
6
  module Tapioca
5
7
  module Dsl
6
8
  module Helpers
7
9
  module ParamHelper
8
10
  extend T::Sig
11
+ include SignaturesHelper
9
12
 
10
13
  sig { params(name: String, type: String).returns(RBI::TypedParam) }
11
14
  def create_param(name, type:)
@@ -44,7 +47,7 @@ module Tapioca
44
47
 
45
48
  sig { params(param: RBI::Param, type: String).returns(RBI::TypedParam) }
46
49
  def create_typed_param(param, type)
47
- RBI::TypedParam.new(param: param, type: type)
50
+ RBI::TypedParam.new(param: param, type: sanitize_signature_types(type))
48
51
  end
49
52
  end
50
53
  end
@@ -53,7 +53,7 @@ module Tapioca
53
53
  end
54
54
  def run(&blk)
55
55
  constants_to_process = gather_constants(requested_constants)
56
- .select { |c| Runtime::Reflection.name_of(c) && Module === c } # Filter anonymous or value constants
56
+ .select { |c| Module === c } # Filter value constants out
57
57
  .sort_by! { |c| T.must(Runtime::Reflection.name_of(c)) }
58
58
 
59
59
  if constants_to_process.empty?
@@ -112,18 +112,49 @@ module Tapioca
112
112
  sig { params(requested_constants: T::Array[Module]).returns(T::Set[Module]) }
113
113
  def gather_constants(requested_constants)
114
114
  constants = compilers.map(&:processable_constants).reduce(Set.new, :union)
115
+ constants = filter_anonymous_and_reloaded_constants(constants)
116
+
115
117
  constants &= requested_constants unless requested_constants.empty?
116
118
  constants
117
119
  end
118
120
 
121
+ sig { params(constants: T::Set[Module]).returns(T::Set[Module]) }
122
+ def filter_anonymous_and_reloaded_constants(constants)
123
+ # Group constants by their names
124
+ constants_by_name = constants
125
+ .group_by { |c| T.must(Runtime::Reflection.name_of(c)) }
126
+ .select { |name, _| !name.nil? }
127
+
128
+ # Find the constants that have been reloaded
129
+ reloaded_constants = constants_by_name.select { |_, constants| constants.size > 1 }.keys
130
+
131
+ unless reloaded_constants.empty?
132
+ reloaded_constant_names = reloaded_constants.map { |name| "`#{name}`" }.join(", ")
133
+
134
+ $stderr.puts("WARNING: Multiple constants with the same name: #{reloaded_constant_names}")
135
+ $stderr.puts("Make sure some object is not holding onto these constants during an app reload.")
136
+ end
137
+
138
+ # Look up all the constants back from their names. The resulting constant set will be the
139
+ # set of constants that are actually in memory with those names.
140
+ constants_by_name
141
+ .keys
142
+ .map { |name| T.cast(Runtime::Reflection.constantize(name), Module) }
143
+ .to_set
144
+ end
145
+
119
146
  sig { params(constant: Module).returns(T.nilable(RBI::File)) }
120
147
  def rbi_for_constant(constant)
121
148
  file = RBI::File.new(strictness: "true")
122
149
 
123
150
  compilers.each do |compiler_class|
124
151
  next unless compiler_class.handles?(constant)
152
+
125
153
  compiler = compiler_class.new(self, file.root, constant)
126
154
  compiler.decorate
155
+ rescue
156
+ $stderr.puts("Error: `#{compiler_class.name}` failed to generate RBI for `#{constant}`")
157
+ raise # This is an unexpected error, so re-raise it
127
158
  end
128
159
 
129
160
  return if file.root.empty?
@@ -0,0 +1,6 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "tapioca"
5
+ require "tapioca/runtime/reflection"
6
+ require "tapioca/dsl/compiler"
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "etc"
5
+ require "parallel"
5
6
 
6
7
  module Tapioca
7
8
  class Executor
@@ -20,10 +21,6 @@ module Tapioca
20
21
  number_of_workers || [Etc.nprocessors, (queue.length.to_f / MINIMUM_ITEMS_PER_WORKER).ceil].min,
21
22
  Integer
22
23
  )
23
-
24
- # The number of items that will be processed per worker, so that we can split the queue into groups and assign
25
- # them to each one of the workers
26
- @items_per_worker = T.let((queue.length.to_f / @number_of_workers).ceil, Integer)
27
24
  end
28
25
 
29
26
  sig do
@@ -32,48 +29,9 @@ module Tapioca
32
29
  ).returns(T::Array[T.type_parameter(:T)])
33
30
  end
34
31
  def run_in_parallel(&block)
35
- # If we only have one worker selected, it's not worth forking, just run sequentially
36
- return @queue.map { |item| block.call(item) } if @number_of_workers == 1
37
-
38
- read_pipes = []
39
- write_pipes = []
40
-
41
- # If we have more than one worker, fork the pool by shifting the expected number of items per worker from the
42
- # queue
43
- workers = (0...@number_of_workers).map do
44
- items = @queue.shift(@items_per_worker)
45
-
46
- # Each worker has their own pair of pipes, so that we can read the result from each worker separately
47
- read, write = IO.pipe
48
- read_pipes << read
49
- write_pipes << write
50
-
51
- fork do
52
- read.close
53
- result = items.map { |item| block.call(item) }
54
-
55
- # Pack the result as a Base64 string of the Marshal dump of the array of values returned by the block that we
56
- # ran in parallel
57
- packed = [Marshal.dump(result)].pack("m")
58
- write.puts(packed)
59
- write.close
60
- end
61
- end
62
-
63
- # Close all the write pipes, then read and close from all the read pipes
64
- write_pipes.each(&:close)
65
- result = read_pipes.map do |pipe|
66
- content = pipe.read
67
- pipe.close
68
- content
69
- end
70
-
71
- # Wait until all the workers finish. Notice that waiting for the PIDs can only happen after we read and close the
72
- # pipe or else we may end up in a condition where writing to the pipe hangs indefinitely
73
- workers.each { |pid| Process.waitpid(pid) }
74
-
75
- # Decode the value back into the Ruby objects by doing the inverse of what each worker does
76
- result.flat_map { |item| T.unsafe(Marshal.load(item.unpack1("m"))) }
32
+ # To have the parallel gem run jobs in the parent process, you must pass 0 as the number of processes
33
+ number_of_processes = @number_of_workers == 1 ? 0 : @number_of_workers
34
+ Parallel.map(@queue, { in_processes: number_of_processes }, &block)
77
35
  end
78
36
  end
79
37
  end
@@ -41,6 +41,7 @@ module Tapioca
41
41
  .each do |visibility, method_list|
42
42
  method_list.sort!.map do |name|
43
43
  next if name == :initialize
44
+
44
45
  vis = case visibility
45
46
  when :protected
46
47
  RBI::Protected.new
@@ -65,7 +66,7 @@ module Tapioca
65
66
  end
66
67
  def compile_method(tree, symbol_name, constant, method, visibility = RBI::Public.new)
67
68
  return unless method
68
- return unless method.owner == constant
69
+ return unless method_owned_by_constant?(method, constant)
69
70
  return if @pipeline.symbol_in_payload?(symbol_name) && !@pipeline.method_in_gem?(method)
70
71
 
71
72
  signature = signature_of(method)
@@ -141,6 +142,29 @@ module Tapioca
141
142
  tree << rbi_method
142
143
  end
143
144
 
145
+ # Check whether the method is defined by the constant.
146
+ #
147
+ # In most cases, it works to check that the constant is the method owner. However,
148
+ # in the case that a method is also defined in a module prepended to the constant, it
149
+ # will be owned by the prepended module, not the constant.
150
+ #
151
+ # This method implements a better way of checking whether a constant defines a method.
152
+ # It walks up the ancestor tree via the `super_method` method; if any of the super
153
+ # methods are owned by the constant, it means that the constant declares the method.
154
+ sig { params(method: UnboundMethod, constant: Module).returns(T::Boolean) }
155
+ def method_owned_by_constant?(method, constant)
156
+ # Widen the type of `method` to be nilable
157
+ method = T.let(method, T.nilable(UnboundMethod))
158
+
159
+ while method
160
+ return true if method.owner == constant
161
+
162
+ method = method.super_method
163
+ end
164
+
165
+ false
166
+ end
167
+
144
168
  sig { params(mod: Module).returns(T::Hash[Symbol, T::Array[Symbol]]) }
145
169
  def method_names_by_visibility(mod)
146
170
  {
@@ -163,6 +187,7 @@ module Tapioca
163
187
  sig { params(name: String).returns(T::Boolean) }
164
188
  def valid_method_name?(name)
165
189
  return true if SPECIAL_METHOD_NAMES.include?(name)
190
+
166
191
  !!name.match(/^[[:word:]]+[?!=]?$/)
167
192
  end
168
193
 
@@ -19,7 +19,7 @@ module Tapioca
19
19
  constant.props.map do |name, prop|
20
20
  type = prop.fetch(:type_object, "T.untyped").to_s.gsub(".returns(<VOID>)", ".void")
21
21
 
22
- default = prop.key?(:default) ? "T.unsafe(nil)" : nil
22
+ default = prop.key?(:default) || prop.key?(:factory) ? "T.unsafe(nil)" : nil
23
23
  node << if prop.fetch(:immutable, false)
24
24
  RBI::TStructConst.new(name.to_s, type, default: default)
25
25
  else
@@ -14,6 +14,7 @@ module Tapioca
14
14
  ancestors = Runtime::Trackers::RequiredAncestor.required_ancestors_by(event.constant)
15
15
  ancestors.each do |ancestor|
16
16
  next unless ancestor # TODO: We should have a way to warn from here
17
+
17
18
  event.node << RBI::RequiresAncestor.new(ancestor.to_s)
18
19
  end
19
20
  end
@@ -8,7 +8,7 @@ module Tapioca
8
8
  extend T::Sig
9
9
 
10
10
  include Runtime::Reflection
11
- include RBIHelper
11
+ include SignaturesHelper
12
12
 
13
13
  TYPE_PARAMETER_MATCHER = /T\.type_parameter\(:?([[:word:]]+)\)/
14
14
 
@@ -8,7 +8,7 @@ module Tapioca
8
8
  class Pipeline
9
9
  extend T::Sig
10
10
  include Runtime::Reflection
11
- include RBIHelper
11
+ include SignaturesHelper
12
12
 
13
13
  IGNORED_SYMBOLS = T.let(["YAML", "MiniTest", "Mutex"], T::Array[String])
14
14
 
@@ -87,6 +87,7 @@ module Tapioca
87
87
  def symbol_in_payload?(symbol_name)
88
88
  symbol_name = symbol_name[2..-1] if symbol_name.start_with?("::")
89
89
  return false unless symbol_name
90
+
90
91
  @payload_symbols.include?(symbol_name)
91
92
  end
92
93
 
@@ -102,9 +103,11 @@ module Tapioca
102
103
  def name_of(constant)
103
104
  name = name_of_proxy_target(constant, super(class_of(constant)))
104
105
  return name if name
106
+
105
107
  name = super(constant)
106
108
  return if name.nil?
107
109
  return unless are_equal?(constant, constantize(name, inherit: true))
110
+
108
111
  name = "Struct" if name =~ /^(::)?Struct::[^:]+$/
109
112
  name
110
113
  end
@@ -350,6 +353,7 @@ module Tapioca
350
353
  sig { params(constant: Module, class_name: T.nilable(String)).returns(T.nilable(String)) }
351
354
  def name_of_proxy_target(constant, class_name)
352
355
  return unless class_name == "ActiveSupport::Deprecation::DeprecatedConstantProxy"
356
+
353
357
  # We are dealing with a ActiveSupport::Deprecation::DeprecatedConstantProxy
354
358
  # so try to get the name of the target class
355
359
  begin
@@ -16,6 +16,50 @@ module Tapioca
16
16
  )
17
17
  end
18
18
 
19
+ # This is a module that gets prepended to `Bundler::Dependency` and
20
+ # makes sure even gems marked as `require: false` are required during
21
+ # `Bundler.require`.
22
+ module AutoRequireHook
23
+ extend T::Sig
24
+ extend T::Helpers
25
+
26
+ requires_ancestor { ::Bundler::Dependency }
27
+
28
+ @exclude = T.let([], T::Array[String])
29
+
30
+ class << self
31
+ extend T::Sig
32
+
33
+ sig { params(exclude: T::Array[String]).returns(T::Array[String]) }
34
+ attr_writer :exclude
35
+
36
+ sig { params(name: T.untyped).returns(T::Boolean) }
37
+ def excluded?(name)
38
+ @exclude.include?(name)
39
+ end
40
+ end
41
+
42
+ sig { returns(T.untyped).checked(:never) }
43
+ def autorequire
44
+ value = super
45
+
46
+ # If the gem is excluded, we don't want to force require it, in case
47
+ # it has side-effects users don't want. For example, `fakefs` gem, if
48
+ # loaded, takes over filesystem operations.
49
+ return value if AutoRequireHook.excluded?(name)
50
+
51
+ # If a gem is marked as `require: false`, then its `autorequire`
52
+ # value will be `[]`. But, we want those gems to be loaded for our
53
+ # purposes as well, so we return `nil` in those cases, instead, which
54
+ # means `require: true`.
55
+ return nil if value == []
56
+
57
+ value
58
+ end
59
+
60
+ ::Bundler::Dependency.prepend(self)
61
+ end
62
+
19
63
  sig { returns(Bundler::Definition) }
20
64
  attr_reader(:definition)
21
65
 
@@ -25,8 +69,9 @@ module Tapioca
25
69
  sig { returns(T::Array[String]) }
26
70
  attr_reader(:missing_specs)
27
71
 
28
- sig { void }
29
- def initialize
72
+ sig { params(exclude: T::Array[String]).void }
73
+ def initialize(exclude)
74
+ AutoRequireHook.exclude = exclude
30
75
  @gemfile = T.let(File.new(Bundler.default_gemfile), File)
31
76
  @lockfile = T.let(File.new(Bundler.default_lockfile), File)
32
77
  @definition = T.let(Bundler::Dsl.evaluate(gemfile, lockfile, {}), Bundler::Definition)
@@ -94,7 +139,8 @@ module Tapioca
94
139
  class GemSpec
95
140
  extend(T::Sig)
96
141
 
97
- IGNORED_GEMS = T.let(["sorbet", "sorbet-static", "sorbet-runtime"].freeze, T::Array[String])
142
+ IGNORED_GEMS = T.let(["sorbet", "sorbet-static", "sorbet-runtime", "sorbet-static-and-runtime"].freeze,
143
+ T::Array[String])
98
144
 
99
145
  sig { returns(String) }
100
146
  attr_reader :full_gem_path, :version
@@ -227,6 +273,7 @@ module Tapioca
227
273
  # one of those folders to see if the path really belongs in the given gem
228
274
  # or not.
229
275
  return false unless Bundler::Source::Git === @spec.source
276
+
230
277
  parent = Pathname.new(path)
231
278
 
232
279
  until parent.root?
@@ -72,6 +72,7 @@ module Tapioca
72
72
  def validate_config!(config_file, config)
73
73
  # To ensure that this is not re-entered, we mark during validation
74
74
  return if @validating_config
75
+
75
76
  @validating_config = T.let(true, T.nilable(T::Boolean))
76
77
 
77
78
  commands = T.cast(self, Thor).class.commands
@@ -125,6 +126,18 @@ module Tapioca
125
126
  error_msg = "invalid value for option `#{config_option_key}` for key `#{config_key}` - expected " \
126
127
  "`#{command_option.type.capitalize}` but found #{config_option_value_type.capitalize}"
127
128
  next build_error(error_msg) unless config_option_value_type == command_option.type
129
+
130
+ case config_option_value_type
131
+ when :array
132
+ error_msg = "invalid value for option `#{config_option_key}` for key `#{config_key}` - expected " \
133
+ "`Array[String]` but found `#{config_option_value}`"
134
+ next build_error(error_msg) unless config_option_value.all? { |v| v.is_a?(String) }
135
+ when :hash
136
+ error_msg = "invalid value for option `#{config_option_key}` for key `#{config_key}` - expected " \
137
+ "`Hash[String, String]` but found `#{config_option_value}`"
138
+ all_strings = (config_option_value.keys + config_option_value.values).all? { |v| v.is_a?(String) }
139
+ next build_error(error_msg) unless all_strings
140
+ end
128
141
  end.compact
129
142
  end
130
143