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.
- checksums.yaml +4 -4
- data/README.md +69 -27
- data/lib/tapioca/cli.rb +11 -1
- data/lib/tapioca/commands/abstract_dsl.rb +6 -1
- data/lib/tapioca/commands/check_shims.rb +3 -0
- data/lib/tapioca/commands/configure.rb +1 -0
- data/lib/tapioca/commands/dsl_generate.rb +15 -0
- data/lib/tapioca/commands/todo.rb +8 -2
- data/lib/tapioca/dsl/compiler.rb +13 -13
- data/lib/tapioca/dsl/compilers/aasm.rb +1 -1
- data/lib/tapioca/dsl/compilers/action_controller_helpers.rb +2 -2
- data/lib/tapioca/dsl/compilers/action_mailer.rb +1 -1
- data/lib/tapioca/dsl/compilers/action_text.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_job.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_model_attributes.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_model_secure_password.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_model_validations_confirmation.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_associations.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_columns.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_delegated_types.rb +65 -17
- data/lib/tapioca/dsl/compilers/active_record_enum.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_fixtures.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_relations.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_scope.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_secure_token.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_store.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_typed_store.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_resource.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_storage.rb +2 -2
- data/lib/tapioca/dsl/compilers/active_support_concern.rb +6 -6
- data/lib/tapioca/dsl/compilers/active_support_current_attributes.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_support_environment_inquirer.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_support_time_ext.rb +1 -1
- data/lib/tapioca/dsl/compilers/config.rb +2 -2
- data/lib/tapioca/dsl/compilers/frozen_record.rb +1 -1
- data/lib/tapioca/dsl/compilers/graphql_input_object.rb +1 -1
- data/lib/tapioca/dsl/compilers/graphql_mutation.rb +1 -1
- data/lib/tapioca/dsl/compilers/identity_cache.rb +2 -2
- data/lib/tapioca/dsl/compilers/json_api_client_resource.rb +1 -1
- data/lib/tapioca/dsl/compilers/kredis.rb +1 -1
- data/lib/tapioca/dsl/compilers/mixed_in_class_attributes.rb +2 -2
- data/lib/tapioca/dsl/compilers/protobuf.rb +1 -1
- data/lib/tapioca/dsl/compilers/rails_generators.rb +1 -1
- data/lib/tapioca/dsl/compilers/sidekiq_worker.rb +1 -1
- data/lib/tapioca/dsl/compilers/smart_properties.rb +1 -1
- data/lib/tapioca/dsl/compilers/state_machines.rb +2 -2
- data/lib/tapioca/dsl/compilers/url_helpers.rb +7 -7
- data/lib/tapioca/dsl/helpers/graphql_type_helper.rb +4 -2
- data/lib/tapioca/dsl/pipeline.rb +13 -37
- data/lib/tapioca/gem/events.rb +7 -7
- data/lib/tapioca/gem/listeners/methods.rb +8 -8
- data/lib/tapioca/gem/listeners/mixins.rb +3 -3
- data/lib/tapioca/gem/listeners/sorbet_signatures.rb +5 -3
- data/lib/tapioca/gem/listeners/sorbet_type_variables.rb +1 -1
- data/lib/tapioca/gem/pipeline.rb +18 -18
- data/lib/tapioca/helpers/sorbet_helper.rb +19 -0
- data/lib/tapioca/helpers/test/dsl_compiler.rb +2 -9
- data/lib/tapioca/internal.rb +2 -0
- data/lib/tapioca/rbi_ext/model.rb +8 -5
- data/lib/tapioca/rbs/rewriter.rb +80 -25
- data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +6 -6
- data/lib/tapioca/runtime/generic_type_registry.rb +17 -18
- data/lib/tapioca/runtime/reflection.rb +20 -20
- data/lib/tapioca/runtime/trackers/constant_definition.rb +3 -3
- data/lib/tapioca/runtime/trackers/method_definition.rb +4 -4
- data/lib/tapioca/runtime/trackers/mixin.rb +5 -5
- data/lib/tapioca/runtime/trackers/required_ancestor.rb +2 -2
- data/lib/tapioca/runtime/trackers/tracker.rb +1 -1
- data/lib/tapioca/sorbet_ext/generic_name_patch.rb +1 -21
- data/lib/tapioca/sorbet_ext/generic_type_patch.rb +48 -0
- data/lib/tapioca/sorbet_ext/void_patch.rb +29 -0
- data/lib/tapioca/static/symbol_loader.rb +19 -1
- data/lib/tapioca/version.rb +1 -1
- metadata +8 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b165a37e65e25d71de2a502703464d1f877232f4b1d1a173052821e0c7e2170
|
|
4
|
+
data.tar.gz: f75734a7d89e0dba3f4fbee6c86ab3754dfad69cdaa0bd2575c872ec3020bb65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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]
|
|
493
|
-
|
|
494
|
-
[--file-header], [--no-file-header], [--skip-file-header]
|
|
495
|
-
|
|
496
|
-
[--only=compiler [compiler ...]]
|
|
497
|
-
[--exclude=compiler [compiler ...]]
|
|
498
|
-
[--verify], [--no-verify], [--skip-verify]
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
[--
|
|
512
|
-
|
|
513
|
-
[--
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
-
|
|
518
|
-
|
|
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[
|
|
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
|
|
@@ -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(
|
|
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
|
data/lib/tapioca/dsl/compiler.rb
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
module Tapioca
|
|
5
5
|
module Dsl
|
|
6
6
|
# @abstract
|
|
7
|
-
#: [ConstantType <
|
|
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[
|
|
22
|
+
@@requested_constants = [] #: Array[Module[top]] # rubocop:disable Style/ClassVars
|
|
23
23
|
|
|
24
24
|
class << self
|
|
25
|
-
#: (
|
|
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[
|
|
31
|
+
#: -> Enumerable[Module[top]]
|
|
32
32
|
def gather_constants = raise NotImplementedError, "Abstract method called"
|
|
33
33
|
|
|
34
|
-
#: -> Set[
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
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
|
-
|
|
127
|
+
rest_type = signature.rest_type
|
|
128
|
+
params << rest_type.to_s if rest_type
|
|
128
129
|
|
|
129
130
|
# keyrest parameter type
|
|
130
|
-
|
|
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[
|
|
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[
|
|
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
|
-
#: (
|
|
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? }
|
|
@@ -89,7 +89,7 @@ module Tapioca
|
|
|
89
89
|
|
|
90
90
|
class << self
|
|
91
91
|
# @override
|
|
92
|
-
#: -> Enumerable[
|
|
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[
|
|
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 }
|
|
@@ -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
|
-
|
|
81
|
-
|
|
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[
|
|
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[
|
|
100
|
-
def populate_role_accessors(mod, role,
|
|
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:
|
|
122
|
+
return_type: build_return_type(resolved_types),
|
|
117
123
|
)
|
|
118
124
|
end
|
|
119
125
|
|
|
120
|
-
#: (RBI::Scope mod, Symbol role, Array[
|
|
121
|
-
def populate_type_helpers(mod, role,
|
|
122
|
-
|
|
123
|
-
populate_type_helper(mod, role,
|
|
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,
|
|
128
|
-
def populate_type_helper(mod, role,
|
|
129
|
-
singular =
|
|
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(#{
|
|
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
|