tapioca 0.10.2 → 0.10.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: 9f969345b0846479b6ab150b42641cbe90a746c59f4d995f10b4cb2dbdcaf588
4
- data.tar.gz: a3dcf9bc8b505fb6be05da572b00a0efbb3f544ce7dd2303a55ed2119b34deef
3
+ metadata.gz: bbed9bfddb52a94735d9455ed4351c4ef1389236ceba2e19ce46d999a05536b0
4
+ data.tar.gz: b1a35528c1e9cfa5fd735a64d4988bfd72c0f980835d58e4a37c62d5694e3daa
5
5
  SHA512:
6
- metadata.gz: 103484877f92cdb0c43d74b2b92462a977082b959ad26a2ed198895941e2e066961884f207eb167db8d6f86b941075b1216bac13d99f69ff6eb85e7bef3f7760
7
- data.tar.gz: 2c11680ac5c9be37fb25b8c944e641f471c59bcb95e0e7f427d5839b56702730468e7afc71f8db22efefcad4f04ffad7a66ad15a981a9097b7217df51ca89d2b
6
+ metadata.gz: 6138060ec0cea66dda6f96484329626d613494404cf99f851d1dbd59249546ffdfa0b0dd58236ef63d65b93acd250eb3a46b5a3eb1a3f8491014125b18a50800
7
+ data.tar.gz: 266205a5dd212e87836d1d26f1f018ab83d64aaacc9b793e9a01b88c6213bda834991194896a602ce6f267ef96e3ef8f78d14464005cefcfc010bb666ddc6f3e
data/README.md CHANGED
@@ -473,6 +473,8 @@ Options:
473
473
  -e, [--environment=ENVIRONMENT] # The Rack/Rails environment to use when generating RBIs
474
474
  # Default: development
475
475
  -l, [--list-compilers], [--no-list-compilers] # List all loaded compilers
476
+ [--app-root=APP_ROOT] # The path to the Rails application
477
+ # Default: .
476
478
  -c, [--config=<config file path>] # Path to the Tapioca configuration file
477
479
  # Default: sorbet/tapioca/config.yml
478
480
  -V, [--verbose], [--no-verbose] # Verbose output for debugging purposes
@@ -635,6 +637,8 @@ There are two main parts to the DSL compiler API: `gather_constants` and `decora
635
637
  * The `gather_constants` class method collects all classes (or modules) that should be processed by this specific DSL compiler.
636
638
  * The `decorate` method defines how to generate the necessary RBI definitions for the gathered constants.
637
639
 
640
+ Every compiler must declare the type member `ConstantType` in order for Sorbet to understand what the return type of the `constant` attribute reader is. It needs to be assigned the correct type variable matching the type of constants that `gather_constants` returns. This generic variable allows Sorbet to type-check method calls on the `constant` reader in your `decorate` method. See the Sorbet documentation on [generics](https://sorbet.org/docs/generics) for more information.
641
+
638
642
  You can now run the new RBI compiler through the normal DSL generation process (your custom compiler will be loaded automatically by Tapioca):
639
643
 
640
644
  ```shell
@@ -822,6 +826,7 @@ dsl:
822
826
  rbi_max_line_length: 120
823
827
  environment: development
824
828
  list_compilers: false
829
+ app_root: "."
825
830
  gem:
826
831
  outdir: sorbet/rbi/gems
827
832
  file_header: true
data/exe/tapioca CHANGED
@@ -3,19 +3,21 @@
3
3
 
4
4
  require "sorbet-runtime"
5
5
 
6
- begin
7
- T::Configuration.default_checked_level = :never
8
- # Suppresses call validation errors
9
- T::Configuration.call_validation_error_handler = ->(*) {}
10
- # Suppresses errors caused by T.cast, T.let, T.must, etc.
11
- T::Configuration.inline_type_error_handler = ->(*) {}
12
- # Suppresses errors caused by incorrect parameter ordering
13
- T::Configuration.sig_validation_error_handler = ->(*) {}
14
- rescue
15
- # Need this rescue so that if another gem has
16
- # already set the checked level by the time we
17
- # get to it, we don't fail outright.
18
- nil
6
+ unless ENV["ENFORCE_TYPECHECKING"] == "1"
7
+ begin
8
+ T::Configuration.default_checked_level = :never
9
+ # Suppresses call validation errors
10
+ T::Configuration.call_validation_error_handler = ->(*) {}
11
+ # Suppresses errors caused by T.cast, T.let, T.must, etc.
12
+ T::Configuration.inline_type_error_handler = ->(*) {}
13
+ # Suppresses errors caused by incorrect parameter ordering
14
+ T::Configuration.sig_validation_error_handler = ->(*) {}
15
+ rescue
16
+ # Need this rescue so that if another gem has
17
+ # already set the checked level by the time we
18
+ # get to it, we don't fail outright.
19
+ nil
20
+ end
19
21
  end
20
22
 
21
23
  require_relative "../lib/tapioca/internal"
data/lib/tapioca/cli.rb CHANGED
@@ -9,6 +9,8 @@ module Tapioca
9
9
 
10
10
  FILE_HEADER_OPTION_DESC = "Add a \"This file is generated\" header on top of each generated RBI file"
11
11
 
12
+ check_unknown_options!
13
+
12
14
  class_option :config,
13
15
  aliases: ["-c"],
14
16
  banner: "<config file path>",
@@ -121,6 +123,10 @@ module Tapioca
121
123
  type: :boolean,
122
124
  desc: "List all loaded compilers",
123
125
  default: false
126
+ option :app_root,
127
+ type: :string,
128
+ desc: "The path to the Rails application",
129
+ default: "."
124
130
  def dsl(*constants)
125
131
  set_environment(options)
126
132
 
@@ -136,6 +142,7 @@ module Tapioca
136
142
  verbose: options[:verbose],
137
143
  number_of_workers: options[:workers],
138
144
  rbi_formatter: rbi_formatter(options),
145
+ app_root: options[:app_root],
139
146
  )
140
147
 
141
148
  Tapioca.silence_warnings do
@@ -279,7 +286,10 @@ module Tapioca
279
286
  payload: options[:payload],
280
287
  number_of_workers: options[:workers],
281
288
  )
282
- command.execute
289
+
290
+ Tapioca.silence_warnings do
291
+ command.execute
292
+ end
283
293
  end
284
294
 
285
295
  desc "annotations", "Pull gem RBI annotations from remote sources"
@@ -305,7 +315,10 @@ module Tapioca
305
315
  netrc_file: netrc_file(options),
306
316
  typed_overrides: options[:typed_overrides],
307
317
  )
308
- command.execute
318
+
319
+ Tapioca.silence_warnings do
320
+ command.execute
321
+ end
309
322
  end
310
323
 
311
324
  map ["--version", "-v"] => :__print_version
@@ -51,26 +51,18 @@ module Tapioca
51
51
  payload_path = T.let(nil, T.nilable(String))
52
52
 
53
53
  if @payload
54
- if sorbet_supports?(:print_payload_sources)
55
- Dir.mktmpdir do |dir|
56
- payload_path = dir
57
- result = sorbet("--no-config --print=payload-sources:#{payload_path}")
58
-
59
- unless result.status
60
- raise Thor::Error, <<~ERROR
61
- "Sorbet failed to dump payload"
62
- #{result.err}
63
- ERROR
64
- end
65
-
66
- index_rbis(index, "payload", payload_path, number_of_workers: @number_of_workers)
54
+ Dir.mktmpdir do |dir|
55
+ payload_path = dir
56
+ result = sorbet("--no-config --print=payload-sources:#{payload_path}")
57
+
58
+ unless result.status
59
+ raise Thor::Error, <<~ERROR
60
+ "Sorbet failed to dump payload"
61
+ #{result.err}
62
+ ERROR
67
63
  end
68
- else
69
- raise Thor::Error, <<~ERROR
70
- The version of Sorbet used in your Gemfile.lock does not support `--print=payload-sources`
71
- Current: v#{SORBET_GEM_SPEC.version}
72
- Required: #{FEATURE_REQUIREMENTS[:print_payload_sources]}
73
- ERROR
64
+
65
+ index_rbis(index, "payload", payload_path, number_of_workers: @number_of_workers)
74
66
  end
75
67
  end
76
68
 
@@ -70,7 +70,7 @@ module Tapioca
70
70
  # typed: true
71
71
  # frozen_string_literal: true
72
72
 
73
- # Add your extra requires here (`#{default_command(:require)}` can be used to boostrap this list)
73
+ # Add your extra requires here (`#{default_command(:require)}` can be used to bootstrap this list)
74
74
  CONTENT
75
75
  end
76
76
 
@@ -92,7 +92,7 @@ module Tapioca
92
92
  @installer ||= Bundler::Installer.new(Bundler.root, Bundler.definition)
93
93
  end
94
94
 
95
- sig { returns(Bundler::StubSpecification) }
95
+ sig { returns(T.any(Bundler::StubSpecification, ::Gem::Specification)) }
96
96
  def spec
97
97
  @spec ||= Bundler.definition.specs.find { |s| s.name == "tapioca" }
98
98
  end
@@ -22,6 +22,7 @@ module Tapioca
22
22
  auto_strictness: T::Boolean,
23
23
  gem_dir: String,
24
24
  rbi_formatter: RBIFormatter,
25
+ app_root: String,
25
26
  ).void
26
27
  end
27
28
  def initialize(
@@ -37,7 +38,8 @@ module Tapioca
37
38
  number_of_workers: nil,
38
39
  auto_strictness: true,
39
40
  gem_dir: DEFAULT_GEM_DIR,
40
- rbi_formatter: DEFAULT_RBI_FORMATTER
41
+ rbi_formatter: DEFAULT_RBI_FORMATTER,
42
+ app_root: "."
41
43
  )
42
44
  @requested_constants = requested_constants
43
45
  @outpath = outpath
@@ -52,6 +54,7 @@ module Tapioca
52
54
  @auto_strictness = auto_strictness
53
55
  @gem_dir = gem_dir
54
56
  @rbi_formatter = rbi_formatter
57
+ @app_root = app_root
55
58
 
56
59
  super()
57
60
  end
@@ -61,6 +64,7 @@ module Tapioca
61
64
  Loaders::Dsl.load_application(
62
65
  tapioca_path: @tapioca_path,
63
66
  eager_load: @requested_constants.empty?,
67
+ app_root: @app_root,
64
68
  )
65
69
 
66
70
  pipeline = create_pipeline
@@ -87,6 +91,7 @@ module Tapioca
87
91
  Loaders::Dsl.load_application(
88
92
  tapioca_path: @tapioca_path,
89
93
  eager_load: @requested_constants.empty?,
94
+ app_root: @app_root,
90
95
  )
91
96
 
92
97
  if @should_verify
@@ -101,6 +101,9 @@ module Tapioca
101
101
  # rest parameter type
102
102
  params << signature.rest_type.to_s if signature.has_rest
103
103
 
104
+ # keyrest parameter type
105
+ params << signature.keyrest_type.to_s if signature.has_keyrest
106
+
104
107
  # special case `.void` in a proc
105
108
  unless signature.block_name.nil?
106
109
  params << signature.block_type.to_s.gsub("returns(<VOID>)", "void")
@@ -159,10 +162,7 @@ module Tapioca
159
162
  def compile_method_return_type_to_rbi(method_def)
160
163
  signature = signature_of(method_def)
161
164
  return_type = signature.nil? ? "T.untyped" : name_of_type(signature.return_type)
162
- return_type = "void" if return_type == "<VOID>"
163
- # Map <NOT-TYPED> to `T.untyped`
164
- return_type = "T.untyped" if return_type == "<NOT-TYPED>"
165
- return_type
165
+ sanitize_signature_types(return_type)
166
166
  end
167
167
  end
168
168
  end
@@ -241,6 +241,7 @@ module Tapioca
241
241
 
242
242
  klass.create_method(
243
243
  association_name.to_s,
244
+ comments: association_comments(reflection),
244
245
  return_type: relation_class,
245
246
  )
246
247
  klass.create_method(
@@ -320,6 +321,40 @@ module Tapioca
320
321
  end
321
322
  end
322
323
 
324
+ sig { params(reflection: ReflectionType).returns(T::Array[RBI::Comment]) }
325
+ def association_comments(reflection)
326
+ anchor_name = case reflection
327
+ when ActiveRecord::Reflection::HasOneReflection
328
+ "the-has-one-association"
329
+ when ActiveRecord::Reflection::HasManyReflection
330
+ "the-has-many-association"
331
+ when ActiveRecord::Reflection::HasAndBelongsToManyReflection
332
+ "the-has-and-belongs-to-many-association"
333
+ when ActiveRecord::Reflection::BelongsToReflection
334
+ "the-belongs-to-association"
335
+ when ActiveRecord::Reflection::ThroughReflection
336
+ delegate_reflection = reflection.send(:delegate_reflection)
337
+ declaration = declaration(delegate_reflection)
338
+ if T.must(declaration).match?("has_one")
339
+ "the-has-one-through-association"
340
+ else
341
+ "the-has-many-through-association"
342
+ end
343
+ end
344
+
345
+ if anchor_name
346
+ url = "https://guides.rubyonrails.org/association_basics.html##{anchor_name}"
347
+ association_name = anchor_name.sub(/^the-(.*)-association$/, '\1')
348
+ comment = <<~MSG
349
+ This method is created by ActiveRecord on the `#{reflection.active_record.name}` class because it declared `#{declaration(reflection)}`.
350
+ 🔗 [Rails guide for `#{association_name.gsub("-", "_")}` association](#{url})
351
+ MSG
352
+ [RBI::Comment.new(comment)]
353
+ else
354
+ []
355
+ end
356
+ end
357
+
323
358
  sig do
324
359
  params(
325
360
  reflection: ReflectionType,
@@ -0,0 +1,163 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "active_record"
6
+ rescue LoadError
7
+ return
8
+ end
9
+
10
+ require "tapioca/dsl/helpers/active_record_column_type_helper"
11
+ require "tapioca/dsl/helpers/active_record_constants_helper"
12
+
13
+ module Tapioca
14
+ module Dsl
15
+ module Compilers
16
+ # `Tapioca::Dsl::Compilers::DelegatedTypes` defines RBI files for subclasses of
17
+ # [`ActiveRecord::Base`](https://api.rubyonrails.org/classes/ActiveRecord/Base.html).
18
+ # This compiler is only responsible for defining the methods that would be created for delegated_types that
19
+ # are defined in the Active Record model.
20
+ #
21
+ # For example, with the following model class:
22
+ #
23
+ # ~~~rb
24
+ # class Entry < ActiveRecord::Base
25
+ # delegated_type :entryable, types: %w[ Message Comment ]
26
+ # end
27
+ # ~~~
28
+ #
29
+ # this compiler will produce the following methods in the RBI file
30
+ # `entry.rbi`:
31
+ #
32
+ # ~~~rbi
33
+ # # entry.rbi
34
+ # # typed: true
35
+ #
36
+ # class Entry
37
+ # include GeneratedDelegatedTypeMethods
38
+ #
39
+ # module GeneratedDelegatedTypeMethods
40
+ # sig { params(args: T.untyped).returns(T.any(Message, Comment)) }
41
+ # def build_entryable(*args); end
42
+ #
43
+ # sig { returns(Class) }
44
+ # def entryable_class; end
45
+ #
46
+ # sig { returns(ActiveSupport::StringInquirer) }
47
+ # def entryable_name; end
48
+ #
49
+ # sig { returns(T::Boolean) }
50
+ # def message?; end
51
+ #
52
+ # sig { returns(T.nilable(Message)) }
53
+ # def message; end
54
+ #
55
+ # sig { returns(T.nilable(Integer)) }
56
+ # def message_id; end
57
+ #
58
+ # sig { returns(T::Boolean) }
59
+ # def comment?; end
60
+ #
61
+ # sig { returns(T.nilable(Comment)) }
62
+ # def comment; end
63
+ #
64
+ # sig { returns(T.nilable(Integer)) }
65
+ # def comment_id; end
66
+ # end
67
+ # end
68
+ #
69
+ # ~~~
70
+ class ActiveRecordDelegatedTypes < Compiler
71
+ extend T::Sig
72
+ include Helpers::ActiveRecordConstantsHelper
73
+
74
+ ConstantType = type_member { { fixed: T.all(T.class_of(ActiveRecord::Base), Extensions::ActiveRecord) } }
75
+
76
+ sig { override.void }
77
+ def decorate
78
+ return if constant.__tapioca_delegated_types.nil?
79
+
80
+ root.create_path(constant) do |model|
81
+ model.create_module(DelegatedTypesModuleName) do |mod|
82
+ constant.__tapioca_delegated_types.each do |role, data|
83
+ types = data.fetch(:types)
84
+ options = data.fetch(:options, {})
85
+ populate_role_accessors(mod, role, types)
86
+ populate_type_helpers(mod, role, types, options)
87
+ end
88
+ end
89
+
90
+ model.create_include(DelegatedTypesModuleName)
91
+ end
92
+ end
93
+
94
+ class << self
95
+ extend T::Sig
96
+
97
+ sig { override.returns(T::Enumerable[Module]) }
98
+ def gather_constants
99
+ descendants_of(::ActiveRecord::Base).reject(&:abstract_class?)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ sig { params(mod: RBI::Scope, role: Symbol, types: T::Array[String]).void }
106
+ def populate_role_accessors(mod, role, types)
107
+ mod.create_method(
108
+ "#{role}_name",
109
+ parameters: [],
110
+ return_type: "ActiveSupport::StringInquirer",
111
+ )
112
+
113
+ mod.create_method(
114
+ "#{role}_class",
115
+ parameters: [],
116
+ return_type: "Class",
117
+ )
118
+
119
+ mod.create_method(
120
+ "build_#{role}",
121
+ parameters: [create_rest_param("args", type: "T.untyped")],
122
+ return_type: "T.any(#{types.join(", ")})",
123
+ )
124
+ end
125
+
126
+ sig { params(mod: RBI::Scope, role: Symbol, types: T::Array[String], options: T::Hash[Symbol, T.untyped]).void }
127
+ def populate_type_helpers(mod, role, types, options)
128
+ types.each do |type|
129
+ populate_type_helper(mod, role, type, options)
130
+ end
131
+ end
132
+
133
+ sig { params(mod: RBI::Scope, role: Symbol, type: String, options: T::Hash[Symbol, T.untyped]).void }
134
+ def populate_type_helper(mod, role, type, options)
135
+ singular = type.tableize.tr("/", "_").singularize
136
+ query = "#{singular}?"
137
+ primary_key = options[:primary_key] || "id"
138
+ role_id = options[:foreign_key] || "#{role}_id"
139
+
140
+ getter_type, _ = Helpers::ActiveRecordColumnTypeHelper.new(constant).type_for(role_id.to_s)
141
+
142
+ mod.create_method(
143
+ query,
144
+ parameters: [],
145
+ return_type: "T::Boolean",
146
+ )
147
+
148
+ mod.create_method(
149
+ singular,
150
+ parameters: [],
151
+ return_type: "T.nilable(#{type})",
152
+ )
153
+
154
+ mod.create_method(
155
+ "#{singular}_#{primary_key}",
156
+ parameters: [],
157
+ return_type: as_nilable_type(getter_type),
158
+ )
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -217,6 +217,7 @@ module Tapioca
217
217
  T::Array[Symbol],
218
218
  )
219
219
  FINDER_METHODS = T.let(ActiveRecord::FinderMethods.instance_methods(false), T::Array[Symbol])
220
+ SIGNED_FINDER_METHODS = T.let(ActiveRecord::SignedId::ClassMethods.instance_methods(false), T::Array[Symbol])
220
221
  CALCULATION_METHODS = T.let(ActiveRecord::Calculations.instance_methods(false), T::Array[Symbol])
221
222
  ENUMERABLE_QUERY_METHODS = T.let([:any?, :many?, :none?, :one?], T::Array[Symbol])
222
223
  FIND_OR_CREATE_METHODS = T.let(
@@ -243,15 +244,9 @@ module Tapioca
243
244
  # The model always extends the generated relation module
244
245
  model.create_extend(RelationMethodsModuleName)
245
246
 
246
- # This feature is only available in versions of Sorbet with special support for
247
- # handling `NilClass` returns from `to_ary`. We should not be typing `to_ary` like
248
- # this for older versions since it will make all flatten operations be
249
- # `T::Array[NilClass]`, otherwise.
250
- if sorbet_supports?(:to_ary_nil_support)
251
- # Type the `to_ary` method as returning `NilClass` so that flatten stops recursing
252
- # See https://github.com/sorbet/sorbet/pull/4706 for details
253
- model.create_method("to_ary", return_type: "NilClass", visibility: RBI::Private.new)
254
- end
247
+ # Type the `to_ary` method as returning `NilClass` so that flatten stops recursing
248
+ # See https://github.com/sorbet/sorbet/pull/4706 for details
249
+ model.create_method("to_ary", return_type: "NilClass", visibility: RBI::Private.new)
255
250
 
256
251
  create_relation_class(model)
257
252
  create_association_relation_class(model)
@@ -599,6 +594,31 @@ module Tapioca
599
594
  end
600
595
  end
601
596
 
597
+ SIGNED_FINDER_METHODS.each do |method_name|
598
+ case method_name
599
+ when :find_signed
600
+ create_common_method(
601
+ "find_signed",
602
+ common_relation_methods_module,
603
+ parameters: [
604
+ create_param("signed_id", type: "T.untyped"),
605
+ create_kw_opt_param("purpose", type: "T.untyped", default: "nil"),
606
+ ],
607
+ return_type: as_nilable_type(constant_name),
608
+ )
609
+ when :find_signed!
610
+ create_common_method(
611
+ "find_signed!",
612
+ common_relation_methods_module,
613
+ parameters: [
614
+ create_param("signed_id", type: "T.untyped"),
615
+ create_kw_opt_param("purpose", type: "T.untyped", default: "nil"),
616
+ ],
617
+ return_type: constant_name,
618
+ )
619
+ end
620
+ end
621
+
602
622
  CALCULATION_METHODS.each do |method_name|
603
623
  case method_name
604
624
  when :average, :maximum, :minimum
@@ -95,7 +95,18 @@ module Tapioca
95
95
  case descriptor
96
96
  when Google::Protobuf::EnumDescriptor
97
97
  descriptor.to_h.each do |sym, val|
98
- klass.create_constant(sym.to_s, value: val.to_s)
98
+ # For each enum value, create a namespaced constant on the root rather than an un-namespaced
99
+ # constant within the class. This is because un-namespaced constants might conflict with reserved
100
+ # Ruby words, such as "BEGIN." By namespacing them, we avoid this problem.
101
+ #
102
+ # Invalid syntax:
103
+ # class Foo
104
+ # BEGIN = 3
105
+ # end
106
+ #
107
+ # Valid syntax:
108
+ # Foo::BEGIN = 3
109
+ root.create_constant("#{constant}::#{sym}", value: val.to_s)
99
110
  end
100
111
 
101
112
  klass.create_method(
@@ -273,6 +284,11 @@ module Tapioca
273
284
  return_type: "void",
274
285
  )
275
286
 
287
+ klass.create_method(
288
+ "clear_#{field.name}",
289
+ return_type: "void",
290
+ )
291
+
276
292
  field
277
293
  end
278
294
 
@@ -0,0 +1,29 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "active_record"
6
+ rescue LoadError
7
+ return
8
+ end
9
+
10
+ module Tapioca
11
+ module Dsl
12
+ module Compilers
13
+ module Extensions
14
+ module ActiveRecord
15
+ attr_reader :__tapioca_delegated_types
16
+
17
+ def delegated_type(role, types:, **options)
18
+ @__tapioca_delegated_types ||= {}
19
+ @__tapioca_delegated_types[role] = { types: types, options: options }
20
+
21
+ super
22
+ end
23
+
24
+ ::ActiveRecord::Base.singleton_class.prepend(self)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -41,6 +41,8 @@ module Tapioca
41
41
  "::ActiveSupport::TimeWithZone"
42
42
  when ActiveRecord::Enum::EnumType
43
43
  "::String"
44
+ when ActiveRecord::Type::Serialized
45
+ serialized_column_type(column_type)
44
46
  else
45
47
  handle_unknown_type(column_type)
46
48
  end
@@ -119,6 +121,23 @@ module Tapioca
119
121
  "T.any(::String, ::Symbol)"
120
122
  end
121
123
  end
124
+
125
+ sig { params(column_type: ActiveRecord::Type::Serialized).returns(String) }
126
+ def serialized_column_type(column_type)
127
+ case column_type.coder
128
+ when ActiveRecord::Coders::YAMLColumn
129
+ case column_type.coder.object_class
130
+ when Array.singleton_class
131
+ "T::Array[T.untyped]"
132
+ when Hash.singleton_class
133
+ "T::Hash[T.untyped, T.untyped]"
134
+ else
135
+ "T.untyped"
136
+ end
137
+ else
138
+ "T.untyped"
139
+ end
140
+ end
122
141
  end
123
142
  end
124
143
  end
@@ -13,6 +13,7 @@ module Tapioca
13
13
 
14
14
  AttributeMethodsModuleName = T.let("GeneratedAttributeMethods", String)
15
15
  AssociationMethodsModuleName = T.let("GeneratedAssociationMethods", String)
16
+ DelegatedTypesModuleName = T.let("GeneratedDelegatedTypeMethods", String)
16
17
 
17
18
  RelationMethodsModuleName = T.let("GeneratedRelationMethods", String)
18
19
  AssociationRelationMethodsModuleName = T.let("GeneratedAssociationRelationMethods", String)
@@ -25,7 +25,7 @@ module Tapioca
25
25
  when GraphQL::Types::ISO8601Date.singleton_class
26
26
  type_for_constant(Date)
27
27
  when GraphQL::Types::ISO8601DateTime.singleton_class
28
- type_for_constant(DateTime)
28
+ type_for_constant(Time)
29
29
  when GraphQL::Types::JSON.singleton_class
30
30
  "T::Hash[::String, T.untyped]"
31
31
  when GraphQL::Schema::Enum.singleton_class
@@ -128,9 +128,11 @@ module Tapioca
128
128
  def filter_anonymous_and_reloaded_constants(constants)
129
129
  # Group constants by their names
130
130
  constants_by_name = constants
131
- .group_by { |c| T.must(Runtime::Reflection.name_of(c)) }
131
+ .group_by { |c| Runtime::Reflection.name_of(c) }
132
132
  .select { |name, _| !name.nil? }
133
133
 
134
+ constants_by_name = T.cast(constants_by_name, T::Hash[String, T::Array[Module]])
135
+
134
136
  # Find the constants that have been reloaded
135
137
  reloaded_constants = constants_by_name.select { |_, constants| constants.size > 1 }.keys
136
138
 
@@ -241,8 +241,7 @@ module Tapioca
241
241
  klass = class_of(value)
242
242
 
243
243
  klass_name = if klass == ObjectSpace::WeakMap
244
- # WeakMap is an implicit generic with one type variable
245
- "ObjectSpace::WeakMap[T.untyped]"
244
+ sorbet_supports?(:non_generic_weak_map) ? "ObjectSpace::WeakMap" : "ObjectSpace::WeakMap[T.untyped]"
246
245
  elsif T::Generic === klass
247
246
  generic_name_of(klass)
248
247
  else
@@ -342,7 +341,7 @@ module Tapioca
342
341
 
343
342
  sig { params(constant: Module, strict: T::Boolean).returns(T::Boolean) }
344
343
  def defined_in_gem?(constant, strict: true)
345
- files = Set.new(get_file_candidates(constant))
344
+ files = get_file_candidates(constant)
346
345
  .merge(Runtime::Trackers::ConstantDefinition.files_for(constant))
347
346
 
348
347
  return !strict if files.empty?
@@ -352,13 +351,11 @@ module Tapioca
352
351
  end
353
352
  end
354
353
 
355
- sig { params(constant: Module).returns(T::Array[String]) }
354
+ sig { params(constant: Module).returns(T::Set[String]) }
356
355
  def get_file_candidates(constant)
357
- wrapped_module = Pry::WrappedModule.new(constant)
358
-
359
- wrapped_module.send(:method_candidates).flatten.filter_map(&:source_file).uniq
356
+ file_candidates_for(constant)
360
357
  rescue ArgumentError, NameError
361
- []
358
+ Set.new
362
359
  end
363
360
 
364
361
  sig { params(name: String).void }
@@ -5,7 +5,7 @@ module Tapioca
5
5
  module GemHelper
6
6
  extend T::Sig
7
7
 
8
- sig { params(app_dir: String, full_gem_path: String).returns(T::Boolean) }
8
+ sig { params(app_dir: T.any(String, Pathname), full_gem_path: String).returns(T::Boolean) }
9
9
  def gem_in_app_dir?(app_dir, full_gem_path)
10
10
  app_dir = to_realpath(app_dir)
11
11
  full_gem_path = to_realpath(full_gem_path)
@@ -161,7 +161,8 @@ module Tapioca
161
161
  RBI::Parser.parse_file(file)
162
162
  rescue RBI::ParseError => e
163
163
  say_error("\nWarning: #{e} (#{e.location})", :yellow)
164
- end
164
+ nil
165
+ end.compact
165
166
 
166
167
  index.visit_all(trees)
167
168
  end
@@ -23,22 +23,14 @@ module Tapioca
23
23
  def serialize_type_variable(type, variance, fixed, upper, lower)
24
24
  variance = nil if variance == :invariant
25
25
 
26
- bounds = []
27
- bounds << "fixed: #{fixed}" if fixed
28
- bounds << "lower: #{lower}" if lower
29
- bounds << "upper: #{upper}" if upper
30
-
31
- parameters = []
32
26
  block = []
27
+ block << "fixed: #{fixed}" if fixed
28
+ block << "lower: #{lower}" if lower
29
+ block << "upper: #{upper}" if upper
33
30
 
31
+ parameters = []
34
32
  parameters << ":#{variance}" if variance
35
33
 
36
- if sorbet_supports?(:type_variable_block_syntax)
37
- block = bounds
38
- else
39
- parameters.concat(bounds)
40
- end
41
-
42
34
  serialized = type.dup
43
35
  serialized << "(#{parameters.join(", ")})" unless parameters.empty?
44
36
  serialized << " { { #{block.join(", ")} } }" unless block.empty?
@@ -20,9 +20,8 @@ module Tapioca
20
20
  SORBET_PAYLOAD_URL = "https://github.com/sorbet/sorbet/tree/master/rbi"
21
21
 
22
22
  FEATURE_REQUIREMENTS = T.let({
23
- to_ary_nil_support: ::Gem::Requirement.new(">= 0.5.9220"), # https://github.com/sorbet/sorbet/pull/4706
24
- print_payload_sources: ::Gem::Requirement.new(">= 0.5.9818"), # https://github.com/sorbet/sorbet/pull/5504
25
- type_variable_block_syntax: ::Gem::Requirement.new(">= 0.5.9892"), # https://github.com/sorbet/sorbet/pull/5639
23
+ # feature_name: ::Gem::Requirement.new(">= ___"), # https://github.com/sorbet/sorbet/pull/___
24
+ non_generic_weak_map: ::Gem::Requirement.new(">= 0.5.10587"), # https://github.com/sorbet/sorbet/pull/6610
26
25
  }.freeze, T::Hash[Symbol, ::Gem::Requirement])
27
26
 
28
27
  sig { params(sorbet_args: String).returns(Spoom::ExecResult) }
@@ -9,9 +9,9 @@ module Tapioca
9
9
  class << self
10
10
  extend T::Sig
11
11
 
12
- sig { params(tapioca_path: String, eager_load: T::Boolean).void }
13
- def load_application(tapioca_path:, eager_load: true)
14
- loader = new(tapioca_path: tapioca_path)
12
+ sig { params(tapioca_path: String, eager_load: T::Boolean, app_root: String).void }
13
+ def load_application(tapioca_path:, eager_load: true, app_root: ".")
14
+ loader = new(tapioca_path: tapioca_path, app_root: app_root)
15
15
  loader.load
16
16
  end
17
17
  end
@@ -26,12 +26,13 @@ module Tapioca
26
26
 
27
27
  protected
28
28
 
29
- sig { params(tapioca_path: String, eager_load: T::Boolean).void }
30
- def initialize(tapioca_path:, eager_load: true)
29
+ sig { params(tapioca_path: String, eager_load: T::Boolean, app_root: String).void }
30
+ def initialize(tapioca_path:, eager_load: true, app_root: ".")
31
31
  super()
32
32
 
33
33
  @tapioca_path = tapioca_path
34
34
  @eager_load = eager_load
35
+ @app_root = app_root
35
36
  end
36
37
 
37
38
  sig { void }
@@ -64,6 +65,7 @@ module Tapioca
64
65
  load_rails_application(
65
66
  environment_load: true,
66
67
  eager_load: @eager_load,
68
+ app_root: @app_root,
67
69
  )
68
70
 
69
71
  say("Done", :green)
@@ -71,7 +73,7 @@ module Tapioca
71
73
 
72
74
  sig { void }
73
75
  def abort_if_pending_migrations!
74
- return unless File.exist?("config/application.rb")
76
+ return unless File.exist?("#{@app_root}/config/application.rb")
75
77
  return unless defined?(::Rake)
76
78
 
77
79
  Rails.application.load_tasks
@@ -33,16 +33,16 @@ module Tapioca
33
33
  load_rails_engines
34
34
  end
35
35
 
36
- sig { params(environment_load: T::Boolean, eager_load: T::Boolean).void }
37
- def load_rails_application(environment_load: false, eager_load: false)
38
- return unless File.exist?("config/application.rb")
36
+ sig { params(environment_load: T::Boolean, eager_load: T::Boolean, app_root: String).void }
37
+ def load_rails_application(environment_load: false, eager_load: false, app_root: ".")
38
+ return unless File.exist?("#{app_root}/config/application.rb")
39
39
 
40
40
  silence_deprecations
41
41
 
42
42
  if environment_load
43
- require "./config/environment"
43
+ require "./#{app_root}/config/environment"
44
44
  else
45
- require "./config/application"
45
+ require "./#{app_root}/config/application"
46
46
  end
47
47
 
48
48
  eager_load_rails_app if eager_load
@@ -83,13 +83,16 @@ module RBI
83
83
  return_type: String,
84
84
  class_method: T::Boolean,
85
85
  visibility: RBI::Visibility,
86
+ comments: T::Array[RBI::Comment],
86
87
  ).void
87
88
  end
88
- def create_method(name, parameters: [], return_type: "T.untyped", class_method: false, visibility: RBI::Public.new)
89
+ def create_method(name, parameters: [], return_type: "T.untyped", class_method: false, visibility: RBI::Public.new,
90
+ comments: [])
89
91
  return unless Tapioca::RBIHelper.valid_method_name?(name)
90
92
 
91
93
  sig = RBI::Sig.new(return_type: return_type)
92
- method = RBI::Method.new(name, sigs: [sig], is_singleton: class_method, visibility: visibility)
94
+ method = RBI::Method.new(name, sigs: [sig], is_singleton: class_method, visibility: visibility,
95
+ comments: comments)
93
96
  parameters.each do |param|
94
97
  method << param.param
95
98
  sig << RBI::SigParam.new(param.param.name, param.type)
@@ -183,6 +183,61 @@ module Tapioca
183
183
 
184
184
  T.cast(result, Module)
185
185
  end
186
+
187
+ sig { params(constant: Module).returns(T::Set[String]) }
188
+ def file_candidates_for(constant)
189
+ relevant_methods_for(constant).filter_map do |method|
190
+ method.source_location&.first
191
+ end.to_set
192
+ end
193
+
194
+ private
195
+
196
+ sig { params(constant: Module).returns(T::Array[UnboundMethod]) }
197
+ def relevant_methods_for(constant)
198
+ methods = methods_for(constant).select(&:source_location)
199
+ .reject { |x| method_defined_by_forwardable_module?(x) }
200
+
201
+ return methods unless methods.empty?
202
+
203
+ constants_of(constant).flat_map do |const_name|
204
+ if (mod = child_module_for_parent_with_name(constant, const_name.to_s))
205
+ relevant_methods_for(mod)
206
+ else
207
+ []
208
+ end
209
+ end
210
+ end
211
+
212
+ sig { params(constant: Module).returns(T::Array[UnboundMethod]) }
213
+ def methods_for(constant)
214
+ modules = [constant, singleton_class_of(constant)]
215
+ method_list_methods = [
216
+ PUBLIC_INSTANCE_METHODS_METHOD,
217
+ PROTECTED_INSTANCE_METHODS_METHOD,
218
+ PRIVATE_INSTANCE_METHODS_METHOD,
219
+ ]
220
+
221
+ modules.product(method_list_methods).flat_map do |mod, method_list_method|
222
+ method_list_method.bind_call(mod, false).map { |name| mod.instance_method(name) }
223
+ end
224
+ end
225
+
226
+ sig { params(parent: Module, name: String).returns(T.nilable(Module)) }
227
+ def child_module_for_parent_with_name(parent, name)
228
+ return if parent.autoload?(name)
229
+
230
+ child = constantize(name, inherit: true, namespace: parent)
231
+ return unless Module === child
232
+ return unless name_of(child) == "#{name_of(parent)}::#{name}"
233
+
234
+ child
235
+ end
236
+
237
+ sig { params(method: UnboundMethod).returns(T::Boolean) }
238
+ def method_defined_by_forwardable_module?(method)
239
+ method.source_location&.first == Object.const_source_location(:Forwardable)&.first
240
+ end
186
241
  end
187
242
  end
188
243
  end
@@ -62,8 +62,8 @@ module T
62
62
  # we've created a clone of that type with the `name` method returning the
63
63
  # appropriate name for that specific concrete type.
64
64
  def name
65
- if T::Generic === @raw_type || Tapioca::TypeVariableModule === @raw_type
66
- # for types that are generic or are type variables, use the name
65
+ if T::Generic === @raw_type
66
+ # for types that are generic, use the name
67
67
  # returned by the "name" method of this instance
68
68
  @name ||= T.unsafe(@raw_type).name.freeze
69
69
  else
@@ -78,20 +78,42 @@ module T
78
78
  end
79
79
 
80
80
  module Utils
81
- module CoercePatch
82
- def coerce(val)
83
- if val.is_a?(Tapioca::TypeVariableModule)
84
- val.coerce_to_type_variable
85
- elsif val.respond_to?(:__tapioca_override_type)
86
- val.__tapioca_override_type
87
- else
88
- super
81
+ # This duplication is required to preserve backwards compatibility with sorbet-runtime versions prior to the
82
+ # introduction of the `Private` module in https://github.com/sorbet/sorbet/pull/6559.
83
+ if defined?(T::Utils::Private)
84
+ module Private
85
+ module PrivateCoercePatch
86
+ def coerce_and_check_module_types(val, check_val, check_module_type)
87
+ if val.is_a?(Tapioca::TypeVariableModule)
88
+ val.coerce_to_type_variable
89
+ elsif val.respond_to?(:__tapioca_override_type)
90
+ val.__tapioca_override_type
91
+ else
92
+ super
93
+ end
94
+ end
95
+ end
96
+
97
+ class << self
98
+ prepend(PrivateCoercePatch)
99
+ end
100
+ end
101
+ else
102
+ module CoercePatch
103
+ def coerce(val)
104
+ if val.is_a?(Tapioca::TypeVariableModule)
105
+ val.coerce_to_type_variable
106
+ elsif val.respond_to?(:__tapioca_override_type)
107
+ val.__tapioca_override_type
108
+ else
109
+ super
110
+ end
89
111
  end
90
112
  end
91
- end
92
113
 
93
- class << self
94
- prepend(CoercePatch)
114
+ class << self
115
+ prepend(CoercePatch)
116
+ end
95
117
  end
96
118
  end
97
119
  end
@@ -62,14 +62,14 @@ module Tapioca
62
62
 
63
63
  sig { params(paths: T::Array[Pathname]).returns(T::Set[String]) }
64
64
  def symbols_from_paths(paths)
65
- output = T.cast(Tempfile.create("sorbet") do |file|
65
+ output = Tempfile.create("sorbet") do |file|
66
66
  file.write(Array(paths).join("\n"))
67
67
  file.flush
68
68
 
69
69
  symbol_table_json_from("@#{file.path.shellescape}")
70
- end, T.nilable(String))
70
+ end
71
71
 
72
- return Set.new if output.nil? || output.empty?
72
+ return Set.new if output.empty?
73
73
 
74
74
  SymbolTableParser.parse_json(output)
75
75
  end
@@ -43,6 +43,12 @@ module Tapioca
43
43
 
44
44
  next if name.nil?
45
45
  next unless SKIP_PARSE_KINDS.include?(kind)
46
+
47
+ # Turn singleton class names to attached class names
48
+ if (match_data = name.match(/<Class:(.*)>/))
49
+ name = match_data[1]
50
+ end
51
+
46
52
  next if name.match?(/[<>()$]/)
47
53
  next if name.match?(/^[0-9]+$/)
48
54
  next if name == "T::Helpers"
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Tapioca
5
- VERSION = "0.10.2"
5
+ VERSION = "0.10.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.10.2
4
+ version: 0.10.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: 2022-09-29 00:00:00.000000000 Z
14
+ date: 2022-12-19 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: bundler
@@ -55,20 +55,6 @@ dependencies:
55
55
  - - ">="
56
56
  - !ruby/object:Gem::Version
57
57
  version: 1.21.0
58
- - !ruby/object:Gem::Dependency
59
- name: pry
60
- requirement: !ruby/object:Gem::Requirement
61
- requirements:
62
- - - ">="
63
- - !ruby/object:Gem::Version
64
- version: 0.12.2
65
- type: :runtime
66
- prerelease: false
67
- version_requirements: !ruby/object:Gem::Requirement
68
- requirements:
69
- - - ">="
70
- - !ruby/object:Gem::Version
71
- version: 0.12.2
72
58
  - !ruby/object:Gem::Dependency
73
59
  name: rbi
74
60
  requirement: !ruby/object:Gem::Requirement
@@ -78,7 +64,7 @@ dependencies:
78
64
  version: 0.0.0
79
65
  - - ">="
80
66
  - !ruby/object:Gem::Version
81
- version: 0.0.14
67
+ version: 0.0.16
82
68
  type: :runtime
83
69
  prerelease: false
84
70
  version_requirements: !ruby/object:Gem::Requirement
@@ -88,21 +74,21 @@ dependencies:
88
74
  version: 0.0.0
89
75
  - - ">="
90
76
  - !ruby/object:Gem::Version
91
- version: 0.0.14
77
+ version: 0.0.16
92
78
  - !ruby/object:Gem::Dependency
93
79
  name: sorbet-static-and-runtime
94
80
  requirement: !ruby/object:Gem::Requirement
95
81
  requirements:
96
82
  - - ">="
97
83
  - !ruby/object:Gem::Version
98
- version: 0.5.9204
84
+ version: 0.5.10187
99
85
  type: :runtime
100
86
  prerelease: false
101
87
  version_requirements: !ruby/object:Gem::Requirement
102
88
  requirements:
103
89
  - - ">="
104
90
  - !ruby/object:Gem::Version
105
- version: 0.5.9204
91
+ version: 0.5.10187
106
92
  - !ruby/object:Gem::Dependency
107
93
  name: spoom
108
94
  requirement: !ruby/object:Gem::Requirement
@@ -185,6 +171,7 @@ files:
185
171
  - lib/tapioca/dsl/compilers/active_model_secure_password.rb
186
172
  - lib/tapioca/dsl/compilers/active_record_associations.rb
187
173
  - lib/tapioca/dsl/compilers/active_record_columns.rb
174
+ - lib/tapioca/dsl/compilers/active_record_delegated_types.rb
188
175
  - lib/tapioca/dsl/compilers/active_record_enum.rb
189
176
  - lib/tapioca/dsl/compilers/active_record_fixtures.rb
190
177
  - lib/tapioca/dsl/compilers/active_record_relations.rb
@@ -206,6 +193,7 @@ files:
206
193
  - lib/tapioca/dsl/compilers/smart_properties.rb
207
194
  - lib/tapioca/dsl/compilers/state_machines.rb
208
195
  - lib/tapioca/dsl/compilers/url_helpers.rb
196
+ - lib/tapioca/dsl/extensions/active_record.rb
209
197
  - lib/tapioca/dsl/extensions/frozen_record.rb
210
198
  - lib/tapioca/dsl/helpers/active_record_column_type_helper.rb
211
199
  - lib/tapioca/dsl/helpers/active_record_constants_helper.rb