tapioca 0.10.2 → 0.10.4

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