tapioca 0.19.0 → 0.19.2

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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -27
  3. data/lib/tapioca/cli.rb +11 -1
  4. data/lib/tapioca/commands/abstract_dsl.rb +6 -1
  5. data/lib/tapioca/commands/check_shims.rb +3 -0
  6. data/lib/tapioca/commands/configure.rb +1 -0
  7. data/lib/tapioca/commands/dsl_generate.rb +15 -0
  8. data/lib/tapioca/commands/todo.rb +8 -2
  9. data/lib/tapioca/dsl/compiler.rb +13 -13
  10. data/lib/tapioca/dsl/compilers/aasm.rb +1 -1
  11. data/lib/tapioca/dsl/compilers/action_controller_helpers.rb +2 -2
  12. data/lib/tapioca/dsl/compilers/action_mailer.rb +1 -1
  13. data/lib/tapioca/dsl/compilers/action_text.rb +1 -1
  14. data/lib/tapioca/dsl/compilers/active_job.rb +1 -1
  15. data/lib/tapioca/dsl/compilers/active_model_attributes.rb +1 -1
  16. data/lib/tapioca/dsl/compilers/active_model_secure_password.rb +1 -1
  17. data/lib/tapioca/dsl/compilers/active_model_validations_confirmation.rb +1 -1
  18. data/lib/tapioca/dsl/compilers/active_record_associations.rb +1 -1
  19. data/lib/tapioca/dsl/compilers/active_record_columns.rb +1 -1
  20. data/lib/tapioca/dsl/compilers/active_record_delegated_types.rb +65 -17
  21. data/lib/tapioca/dsl/compilers/active_record_enum.rb +1 -1
  22. data/lib/tapioca/dsl/compilers/active_record_fixtures.rb +1 -1
  23. data/lib/tapioca/dsl/compilers/active_record_relations.rb +1 -1
  24. data/lib/tapioca/dsl/compilers/active_record_scope.rb +1 -1
  25. data/lib/tapioca/dsl/compilers/active_record_secure_token.rb +1 -1
  26. data/lib/tapioca/dsl/compilers/active_record_store.rb +1 -1
  27. data/lib/tapioca/dsl/compilers/active_record_typed_store.rb +1 -1
  28. data/lib/tapioca/dsl/compilers/active_resource.rb +1 -1
  29. data/lib/tapioca/dsl/compilers/active_storage.rb +2 -2
  30. data/lib/tapioca/dsl/compilers/active_support_concern.rb +6 -6
  31. data/lib/tapioca/dsl/compilers/active_support_current_attributes.rb +1 -1
  32. data/lib/tapioca/dsl/compilers/active_support_environment_inquirer.rb +1 -1
  33. data/lib/tapioca/dsl/compilers/active_support_time_ext.rb +1 -1
  34. data/lib/tapioca/dsl/compilers/config.rb +2 -2
  35. data/lib/tapioca/dsl/compilers/frozen_record.rb +1 -1
  36. data/lib/tapioca/dsl/compilers/graphql_input_object.rb +1 -1
  37. data/lib/tapioca/dsl/compilers/graphql_mutation.rb +1 -1
  38. data/lib/tapioca/dsl/compilers/identity_cache.rb +2 -2
  39. data/lib/tapioca/dsl/compilers/json_api_client_resource.rb +1 -1
  40. data/lib/tapioca/dsl/compilers/kredis.rb +1 -1
  41. data/lib/tapioca/dsl/compilers/mixed_in_class_attributes.rb +2 -2
  42. data/lib/tapioca/dsl/compilers/protobuf.rb +1 -1
  43. data/lib/tapioca/dsl/compilers/rails_generators.rb +1 -1
  44. data/lib/tapioca/dsl/compilers/sidekiq_worker.rb +1 -1
  45. data/lib/tapioca/dsl/compilers/smart_properties.rb +1 -1
  46. data/lib/tapioca/dsl/compilers/state_machines.rb +2 -2
  47. data/lib/tapioca/dsl/compilers/url_helpers.rb +7 -7
  48. data/lib/tapioca/dsl/helpers/graphql_type_helper.rb +4 -2
  49. data/lib/tapioca/dsl/pipeline.rb +13 -37
  50. data/lib/tapioca/gem/events.rb +7 -7
  51. data/lib/tapioca/gem/listeners/methods.rb +8 -8
  52. data/lib/tapioca/gem/listeners/mixins.rb +3 -3
  53. data/lib/tapioca/gem/listeners/sorbet_signatures.rb +5 -3
  54. data/lib/tapioca/gem/listeners/sorbet_type_variables.rb +1 -1
  55. data/lib/tapioca/gem/pipeline.rb +18 -18
  56. data/lib/tapioca/helpers/sorbet_helper.rb +19 -0
  57. data/lib/tapioca/helpers/test/dsl_compiler.rb +2 -9
  58. data/lib/tapioca/internal.rb +2 -0
  59. data/lib/tapioca/rbi_ext/model.rb +8 -5
  60. data/lib/tapioca/rbs/rewriter.rb +80 -25
  61. data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +6 -6
  62. data/lib/tapioca/runtime/generic_type_registry.rb +17 -18
  63. data/lib/tapioca/runtime/reflection.rb +20 -20
  64. data/lib/tapioca/runtime/trackers/constant_definition.rb +3 -3
  65. data/lib/tapioca/runtime/trackers/method_definition.rb +4 -4
  66. data/lib/tapioca/runtime/trackers/mixin.rb +5 -5
  67. data/lib/tapioca/runtime/trackers/required_ancestor.rb +2 -2
  68. data/lib/tapioca/runtime/trackers/tracker.rb +1 -1
  69. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +1 -21
  70. data/lib/tapioca/sorbet_ext/generic_type_patch.rb +48 -0
  71. data/lib/tapioca/sorbet_ext/void_patch.rb +29 -0
  72. data/lib/tapioca/static/symbol_loader.rb +19 -1
  73. data/lib/tapioca/version.rb +1 -1
  74. metadata +8 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1af20de26e446268513156110cac416ce7ba2b35f96f4bb7898960d640aa4333
4
- data.tar.gz: 4d760ed85be418d9c9dc166d61ea59ccaf312c929bd4930709abc4b946f3876f
3
+ metadata.gz: 0b165a37e65e25d71de2a502703464d1f877232f4b1d1a173052821e0c7e2170
4
+ data.tar.gz: f75734a7d89e0dba3f4fbee6c86ab3754dfad69cdaa0bd2575c872ec3020bb65
5
5
  SHA512:
6
- metadata.gz: 1bf6bbf64b782f072a23109ca90cf9c3743b82501bf304badcd97b816583f2fff3ec3574d58df22f05af716d750ba86589074f43a1ee5d3f9e383fdb83c8ee02
7
- data.tar.gz: a4d066179177ee84ee349bb7a8514d9b2ad8836051b33adc45ca3708948ddc01a002d6d54fd9c42cc0993ca5e30f74a61f5f5ad8a603ef493b50bee0c7d6ef93
6
+ metadata.gz: 54ab42e4c24755c84f831c8fc5f40db823061558507f4b810af16f7b98c3adac9e02a53b64f9be1f0fb824efec2ca87bec4d6b133b63c37ddb247af1e38b3c45
7
+ data.tar.gz: 8465d7d2a74d715a966addd29069ec850f4d002662674dab172d0165f5d22228ba9855d70c16a15e8564613b758c76921f9919995da079ad04d8d03e039c47ba
data/README.md CHANGED
@@ -50,6 +50,9 @@ Tapioca makes it easy to work with [Sorbet](https://sorbet.org) in your codebase
50
50
  * [Using DSL compiler options](#using-dsl-compiler-options)
51
51
  * [Writing custom DSL compilers](#writing-custom-dsl-compilers)
52
52
  * [Writing custom DSL extensions](#writing-custom-dsl-extensions)
53
+ * [Rewriting RBS comments to Sorbet signatures](#rewriting-rbs-comments-to-sorbet-signatures)
54
+ * [Caching rewrites with Bootsnap](#caching-rewrites-with-bootsnap)
55
+ * [Priming the cache from CI](#priming-the-cache-from-ci)
53
56
  * [RBI files for missing constants and methods](#rbi-files-for-missing-constants-and-methods)
54
57
  * [Configuration](#configuration)
55
58
  * [Editor Integration](#editor-integration)
@@ -489,33 +492,35 @@ Usage:
489
492
  tapioca dsl [constant...]
490
493
 
491
494
  Options:
492
- --out, -o, [--outdir=directory] # The output directory for generated DSL RBI files
493
- # Default: sorbet/rbi/dsl
494
- [--file-header], [--no-file-header], [--skip-file-header] # Add a "This file is generated" header on top of each generated RBI file
495
- # Default: true
496
- [--only=compiler [compiler ...]] # Only run supplied DSL compiler(s)
497
- [--exclude=compiler [compiler ...]] # Exclude supplied DSL compiler(s)
498
- [--verify], [--no-verify], [--skip-verify] # Verifies RBIs are up-to-date
499
- # Default: false
500
- -q, [--quiet], [--no-quiet], [--skip-quiet] # Suppresses file creation output
501
- # Default: false
502
- -w, [--workers=N] # Number of parallel workers to use when generating RBIs (default: auto)
503
- [--rbi-max-line-length=N] # Set the max line length of generated RBIs. Signatures longer than the max line length will be wrapped
504
- # Default: 120
505
- -e, [--environment=ENVIRONMENT] # The Rack/Rails environment to use when generating RBIs
506
- # Default: development
507
- -l, [--list-compilers], [--no-list-compilers], [--skip-list-compilers] # List all loaded compilers
508
- # Default: false
509
- [--app-root=APP_ROOT] # The path to the Rails application
510
- # Default: .
511
- [--halt-upon-load-error], [--no-halt-upon-load-error], [--skip-halt-upon-load-error] # Halt upon a load error while loading the Rails application
512
- # Default: true
513
- [--skip-constant=constant [constant ...]] # Do not generate RBI definitions for the given application constant(s)
514
- [--compiler-options=key:value] # Options to pass to the DSL compilers
515
- -c, [--config=<config file path>] # Path to the Tapioca configuration file
516
- # Default: sorbet/tapioca/config.yml
517
- -V, [--verbose], [--no-verbose], [--skip-verbose] # Verbose output for debugging purposes
518
- # Default: false
495
+ --out, -o, [--outdir=directory] # The output directory for generated DSL RBI files
496
+ # Default: sorbet/rbi/dsl
497
+ [--file-header], [--no-file-header], [--skip-file-header] # Add a "This file is generated" header on top of each generated RBI file
498
+ # Default: true
499
+ [--only=compiler [compiler ...]] # Only run supplied DSL compiler(s)
500
+ [--exclude=compiler [compiler ...]] # Exclude supplied DSL compiler(s)
501
+ [--verify], [--no-verify], [--skip-verify] # Verifies RBIs are up-to-date
502
+ # Default: false
503
+ [--only-bootsnap-rbs-cache], [--no-only-bootsnap-rbs-cache], [--skip-only-bootsnap-rbs-cache] # Only boot the application and load DSL extensions/compilers to populate the bootsnap iseq cache, then exit. Skips compiler execution and RBI generation. Mutually exclusive with --verify and --list-compilers.
504
+ # Default: false
505
+ -q, [--quiet], [--no-quiet], [--skip-quiet] # Suppresses file creation output
506
+ # Default: false
507
+ -w, [--workers=N] # Number of parallel workers to use when generating RBIs (default: auto)
508
+ [--rbi-max-line-length=N] # Set the max line length of generated RBIs. Signatures longer than the max line length will be wrapped
509
+ # Default: 120
510
+ -e, [--environment=ENVIRONMENT] # The Rack/Rails environment to use when generating RBIs
511
+ # Default: development
512
+ -l, [--list-compilers], [--no-list-compilers], [--skip-list-compilers] # List all loaded compilers
513
+ # Default: false
514
+ [--app-root=APP_ROOT] # The path to the Rails application
515
+ # Default: .
516
+ [--halt-upon-load-error], [--no-halt-upon-load-error], [--skip-halt-upon-load-error] # Halt upon a load error while loading the Rails application
517
+ # Default: true
518
+ [--skip-constant=constant [constant ...]] # Do not generate RBI definitions for the given application constant(s)
519
+ [--compiler-options=key:value] # Options to pass to the DSL compilers
520
+ -c, [--config=<config file path>] # Path to the Tapioca configuration file
521
+ # Default: sorbet/tapioca/config.yml
522
+ -V, [--verbose], [--no-verbose], [--skip-verbose] # Verbose output for debugging purposes
523
+ # Default: false
519
524
 
520
525
  Generate RBIs for dynamic methods
521
526
  ```
@@ -836,6 +841,42 @@ In order for DSL extensions to be discovered by Tapioca, they either needs to be
836
841
 
837
842
  For more concrete and advanced examples, take a look at [Tapioca's default DSL extensions](https://github.com/Shopify/tapioca/tree/main/lib/tapioca/dsl/extensions).
838
843
 
844
+ ### Rewriting RBS comments to Sorbet signatures
845
+
846
+ Tapioca translates [RBS comments](https://sorbet.org/docs/rbs-comments) into Sorbet `sig {}` blocks at file load time, so `sorbet-runtime` wraps the methods as if they had been written with native sigs. This is what lets the DSL command introspect signatures that were originally documented as RBS comments.
847
+
848
+ The rewriting is automatic on every `tapioca` invocation: [`require-hooks`](https://github.com/Shopify/require-hooks) intercepts `.rb` loads and `Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigs` translates the source before Ruby compiles it to bytecode.
849
+
850
+ #### Caching rewrites with Bootsnap
851
+
852
+ `tapioca dsl` boots the app and eager-loads source files for introspection, so the rewrite runs across the whole codebase. On large applications this adds noticeable overhead. To cache the rewrite output across runs using [bootsnap](https://github.com/Shopify/bootsnap)'s iseq cache, you can set `TAPIOCA_RBS_CACHE=1`:
853
+
854
+ ```shell
855
+ $ TAPIOCA_RBS_CACHE=1 bin/tapioca dsl
856
+ ```
857
+
858
+ Tapioca configures Bootsnap's iseq cache against a dedicated directory (`tmp/cache/bootsnap-tapioca-rbs` by default; override with `TAPIOCA_BOOTSNAP_CACHE_DIR`). The first run is slower because every file is rewritten and the result is baked into the iseq cache; subsequent runs against the same directory skip the rewrite entirely.
859
+
860
+ `Bootsnap.setup` mutates a process-wide singleton, and a second call would overwrite Tapioca's dedicated cache directory and start writing rewritten iseqs into the host's normal cache. Tapioca enforces this under `TAPIOCA_RBS_CACHE=1`: after its own setup runs, any subsequent `Bootsnap.setup` raises a clear error pointing at the fix. Gate your host's `Bootsnap.setup` on the same env var. Rails apps do this in `config/boot.rb`:
861
+
862
+ ```ruby
863
+ # e.g. config/boot.rb
864
+ require "bootsnap/setup" unless ENV["TAPIOCA_RBS_CACHE"] == "1"
865
+ ```
866
+
867
+ #### Priming the cache from CI
868
+
869
+ For CI pipelines that want to populate the cache once and have downstream jobs read from a warm copy, use `--only-bootsnap-rbs-cache`. This pattern lets you scope cache writes to a single job (the prime) so PR-side jobs read from it without uploading on every successful build:
870
+
871
+ ```shell
872
+ # Prime: populate the cache.
873
+ $ TAPIOCA_RBS_CACHE=1 bin/tapioca dsl --only-bootsnap-rbs-cache
874
+
875
+ # Consumer: read from the populated cache.
876
+ # BOOTSNAP_READONLY=1 prevents bootsnap from writing back to a read-only mount.
877
+ $ TAPIOCA_RBS_CACHE=1 BOOTSNAP_READONLY=1 bin/tapioca dsl
878
+ ```
879
+
839
880
  ### RBI files for missing constants and methods
840
881
 
841
882
  Even after generating the RBIs, it is possible that some constants or methods are still undefined for Sorbet.
@@ -957,6 +998,7 @@ dsl:
957
998
  only: []
958
999
  exclude: []
959
1000
  verify: false
1001
+ only_bootsnap_rbs_cache: false
960
1002
  quiet: false
961
1003
  workers: 1
962
1004
  rbi_max_line_length: 120
data/lib/tapioca/cli.rb CHANGED
@@ -103,6 +103,10 @@ module Tapioca
103
103
  type: :boolean,
104
104
  default: false,
105
105
  desc: "Verifies RBIs are up-to-date"
106
+ option :only_bootsnap_rbs_cache,
107
+ type: :boolean,
108
+ default: false,
109
+ desc: "Only boot the application and load DSL extensions/compilers to populate the bootsnap iseq cache, then exit. Skips compiler execution and RBI generation. Mutually exclusive with --verify and --list-compilers."
106
110
  option :quiet,
107
111
  aliases: ["-q"],
108
112
  type: :boolean,
@@ -146,6 +150,12 @@ module Tapioca
146
150
  def dsl(*constant_or_paths)
147
151
  set_environment(options)
148
152
 
153
+ if options[:only_bootsnap_rbs_cache] && (options[:verify] || options[:list_compilers])
154
+ conflicting = options[:verify] ? "--verify" : "--list-compilers"
155
+ raise MalformattedArgumentError,
156
+ "Options '--only-bootsnap-rbs-cache' and '#{conflicting}' are mutually exclusive"
157
+ end
158
+
149
159
  # Assume anything starting with a capital letter or colon is a class, otherwise a path
150
160
  constants, paths = constant_or_paths.partition { |c| c =~ /\A[A-Z:]/ }
151
161
 
@@ -173,7 +183,7 @@ module Tapioca
173
183
  elsif options[:list_compilers]
174
184
  Commands::DslCompilerList.new(**command_args)
175
185
  else
176
- Commands::DslGenerate.new(**command_args)
186
+ Commands::DslGenerate.new(**command_args, only_bootsnap_rbs_cache: options[:only_bootsnap_rbs_cache])
177
187
  end
178
188
 
179
189
  command.run
@@ -95,6 +95,11 @@ module Tapioca
95
95
  )
96
96
  end.compact
97
97
 
98
+ if @only.any?
99
+ say("Skipping stale RBI removal because `--only` is set.", :yellow)
100
+ return Set.new
101
+ end
102
+
98
103
  files_to_purge = existing_rbi_files - generated_files
99
104
 
100
105
  files_to_purge
@@ -162,7 +167,7 @@ module Tapioca
162
167
  filenames.to_set
163
168
  end
164
169
 
165
- #: (Array[String] constant_names, ?ignore_missing: bool) -> Array[T::Module[top]]
170
+ #: (Array[String] constant_names, ?ignore_missing: bool) -> Array[Module[top]]
166
171
  def constantize(constant_names, ignore_missing: false)
167
172
  constant_map = constant_names.to_h do |name|
168
173
  [name, Object.const_get(name)]
@@ -53,6 +53,9 @@ module Tapioca
53
53
  if @payload
54
54
  Dir.mktmpdir do |dir|
55
55
  payload_path = dir
56
+
57
+ # No need for `--stop-after`, because Sorbet exits immediately after dumping the payload.
58
+ # https://github.com/sorbet/sorbet/blob/41ae325/main/realmain.cc#L522-L552
56
59
  result = sorbet("--no-config --print=payload-sources:#{payload_path}")
57
60
 
58
61
  unless result.status
@@ -38,6 +38,7 @@ module Tapioca
38
38
  .
39
39
  --ignore=tmp/
40
40
  --ignore=vendor/
41
+ --parser=prism
41
42
  CONTENT
42
43
  end
43
44
 
@@ -4,6 +4,12 @@
4
4
  module Tapioca
5
5
  module Commands
6
6
  class DslGenerate < AbstractDsl
7
+ #: (?only_bootsnap_rbs_cache: bool, **untyped) -> void
8
+ def initialize(only_bootsnap_rbs_cache: false, **kwargs)
9
+ @only_bootsnap_rbs_cache = only_bootsnap_rbs_cache
10
+ super(**T.unsafe(kwargs))
11
+ end
12
+
7
13
  private
8
14
 
9
15
  # @override
@@ -11,6 +17,15 @@ module Tapioca
11
17
  def execute
12
18
  load_application
13
19
 
20
+ if @only_bootsnap_rbs_cache
21
+ if ENV["TAPIOCA_RBS_CACHE"] == "1"
22
+ say("Bootsnap RBS cache populated, exiting before RBI generation.", :green)
23
+ else
24
+ say_error("Warning: --only-bootsnap-rbs-cache requires TAPIOCA_RBS_CACHE=1 to populate the cache", :yellow)
25
+ end
26
+ return
27
+ end
28
+
14
29
  say("Compiling DSL RBI files...")
15
30
  say("")
16
31
 
@@ -83,12 +83,18 @@ module Tapioca
83
83
  #: -> Array[String]
84
84
  def unresolved_constants
85
85
  # Taken from https://github.com/sorbet/sorbet/blob/master/gems/sorbet/lib/todo-rbi.rb
86
- sorbet("--print=missing-constants", "--quiet", "--stdout-hup-hack", "--no-error-count")
86
+ sorbet(
87
+ "--print=missing-constants",
88
+ "--stop-after=resolver",
89
+ "--quiet",
90
+ "--stdout-hup-hack",
91
+ "--no-error-count",
92
+ )
87
93
  .out
88
94
  .strip
89
95
  .each_line
90
96
  .filter_map do |line|
91
- next if line.include?("<")
97
+ next if line.include?("<") # Skip singleton classes like `#<Class:String>`
92
98
 
93
99
  line.strip
94
100
  .gsub(/T\.class_of\(([:\w]+)\)/, '\1') # Turn T.class_of(Foo)::Bar into Foo::Bar
@@ -4,7 +4,7 @@
4
4
  module Tapioca
5
5
  module Dsl
6
6
  # @abstract
7
- #: [ConstantType < T::Module[top]]
7
+ #: [ConstantType < Module[top]]
8
8
  class Compiler
9
9
  include RBIHelper
10
10
  include Runtime::Reflection
@@ -19,24 +19,24 @@ module Tapioca
19
19
  #: Hash[String, untyped]
20
20
  attr_reader :options
21
21
 
22
- @@requested_constants = [] #: Array[T::Module[top]] # rubocop:disable Style/ClassVars
22
+ @@requested_constants = [] #: Array[Module[top]] # rubocop:disable Style/ClassVars
23
23
 
24
24
  class << self
25
- #: (T::Module[top] constant) -> bool
25
+ #: (Module[top] constant) -> bool
26
26
  def handles?(constant)
27
27
  processable_constants.include?(constant)
28
28
  end
29
29
 
30
30
  # @abstract
31
- #: -> Enumerable[T::Module[top]]
31
+ #: -> Enumerable[Module[top]]
32
32
  def gather_constants = raise NotImplementedError, "Abstract method called"
33
33
 
34
- #: -> Set[T::Module[top]]
34
+ #: -> Set[Module[top]]
35
35
  def processable_constants
36
- @processable_constants ||= T::Set[T::Module[T.anything]].new.compare_by_identity.merge(gather_constants) #: Set[T::Module[top]]?
36
+ @processable_constants ||= T::Set[T::Module[T.anything]].new.compare_by_identity.merge(gather_constants) #: Set[Module[top]]?
37
37
  end
38
38
 
39
- #: (Array[T::Module[top]] constants) -> void
39
+ #: (Array[Module[top]] constants) -> void
40
40
  def requested_constants=(constants)
41
41
  @@requested_constants = constants # rubocop:disable Style/ClassVars
42
42
  end
@@ -69,13 +69,13 @@ module Tapioca
69
69
  @all_classes ||= all_modules.grep(Class).freeze #: Enumerable[Class[top]]?
70
70
  end
71
71
 
72
- #: -> Enumerable[T::Module[top]]
72
+ #: -> Enumerable[Module[top]]
73
73
  def all_modules
74
74
  @all_modules ||= if @@requested_constants.any?
75
75
  @@requested_constants.grep(Module)
76
76
  else
77
77
  ObjectSpace.each_object(Module).to_a
78
- end.freeze #: Enumerable[T::Module[top]]?
78
+ end.freeze #: Enumerable[Module[top]]?
79
79
  end
80
80
  end
81
81
 
@@ -124,10 +124,12 @@ module Tapioca
124
124
  signature.kwarg_types.each { |_, kwarg_type| params << kwarg_type.to_s }
125
125
 
126
126
  # rest parameter type
127
- params << signature.rest_type.to_s if signature.has_rest
127
+ rest_type = signature.rest_type
128
+ params << rest_type.to_s if rest_type
128
129
 
129
130
  # keyrest parameter type
130
- params << signature.keyrest_type.to_s if signature.has_keyrest
131
+ keyrest_type = signature.keyrest_type
132
+ params << keyrest_type.to_s if keyrest_type
131
133
 
132
134
  # special case `.void` in a proc
133
135
  unless signature.block_name.nil?
@@ -141,14 +143,12 @@ module Tapioca
141
143
  def create_method_from_def(scope, method_def, class_method: false)
142
144
  parameters = compile_method_parameters_to_rbi(method_def)
143
145
  return_type = compile_method_return_type_to_rbi(method_def)
144
- type_params = extract_type_parameters(parameters.map(&:type).push(return_type))
145
146
 
146
147
  scope.create_method(
147
148
  method_def.name.to_s,
148
149
  parameters: parameters,
149
150
  return_type: return_type,
150
151
  class_method: class_method,
151
- type_params: type_params,
152
152
  )
153
153
  end
154
154
 
@@ -202,7 +202,7 @@ module Tapioca
202
202
 
203
203
  class << self
204
204
  # @override
205
- #: -> Enumerable[T::Module[top]]
205
+ #: -> Enumerable[Module[top]]
206
206
  def gather_constants
207
207
  T.cast(ObjectSpace.each_object(::AASM::ClassMethods), T::Enumerable[T::Module[T.anything]])
208
208
  end
@@ -117,7 +117,7 @@ module Tapioca
117
117
 
118
118
  class << self
119
119
  # @override
120
- #: -> Enumerable[T::Module[top]]
120
+ #: -> Enumerable[Module[top]]
121
121
  def gather_constants
122
122
  descendants_of(::ActionController::Base).select(&:name).select do |klass|
123
123
  klass.const_defined?(:HelperMethods, false)
@@ -148,7 +148,7 @@ module Tapioca
148
148
  )
149
149
  end
150
150
 
151
- #: (T::Module[top] mod) -> Array[String]
151
+ #: (Module[top] mod) -> Array[String]
152
152
  def gather_includes(mod)
153
153
  mod.ancestors
154
154
  .reject { |ancestor| ancestor.is_a?(Class) || ancestor == mod || name_of(ancestor).nil? }
@@ -50,7 +50,7 @@ module Tapioca
50
50
 
51
51
  class << self
52
52
  # @override
53
- #: -> Enumerable[T::Module[top]]
53
+ #: -> Enumerable[Module[top]]
54
54
  def gather_constants
55
55
  descendants_of(::ActionMailer::Base).reject(&:abstract?)
56
56
  end
@@ -80,7 +80,7 @@ module Tapioca
80
80
  end
81
81
 
82
82
  # @override
83
- #: -> Enumerable[T::Module[top]]
83
+ #: -> Enumerable[Module[top]]
84
84
  def gather_constants
85
85
  descendants_of(::ActiveRecord::Base)
86
86
  .reject(&:abstract_class?)
@@ -84,7 +84,7 @@ module Tapioca
84
84
 
85
85
  class << self
86
86
  # @override
87
- #: -> Enumerable[T::Module[top]]
87
+ #: -> Enumerable[Module[top]]
88
88
  def gather_constants
89
89
  descendants_of(::ActiveJob::Base)
90
90
  end
@@ -59,7 +59,7 @@ module Tapioca
59
59
 
60
60
  class << self
61
61
  # @override
62
- #: -> Enumerable[T::Module[top]]
62
+ #: -> Enumerable[Module[top]]
63
63
  def gather_constants
64
64
  all_classes.grep(::ActiveModel::Attributes::ClassMethods)
65
65
  end
@@ -89,7 +89,7 @@ module Tapioca
89
89
 
90
90
  class << self
91
91
  # @override
92
- #: -> Enumerable[T::Module[top]]
92
+ #: -> Enumerable[Module[top]]
93
93
  def gather_constants
94
94
  # In some versions of Rails 8.1, `ActiveModel::SecurePassword` uses `Numeric#minutes`
95
95
  # which isn't explicitly required in the gem, and it might not be loaded already.
@@ -44,7 +44,7 @@ module Tapioca
44
44
  class ActiveModelValidationsConfirmation < Compiler
45
45
  class << self
46
46
  # @override
47
- #: -> Enumerable[T::Module[top]]
47
+ #: -> Enumerable[Module[top]]
48
48
  def gather_constants
49
49
  # Collect all the classes that include ActiveModel::Validations
50
50
  all_classes.select { |c| ActiveModel::Validations > c }
@@ -136,7 +136,7 @@ module Tapioca
136
136
 
137
137
  class << self
138
138
  # @override
139
- #: -> Enumerable[T::Module[top]]
139
+ #: -> Enumerable[Module[top]]
140
140
  def gather_constants
141
141
  descendants_of(::ActiveRecord::Base).reject(&:abstract_class?)
142
142
  end
@@ -163,7 +163,7 @@ module Tapioca
163
163
 
164
164
  class << self
165
165
  # @override
166
- #: -> Enumerable[T::Module[top]]
166
+ #: -> Enumerable[Module[top]]
167
167
  def gather_constants
168
168
  descendants_of(::ActiveRecord::Base).reject(&:abstract_class?)
169
169
  end
@@ -33,7 +33,7 @@ module Tapioca
33
33
  # include GeneratedDelegatedTypeMethods
34
34
  #
35
35
  # module GeneratedDelegatedTypeMethods
36
- # sig { params(args: T.untyped).returns(T.any(Message, Comment)) }
36
+ # sig { params(args: T.untyped).returns(T.any(::Message, ::Comment)) }
37
37
  # def build_entryable(*args); end
38
38
  #
39
39
  # sig { returns(Class) }
@@ -45,7 +45,7 @@ module Tapioca
45
45
  # sig { returns(T::Boolean) }
46
46
  # def message?; end
47
47
  #
48
- # sig { returns(T.nilable(Message)) }
48
+ # sig { returns(T.nilable(::Message)) }
49
49
  # def message; end
50
50
  #
51
51
  # sig { returns(T.nilable(Integer)) }
@@ -54,7 +54,7 @@ module Tapioca
54
54
  # sig { returns(T::Boolean) }
55
55
  # def comment?; end
56
56
  #
57
- # sig { returns(T.nilable(Comment)) }
57
+ # sig { returns(T.nilable(::Comment)) }
58
58
  # def comment; end
59
59
  #
60
60
  # sig { returns(T.nilable(Integer)) }
@@ -67,6 +67,9 @@ module Tapioca
67
67
  class ActiveRecordDelegatedTypes < Compiler
68
68
  include Helpers::ActiveRecordConstantsHelper
69
69
 
70
+ # A delegated type entry paired with the fully-qualified constant name it resolves to.
71
+ ResolvedType = Struct.new(:raw_name, :qualified_name, keyword_init: true)
72
+
70
73
  # @override
71
74
  #: -> void
72
75
  def decorate
@@ -77,8 +80,11 @@ module Tapioca
77
80
  constant.__tapioca_delegated_types.each do |role, data|
78
81
  types = data.fetch(:types)
79
82
  options = data.fetch(:options, {})
80
- populate_role_accessors(mod, role, types)
81
- populate_type_helpers(mod, role, types, options)
83
+ resolved_types = types.map do |type|
84
+ ResolvedType.new(raw_name: type, qualified_name: qualified_type_name(type, role))
85
+ end
86
+ populate_role_accessors(mod, role, resolved_types)
87
+ populate_type_helpers(mod, role, resolved_types, options)
82
88
  end
83
89
  end
84
90
 
@@ -88,7 +94,7 @@ module Tapioca
88
94
 
89
95
  class << self
90
96
  # @override
91
- #: -> Enumerable[T::Module[top]]
97
+ #: -> Enumerable[Module[top]]
92
98
  def gather_constants
93
99
  descendants_of(::ActiveRecord::Base).reject(&:abstract_class?)
94
100
  end
@@ -96,8 +102,8 @@ module Tapioca
96
102
 
97
103
  private
98
104
 
99
- #: (RBI::Scope mod, Symbol role, Array[String] types) -> void
100
- def populate_role_accessors(mod, role, types)
105
+ #: (RBI::Scope mod, Symbol role, Array[ResolvedType] resolved_types) -> void
106
+ def populate_role_accessors(mod, role, resolved_types)
101
107
  mod.create_method(
102
108
  "#{role}_name",
103
109
  parameters: [],
@@ -113,20 +119,20 @@ module Tapioca
113
119
  mod.create_method(
114
120
  "build_#{role}",
115
121
  parameters: [create_rest_param("args", type: "T.untyped")],
116
- return_type: types.size == 1 ? types.first : "T.any(#{types.join(", ")})",
122
+ return_type: build_return_type(resolved_types),
117
123
  )
118
124
  end
119
125
 
120
- #: (RBI::Scope mod, Symbol role, Array[String] types, Hash[Symbol, untyped] options) -> void
121
- def populate_type_helpers(mod, role, types, options)
122
- types.each do |type|
123
- populate_type_helper(mod, role, type, options)
126
+ #: (RBI::Scope mod, Symbol role, Array[ResolvedType] resolved_types, Hash[Symbol, untyped] options) -> void
127
+ def populate_type_helpers(mod, role, resolved_types, options)
128
+ resolved_types.each do |resolved_type|
129
+ populate_type_helper(mod, role, resolved_type, options)
124
130
  end
125
131
  end
126
132
 
127
- #: (RBI::Scope mod, Symbol role, String type, Hash[Symbol, untyped] options) -> void
128
- def populate_type_helper(mod, role, type, options)
129
- singular = type.tableize.tr("/", "_").singularize
133
+ #: (RBI::Scope mod, Symbol role, ResolvedType resolved_type, Hash[Symbol, untyped] options) -> void
134
+ def populate_type_helper(mod, role, resolved_type, options)
135
+ singular = resolved_type.raw_name.tableize.tr("/", "_").singularize
130
136
  query = "#{singular}?"
131
137
  primary_key = options[:primary_key] || "id"
132
138
  role_id = options[:foreign_key] || "#{role}_id"
@@ -142,7 +148,7 @@ module Tapioca
142
148
  mod.create_method(
143
149
  singular,
144
150
  parameters: [],
145
- return_type: "T.nilable(#{type})",
151
+ return_type: "T.nilable(#{resolved_type.qualified_name})",
146
152
  )
147
153
 
148
154
  mod.create_method(
@@ -151,6 +157,48 @@ module Tapioca
151
157
  return_type: as_nilable_type(getter_type),
152
158
  )
153
159
  end
160
+
161
+ # Collapses to `T.untyped` if any member is `T.untyped`, since `T.any(::Foo, T.untyped)`
162
+ # is equivalent to `T.untyped` in Sorbet and the per-type error has already been recorded.
163
+ #: (Array[ResolvedType] resolved_types) -> String
164
+ def build_return_type(resolved_types)
165
+ qualified_types = resolved_types.map(&:qualified_name)
166
+ if qualified_types.include?("T.untyped")
167
+ "T.untyped"
168
+ elsif qualified_types.size == 1
169
+ qualified_types.fetch(0)
170
+ else
171
+ "T.any(#{qualified_types.join(", ")})"
172
+ end
173
+ end
174
+
175
+ # Resolves a delegated type entry to a fully-qualified constant name. The strings passed
176
+ # to `delegated_type(..., types: %w[...])` are written verbatim into the generated RBI,
177
+ # but the surrounding `class A::B::C` scope omits `A` and `A::B` from Sorbet's lexical
178
+ # nesting, so a bare `D` reference fails to resolve to `A::B::D` even when that constant
179
+ # exists. `compute_type` is `ActiveRecord::Base`'s own (private) namespace-walking lookup
180
+ # — the same one Rails uses for STI and polymorphic associations — so it resolves both
181
+ # bare and fully-qualified names. When the constant can't be resolved (NameError) or its
182
+ # qualified name can't be derived (anonymous class) we record a compiler error and emit
183
+ # `T.untyped`, which both surfaces the problem and keeps the generated RBI type-checkable.
184
+ #: (String type, Symbol role) -> String
185
+ def qualified_type_name(type, role)
186
+ klass = constant.send(:compute_type, type)
187
+ qualified_name = qualified_name_of(klass)
188
+ return qualified_name if qualified_name
189
+
190
+ add_unresolvable_type_error(type, role)
191
+ rescue NameError, LoadError
192
+ add_unresolvable_type_error(type, role)
193
+ end
194
+
195
+ #: (String type, Symbol role) -> String
196
+ def add_unresolvable_type_error(type, role)
197
+ add_error(<<~MSG.strip)
198
+ Cannot generate delegated_type `#{role}` on `#{constant}` since the type `#{type}` could not be resolved.
199
+ MSG
200
+ "T.untyped"
201
+ end
154
202
  end
155
203
  end
156
204
  end
@@ -74,7 +74,7 @@ module Tapioca
74
74
 
75
75
  class << self
76
76
  # @override
77
- #: -> Enumerable[T::Module[top]]
77
+ #: -> Enumerable[Module[top]]
78
78
  def gather_constants
79
79
  descendants_of(::ActiveRecord::Base)
80
80
  end
@@ -58,7 +58,7 @@ module Tapioca
58
58
 
59
59
  class << self
60
60
  # @override
61
- #: -> Enumerable[T::Module[top]]
61
+ #: -> Enumerable[Module[top]]
62
62
  def gather_constants
63
63
  return [] unless defined?(Rails.application) && Rails.application
64
64