tapioca 0.11.8 → 0.11.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +116 -49
  3. data/lib/tapioca/cli.rb +76 -67
  4. data/lib/tapioca/commands/{dsl.rb → abstract_dsl.rb} +32 -78
  5. data/lib/tapioca/commands/{gem.rb → abstract_gem.rb} +26 -93
  6. data/lib/tapioca/commands/annotations.rb +9 -7
  7. data/lib/tapioca/commands/check_shims.rb +2 -0
  8. data/lib/tapioca/commands/command.rb +9 -2
  9. data/lib/tapioca/commands/configure.rb +2 -2
  10. data/lib/tapioca/commands/dsl_compiler_list.rb +31 -0
  11. data/lib/tapioca/commands/dsl_generate.rb +40 -0
  12. data/lib/tapioca/commands/dsl_verify.rb +25 -0
  13. data/lib/tapioca/commands/gem_generate.rb +51 -0
  14. data/lib/tapioca/commands/gem_sync.rb +37 -0
  15. data/lib/tapioca/commands/gem_verify.rb +36 -0
  16. data/lib/tapioca/commands/require.rb +2 -0
  17. data/lib/tapioca/commands/todo.rb +21 -2
  18. data/lib/tapioca/commands.rb +8 -2
  19. data/lib/tapioca/dsl/compiler.rb +8 -4
  20. data/lib/tapioca/dsl/compilers/action_controller_helpers.rb +3 -1
  21. data/lib/tapioca/dsl/compilers/active_model_validations_confirmation.rb +94 -0
  22. data/lib/tapioca/dsl/compilers/active_record_columns.rb +19 -9
  23. data/lib/tapioca/dsl/compilers/active_record_fixtures.rb +11 -14
  24. data/lib/tapioca/dsl/compilers/active_record_store.rb +149 -0
  25. data/lib/tapioca/dsl/compilers/active_record_typed_store.rb +3 -2
  26. data/lib/tapioca/dsl/compilers/active_support_concern.rb +1 -1
  27. data/lib/tapioca/dsl/compilers/graphql_input_object.rb +1 -1
  28. data/lib/tapioca/dsl/compilers/graphql_mutation.rb +9 -2
  29. data/lib/tapioca/dsl/compilers/json_api_client_resource.rb +208 -0
  30. data/lib/tapioca/dsl/extensions/active_record.rb +9 -0
  31. data/lib/tapioca/dsl/helpers/active_record_column_type_helper.rb +23 -4
  32. data/lib/tapioca/dsl/helpers/active_record_constants_helper.rb +1 -0
  33. data/lib/tapioca/dsl/helpers/graphql_type_helper.rb +18 -3
  34. data/lib/tapioca/dsl/pipeline.rb +6 -4
  35. data/lib/tapioca/gem/pipeline.rb +103 -36
  36. data/lib/tapioca/gemfile.rb +13 -7
  37. data/lib/tapioca/helpers/git_attributes.rb +34 -0
  38. data/lib/tapioca/helpers/test/template.rb +4 -4
  39. data/lib/tapioca/internal.rb +1 -0
  40. data/lib/tapioca/loaders/dsl.rb +11 -1
  41. data/lib/tapioca/loaders/loader.rb +17 -2
  42. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +0 -27
  43. data/lib/tapioca/static/symbol_loader.rb +10 -9
  44. data/lib/tapioca/version.rb +1 -1
  45. metadata +20 -10
@@ -0,0 +1,208 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "json_api_client"
6
+ rescue LoadError
7
+ # means JsonApiClient is not installed,
8
+ # so let's not even define the compiler.
9
+ return
10
+ end
11
+
12
+ module Tapioca
13
+ module Dsl
14
+ module Compilers
15
+ # `Tapioca::Dsl::Compilers::JsonApiClientResource` generates RBI files for classes that inherit
16
+ # [`JsonApiClient::Resource`](https://github.com/JsonApiClient/json_api_client).
17
+ #
18
+ # For example, with the following classes that inherits `JsonApiClient::Resource`:
19
+ #
20
+ # ~~~rb
21
+ # # user.rb
22
+ # class User < JsonApiClient::Resource
23
+ # has_many :posts
24
+ #
25
+ # property :name, type: :string
26
+ # property :is_admin, type: :boolean, default: false
27
+ # end
28
+ #
29
+ # # post.rb
30
+ # class Post < JsonApiClient::Resource
31
+ # belongs_to :user
32
+ #
33
+ # property :title, type: :string
34
+ # end
35
+ # ~~~
36
+ #
37
+ # this compiler will produce RBI files with the following content:
38
+ #
39
+ # ~~~rbi
40
+ # # user.rbi
41
+ # # typed: strong
42
+ #
43
+ # class User
44
+ # include JsonApiClientResourceGeneratedMethods
45
+ #
46
+ # module JsonApiClientResourceGeneratedMethods
47
+ # sig { returns(T::Boolean) }
48
+ # def is_admin; end
49
+ #
50
+ # sig { params(is_admin: T::Boolean).returns(T::Boolean) }
51
+ # def is_admin=(is_admin); end
52
+ #
53
+ # sig { returns(T.nilable(::String)) }
54
+ # def name; end
55
+ #
56
+ # sig { params(name: T.nilable(::String)).returns(T.nilable(::String)) }
57
+ # def name=(name); end
58
+ #
59
+ # sig { returns(T.nilable(T::Array[Post])) }
60
+ # def posts; end
61
+ #
62
+ # sig { params(posts: T.nilable(T::Array[Post])).returns(T.nilable(T::Array[Post])) }
63
+ # def posts=(posts); end
64
+ # end
65
+ # end
66
+ #
67
+ # # post.rbi
68
+ # # typed: strong
69
+ #
70
+ # class Post
71
+ # include JsonApiClientResourceGeneratedMethods
72
+ #
73
+ # module JsonApiClientResourceGeneratedMethods
74
+ # sig { returns(T.nilable(::String)) }
75
+ # def title; end
76
+ #
77
+ # sig { params(title: T.nilable(::String)).returns(T.nilable(::String)) }
78
+ # def title=(title); end
79
+ #
80
+ # sig { returns(T.nilable(::String)) }
81
+ # def user_id; end
82
+ #
83
+ # sig { params(user_id: T.nilable(::String)).returns(T.nilable(::String)) }
84
+ # def user_id=(user_id); end
85
+ # end
86
+ # end
87
+ # ~~~
88
+ class JsonApiClientResource < Compiler
89
+ extend T::Sig
90
+
91
+ ConstantType = type_member { { fixed: T.class_of(::JsonApiClient::Resource) } }
92
+
93
+ sig { override.void }
94
+ def decorate
95
+ schema = resource_schema
96
+ return if schema.nil? && constant.associations.empty?
97
+
98
+ root.create_path(constant) do |k|
99
+ module_name = "JsonApiClientResourceGeneratedMethods"
100
+ k.create_module(module_name) do |mod|
101
+ schema&.each_property do |property|
102
+ generate_methods_for_property(mod, property)
103
+ end
104
+
105
+ constant.associations.each do |association|
106
+ generate_methods_for_association(mod, association)
107
+ end
108
+ end
109
+
110
+ k.create_include(module_name)
111
+ end
112
+ end
113
+
114
+ class << self
115
+ extend T::Sig
116
+
117
+ sig { override.returns(T::Enumerable[Module]) }
118
+ def gather_constants
119
+ all_modules.select do |c|
120
+ name_of(c) && c < ::JsonApiClient::Resource
121
+ end
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ sig { returns(T.nilable(::JsonApiClient::Schema)) }
128
+ def resource_schema
129
+ schema = constant.schema
130
+
131
+ # empty? does not exist on JsonApiClient::Schema
132
+ schema if schema.size > 0 # rubocop:disable Style/ZeroLengthPredicate
133
+ end
134
+
135
+ sig do
136
+ params(
137
+ mod: RBI::Scope,
138
+ property: ::JsonApiClient::Schema::Property,
139
+ ).void
140
+ end
141
+ def generate_methods_for_property(mod, property)
142
+ type = type_for(property)
143
+
144
+ name = property.name.to_s
145
+
146
+ mod.create_method(name, return_type: type)
147
+ mod.create_method("#{name}=", parameters: [create_param(name, type: type)], return_type: type)
148
+ end
149
+
150
+ sig { params(property: ::JsonApiClient::Schema::Property).returns(String) }
151
+ def type_for(property)
152
+ type = ::JsonApiClient::Schema::TypeFactory.type_for(property.type)
153
+ return "T.untyped" if type.nil?
154
+
155
+ sorbet_type = if type.respond_to?(:sorbet_type)
156
+ type.sorbet_type
157
+ elsif type == ::JsonApiClient::Schema::Types::Integer
158
+ "::Integer"
159
+ elsif type == ::JsonApiClient::Schema::Types::String
160
+ "::String"
161
+ elsif type == ::JsonApiClient::Schema::Types::Float
162
+ "::Float"
163
+ elsif type == ::JsonApiClient::Schema::Types::Time
164
+ "::Time"
165
+ elsif type == ::JsonApiClient::Schema::Types::Decimal
166
+ "::BigDecimal"
167
+ elsif type == ::JsonApiClient::Schema::Types::Boolean
168
+ "T::Boolean"
169
+ else
170
+ "T.untyped"
171
+ end
172
+
173
+ if property.default.nil?
174
+ as_nilable_type(sorbet_type)
175
+ else
176
+ sorbet_type
177
+ end
178
+ end
179
+
180
+ sig do
181
+ params(
182
+ mod: RBI::Scope,
183
+ association: JsonApiClient::Associations::BaseAssociation,
184
+ ).void
185
+ end
186
+ def generate_methods_for_association(mod, association)
187
+ # If the association is broken, it will raise a NameError when trying to access the association_class
188
+ klass = association.association_class
189
+
190
+ name, type = case association
191
+ when ::JsonApiClient::Associations::BelongsTo::Association
192
+ # id must be a string: # https://jsonapi.org/format/#document-resource-object-identification
193
+ [association.param.to_s, "T.nilable(::String)"]
194
+ when ::JsonApiClient::Associations::HasOne::Association
195
+ [association.attr_name.to_s, "T.nilable(#{klass})"]
196
+ when ::JsonApiClient::Associations::HasMany::Association
197
+ [association.attr_name.to_s, "T.nilable(T::Array[#{klass}])"]
198
+ else
199
+ return # Unsupported association type
200
+ end
201
+
202
+ mod.create_method(name, return_type: type)
203
+ mod.create_method("#{name}=", parameters: [create_param(name, type: type)], return_type: type)
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -30,6 +30,15 @@ module Tapioca
30
30
  super
31
31
  end
32
32
 
33
+ attr_reader :__tapioca_stored_attributes
34
+
35
+ def store_accessor(store_attribute, *keys, prefix: nil, suffix: nil)
36
+ @__tapioca_stored_attributes ||= []
37
+ @__tapioca_stored_attributes << [store_attribute, keys, prefix, suffix]
38
+
39
+ super
40
+ end
41
+
33
42
  ::ActiveRecord::Base.singleton_class.prepend(self)
34
43
  end
35
44
  end
@@ -13,8 +13,26 @@ module Tapioca
13
13
  @constant = constant
14
14
  end
15
15
 
16
+ sig { params(attribute_name: String, column_name: String).returns([String, String]) }
17
+ def type_for(attribute_name, column_name = attribute_name)
18
+ return id_type if attribute_name == "id"
19
+
20
+ column_type_for(column_name)
21
+ end
22
+
23
+ private
24
+
25
+ sig { returns([String, String]) }
26
+ def id_type
27
+ if @constant.respond_to?(:composite_primary_key?) && T.unsafe(@constant).composite_primary_key?
28
+ @constant.primary_key.map(&method(:column_type_for)).map { |tuple| "[#{tuple.join(", ")}]" }
29
+ else
30
+ column_type_for(@constant.primary_key)
31
+ end
32
+ end
33
+
16
34
  sig { params(column_name: String).returns([String, String]) }
17
- def type_for(column_name)
35
+ def column_type_for(column_name)
18
36
  return ["T.untyped", "T.untyped"] if do_not_generate_strong_types?(@constant)
19
37
 
20
38
  column = @constant.columns_hash[column_name]
@@ -33,7 +51,7 @@ module Tapioca
33
51
  return [getter_type, as_nilable_type(setter_type)]
34
52
  end
35
53
 
36
- if column_name == @constant.primary_key ||
54
+ if Array(@constant.primary_key).include?(column_name) ||
37
55
  column_name == "created_at" ||
38
56
  column_name == "updated_at"
39
57
  getter_type = as_nilable_type(getter_type)
@@ -42,8 +60,6 @@ module Tapioca
42
60
  [getter_type, setter_type]
43
61
  end
44
62
 
45
- private
46
-
47
63
  sig { params(column_type: T.untyped).returns(String) }
48
64
  def type_for_activerecord_value(column_type)
49
65
  case column_type
@@ -69,6 +85,9 @@ module Tapioca
69
85
  "::String"
70
86
  when ActiveRecord::Type::Serialized
71
87
  serialized_column_type(column_type)
88
+ when defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid) &&
89
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid
90
+ "::String"
72
91
  when defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Hstore) &&
73
92
  ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Hstore
74
93
  "T::Hash[::String, ::String]"
@@ -15,6 +15,7 @@ module Tapioca
15
15
  AssociationMethodsModuleName = T.let("GeneratedAssociationMethods", String)
16
16
  DelegatedTypesModuleName = T.let("GeneratedDelegatedTypeMethods", String)
17
17
  SecureTokensModuleName = T.let("GeneratedSecureTokenMethods", String)
18
+ StoredAttributesModuleName = T.let("GeneratedStoredAttributesMethods", String)
18
19
 
19
20
  RelationMethodsModuleName = T.let("GeneratedRelationMethods", String)
20
21
  AssociationRelationMethodsModuleName = T.let("GeneratedAssociationRelationMethods", String)
@@ -9,8 +9,16 @@ module Tapioca
9
9
 
10
10
  extend T::Sig
11
11
 
12
- sig { params(type: GraphQL::Schema::Wrapper).returns(String) }
13
- def type_for(type)
12
+ sig { params(argument: GraphQL::Schema::Argument).returns(String) }
13
+ def type_for(argument)
14
+ type = if argument.loads
15
+ loads_type = ::GraphQL::Schema::Wrapper.new(argument.loads)
16
+ loads_type = loads_type.to_list_type if argument.type.list?
17
+ loads_type = loads_type.to_non_null_type if argument.type.non_null?
18
+ loads_type
19
+ else
20
+ argument.type
21
+ end
14
22
  unwrapped_type = type.unwrap
15
23
 
16
24
  parsed_type = case unwrapped_type
@@ -39,6 +47,8 @@ module Tapioca
39
47
  end
40
48
  when GraphQL::Schema::InputObject.singleton_class
41
49
  type_for_constant(unwrapped_type)
50
+ when Module
51
+ Runtime::Reflection.qualified_name_of(unwrapped_type) || "T.untyped"
42
52
  else
43
53
  "T.untyped"
44
54
  end
@@ -47,7 +57,7 @@ module Tapioca
47
57
  parsed_type = "T::Array[#{parsed_type}]"
48
58
  end
49
59
 
50
- unless type.non_null?
60
+ unless type.non_null? || has_replaceable_default?(argument)
51
61
  parsed_type = RBIHelper.as_nilable_type(parsed_type)
52
62
  end
53
63
 
@@ -60,6 +70,11 @@ module Tapioca
60
70
  def type_for_constant(constant)
61
71
  Runtime::Reflection.qualified_name_of(constant) || "T.untyped"
62
72
  end
73
+
74
+ sig { params(argument: GraphQL::Schema::Argument).returns(T::Boolean) }
75
+ def has_replaceable_default?(argument)
76
+ !!argument.replace_null_with_default? && !argument.default_value.nil?
77
+ end
63
78
  end
64
79
  end
65
80
  end
@@ -105,10 +105,12 @@ module Tapioca
105
105
 
106
106
  sig { returns(T::Array[T.class_of(Compiler)]) }
107
107
  def compilers
108
- @compilers = T.let(@compilers, T.nilable(T::Array[T.class_of(Compiler)]))
109
- @compilers ||= Runtime::Reflection.descendants_of(Compiler).sort_by do |compiler|
110
- T.must(compiler.name)
111
- end
108
+ @compilers ||= T.let(
109
+ Runtime::Reflection.descendants_of(Compiler).sort_by do |compiler|
110
+ T.must(compiler.name)
111
+ end,
112
+ T.nilable(T::Array[T.class_of(Compiler)]),
113
+ )
112
114
  end
113
115
 
114
116
  private
@@ -50,6 +50,8 @@ module Tapioca
50
50
  @root
51
51
  end
52
52
 
53
+ # Events handling
54
+
53
55
  sig { params(symbol: String).void }
54
56
  def push_symbol(symbol)
55
57
  @events << Gem::SymbolFound.new(symbol)
@@ -98,6 +100,8 @@ module Tapioca
98
100
  @events << Gem::MethodNodeAdded.new(symbol, constant, method, node, signature, parameters)
99
101
  end
100
102
 
103
+ # Constants and properties filtering
104
+
101
105
  sig { params(symbol_name: String).returns(T::Boolean) }
102
106
  def symbol_in_payload?(symbol_name)
103
107
  symbol_name = symbol_name[2..-1] if symbol_name.start_with?("::")
@@ -106,16 +110,27 @@ module Tapioca
106
110
  @payload_symbols.include?(symbol_name)
107
111
  end
108
112
 
113
+ # this looks something like:
114
+ # "(eval at /path/to/file.rb:123)"
115
+ # and we are just interested in the "/path/to/file.rb" part
116
+ EVAL_SOURCE_FILE_PATTERN = T.let(/\(eval at (.+):\d+\)/, Regexp)
117
+
109
118
  sig { params(name: T.any(String, Symbol)).returns(T::Boolean) }
110
119
  def constant_in_gem?(name)
111
120
  return true unless Object.respond_to?(:const_source_location)
112
121
 
113
- source_location, _ = Object.const_source_location(name)
114
- return true unless source_location
122
+ source_file, _ = Object.const_source_location(name)
123
+ return true unless source_file
115
124
  # If the source location of the constant is "(eval)", all bets are off.
116
- return true if source_location == "(eval)"
125
+ return true if source_file == "(eval)"
117
126
 
118
- gem.contains_path?(source_location)
127
+ # Ruby 3.3 adds automatic definition of source location for evals if
128
+ # `file` and `line` arguments are not provided. This results in the source
129
+ # file being something like `(eval at /path/to/file.rb:123)`. We try to parse
130
+ # this string to get the actual source file.
131
+ source_file = source_file.sub(EVAL_SOURCE_FILE_PATTERN, "\\1")
132
+
133
+ gem.contains_path?(source_file)
119
134
  end
120
135
 
121
136
  sig { params(method: UnboundMethod).returns(T::Boolean) }
@@ -126,6 +141,8 @@ module Tapioca
126
141
  @gem.contains_path?(source_location)
127
142
  end
128
143
 
144
+ # Helpers
145
+
129
146
  sig { params(constant: Module).returns(T.nilable(String)) }
130
147
  def name_of(constant)
131
148
  name = name_of_proxy_target(constant, super(class_of(constant)))
@@ -149,6 +166,8 @@ module Tapioca
149
166
  gem_symbols.union(engine_symbols)
150
167
  end
151
168
 
169
+ # Events handling
170
+
152
171
  sig { returns(Gem::Event) }
153
172
  def next_event
154
173
  T.must(@events.shift)
@@ -171,7 +190,7 @@ module Tapioca
171
190
  sig { params(event: Gem::SymbolFound).void }
172
191
  def on_symbol(event)
173
192
  symbol = event.symbol.delete_prefix("::")
174
- return if symbol_in_payload?(symbol) && !@bootstrap_symbols.include?(symbol)
193
+ return if skip_symbol?(symbol)
175
194
 
176
195
  constant = constantize(symbol)
177
196
  push_constant(symbol, constant) if Runtime::Reflection.constant_defined?(constant)
@@ -180,13 +199,7 @@ module Tapioca
180
199
  sig { params(event: Gem::ConstantFound).void.checked(:never) }
181
200
  def on_constant(event)
182
201
  name = event.symbol
183
-
184
- return if name.strip.empty?
185
- return if name.start_with?("#<")
186
- return if name.downcase == name
187
- return if alias_namespaced?(name)
188
-
189
- return if T::Enum === event.constant # T::Enum instances are defined via `compile_enums`
202
+ return if skip_constant?(name, event.constant)
190
203
 
191
204
  if event.is_a?(Gem::ForeignConstantFound)
192
205
  compile_foreign_constant(name, event.constant)
@@ -200,11 +213,17 @@ module Tapioca
200
213
  @node_listeners.each { |listener| listener.dispatch(event) }
201
214
  end
202
215
 
203
- # Compile
216
+ # Compiling
204
217
 
205
218
  sig { params(symbol: String, constant: Module).void }
206
219
  def compile_foreign_constant(symbol, constant)
207
- compile_module(symbol, constant, foreign_constant: true)
220
+ return if skip_foreign_constant?(symbol, constant)
221
+ return if seen?(symbol)
222
+
223
+ seen!(symbol)
224
+
225
+ scope = compile_scope(symbol, constant)
226
+ push_foreign_scope(symbol, constant, scope)
208
227
  end
209
228
 
210
229
  sig { params(symbol: String, constant: BasicObject).void.checked(:never) }
@@ -225,10 +244,9 @@ module Tapioca
225
244
  def compile_alias(name, constant)
226
245
  return if seen?(name)
227
246
 
228
- mark_seen(name)
247
+ seen!(name)
229
248
 
230
- return if symbol_in_payload?(name)
231
- return unless constant_in_gem?(name)
249
+ return if skip_alias?(name, constant)
232
250
 
233
251
  target = name_of(constant)
234
252
  # If target has no name, let's make it an anonymous class or module with `Class.new` or `Module.new`
@@ -247,10 +265,9 @@ module Tapioca
247
265
  def compile_object(name, value)
248
266
  return if seen?(name)
249
267
 
250
- mark_seen(name)
268
+ seen!(name)
251
269
 
252
- return if symbol_in_payload?(name)
253
- return unless constant_in_gem?(name)
270
+ return if skip_object?(name, value)
254
271
 
255
272
  klass = class_of(value)
256
273
 
@@ -279,29 +296,29 @@ module Tapioca
279
296
  @root << node
280
297
  end
281
298
 
282
- sig { params(name: String, constant: Module, foreign_constant: T::Boolean).void }
283
- def compile_module(name, constant, foreign_constant: false)
284
- return unless defined_in_gem?(constant, strict: false) || foreign_constant
285
- return if Tapioca::TypeVariableModule === constant
299
+ sig { params(name: String, constant: Module).void }
300
+ def compile_module(name, constant)
301
+ return if skip_module?(name, constant)
286
302
  return if seen?(name)
287
303
 
288
- mark_seen(name)
304
+ seen!(name)
289
305
 
290
- scope =
291
- if constant.is_a?(Class)
292
- superclass = compile_superclass(constant)
293
- RBI::Class.new(name, superclass_name: superclass)
294
- else
295
- RBI::Module.new(name)
296
- end
306
+ scope = compile_scope(name, constant)
307
+ push_scope(name, constant, scope)
308
+ end
297
309
 
298
- if foreign_constant
299
- push_foreign_scope(name, constant, scope)
310
+ sig { params(name: String, constant: Module).returns(RBI::Scope) }
311
+ def compile_scope(name, constant)
312
+ scope = if constant.is_a?(Class)
313
+ superclass = compile_superclass(constant)
314
+ RBI::Class.new(name, superclass_name: superclass)
300
315
  else
301
- push_scope(name, constant, scope)
316
+ RBI::Module.new(name)
302
317
  end
303
318
 
304
319
  @root << scope
320
+
321
+ scope
305
322
  end
306
323
 
307
324
  sig { params(constant: T::Class[T.anything]).returns(T.nilable(String)) }
@@ -353,6 +370,54 @@ module Tapioca
353
370
  "::#{name}"
354
371
  end
355
372
 
373
+ # Constants and properties filtering
374
+
375
+ sig { params(name: String).returns(T::Boolean) }
376
+ def skip_symbol?(name)
377
+ symbol_in_payload?(name) && !@bootstrap_symbols.include?(name)
378
+ end
379
+
380
+ sig { params(name: String, constant: T.anything).returns(T::Boolean).checked(:never) }
381
+ def skip_constant?(name, constant)
382
+ return true if name.strip.empty?
383
+ return true if name.start_with?("#<")
384
+ return true if name.downcase == name
385
+ return true if alias_namespaced?(name)
386
+
387
+ return true if T::Enum === constant # T::Enum instances are defined via `compile_enums`
388
+
389
+ false
390
+ end
391
+
392
+ sig { params(name: String, constant: Module).returns(T::Boolean) }
393
+ def skip_alias?(name, constant)
394
+ return true if symbol_in_payload?(name)
395
+ return true unless constant_in_gem?(name)
396
+
397
+ false
398
+ end
399
+
400
+ sig { params(name: String, constant: BasicObject).returns(T::Boolean).checked(:never) }
401
+ def skip_object?(name, constant)
402
+ return true if symbol_in_payload?(name)
403
+ return true unless constant_in_gem?(name)
404
+
405
+ false
406
+ end
407
+
408
+ sig { params(name: String, constant: Module).returns(T::Boolean) }
409
+ def skip_foreign_constant?(name, constant)
410
+ Tapioca::TypeVariableModule === constant
411
+ end
412
+
413
+ sig { params(name: String, constant: Module).returns(T::Boolean) }
414
+ def skip_module?(name, constant)
415
+ return true unless defined_in_gem?(constant, strict: false)
416
+ return true if Tapioca::TypeVariableModule === constant
417
+
418
+ false
419
+ end
420
+
356
421
  sig { params(constant: Module, strict: T::Boolean).returns(T::Boolean) }
357
422
  def defined_in_gem?(constant, strict: true)
358
423
  files = get_file_candidates(constant)
@@ -385,7 +450,7 @@ module Tapioca
385
450
  end
386
451
 
387
452
  sig { params(name: String).void }
388
- def mark_seen(name)
453
+ def seen!(name)
389
454
  @seen.add(name)
390
455
  end
391
456
 
@@ -394,6 +459,8 @@ module Tapioca
394
459
  @seen.include?(name)
395
460
  end
396
461
 
462
+ # Helpers
463
+
397
464
  sig { params(constant: T.all(Module, T::Generic)).returns(String) }
398
465
  def generic_name_of(constant)
399
466
  type_name = T.must(constant.name)
@@ -156,6 +156,11 @@ module Tapioca
156
156
  @spec.name
157
157
  end
158
158
 
159
+ sig { returns(T::Array[::Gem::Dependency]) }
160
+ def dependencies
161
+ @spec.dependencies
162
+ end
163
+
159
164
  sig { returns(String) }
160
165
  def rbi_file_name
161
166
  "#{name}@#{version}.rbi"
@@ -230,13 +235,14 @@ module Tapioca
230
235
 
231
236
  sig { returns(Regexp) }
232
237
  def require_paths_prefix_matcher
233
- @require_paths_prefix_matcher = T.let(@require_paths_prefix_matcher, T.nilable(Regexp))
234
-
235
- @require_paths_prefix_matcher ||= begin
236
- require_paths = T.unsafe(@spec).require_paths
237
- prefix_matchers = require_paths.map { |rp| Regexp.new("^#{rp}/") }
238
- Regexp.union(prefix_matchers)
239
- end
238
+ @require_paths_prefix_matcher ||= T.let(
239
+ begin
240
+ require_paths = T.unsafe(@spec).require_paths
241
+ prefix_matchers = require_paths.map { |rp| Regexp.new("^#{rp}/") }
242
+ Regexp.union(prefix_matchers)
243
+ end,
244
+ T.nilable(Regexp),
245
+ )
240
246
  end
241
247
 
242
248
  sig { params(file: String).returns(Pathname) }
@@ -0,0 +1,34 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ class GitAttributes
5
+ class << self
6
+ extend T::Sig
7
+
8
+ sig { params(path: Pathname).void }
9
+ def create_generated_attribute_file(path)
10
+ create_gitattributes_file(path, <<~CONTENT)
11
+ **/*.rbi linguist-generated=true
12
+ CONTENT
13
+ end
14
+
15
+ sig { params(path: Pathname).void }
16
+ def create_vendored_attribute_file(path)
17
+ create_gitattributes_file(path, <<~CONTENT)
18
+ **/*.rbi linguist-vendored=true
19
+ CONTENT
20
+ end
21
+
22
+ private
23
+
24
+ sig { params(path: Pathname, content: String).void }
25
+ def create_gitattributes_file(path, content)
26
+ # We don't want to start creating folders, just to write
27
+ # the `.gitattributes` file. So, if the folder doesn't
28
+ # exist, we just return.
29
+ return unless path.exist?
30
+
31
+ File.write(path.join(".gitattributes"), content)
32
+ end
33
+ end
34
+ end
@@ -24,12 +24,12 @@ module Tapioca
24
24
  ::Gem::Requirement.new(selector).satisfied_by?(ActiveSupport.gem_version)
25
25
  end
26
26
 
27
- sig { params(src: String).returns(String) }
28
- def template(src)
27
+ sig { params(src: String, trim_mode: String).returns(String) }
28
+ def template(src, trim_mode: ">")
29
29
  erb = if ERB_SUPPORTS_KVARGS
30
- ::ERB.new(src, trim_mode: ">")
30
+ ::ERB.new(src, trim_mode: trim_mode)
31
31
  else
32
- ::ERB.new(src, nil, ">")
32
+ ::ERB.new(src, nil, trim_mode)
33
33
  end
34
34
 
35
35
  erb.result(binding)