tapioca 0.14.3 → 0.15.0

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: 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