tapioca 0.14.3 → 0.15.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7488dd17c4950978384fa4da4908f6e2f7ad0ad87266a018044e21d565f704aa
4
- data.tar.gz: 572b9cb88a228d86c46b36c3bb1b938781dfaf528301c1c0a61e0e3258e5b65a
3
+ metadata.gz: f93b130a7096e0712ccd91b8406d92ce537287afafc8aad7586e7f670d88a131
4
+ data.tar.gz: b2cc1e6d2658002600c1edd9c5c88b22704e3f4170ff19aff93bb9fc21df688e
5
5
  SHA512:
6
- metadata.gz: 1565590d51799b64e7bf57a4d4d25bfe5e20d1455345183412773be5eb5c4f3c7bfecca49438aa2ce67283d1260695d2f072303a5951d7daa1540bd3d4f8ef5c
7
- data.tar.gz: 00e81fc94389204d15f54e0eb469ca1da367d3b69fedd73e2146f2b0a768d3928848decc94c52fd212e7405d7e8d177571de8e92986090c07ba694c030ae7e66
6
+ metadata.gz: aa10ff7ffd4768cea802b63dc68273fa0abf7f69b61a83b1325669e97d706cb8afcc704093c93596a830662fe04b01ad2ded3c166abd69a5c69097bd28f586c9
7
+ data.tar.gz: 63499428de9374990372beb46ece3f98d0f446ba28ef02bfa181179113e37e587ff9867bbe0d729d67c03ddb2813943f6f04d14f2065589a603a766a06ce8096
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
 
@@ -46,22 +46,22 @@ module Tapioca
46
46
  GitAttributes.create_vendored_attribute_file(@outpath)
47
47
  end
48
48
 
49
- sig { returns(T::Array[String]) }
49
+ sig { returns(T::Array[GemInfo]) }
50
50
  def list_gemfile_gems
51
51
  say("Listing gems from Gemfile.lock... ", [:blue, :bold])
52
52
  gemfile = Bundler.read_file("Gemfile.lock")
53
53
  parser = Bundler::LockfileParser.new(gemfile)
54
- gem_names = parser.specs.map(&:name)
54
+ gem_info = parser.specs.map { |spec| GemInfo.from_spec(spec) }
55
55
  say("Done", :green)
56
- gem_names
56
+ gem_info
57
57
  end
58
58
 
59
- sig { params(project_gems: T::Array[String]).void }
59
+ sig { params(project_gems: T::Array[GemInfo]).void }
60
60
  def remove_expired_annotations(project_gems)
61
61
  say("Removing annotations for gems that have been removed... ", [:blue, :bold])
62
62
 
63
63
  annotations = Pathname.glob(@outpath.join("*.rbi")).map { |f| f.basename(".*").to_s }
64
- expired = annotations - project_gems
64
+ expired = annotations - project_gems.map(&:name)
65
65
 
66
66
  if expired.empty?
67
67
  say(" Nothing to do")
@@ -109,14 +109,14 @@ module Tapioca
109
109
  index
110
110
  end
111
111
 
112
- sig { params(gem_names: T::Array[String]).returns(T::Array[String]) }
113
- def fetch_annotations(gem_names)
112
+ sig { params(project_gems: T::Array[GemInfo]).returns(T::Array[String]) }
113
+ def fetch_annotations(project_gems)
114
114
  say("Fetching gem annotations from central repository... ", [:blue, :bold])
115
- fetchable_gems = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[String, T::Array[String]])
115
+ fetchable_gems = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[GemInfo, T::Array[String]])
116
116
 
117
- gem_names.each_with_object(fetchable_gems) do |gem_name, hash|
117
+ project_gems.each_with_object(fetchable_gems) do |gem_info, hash|
118
118
  @indexes.each do |uri, index|
119
- T.must(hash[gem_name]) << uri if index.has_gem?(gem_name)
119
+ T.must(hash[gem_info]) << uri if index.has_gem?(gem_info.name)
120
120
  end
121
121
  end
122
122
 
@@ -127,13 +127,16 @@ module Tapioca
127
127
  end
128
128
 
129
129
  say("\n")
130
- fetched_gems = fetchable_gems.select { |gem_name, repo_uris| fetch_annotation(repo_uris, gem_name) }
130
+ fetched_gems = fetchable_gems.select { |gem_info, repo_uris| fetch_annotation(repo_uris, gem_info) }
131
131
  say("\nDone", :green)
132
- fetched_gems.keys.sort
132
+ fetched_gems.keys.map(&:name).sort
133
133
  end
134
134
 
135
- sig { params(repo_uris: T::Array[String], gem_name: String).void }
136
- def fetch_annotation(repo_uris, gem_name)
135
+ sig { params(repo_uris: T::Array[String], gem_info: GemInfo).void }
136
+ def fetch_annotation(repo_uris, gem_info)
137
+ gem_name = gem_info.name
138
+ gem_version = gem_info.version
139
+
137
140
  contents = repo_uris.map do |repo_uri|
138
141
  fetch_file(repo_uri, "#{CENTRAL_REPO_ANNOTATIONS_DIR}/#{gem_name}.rbi")
139
142
  end
@@ -142,6 +145,7 @@ module Tapioca
142
145
  return unless content
143
146
 
144
147
  content = apply_typed_override(gem_name, content)
148
+ content = filter_versions(gem_version, content)
145
149
  content = add_header(gem_name, content)
146
150
 
147
151
  say("\n Fetched #{set_color(gem_name, :yellow, :bold)}", :green)
@@ -221,6 +225,14 @@ module Tapioca
221
225
  Spoom::Sorbet::Sigils.update_sigil(content, strictness)
222
226
  end
223
227
 
228
+ sig { params(gem_version: ::Gem::Version, content: String).returns(String) }
229
+ def filter_versions(gem_version, content)
230
+ rbi = RBI::Parser.parse_string(content)
231
+ rbi.filter_versions!(gem_version)
232
+
233
+ rbi.string
234
+ end
235
+
224
236
  sig { params(gem_name: String, contents: T::Array[String]).returns(T.nilable(String)) }
225
237
  def merge_files(gem_name, contents)
226
238
  return if contents.empty?
@@ -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
@@ -190,7 +190,7 @@ module Tapioca
190
190
  # Grab all Spawn methods
191
191
  query_methods |= ActiveRecord::SpawnMethods.instance_methods(false)
192
192
  # Remove the ones we know are private API
193
- query_methods -= [:arel, :build_subquery, :construct_join_dependency, :extensions, :spawn]
193
+ query_methods -= [:all, :arel, :build_subquery, :construct_join_dependency, :extensions, :spawn]
194
194
  # Remove "group" which needs a custom return type for GroupChains
195
195
  query_methods -= [:group]
196
196
  # Remove "where" which needs a custom return type for WhereChains
@@ -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
 
@@ -228,7 +231,7 @@ module Tapioca
228
231
  "fetch_#{suffix}",
229
232
  class_method: true,
230
233
  parameters: parameters,
231
- return_type: type,
234
+ return_type: field.unique ? type : COLLECTION_TYPE.call(type),
232
235
  )
233
236
 
234
237
  klass.create_method(
@@ -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,28 +113,24 @@ 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)]
54
- 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)
120
+ [getter_type, as_nilable_type(setter_type)]
60
121
  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"
72
- when ActiveRecord::Encryption::EncryptedAttributeType
131
+ when ->(type) {
132
+ defined?(ActiveRecord::Encryption) && ActiveRecord::Encryption::EncryptedAttributeType === type
133
+ }
73
134
  # Reflect to see if `ActiveModel::Type::Value` is being used first.
74
135
  getter_type = Tapioca::Dsl::Helpers::ActiveModelTypeHelper.type_for(column_type)
75
136
  return getter_type unless getter_type == "T.untyped"
@@ -95,32 +156,36 @@ module Tapioca
95
156
  "::String"
96
157
  when ActiveRecord::Type::Serialized
97
158
  serialized_column_type(column_type)
98
- when defined?(ActiveRecord::Normalization::NormalizedValueType) &&
99
- ActiveRecord::Normalization::NormalizedValueType
159
+ when ->(type) {
160
+ defined?(ActiveRecord::Normalization::NormalizedValueType) &&
161
+ ActiveRecord::Normalization::NormalizedValueType === type
162
+ }
100
163
  type_for_activerecord_value(column_type.cast_type)
101
- when defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid) &&
102
- ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid
164
+ when ->(type) {
165
+ defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid) &&
166
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid === type
167
+ }
103
168
  "::String"
104
- when defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Cidr) &&
105
- ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Cidr
169
+ when ->(type) {
170
+ defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Cidr) &&
171
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Cidr === type
172
+ }
106
173
  "::IPAddr"
107
- when defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Hstore) &&
108
- ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Hstore
174
+ when ->(type) {
175
+ defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Hstore) &&
176
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Hstore === type
177
+ }
109
178
  "T::Hash[::String, ::String]"
110
- when defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array) &&
111
- ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array
179
+ when ->(type) {
180
+ defined?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array) &&
181
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array === type
182
+ }
112
183
  "T::Array[#{type_for_activerecord_value(column_type.subtype)}]"
113
184
  else
114
185
  ActiveModelTypeHelper.type_for(column_type)
115
186
  end
116
187
  end
117
188
 
118
- sig { params(constant: Module).returns(T::Boolean) }
119
- def do_not_generate_strong_types?(constant)
120
- Object.const_defined?(:StrongTypeGeneration) &&
121
- !(constant.singleton_class < Object.const_get(:StrongTypeGeneration))
122
- end
123
-
124
189
  sig { params(column_type: ActiveRecord::Enum::EnumType).returns(String) }
125
190
  def enum_setter_type(column_type)
126
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}`")
@@ -0,0 +1,18 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ class GemInfo < T::Struct
6
+ const :name, String
7
+ const :version, ::Gem::Version
8
+
9
+ class << self
10
+ extend(T::Sig)
11
+
12
+ sig { params(spec: Bundler::LazySpecification).returns(GemInfo) }
13
+ def from_spec(spec)
14
+ new(name: spec.name, version: spec.version)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -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
@@ -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)
@@ -45,6 +45,7 @@ require "tapioca/helpers/env_helper"
45
45
 
46
46
  require "tapioca/repo_index"
47
47
  require "tapioca/gemfile"
48
+ require "tapioca/gem_info"
48
49
  require "tapioca/executor"
49
50
 
50
51
  require "tapioca/static/symbol_table_parser"
@@ -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.3"
5
+ VERSION = "0.15.0"
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.3
4
+ version: 0.15.0
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-30 00:00:00.000000000 Z
14
+ date: 2024-06-27 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: bundler
@@ -228,6 +228,7 @@ files:
228
228
  - lib/tapioca/gem/listeners/subconstants.rb
229
229
  - lib/tapioca/gem/listeners/yard_doc.rb
230
230
  - lib/tapioca/gem/pipeline.rb
231
+ - lib/tapioca/gem_info.rb
231
232
  - lib/tapioca/gemfile.rb
232
233
  - lib/tapioca/helpers/cli_helper.rb
233
234
  - lib/tapioca/helpers/config_helper.rb
@@ -288,7 +289,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
288
289
  - !ruby/object:Gem::Version
289
290
  version: '0'
290
291
  requirements: []
291
- rubygems_version: 3.5.10
292
+ rubygems_version: 3.5.13
292
293
  signing_key:
293
294
  specification_version: 4
294
295
  summary: A Ruby Interface file generator for gems, core types and the Ruby standard