tapioca 0.11.8 → 0.11.10

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 (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)