tapioca 0.14.2 → 0.14.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f8cfe1d421099c152f602f65792f694adf8a880014589ff23dc16547c60897d
4
- data.tar.gz: c941e055640e4ef5caf4a26ed50268908fce7098b51c4423144615e04295f6a1
3
+ metadata.gz: 6f75efebcf13cb6839056d8869fe4d2d6e2051605a90edf517904d8a14a33913
4
+ data.tar.gz: 41fcfa1d3da8c919577146cbbc6a92ad07a62b9e7ac077642ac78290ffb2ecc2
5
5
  SHA512:
6
- metadata.gz: 3d6522f57d604948443018620f625859e8676ddd2978a6e2c6b9252ee39a055526a1fe7dd3ff76f3ed330e3c00f9580fed5ef2fb4a86fdb87c572007f1b43297
7
- data.tar.gz: f1e3c8fbfcfe728906c76b55d8f89762d3e55178c5d89631af7161296b7bb117e3b64bc0b8b03c31ca36cb0181b4b0b3eaede640c54fd1312daacb8ec35f6b4a
6
+ metadata.gz: ea78667fe6cf4f54a0b4dd004de2ab465548a231aa2b35d52de83d89e43a0cbd0b21989e249c90415f41dfb5ac8c5082b9a84575bc4eed83418eab9a4a4f33b1
7
+ data.tar.gz: 05ea62993f5983a3fb69285f13a07678ddc912e5b7aa5657e81e02906747ab19f17d63a26141deb764d217827ba8097643e3797acf98c31f2323390a52780659
data/README.md CHANGED
@@ -47,6 +47,7 @@ Tapioca makes it easy to work with [Sorbet](https://sorbet.org) in your codebase
47
47
  * [Changing the typed strictness of annotations files](#changing-the-typed-strictness-of-annotations-files)
48
48
  * [Generating RBI files for Rails and other DSLs](#generating-rbi-files-for-rails-and-other-dsls)
49
49
  * [Keeping RBI files for DSLs up-to-date](#keeping-rbi-files-for-dsls-up-to-date)
50
+ * [Using DSL compiler options](#using-dsl-compiler-options)
50
51
  * [Writing custom DSL compilers](#writing-custom-dsl-compilers)
51
52
  * [Writing custom DSL extensions](#writing-custom-dsl-extensions)
52
53
  * [RBI files for missing constants and methods](#rbi-files-for-missing-constants-and-methods)
@@ -501,6 +502,7 @@ Options:
501
502
  [--halt-upon-load-error], [--no-halt-upon-load-error], [--skip-halt-upon-load-error] # Halt upon a load error while loading the Rails application
502
503
  # Default: true
503
504
  [--skip-constant=constant [constant ...]] # Do not generate RBI definitions for the given application constant(s)
505
+ [--compiler-options=key:value] # Options to pass to the DSL compilers
504
506
  -c, [--config=<config file path>] # Path to the Tapioca configuration file
505
507
  # Default: sorbet/tapioca/config.yml
506
508
  -V, [--verbose], [--no-verbose], [--skip-verbose] # Verbose output for debugging purposes
@@ -545,6 +547,25 @@ if Rails.env.development?
545
547
  end
546
548
  ```
547
549
 
550
+ #### Using DSL compiler options
551
+
552
+ Some DSL compilers are able to change their behaviour based on the options passed to them. For example, the
553
+ `ActiveRecordColumns` compiler can be configured to change how it generates types for method related to Active Record
554
+ column attributes. To pass options during DSL RBI generation, use the `--compiler-options` flag:
555
+ ```shell
556
+ $ bin/tapioca dsl --compiler-options=ActiveRecordColumnTypes:untyped
557
+ ```
558
+ which will make the `ActiveRecordColumns` compiler generate untyped signatures for column attribute methods.
559
+
560
+ Compiler options can be passed through the configuration file, as like any other option, and we expect most users to
561
+ configure them this way. For example, to configure the `ActiveRecordColumns` compiler to generate untyped signatures,
562
+ you need to add the following to your `sorbet/tapioca/config.yml` file:
563
+ ```yaml
564
+ dsl:
565
+ compiler_options:
566
+ ActiveRecordColumnTypes: untyped
567
+ ```
568
+
548
569
  #### Writing custom DSL compilers
549
570
 
550
571
  It is possible to create your own compilers for DSLs not supported by Tapioca out of the box.
@@ -935,6 +956,7 @@ dsl:
935
956
  app_root: "."
936
957
  halt_upon_load_error: true
937
958
  skip_constant: []
959
+ compiler_options: {}
938
960
  gem:
939
961
  outdir: sorbet/rbi/gems
940
962
  file_header: true
data/lib/tapioca/cli.rb CHANGED
@@ -140,6 +140,10 @@ module Tapioca
140
140
  banner: "constant [constant ...]",
141
141
  desc: "Do not generate RBI definitions for the given application constant(s)",
142
142
  default: []
143
+ option :compiler_options,
144
+ type: :hash,
145
+ desc: "Options to pass to the DSL compilers",
146
+ default: {}
143
147
  def dsl(*constant_or_paths)
144
148
  set_environment(options)
145
149
 
@@ -161,6 +165,7 @@ module Tapioca
161
165
  rbi_formatter: rbi_formatter(options),
162
166
  app_root: options[:app_root],
163
167
  halt_upon_load_error: options[:halt_upon_load_error],
168
+ compiler_options: options[:compiler_options],
164
169
  }
165
170
 
166
171
  command = if options[:verify]
@@ -27,6 +27,7 @@ module Tapioca
27
27
  rbi_formatter: RBIFormatter,
28
28
  app_root: String,
29
29
  halt_upon_load_error: T::Boolean,
30
+ compiler_options: T::Hash[String, T.untyped],
30
31
  ).void
31
32
  end
32
33
  def initialize(
@@ -45,7 +46,8 @@ module Tapioca
45
46
  gem_dir: DEFAULT_GEM_DIR,
46
47
  rbi_formatter: DEFAULT_RBI_FORMATTER,
47
48
  app_root: ".",
48
- halt_upon_load_error: true
49
+ halt_upon_load_error: true,
50
+ compiler_options: {}
49
51
  )
50
52
  @requested_constants = requested_constants
51
53
  @requested_paths = requested_paths
@@ -63,6 +65,7 @@ module Tapioca
63
65
  @app_root = app_root
64
66
  @halt_upon_load_error = halt_upon_load_error
65
67
  @skip_constant = skip_constant
68
+ @compiler_options = compiler_options
66
69
 
67
70
  super()
68
71
  end
@@ -129,6 +132,7 @@ module Tapioca
129
132
  },
130
133
  skipped_constants: constantize(@skip_constant, ignore_missing: true),
131
134
  number_of_workers: @number_of_workers,
135
+ compiler_options: @compiler_options,
132
136
  )
133
137
  end
134
138
 
@@ -22,6 +22,9 @@ module Tapioca
22
22
  sig { returns(RBI::Tree) }
23
23
  attr_reader :root
24
24
 
25
+ sig { returns(T::Hash[String, T.untyped]) }
26
+ attr_reader :options
27
+
25
28
  class << self
26
29
  extend T::Sig
27
30
 
@@ -60,11 +63,19 @@ module Tapioca
60
63
  end
61
64
  end
62
65
 
63
- sig { params(pipeline: Tapioca::Dsl::Pipeline, root: RBI::Tree, constant: ConstantType).void }
64
- def initialize(pipeline, root, constant)
66
+ sig do
67
+ params(
68
+ pipeline: Tapioca::Dsl::Pipeline,
69
+ root: RBI::Tree,
70
+ constant: ConstantType,
71
+ options: T::Hash[String, T.untyped],
72
+ ).void
73
+ end
74
+ def initialize(pipeline, root, constant, options = {})
65
75
  @pipeline = pipeline
66
76
  @root = root
67
77
  @constant = constant
78
+ @options = options
68
79
  @errors = T.let([], T::Array[String])
69
80
  end
70
81
 
@@ -15,6 +15,21 @@ module Tapioca
15
15
  # created for columns and virtual attributes that are defined in the Active Record
16
16
  # model.
17
17
  #
18
+ # This compiler accepts a `ActiveRecordColumnTypes` option that can be used to specify
19
+ # how the types of the column related methods should be generated. The option can be one of the following:
20
+ # - `persisted` (_default_): The methods will be generated with the type that matches the actual database
21
+ # column type as the return type. This means that if the column is a string, the method return type
22
+ # will be `String`, but if the column is also nullable, then the return type will be `T.nilable(String)`. This
23
+ # mode basically treats each model as if it was a valid and persisted model. Note that this makes typing
24
+ # Active Record models easier, but does not match the behaviour of non-persisted or invalid models, which can
25
+ # have all kinds of non-sensical values in their column attributes.
26
+ # - `nilable`: All column methods will be generated with `T.nilable` return types. This is strictly the most
27
+ # correct way to type the methods, but it can make working with the models more cumbersome, as you will have to
28
+ # handle the `nil` cases explicitly using `T.must` or the safe navigation operator `&.`, even for valid
29
+ # persisted models.
30
+ # - `untyped`: The methods will be generated with `T.untyped` return types. This mode is practical if you are not
31
+ # ready to start typing your models strictly yet, but still want to generate RBI files for them.
32
+ #
18
33
  # For example, with the following model class:
19
34
  # ~~~rb
20
35
  # class Post < ActiveRecord::Base
@@ -33,7 +48,7 @@ module Tapioca
33
48
  # end
34
49
  # ~~~
35
50
  #
36
- # this compiler will produce the following methods in the RBI file
51
+ # this compiler will, by default, produce the following methods in the RBI file
37
52
  # `post.rbi`:
38
53
  #
39
54
  # ~~~rbi
@@ -94,6 +109,17 @@ module Tapioca
94
109
  # end
95
110
  # end
96
111
  # ~~~
112
+ #
113
+ # However, if `ActiveRecordColumnTypes` is set to `nilable`, the `title` method will be generated as:
114
+ # ~~~rbi
115
+ # sig { returns(T.nilable(::String)) }
116
+ # def title; end
117
+ # ~~~
118
+ # and if the option is set to `untyped`, the `title` method will be generated as:
119
+ # ~~~rbi
120
+ # sig { returns(T.untyped) }
121
+ # def title; end
122
+ # ~~~
97
123
  class ActiveRecordColumns < Compiler
98
124
  extend T::Sig
99
125
  include Helpers::ActiveRecordConstantsHelper
@@ -147,6 +173,21 @@ module Tapioca
147
173
 
148
174
  private
149
175
 
176
+ ColumnTypeOption = Helpers::ActiveRecordColumnTypeHelper::ColumnTypeOption
177
+
178
+ sig { returns(ColumnTypeOption) }
179
+ def column_type_option
180
+ @column_type_option ||= T.let(
181
+ ColumnTypeOption.from_options(options) do |value, default_column_type_option|
182
+ add_error(<<~MSG.strip)
183
+ Unknown value for compiler option `ActiveRecordColumnTypes` given: `#{value}`.
184
+ Proceeding with the default value: `#{default_column_type_option.serialize}`.
185
+ MSG
186
+ end,
187
+ T.nilable(ColumnTypeOption),
188
+ )
189
+ end
190
+
150
191
  sig do
151
192
  params(
152
193
  klass: RBI::Scope,
@@ -174,7 +215,7 @@ module Tapioca
174
215
  end
175
216
  def add_methods_for_attribute(klass, attribute_name, column_name = attribute_name, methods_to_add = nil)
176
217
  getter_type, setter_type = Helpers::ActiveRecordColumnTypeHelper
177
- .new(constant)
218
+ .new(constant, column_type_option: column_type_option)
178
219
  .type_for(attribute_name, column_name)
179
220
 
180
221
  # Added by ActiveRecord::AttributeMethods::Read
@@ -37,6 +37,7 @@ module Tapioca
37
37
  extend T::Sig
38
38
 
39
39
  ConstantType = type_member { { fixed: T.class_of(ActiveSupport::TestCase) } }
40
+ MISSING = Object.new
40
41
 
41
42
  sig { override.void }
42
43
  def decorate
@@ -46,6 +47,7 @@ module Tapioca
46
47
  method_names_from_eager_fixture_loader
47
48
  end
48
49
 
50
+ method_names.select! { |name| fixture_class_mapping_from_fixture_files[name] != MISSING }
49
51
  return if method_names.empty?
50
52
 
51
53
  root.create_path(constant) do |mod|
@@ -96,15 +98,14 @@ module Tapioca
96
98
  T.unsafe(fixture_loader).fixture_sets.keys
97
99
  end
98
100
 
99
- sig { returns(T::Array[Symbol]) }
101
+ sig { returns(T::Array[String]) }
100
102
  def method_names_from_eager_fixture_loader
101
103
  fixture_loader.ancestors # get all ancestors from class that includes AR fixtures
102
104
  .drop(1) # drop the anonymous class itself from the array
103
105
  .reject(&:name) # only collect anonymous ancestors because fixture methods are always on an anonymous module
104
- .map! do |mod|
105
- [mod.private_instance_methods(false), mod.instance_methods(false)]
106
+ .flat_map do |mod|
107
+ mod.private_instance_methods(false).map(&:to_s) + mod.instance_methods(false).map(&:to_s)
106
108
  end
107
- .flatten # merge methods into a single list
108
109
  end
109
110
 
110
111
  sig { params(mod: RBI::Scope, name: String).void }
@@ -190,10 +191,14 @@ module Tapioca
190
191
  next unless ::File.file?(file)
191
192
 
192
193
  ActiveRecord::FixtureSet::File.open(file) do |fh|
194
+ fixture_name = file.delete_prefix(path.to_s).delete_prefix("/").delete_suffix(".yml")
193
195
  next unless fh.model_class
194
196
 
195
- fixture_name = file.delete_prefix(path.to_s).delete_prefix("/").delete_suffix(".yml")
196
197
  mapping[fixture_name] = fh.model_class
198
+ rescue ActiveRecord::Fixture::FormatError
199
+ # For fixtures that are not associated to any models and just contain raw data or fixtures that
200
+ # contain invalid formatting, we want to skip them and avoid crashing
201
+ mapping[fixture_name] = MISSING
197
202
  end
198
203
  end
199
204
  end
@@ -98,8 +98,7 @@ module Tapioca
98
98
  stores.values.each do |store_data|
99
99
  store_data.accessors.each do |accessor, name|
100
100
  field = store_data.fields.fetch(accessor)
101
- type = type_for(field.type_sym)
102
- type = as_nilable_type(type) if field.null
101
+ type = type_for(field)
103
102
  name ||= field.name # support < 1.5.0
104
103
 
105
104
  generate_methods(store_accessors_module, name.to_s, type)
@@ -136,9 +135,23 @@ module Tapioca
136
135
  T::Hash[Symbol, String],
137
136
  )
138
137
 
139
- sig { params(attr_type: Symbol).returns(String) }
140
- def type_for(attr_type)
141
- TYPES.fetch(attr_type, "T.untyped")
138
+ sig { params(field: ActiveRecord::TypedStore::Field).returns(String) }
139
+ def type_for(field)
140
+ type = TYPES.fetch(field.type_sym, "T.untyped")
141
+
142
+ type = if field.array
143
+ # `null: false` applies to the array itself, not the elements, which are always nilable.
144
+ # https://github.com/byroot/activerecord-typedstore/blob/2f3fb98/spec/support/models.rb#L46C34-L46C45
145
+ # https://github.com/byroot/activerecord-typedstore/blob/2f3fb98/spec/active_record/typed_store_spec.rb#L854-L857
146
+ nilable_element_type = as_nilable_type(type)
147
+ "T::Array[#{nilable_element_type}]"
148
+ else
149
+ type
150
+ end
151
+
152
+ type = as_nilable_type(type) if field.null
153
+
154
+ type
142
155
  end
143
156
 
144
157
  sig do
@@ -49,7 +49,7 @@ module Tapioca
49
49
  mixed_in_class_methods = dependencies
50
50
  .uniq # Deduplicate
51
51
  .filter_map do |concern| # Map to class methods module name, if exists
52
- "#{qualified_name_of(concern)}::ClassMethods" if concern.const_defined?(:ClassMethods)
52
+ "#{qualified_name_of(concern)}::ClassMethods" if concern.const_defined?(:ClassMethods, false)
53
53
  end
54
54
 
55
55
  return if mixed_in_class_methods.empty?
@@ -216,7 +216,10 @@ module Tapioca
216
216
  ).void
217
217
  end
218
218
  def create_aliased_fetch_by_methods(field, klass)
219
- type, _ = Helpers::ActiveRecordColumnTypeHelper.new(constant).type_for(field.alias_name.to_s)
219
+ type, _ = Helpers::ActiveRecordColumnTypeHelper.new(
220
+ constant,
221
+ column_type_option: Helpers::ActiveRecordColumnTypeHelper::ColumnTypeOption::Nilable,
222
+ ).type_for(field.alias_name.to_s)
220
223
  multi_type = type.delete_prefix("T.nilable(").delete_suffix(")").delete_prefix("::")
221
224
  suffix = field.send(:fetch_method_suffix)
222
225
 
@@ -29,7 +29,7 @@ module Tapioca
29
29
  # ~~~rbi
30
30
  # # cart.rbi
31
31
  # # typed: strong
32
- # class Cart
32
+ # class Cart < Google::Protobuf::AbstractMessage
33
33
  # sig { returns(Integer) }
34
34
  # def customer_id; end
35
35
  #
@@ -123,6 +123,9 @@ module Tapioca
123
123
  class_method: true,
124
124
  )
125
125
  when Google::Protobuf::Descriptor
126
+ raise "#{klass} is not a RBI::Class" unless klass.is_a?(RBI::Class)
127
+
128
+ klass.superclass_name = "Google::Protobuf::AbstractMessage"
126
129
  descriptor.each_oneof { |oneof| create_oneof_method(klass, oneof) }
127
130
  fields = descriptor.map { |desc| create_descriptor_method(klass, desc) }
128
131
  fields.sort_by!(&:name)
@@ -10,12 +10,73 @@ module Tapioca
10
10
  extend T::Sig
11
11
  include RBIHelper
12
12
 
13
- sig { params(constant: T.class_of(ActiveRecord::Base)).void }
14
- def initialize(constant)
13
+ class ColumnTypeOption < T::Enum
14
+ extend T::Sig
15
+
16
+ enums do
17
+ Untyped = new("untyped")
18
+ Nilable = new("nilable")
19
+ Persisted = new("persisted")
20
+ end
21
+
22
+ class << self
23
+ extend T::Sig
24
+
25
+ sig do
26
+ params(
27
+ options: T::Hash[String, T.untyped],
28
+ block: T.proc.params(value: String, default_column_type_option: ColumnTypeOption).void,
29
+ ).returns(ColumnTypeOption)
30
+ end
31
+ def from_options(options, &block)
32
+ column_type_option = Persisted
33
+ value = options["ActiveRecordColumnTypes"]
34
+
35
+ if value
36
+ if has_serialized?(value)
37
+ column_type_option = from_serialized(value)
38
+ else
39
+ block.call(value, column_type_option)
40
+ end
41
+ end
42
+
43
+ column_type_option
44
+ end
45
+ end
46
+
47
+ sig { returns(T::Boolean) }
48
+ def persisted?
49
+ self == ColumnTypeOption::Persisted
50
+ end
51
+
52
+ sig { returns(T::Boolean) }
53
+ def nilable?
54
+ self == ColumnTypeOption::Nilable
55
+ end
56
+
57
+ sig { returns(T::Boolean) }
58
+ def untyped?
59
+ self == ColumnTypeOption::Untyped
60
+ end
61
+ end
62
+
63
+ sig do
64
+ params(
65
+ constant: T.class_of(ActiveRecord::Base),
66
+ column_type_option: ColumnTypeOption,
67
+ ).void
68
+ end
69
+ def initialize(constant, column_type_option: ColumnTypeOption::Persisted)
15
70
  @constant = constant
71
+ @column_type_option = column_type_option
16
72
  end
17
73
 
18
- sig { params(attribute_name: String, column_name: String).returns([String, String]) }
74
+ sig do
75
+ params(
76
+ attribute_name: String,
77
+ column_name: String,
78
+ ).returns([String, String])
79
+ end
19
80
  def type_for(attribute_name, column_name = attribute_name)
20
81
  return id_type if attribute_name == "id"
21
82
 
@@ -27,7 +88,11 @@ module Tapioca
27
88
  sig { returns([String, String]) }
28
89
  def id_type
29
90
  if @constant.respond_to?(:composite_primary_key?) && T.unsafe(@constant).composite_primary_key?
30
- @constant.primary_key.map(&method(:column_type_for)).map { |tuple| "[#{tuple.join(", ")}]" }
91
+ @constant.primary_key.map do |column|
92
+ column_type_for(column)
93
+ end.map do |tuple|
94
+ "[#{tuple.join(", ")}]"
95
+ end
31
96
  else
32
97
  column_type_for(@constant.primary_key)
33
98
  end
@@ -35,7 +100,7 @@ module Tapioca
35
100
 
36
101
  sig { params(column_name: String).returns([String, String]) }
37
102
  def column_type_for(column_name)
38
- return ["T.untyped", "T.untyped"] if do_not_generate_strong_types?(@constant)
103
+ return ["T.untyped", "T.untyped"] if @column_type_option.untyped?
39
104
 
40
105
  column = @constant.columns_hash[column_name]
41
106
  column_type = @constant.attribute_types[column_name]
@@ -48,27 +113,31 @@ module Tapioca
48
113
  getter_type
49
114
  end
50
115
 
51
- if column&.null
116
+ if @column_type_option.persisted? && !column&.null
117
+ [getter_type, setter_type]
118
+ else
52
119
  getter_type = as_nilable_type(getter_type) unless not_nilable_serialized_column?(column_type)
53
- return [getter_type, as_nilable_type(setter_type)]
120
+ [getter_type, as_nilable_type(setter_type)]
54
121
  end
55
-
56
- if Array(@constant.primary_key).include?(column_name) ||
57
- column_name == "created_at" ||
58
- column_name == "updated_at"
59
- getter_type = as_nilable_type(getter_type)
60
- end
61
-
62
- [getter_type, setter_type]
63
122
  end
64
123
 
65
124
  sig { params(column_type: T.untyped).returns(String) }
66
125
  def type_for_activerecord_value(column_type)
67
126
  case column_type
68
- when defined?(MoneyColumn) && MoneyColumn::ActiveRecordType
127
+ when ->(type) { defined?(MoneyColumn) && MoneyColumn::ActiveRecordType === type }
69
128
  "::Money"
70
129
  when ActiveRecord::Type::Integer
71
130
  "::Integer"
131
+ when ->(type) {
132
+ defined?(ActiveRecord::Encryption) && ActiveRecord::Encryption::EncryptedAttributeType === type
133
+ }
134
+ # Reflect to see if `ActiveModel::Type::Value` is being used first.
135
+ getter_type = Tapioca::Dsl::Helpers::ActiveModelTypeHelper.type_for(column_type)
136
+ return getter_type unless getter_type == "T.untyped"
137
+
138
+ # Otherwise fallback to String as `ActiveRecord::Encryption::EncryptedAttributeType` inherits from
139
+ # `ActiveRecord::Type::Text` which inherits from `ActiveModel::Type::String`.
140
+ "::String"
72
141
  when ActiveRecord::Type::String
73
142
  "::String"
74
143
  when ActiveRecord::Type::Date
@@ -87,29 +156,36 @@ module Tapioca
87
156
  "::String"
88
157
  when ActiveRecord::Type::Serialized
89
158
  serialized_column_type(column_type)
90
- when defined?(ActiveRecord::Normalization::NormalizedValueType) &&
91
- ActiveRecord::Normalization::NormalizedValueType
159
+ when ->(type) {
160
+ defined?(ActiveRecord::Normalization::NormalizedValueType) &&
161
+ ActiveRecord::Normalization::NormalizedValueType === type
162
+ }
92
163
  type_for_activerecord_value(column_type.cast_type)
93
- when defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid) &&
94
- ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid
164
+ when ->(type) {
165
+ defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid) &&
166
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid === type
167
+ }
95
168
  "::String"
96
- when defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Hstore) &&
97
- ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Hstore
169
+ when ->(type) {
170
+ defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Cidr) &&
171
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Cidr === type
172
+ }
173
+ "::IPAddr"
174
+ when ->(type) {
175
+ defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Hstore) &&
176
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Hstore === type
177
+ }
98
178
  "T::Hash[::String, ::String]"
99
- when defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array) &&
100
- ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array
179
+ when ->(type) {
180
+ defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array) &&
181
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array === type
182
+ }
101
183
  "T::Array[#{type_for_activerecord_value(column_type.subtype)}]"
102
184
  else
103
185
  ActiveModelTypeHelper.type_for(column_type)
104
186
  end
105
187
  end
106
188
 
107
- sig { params(constant: Module).returns(T::Boolean) }
108
- def do_not_generate_strong_types?(constant)
109
- Object.const_defined?(:StrongTypeGeneration) &&
110
- !(constant.singleton_class < Object.const_get(:StrongTypeGeneration))
111
- end
112
-
113
189
  sig { params(column_type: ActiveRecord::Enum::EnumType).returns(String) }
114
190
  def enum_setter_type(column_type)
115
191
  # In Rails < 7 this method is private. When support for that is dropped we can call the method directly
@@ -33,6 +33,7 @@ module Tapioca
33
33
  error_handler: T.proc.params(error: String).void,
34
34
  skipped_constants: T::Array[Module],
35
35
  number_of_workers: T.nilable(Integer),
36
+ compiler_options: T::Hash[String, T.untyped],
36
37
  ).void
37
38
  end
38
39
  def initialize(
@@ -42,7 +43,8 @@ module Tapioca
42
43
  excluded_compilers: [],
43
44
  error_handler: $stderr.method(:puts).to_proc,
44
45
  skipped_constants: [],
45
- number_of_workers: nil
46
+ number_of_workers: nil,
47
+ compiler_options: {}
46
48
  )
47
49
  @active_compilers = T.let(
48
50
  gather_active_compilers(requested_compilers, excluded_compilers),
@@ -53,6 +55,7 @@ module Tapioca
53
55
  @error_handler = error_handler
54
56
  @skipped_constants = skipped_constants
55
57
  @number_of_workers = number_of_workers
58
+ @compiler_options = compiler_options
56
59
  @errors = T.let([], T::Array[String])
57
60
  end
58
61
 
@@ -197,7 +200,7 @@ module Tapioca
197
200
  active_compilers.each do |compiler_class|
198
201
  next unless compiler_class.handles?(constant)
199
202
 
200
- compiler = compiler_class.new(self, file.root, constant)
203
+ compiler = compiler_class.new(self, file.root, constant, @compiler_options)
201
204
  compiler.decorate
202
205
  rescue
203
206
  $stderr.puts("Error: `#{compiler_class.name}` failed to generate RBI for `#{constant}`")
@@ -132,7 +132,9 @@ module Tapioca
132
132
  when :hash
133
133
  error_msg = "invalid value for option `#{config_option_key}` for key `#{config_key}` - expected " \
134
134
  "`Hash[String, String]` but found `#{config_option_value}`"
135
- all_strings = (config_option_value.keys + config_option_value.values).all? { |v| v.is_a?(String) }
135
+ values_to_validate = config_option_value.keys
136
+ values_to_validate += config_option_value.values
137
+ all_strings = values_to_validate.all? { |v| v.is_a?(String) }
136
138
  next build_error(error_msg) unless all_strings
137
139
  end
138
140
  end
@@ -174,7 +174,21 @@ module Tapioca
174
174
  shims_or_todos = extract_shims_and_todos(nodes, shim_rbi_dir: shim_rbi_dir, todo_rbi_file: todo_rbi_file)
175
175
  return false if shims_or_todos.empty?
176
176
 
177
+ not_shims_or_todos = nodes - shims_or_todos
177
178
  shims_or_todos_empty_scopes = extract_empty_scopes(shims_or_todos)
179
+
180
+ # We need to discard classes that are redefining the parent of a class
181
+ shims_or_todos_empty_scopes.select! do |scope|
182
+ # Empty modules are always duplicates
183
+ next true unless scope.is_a?(RBI::Class)
184
+
185
+ # Empty classes without parents are also duplicates
186
+ parent_name = scope.superclass_name
187
+ next true unless parent_name
188
+
189
+ # Empty classes that are not redefining the parent are also duplicates
190
+ not_shims_or_todos.any? { |node| node.is_a?(RBI::Class) && node.superclass_name == parent_name }
191
+ end
178
192
  return true unless shims_or_todos_empty_scopes.empty?
179
193
 
180
194
  mixins = extract_mixins(shims_or_todos)
@@ -29,9 +29,14 @@ module Tapioca
29
29
  context.activate_other_dsl_compilers(compiler_classes)
30
30
  end
31
31
 
32
- sig { params(constant_name: T.any(Symbol, String)).returns(String) }
33
- def rbi_for(constant_name)
34
- context.rbi_for(constant_name)
32
+ sig do
33
+ params(
34
+ constant_name: T.any(Symbol, String),
35
+ compiler_options: T::Hash[Symbol, T.untyped],
36
+ ).returns(String)
37
+ end
38
+ def rbi_for(constant_name, compiler_options: {})
39
+ context.rbi_for(constant_name, compiler_options: compiler_options)
35
40
  end
36
41
 
37
42
  sig { returns(T::Array[String]) }
@@ -85,8 +90,13 @@ module Tapioca
85
90
  compiler_class.processable_constants.filter_map(&:name).sort
86
91
  end
87
92
 
88
- sig { params(constant_name: T.any(Symbol, String)).returns(String) }
89
- def rbi_for(constant_name)
93
+ sig do
94
+ params(
95
+ constant_name: T.any(Symbol, String),
96
+ compiler_options: T::Hash[Symbol, T.untyped],
97
+ ).returns(String)
98
+ end
99
+ def rbi_for(constant_name, compiler_options: {})
90
100
  # Make sure this is a constant that we can handle.
91
101
  unless gathered_constants.include?(constant_name.to_s)
92
102
  raise "`#{constant_name}` is not processable by the `#{compiler_class}` compiler."
@@ -95,7 +105,7 @@ module Tapioca
95
105
  file = RBI::File.new(strictness: "strong")
96
106
  constant = Object.const_get(constant_name)
97
107
 
98
- compiler = compiler_class.new(pipeline, file.root, constant)
108
+ compiler = compiler_class.new(pipeline, file.root, constant, compiler_options.transform_keys(&:to_s))
99
109
  compiler.decorate
100
110
 
101
111
  rbi = Tapioca::DEFAULT_RBI_FORMATTER.print_file(file)
@@ -22,7 +22,7 @@ module Tapioca
22
22
  sig { params(gem: Gemfile::GemSpec).returns(T::Set[String]) }
23
23
  def engine_symbols(gem)
24
24
  gem_engine = engines.find do |engine|
25
- gem.contains_path?(engine.config.root.to_s)
25
+ gem.full_gem_path == engine.config.root.to_s
26
26
  end
27
27
 
28
28
  return Set.new unless gem_engine
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Tapioca
5
- VERSION = "0.14.2"
5
+ VERSION = "0.14.4"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tapioca
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.2
4
+ version: 0.14.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ufuk Kayserilioglu
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: exe
13
13
  cert_chain: []
14
- date: 2024-05-15 00:00:00.000000000 Z
14
+ date: 2024-06-20 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: bundler
@@ -288,7 +288,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
288
288
  - !ruby/object:Gem::Version
289
289
  version: '0'
290
290
  requirements: []
291
- rubygems_version: 3.5.10
291
+ rubygems_version: 3.5.13
292
292
  signing_key:
293
293
  specification_version: 4
294
294
  summary: A Ruby Interface file generator for gems, core types and the Ruby standard