tapioca 0.7.0 → 0.8.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 (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