tapioca 0.19.1 → 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 +5 -0
- data/lib/tapioca/commands/dsl_generate.rb +15 -0
- data/lib/tapioca/dsl/compilers/active_record_delegated_types.rb +64 -16
- data/lib/tapioca/dsl/helpers/graphql_type_helper.rb +3 -1
- data/lib/tapioca/dsl/pipeline.rb +3 -27
- data/lib/tapioca/internal.rb +1 -0
- data/lib/tapioca/rbs/rewriter.rb +80 -25
- data/lib/tapioca/runtime/generic_type_registry.rb +11 -12
- data/lib/tapioca/sorbet_ext/generic_name_patch.rb +0 -20
- data/lib/tapioca/sorbet_ext/generic_type_patch.rb +48 -0
- data/lib/tapioca/version.rb +1 -1
- metadata +4 -3
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
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -80,7 +80,9 @@ module Tapioca
|
|
|
80
80
|
signature = Runtime::Reflection.signature_of(method)
|
|
81
81
|
return_type = signature&.return_type
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
# Wrap as non-nilable for required arguments. `coerce_input` supports both
|
|
84
|
+
# required and optional; optional arguments are re-wrapped below based on `type.non_null?`
|
|
85
|
+
valid_return_type?(return_type) ? (T::Utils.unwrap_nilable(return_type) || return_type).to_s : "T.untyped"
|
|
84
86
|
when GraphQL::Schema::InputObject.singleton_class
|
|
85
87
|
type_for_constant(unwrapped_type)
|
|
86
88
|
when Module
|
data/lib/tapioca/dsl/pipeline.rb
CHANGED
|
@@ -138,7 +138,7 @@ module Tapioca
|
|
|
138
138
|
active_compilers.each do |compiler|
|
|
139
139
|
constants.merge(compiler.processable_constants)
|
|
140
140
|
end
|
|
141
|
-
constants =
|
|
141
|
+
constants = filter_anonymous_constants(constants)
|
|
142
142
|
constants -= skipped_constants
|
|
143
143
|
|
|
144
144
|
unless requested_constants.empty? && requested_paths.empty?
|
|
@@ -154,32 +154,8 @@ module Tapioca
|
|
|
154
154
|
end
|
|
155
155
|
|
|
156
156
|
#: (Set[Module[top]] constants) -> Set[Module[top]]
|
|
157
|
-
def
|
|
158
|
-
|
|
159
|
-
constants_by_name = constants
|
|
160
|
-
.group_by { |c| Runtime::Reflection.name_of(c) }
|
|
161
|
-
.select { |name, _| !name.nil? }
|
|
162
|
-
|
|
163
|
-
constants_by_name = T.cast(constants_by_name, T::Hash[String, T::Array[T::Module[T.anything]]])
|
|
164
|
-
|
|
165
|
-
# Find the constants that have been reloaded
|
|
166
|
-
reloaded_constants = constants_by_name.select { |_, constants| constants.size > 1 }.keys
|
|
167
|
-
|
|
168
|
-
unless reloaded_constants.empty? || @lsp_addon
|
|
169
|
-
reloaded_constant_names = reloaded_constants.map { |name| "`#{name}`" }.join(", ")
|
|
170
|
-
|
|
171
|
-
$stderr.puts("WARNING: Multiple constants with the same name: #{reloaded_constant_names}")
|
|
172
|
-
$stderr.puts("Make sure some object is not holding onto these constants during an app reload.")
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Look up all the constants back from their names. The resulting constant set will be the
|
|
176
|
-
# set of constants that are actually in memory with those names.
|
|
177
|
-
filtered_constants = constants_by_name
|
|
178
|
-
.keys
|
|
179
|
-
.map { |name| T.cast(Runtime::Reflection.constantize(name), T::Module[T.anything]) }
|
|
180
|
-
.select { |mod| Runtime::Reflection.constant_defined?(mod) }
|
|
181
|
-
|
|
182
|
-
Set.new.compare_by_identity.merge(filtered_constants)
|
|
157
|
+
def filter_anonymous_constants(constants)
|
|
158
|
+
constants.keep_if { |constant| Runtime::Reflection.name_of(constant) }
|
|
183
159
|
end
|
|
184
160
|
|
|
185
161
|
#: (Module[top] constant) -> RBI::File?
|
data/lib/tapioca/internal.rb
CHANGED
|
@@ -12,6 +12,7 @@ require "tapioca/runtime/dynamic_mixin_compiler"
|
|
|
12
12
|
require "tapioca/sorbet_ext/backcompat_patches"
|
|
13
13
|
require "tapioca/sorbet_ext/name_patch"
|
|
14
14
|
require "tapioca/sorbet_ext/generic_name_patch"
|
|
15
|
+
require "tapioca/sorbet_ext/generic_type_patch"
|
|
15
16
|
require "tapioca/sorbet_ext/proc_bind_patch"
|
|
16
17
|
require "tapioca/sorbet_ext/void_patch"
|
|
17
18
|
require "tapioca/runtime/generic_type_registry"
|
data/lib/tapioca/rbs/rewriter.rb
CHANGED
|
@@ -1,39 +1,93 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "require-hooks/setup"
|
|
5
|
-
|
|
6
4
|
# This code rewrites RBS comments back into Sorbet's signatures as the files are being loaded.
|
|
7
5
|
# This will allow `sorbet-runtime` to wrap the methods as if they were originally written with the `sig{}` blocks.
|
|
8
6
|
# This will in turn allow Tapioca to use this signatures to generate typed RBI files.
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
8
|
+
module Tapioca
|
|
9
|
+
module RBS
|
|
10
|
+
class HostBootsnapSetupError < StandardError; end
|
|
11
|
+
|
|
12
|
+
# Raises when the host calls `Bootsnap.setup` after tapioca's setup. Host's call
|
|
13
|
+
# would overwrite tapioca's cache directory, so rewritten iseqs would end up in
|
|
14
|
+
# the host's regular cache.
|
|
15
|
+
module BootsnapGuard
|
|
16
|
+
extend T::Sig
|
|
17
|
+
|
|
18
|
+
sig { params(_kwargs: T.untyped).void }
|
|
19
|
+
def setup(**_kwargs)
|
|
20
|
+
Kernel.raise HostBootsnapSetupError, <<~MSG
|
|
21
|
+
Bootsnap.setup was called while TAPIOCA_RBS_CACHE=1 is set. Tapioca already
|
|
22
|
+
configured bootsnap with a dedicated cache directory; re-running setup
|
|
23
|
+
would overwrite that config and start writing rewritten iseqs into your
|
|
24
|
+
host's cache.
|
|
25
|
+
|
|
26
|
+
Gate your host's Bootsnap.setup on the env var, e.g. in config/boot.rb:
|
|
27
|
+
|
|
28
|
+
require "bootsnap/setup" unless ENV["TAPIOCA_RBS_CACHE"] == "1"
|
|
29
|
+
MSG
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# When TAPIOCA_RBS_CACHE=1, set up bootsnap with a dedicated cache directory
|
|
36
|
+
# and load require-hooks so the RBS-rewritten iseqs get cached. Subsequent
|
|
37
|
+
# runs read the rewritten iseq directly and skip the rewrite.
|
|
38
|
+
#
|
|
39
|
+
# After our setup, BootsnapGuard is prepended so the host application can't
|
|
40
|
+
# replace our cache directory.
|
|
41
|
+
if ENV["TAPIOCA_RBS_CACHE"] == "1"
|
|
42
|
+
begin
|
|
43
|
+
require "bootsnap"
|
|
44
|
+
# Respect BOOTSNAP_READONLY for consumers reading a pre-populated cache
|
|
45
|
+
# (e.g. a CI prime step).
|
|
46
|
+
readonly = !["0", "false", false].include?(ENV.fetch("BOOTSNAP_READONLY") { false })
|
|
47
|
+
Bootsnap.setup(
|
|
48
|
+
cache_dir: ENV.fetch("TAPIOCA_BOOTSNAP_CACHE_DIR", File.join(Dir.pwd, "tmp/cache/bootsnap-tapioca-rbs")),
|
|
49
|
+
development_mode: true,
|
|
50
|
+
load_path_cache: true,
|
|
51
|
+
compile_cache_iseq: true,
|
|
52
|
+
compile_cache_yaml: true,
|
|
53
|
+
readonly: readonly,
|
|
54
|
+
revalidation: true,
|
|
55
|
+
)
|
|
56
|
+
Bootsnap.log_stats!
|
|
57
|
+
Bootsnap.singleton_class.prepend(Tapioca::RBS::BootsnapGuard)
|
|
58
|
+
rescue LoadError
|
|
59
|
+
# Bootsnap is not in the bundle, skip iseq caching.
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
require "require-hooks/setup"
|
|
63
|
+
else
|
|
64
|
+
require "require-hooks/setup"
|
|
65
|
+
|
|
66
|
+
begin
|
|
67
|
+
# Disable Bootsnap's iseq cache unless TAPIOCA_RBS_CACHE=1 enabled the separate cache above.
|
|
68
|
+
#
|
|
69
|
+
# This is necessary because host apps can call Bootsnap.setup after tapioca loads this file. When that happens,
|
|
70
|
+
# Bootsnap installs `load_iseq` and serves files from its cache, which bypasses RequireHooks.source_transform.
|
|
71
|
+
# Preloading bootsnap's iseq support lets us override `load_iseq` before setup installs it, preserving the default
|
|
72
|
+
# RBS rewrite behavior at the cost of slower app boot.
|
|
73
|
+
require "bootsnap"
|
|
74
|
+
require "bootsnap/compile_cache/iseq"
|
|
75
|
+
|
|
76
|
+
module Bootsnap
|
|
77
|
+
module CompileCache
|
|
78
|
+
module ISeq
|
|
79
|
+
module InstructionSequenceMixin
|
|
80
|
+
#: (String) -> RubyVM::InstructionSequence
|
|
81
|
+
def load_iseq(path)
|
|
82
|
+
super if defined?(super) # Disable Bootsnap's hook, but trigger any others.
|
|
83
|
+
end
|
|
30
84
|
end
|
|
31
85
|
end
|
|
32
86
|
end
|
|
33
87
|
end
|
|
88
|
+
rescue LoadError
|
|
89
|
+
# Bootsnap is not in the bundle, we don't need to do anything.
|
|
34
90
|
end
|
|
35
|
-
rescue LoadError
|
|
36
|
-
# Bootsnap is not in the bundle, we don't need to do anything.
|
|
37
91
|
end
|
|
38
92
|
|
|
39
93
|
# We need to include `T::Sig` very early to make sure that the `sig` method is available since gems using RBS comments
|
|
@@ -47,7 +101,8 @@ RequireHooks.source_transform(patterns: ["**/*.rb"]) do |path, source|
|
|
|
47
101
|
|
|
48
102
|
# For performance reasons, we only rewrite files that use Sorbet.
|
|
49
103
|
if source =~ /^\s*#\s*typed: (ignore|false|true|strict|strong|__STDLIB_INTERNAL)/
|
|
50
|
-
|
|
104
|
+
# Sorbet runtime only supports one signature per method, so keep the last overload.
|
|
105
|
+
Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigs(source, file: path, overloads_strategy: :translate_last)
|
|
51
106
|
end
|
|
52
107
|
rescue Spoom::Sorbet::Translate::Error
|
|
53
108
|
# If we can't translate the RBS comments back into Sorbet's signatures, we just skip the file.
|
|
@@ -6,7 +6,7 @@ module Tapioca
|
|
|
6
6
|
# This class is responsible for storing and looking up information related to generic types.
|
|
7
7
|
#
|
|
8
8
|
# The class stores 2 different kinds of data, in two separate lookup tables:
|
|
9
|
-
# 1. a lookup of generic type instances by name: `@generic_instances`
|
|
9
|
+
# 1. a lookup of generic type instances by constant and name: `@generic_instances`
|
|
10
10
|
# 2. a lookup of type variable serializer by constant and type variable
|
|
11
11
|
# instance: `@type_variables`
|
|
12
12
|
#
|
|
@@ -21,7 +21,7 @@ module Tapioca
|
|
|
21
21
|
# variable to type variable serializers. This allows us to associate type variables
|
|
22
22
|
# to the constant names that represent them, easily.
|
|
23
23
|
module GenericTypeRegistry
|
|
24
|
-
@generic_instances = {} #: Hash[String, Module[top]]
|
|
24
|
+
@generic_instances = {}.compare_by_identity #: Hash[Module[top], Hash[String, Module[top]]]
|
|
25
25
|
|
|
26
26
|
@type_variables = {}.compare_by_identity #: Hash[Module[top], Array[TypeVariableModule]]
|
|
27
27
|
|
|
@@ -45,8 +45,9 @@ module Tapioca
|
|
|
45
45
|
# and cloning the given constant so that we can return a type that is the same
|
|
46
46
|
# as the current type but is a different instance and has a different name method.
|
|
47
47
|
#
|
|
48
|
-
# We cache those cloned instances by their
|
|
49
|
-
# we don't keep instantiating a new type every single
|
|
48
|
+
# We cache those cloned instances by their original constant and their name in
|
|
49
|
+
# `@generic_instances`, so that we don't keep instantiating a new type every single
|
|
50
|
+
# time it is referenced.
|
|
50
51
|
# For example, `[Foo[Integer], Foo[Integer], Foo[Integer], Foo[String]]` will only
|
|
51
52
|
# result in 2 clones (1 for `Foo[Integer]` and another for `Foo[String]`) and
|
|
52
53
|
# 2 hash lookups (for the other two `Foo[Integer]`s).
|
|
@@ -64,12 +65,15 @@ module Tapioca
|
|
|
64
65
|
#
|
|
65
66
|
# Also, we try to memoize the generic type based on the name, so that
|
|
66
67
|
# we don't have to keep recreating them all the time.
|
|
67
|
-
@generic_instances[
|
|
68
|
+
generic_instances = @generic_instances[constant] ||= {}
|
|
69
|
+
generic_instances[name] ||= create_generic_type(constant, name)
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
#: (Object instance) -> bool
|
|
71
73
|
def generic_type_instance?(instance)
|
|
72
|
-
@generic_instances.values.any?
|
|
74
|
+
@generic_instances.values.any? do |generic_instances|
|
|
75
|
+
generic_instances.values.any? { |generic_type| generic_type === instance }
|
|
76
|
+
end
|
|
73
77
|
end
|
|
74
78
|
|
|
75
79
|
#: (Module[top] constant) -> Array[TypeVariableModule]?
|
|
@@ -88,7 +92,7 @@ module Tapioca
|
|
|
88
92
|
# can return it from the original methods as well.
|
|
89
93
|
#: (untyped constant, TypeVariableModule type_variable) -> void
|
|
90
94
|
def register_type_variable(constant, type_variable)
|
|
91
|
-
type_variables =
|
|
95
|
+
type_variables = @type_variables[constant] ||= []
|
|
92
96
|
|
|
93
97
|
type_variables << type_variable
|
|
94
98
|
end
|
|
@@ -163,11 +167,6 @@ module Tapioca
|
|
|
163
167
|
owner.send(:define_method, :inherited, inherited_method)
|
|
164
168
|
end
|
|
165
169
|
end
|
|
166
|
-
|
|
167
|
-
#: (Module[top] constant) -> Array[TypeVariableModule]
|
|
168
|
-
def lookup_or_initialize_type_variables(constant)
|
|
169
|
-
@type_variables[constant] ||= []
|
|
170
|
-
end
|
|
171
170
|
end
|
|
172
171
|
end
|
|
173
172
|
end
|
|
@@ -82,26 +82,6 @@ module T
|
|
|
82
82
|
prepend GenericPatch
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
|
-
|
|
86
|
-
module Utils
|
|
87
|
-
module Private
|
|
88
|
-
module PrivateCoercePatch
|
|
89
|
-
def coerce_and_check_module_types(val, check_val, check_module_type)
|
|
90
|
-
if val.is_a?(Tapioca::TypeVariableModule)
|
|
91
|
-
val.coerce_to_type_variable
|
|
92
|
-
elsif val.respond_to?(:__tapioca_override_type)
|
|
93
|
-
val.__tapioca_override_type
|
|
94
|
-
else
|
|
95
|
-
super
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
class << self
|
|
101
|
-
prepend(PrivateCoercePatch)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
85
|
end
|
|
106
86
|
|
|
107
87
|
module Tapioca
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module T
|
|
5
|
+
module Utils
|
|
6
|
+
module Private
|
|
7
|
+
# Preserve Tapioca's generic type variables and instantiated generic
|
|
8
|
+
# names when Sorbet coerces them into runtime types.
|
|
9
|
+
module TapiocaGenericTypeCoercePatch
|
|
10
|
+
def coerce_and_check_module_types(val, check_val, check_module_type)
|
|
11
|
+
if val.is_a?(Tapioca::TypeVariableModule)
|
|
12
|
+
val.coerce_to_type_variable
|
|
13
|
+
elsif val.respond_to?(:__tapioca_override_type)
|
|
14
|
+
val.__tapioca_override_type
|
|
15
|
+
else
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
prepend(TapiocaGenericTypeCoercePatch)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module Private
|
|
28
|
+
module Casts
|
|
29
|
+
module TapiocaGenericTypeCastPatch
|
|
30
|
+
# https://github.com/sorbet/sorbet/commit/b8d64c7fd9a08e2b9159b5d592bc2de6d586b44a
|
|
31
|
+
# inlines the Module fast path in `T.let`, `T.cast`, `T.bind`, and
|
|
32
|
+
# `T.assert_type!`, so generic module clones can reach this cast path
|
|
33
|
+
# without going through `T::Utils::Private::TapiocaGenericTypeCoercePatch`.
|
|
34
|
+
def cast(value, type, cast_method)
|
|
35
|
+
if type.respond_to?(:__tapioca_override_type)
|
|
36
|
+
type = type.__tapioca_override_type
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
super(value, type, cast_method)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
prepend(TapiocaGenericTypeCastPatch)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/tapioca/version.rb
CHANGED
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.19.
|
|
4
|
+
version: 0.19.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ufuk Kayserilioglu
|
|
@@ -144,14 +144,14 @@ dependencies:
|
|
|
144
144
|
requirements:
|
|
145
145
|
- - ">="
|
|
146
146
|
- !ruby/object:Gem::Version
|
|
147
|
-
version: 1.7.
|
|
147
|
+
version: 1.7.16
|
|
148
148
|
type: :runtime
|
|
149
149
|
prerelease: false
|
|
150
150
|
version_requirements: !ruby/object:Gem::Requirement
|
|
151
151
|
requirements:
|
|
152
152
|
- - ">="
|
|
153
153
|
- !ruby/object:Gem::Version
|
|
154
|
-
version: 1.7.
|
|
154
|
+
version: 1.7.16
|
|
155
155
|
- !ruby/object:Gem::Dependency
|
|
156
156
|
name: tsort
|
|
157
157
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -305,6 +305,7 @@ files:
|
|
|
305
305
|
- lib/tapioca/runtime/trackers/tracker.rb
|
|
306
306
|
- lib/tapioca/sorbet_ext/backcompat_patches.rb
|
|
307
307
|
- lib/tapioca/sorbet_ext/generic_name_patch.rb
|
|
308
|
+
- lib/tapioca/sorbet_ext/generic_type_patch.rb
|
|
308
309
|
- lib/tapioca/sorbet_ext/name_patch.rb
|
|
309
310
|
- lib/tapioca/sorbet_ext/proc_bind_patch.rb
|
|
310
311
|
- lib/tapioca/sorbet_ext/void_patch.rb
|