tapioca 0.4.1 → 0.4.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/README.md +8 -4
- data/Rakefile +1 -0
- data/lib/tapioca.rb +4 -2
- data/lib/tapioca/cli.rb +1 -1
- data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +1 -0
- data/lib/tapioca/compilers/dsl/active_record_associations.rb +1 -19
- data/lib/tapioca/compilers/dsl/active_record_columns.rb +42 -17
- data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +2 -3
- data/lib/tapioca/compilers/dsl/active_record_scope.rb +1 -1
- data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +0 -2
- data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +1 -1
- data/lib/tapioca/compilers/dsl/base.rb +2 -0
- data/lib/tapioca/compilers/dsl/protobuf.rb +8 -8
- data/lib/tapioca/compilers/dsl/url_helpers.rb +83 -6
- data/lib/tapioca/compilers/requires_compiler.rb +4 -4
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +64 -18
- data/lib/tapioca/gemfile.rb +32 -9
- data/lib/tapioca/generator.rb +14 -4
- data/lib/tapioca/loader.rb +10 -0
- data/lib/tapioca/version.rb +1 -1
- metadata +17 -4
- data/lib/tapioca/sorbet_config_parser.rb +0 -77
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dd111409d415b9a17a11cd2d1a5d6d60fdab411934fc5bd1ade87b4bee9e27a3
|
4
|
+
data.tar.gz: 6cc495adc75090725de7f8cb873672e804452c16931d5a995c543827ab6400cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 993e27b7960ce9830d58098a3a965214342821d87c3753e3b6a72ab8d9d33e86c1a5f454141cd2f19b73565ecfe9f3454c7c7d45825256c27c02ccd2d2396780
|
7
|
+
data.tar.gz: bfeb2377b5a5b059ce4897cbe1b7e39cef0657d799479ffd75c646897236928cd7fe317fd9cf9a49597b8dd28fc7657e90d9386eb82927cc9cdbb806203c4538
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -8,6 +8,8 @@ As yet, no gem exports type information in a consumable format and it would be a
|
|
8
8
|
|
9
9
|
When you run `tapioca sync` in a project, `tapioca` loads all the gems that are in your dependency list from the Gemfile into memory. It then performs runtime introspection on the loaded types to understand their structure and generates an appropriate RBI file for each gem with a versioned filename.
|
10
10
|
|
11
|
+
## Manual gem requires
|
12
|
+
|
11
13
|
For gems that have a normal default `require` and load all of their constants through such a require, everything works seamlessly. However, for gems that are marked as `require: false` in the Gemfile, or for gems that export optionally loaded types via different requires, where a single require does not load the whole gem code into memory, `tapioca` will not be able to load some of the types into memory and, thus, won't be able to generate complete RBIs for them. For this reason, we need to keep a small external file named `sorbet/tapioca/require.rb` that is executed after all the gems in the Gemfile have been required and before generation of gem RBIs have started. This file is responsible for adding the requires for additional files from gems, which are not covered by the default require.
|
12
14
|
|
13
15
|
For example, suppose you are using the class `BetterHtml::Parser` exported from the `better_html` gem. Just doing a `require "better_html"` (which is the default require) does not load that type:
|
@@ -95,6 +97,12 @@ Command: `tapioca todo`
|
|
95
97
|
|
96
98
|
This will generate the file `sorbet/rbi/todo.rbi` defining all unresolved constants as empty modules.
|
97
99
|
|
100
|
+
### Generate DSL RBI files
|
101
|
+
|
102
|
+
Command: `tapioca dsl [constant...]`
|
103
|
+
|
104
|
+
This will generate DSL RBIs for specified constants (or for all handled constants, if a constant name is not supplied). You can read about DSL RBI generators supplied by `tapioca` in [the manual](manual/generators.md).
|
105
|
+
|
98
106
|
### Flags
|
99
107
|
|
100
108
|
- `--prerequire [file]`: A file to be required before `Bundler.require` is called.
|
@@ -103,10 +111,6 @@ This will generate the file `sorbet/rbi/todo.rbi` defining all unresolved consta
|
|
103
111
|
- `--generate-command [command]`: The command to run to regenerate RBI files (used in header comment of the RBI files), defaults to the current command.
|
104
112
|
- `--typed-overrides [gem:level]`: Overrides typed sigils for generated gem RBIs for gem `gem` to level `level` (`level` can be one of `ignore`, `false`, `true`, `strict`, or `strong`, see [the Sorbet docs](https://sorbet.org/docs/static#file-level-granularity-strictness-levels) for more details).
|
105
113
|
|
106
|
-
### Strong typing option for ActiveRecord column methods
|
107
|
-
|
108
|
-
`tapioca` gives you the option to generate stricter type signatures for your ActiveRecord column types. By default, methods generated for columns that are defined in the schema have signatures of T.untyped. However, if the object extends a module with name StrongTypeGeneration, tapioca will generate stricter signatures that follow closely with the types defined in the schema. Expectation is the StrongTypeGeneration module you define in your application won't allow objects to be initialized with "bad state". It will check all the attributes that are not nillable to ensure they are not nil.
|
109
|
-
|
110
114
|
## Contributing
|
111
115
|
|
112
116
|
Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/tapioca. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](https://github.com/Shopify/tapioca/blob/master/CODE_OF_CONDUCT.md) code of conduct.
|
data/Rakefile
CHANGED
data/lib/tapioca.rb
CHANGED
@@ -4,10 +4,12 @@
|
|
4
4
|
require "sorbet-runtime"
|
5
5
|
|
6
6
|
module Tapioca
|
7
|
-
def self.silence_warnings
|
7
|
+
def self.silence_warnings(&blk)
|
8
8
|
original_verbosity = $VERBOSE
|
9
9
|
$VERBOSE = nil
|
10
|
-
|
10
|
+
Gem::DefaultUserInteraction.use_ui(Gem::SilentUI.new) do
|
11
|
+
blk.call
|
12
|
+
end
|
11
13
|
ensure
|
12
14
|
$VERBOSE = original_verbosity
|
13
15
|
end
|
data/lib/tapioca/cli.rb
CHANGED
@@ -132,11 +132,7 @@ module Tapioca
|
|
132
132
|
end
|
133
133
|
def populate_single_assoc_getter_setter(klass, constant, association_name, reflection)
|
134
134
|
association_class = type_for(constant, reflection)
|
135
|
-
association_type =
|
136
|
-
association_class
|
137
|
-
else
|
138
|
-
"T.nilable(#{association_class})"
|
139
|
-
end
|
135
|
+
association_type = "T.nilable(#{association_class})"
|
140
136
|
|
141
137
|
create_method(
|
142
138
|
klass,
|
@@ -227,20 +223,6 @@ module Tapioca
|
|
227
223
|
)
|
228
224
|
end
|
229
225
|
|
230
|
-
sig do
|
231
|
-
params(
|
232
|
-
constant: T.class_of(ActiveRecord::Base),
|
233
|
-
reflection: ReflectionType
|
234
|
-
).returns(T::Boolean)
|
235
|
-
end
|
236
|
-
def belongs_to_and_required?(constant, reflection)
|
237
|
-
return false unless constant.table_exists?
|
238
|
-
return false unless reflection.belongs_to?
|
239
|
-
column_definition = constant.columns_hash[reflection.foreign_key.to_s]
|
240
|
-
|
241
|
-
!column_definition.nil? && !column_definition.null
|
242
|
-
end
|
243
|
-
|
244
226
|
sig do
|
245
227
|
params(
|
246
228
|
constant: T.class_of(ActiveRecord::Base),
|
@@ -17,6 +17,17 @@ module Tapioca
|
|
17
17
|
# responsible for defining the attribute methods that would be created for the columns that
|
18
18
|
# are defined in the Active Record model.
|
19
19
|
#
|
20
|
+
# **Note:** This generator, by default, generates weak signatures for column methods and treats each
|
21
|
+
# column to be `T.untyped`. This is done on purpose to ensure that the nilability of Active Record
|
22
|
+
# columns do not make it hard for existing code to adopt gradual typing. It is possible, however, to
|
23
|
+
# generate stricter type signatures for your ActiveRecord column types. If your ActiveRecord model extends
|
24
|
+
# a module with name `StrongTypeGeneration`, this generator will generate stricter signatures that follow
|
25
|
+
# closely with the types defined in the schema.
|
26
|
+
#
|
27
|
+
# The `StrongTypeGeneration` module you define in your application should add an `after_initialize` callback
|
28
|
+
# to the model and ensure that all the non-nilable attributes of the model are actually initialized with non-`nil`
|
29
|
+
# values.
|
30
|
+
#
|
20
31
|
# For example, with the following model class:
|
21
32
|
#
|
22
33
|
# ~~~rb
|
@@ -196,25 +207,25 @@ module Tapioca
|
|
196
207
|
klass,
|
197
208
|
"#{attribute_name}_before_last_save",
|
198
209
|
methods_to_add,
|
199
|
-
return_type: getter_type
|
210
|
+
return_type: as_nilable_type(getter_type)
|
200
211
|
)
|
201
212
|
add_method(
|
202
213
|
klass,
|
203
214
|
"#{attribute_name}_change_to_be_saved",
|
204
215
|
methods_to_add,
|
205
|
-
return_type: "[#{getter_type}, #{getter_type}]"
|
216
|
+
return_type: "T.nilable([#{getter_type}, #{getter_type}])"
|
206
217
|
)
|
207
218
|
add_method(
|
208
219
|
klass,
|
209
220
|
"#{attribute_name}_in_database",
|
210
221
|
methods_to_add,
|
211
|
-
return_type: getter_type
|
222
|
+
return_type: as_nilable_type(getter_type)
|
212
223
|
)
|
213
224
|
add_method(
|
214
225
|
klass,
|
215
226
|
"saved_change_to_#{attribute_name}",
|
216
227
|
methods_to_add,
|
217
|
-
return_type: "[#{getter_type}, #{getter_type}]"
|
228
|
+
return_type: "T.nilable([#{getter_type}, #{getter_type}])"
|
218
229
|
)
|
219
230
|
add_method(
|
220
231
|
klass,
|
@@ -235,7 +246,7 @@ module Tapioca
|
|
235
246
|
klass,
|
236
247
|
"#{attribute_name}_change",
|
237
248
|
methods_to_add,
|
238
|
-
return_type: "[#{getter_type}, #{getter_type}]"
|
249
|
+
return_type: "T.nilable([#{getter_type}, #{getter_type}])"
|
239
250
|
)
|
240
251
|
add_method(
|
241
252
|
klass,
|
@@ -252,13 +263,13 @@ module Tapioca
|
|
252
263
|
klass,
|
253
264
|
"#{attribute_name}_was",
|
254
265
|
methods_to_add,
|
255
|
-
return_type: getter_type
|
266
|
+
return_type: as_nilable_type(getter_type)
|
256
267
|
)
|
257
268
|
add_method(
|
258
269
|
klass,
|
259
270
|
"#{attribute_name}_previous_change",
|
260
271
|
methods_to_add,
|
261
|
-
return_type: "[#{getter_type}, #{getter_type}]"
|
272
|
+
return_type: "T.nilable([#{getter_type}, #{getter_type}])"
|
262
273
|
)
|
263
274
|
add_method(
|
264
275
|
klass,
|
@@ -270,7 +281,7 @@ module Tapioca
|
|
270
281
|
klass,
|
271
282
|
"#{attribute_name}_previously_was",
|
272
283
|
methods_to_add,
|
273
|
-
return_type: getter_type
|
284
|
+
return_type: as_nilable_type(getter_type)
|
274
285
|
)
|
275
286
|
add_method(
|
276
287
|
klass,
|
@@ -349,9 +360,9 @@ module Tapioca
|
|
349
360
|
!(constant.singleton_class < Object.const_get(:StrongTypeGeneration))
|
350
361
|
end
|
351
362
|
|
352
|
-
sig { params(column_type:
|
363
|
+
sig { params(column_type: Object).returns(String) }
|
353
364
|
def handle_unknown_type(column_type)
|
354
|
-
return "T.untyped" unless
|
365
|
+
return "T.untyped" unless ActiveModel::Type::Value === column_type
|
355
366
|
|
356
367
|
lookup_return_type_of_method(column_type, :deserialize) ||
|
357
368
|
lookup_return_type_of_method(column_type, :cast) ||
|
@@ -359,19 +370,33 @@ module Tapioca
|
|
359
370
|
"T.untyped"
|
360
371
|
end
|
361
372
|
|
362
|
-
sig { params(column_type:
|
373
|
+
sig { params(column_type: ActiveModel::Type::Value, method: Symbol).returns(T.nilable(String)) }
|
363
374
|
def lookup_return_type_of_method(column_type, method)
|
364
|
-
signature = T::Private::Methods.signature_for_method(column_type.
|
375
|
+
signature = T::Private::Methods.signature_for_method(column_type.method(method))
|
365
376
|
return unless signature
|
366
377
|
|
367
|
-
return_type = signature.return_type
|
368
|
-
|
378
|
+
return_type = signature.return_type
|
379
|
+
return if T::Types::Simple === return_type && T::Generic === return_type.raw_type
|
380
|
+
return if return_type == T::Private::Types::Void || return_type == T::Private::Types::NotTyped
|
381
|
+
|
382
|
+
return_type.to_s
|
369
383
|
end
|
370
384
|
|
371
|
-
sig { params(column_type:
|
385
|
+
sig { params(column_type: ActiveModel::Type::Value, method: Symbol).returns(T.nilable(String)) }
|
372
386
|
def lookup_arg_type_of_method(column_type, method)
|
373
|
-
signature = T::Private::Methods.signature_for_method(column_type.
|
374
|
-
|
387
|
+
signature = T::Private::Methods.signature_for_method(column_type.method(method))
|
388
|
+
return unless signature
|
389
|
+
|
390
|
+
arg_type = signature.arg_types.first.last
|
391
|
+
return if T::Types::Simple === arg_type && T::Generic === arg_type.raw_type
|
392
|
+
|
393
|
+
arg_type.to_s
|
394
|
+
end
|
395
|
+
|
396
|
+
sig { params(type: String).returns(String) }
|
397
|
+
def as_nilable_type(type)
|
398
|
+
return type if type.start_with?("T.nilable(")
|
399
|
+
"T.nilable(#{type})"
|
375
400
|
end
|
376
401
|
end
|
377
402
|
end
|
@@ -16,7 +16,7 @@ module Tapioca
|
|
16
16
|
module Compilers
|
17
17
|
module Dsl
|
18
18
|
# `Tapioca::Compilers::DSL::ActiveRecordIdentityCache` generates RBI files for ActiveRecord models
|
19
|
-
# that use `include IdentityCache
|
19
|
+
# that use `include IdentityCache`.
|
20
20
|
# `IdentityCache` is a blob level caching solution to plug into ActiveRecord. (see https://github.com/Shopify/identity_cache).
|
21
21
|
#
|
22
22
|
# For example, with the following ActiveRecord class:
|
@@ -61,7 +61,6 @@ module Tapioca
|
|
61
61
|
# def fetch_by_title_and_review_date(title, review_date, includes: nil); end
|
62
62
|
# end
|
63
63
|
# ~~~
|
64
|
-
|
65
64
|
class ActiveRecordIdentityCache < Base
|
66
65
|
extend T::Sig
|
67
66
|
|
@@ -127,7 +126,7 @@ module Tapioca
|
|
127
126
|
if returns_collection
|
128
127
|
COLLECTION_TYPE.call(cache_type)
|
129
128
|
else
|
130
|
-
"::#{cache_type}"
|
129
|
+
"T.nilable(::#{cache_type})"
|
131
130
|
end
|
132
131
|
rescue ArgumentError
|
133
132
|
"T.untyped"
|
@@ -37,7 +37,7 @@ module Tapioca
|
|
37
37
|
# module Post::GeneratedRelationMethods
|
38
38
|
# sig { params(args: T.untyped, blk: T.untyped).returns(T.untyped) }
|
39
39
|
# def private_kind(*args, &blk); end
|
40
|
-
|
40
|
+
#
|
41
41
|
# sig { params(args: T.untyped, blk: T.untyped).returns(T.untyped) }
|
42
42
|
# def public_kind(*args, &blk); end
|
43
43
|
# end
|
@@ -12,19 +12,19 @@ module Tapioca
|
|
12
12
|
module Compilers
|
13
13
|
module Dsl
|
14
14
|
# `Tapioca::Compilers::Dsl::Protobuf` decorates RBI files for subclasses of
|
15
|
-
# `Google::Protobuf::MessageExts
|
16
|
-
# (see https://github.com/coinbase/protoc-gen-rbi).
|
15
|
+
# `Google::Protobuf::MessageExts` (see https://github.com/protocolbuffers/protobuf/tree/master/ruby).
|
17
16
|
#
|
18
17
|
# For example, with the following "cart.rb" file:
|
19
18
|
#
|
20
19
|
# ~~~rb
|
21
20
|
# Google::Protobuf::DescriptorPool.generated_pool.build do
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
21
|
+
# add_file("cart.proto", :syntax => :proto3) do
|
22
|
+
# add_message "MyCart" do
|
23
|
+
# optional :shop_id, :int32, 1
|
24
|
+
# optional :customer_id, :int64, 2
|
25
|
+
# optional :number_value, :double, 3
|
26
|
+
# optional :string_value, :string, 4
|
27
|
+
# end
|
28
28
|
# end
|
29
29
|
# end
|
30
30
|
# ~~~
|
@@ -6,6 +6,7 @@ require "parlour"
|
|
6
6
|
begin
|
7
7
|
require "rails"
|
8
8
|
require "action_controller"
|
9
|
+
require "action_view"
|
9
10
|
rescue LoadError
|
10
11
|
return
|
11
12
|
end
|
@@ -13,6 +14,74 @@ end
|
|
13
14
|
module Tapioca
|
14
15
|
module Compilers
|
15
16
|
module Dsl
|
17
|
+
# `Tapioca::Compilers::Dsl::UrlHelpers` generates RBI files for classes that include or extend
|
18
|
+
# `Rails.application.routes.url_helpers`
|
19
|
+
# (see https://api.rubyonrails.org/v5.1.7/classes/ActionDispatch/Routing/UrlFor.html#module-ActionDispatch::Routing::UrlFor-label-URL+generation+for+named+routes).
|
20
|
+
#
|
21
|
+
# For example, with the following setup:
|
22
|
+
#
|
23
|
+
# ~~~rb
|
24
|
+
# # config/application.rb
|
25
|
+
# class Application < Rails::Application
|
26
|
+
# routes.draw do
|
27
|
+
# resource :index
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
# ~~~
|
31
|
+
#
|
32
|
+
# ~~~rb
|
33
|
+
# app/models/post.rb
|
34
|
+
# class Post
|
35
|
+
# include Rails.application.routes.url_helpers
|
36
|
+
# end
|
37
|
+
# ~~~
|
38
|
+
#
|
39
|
+
# this generator will produce the following RBI files:
|
40
|
+
#
|
41
|
+
# ~~~rbi
|
42
|
+
# # generated_path_helpers_module.rbi
|
43
|
+
# # typed: true
|
44
|
+
# module GeneratedPathHelpersModule
|
45
|
+
# include ActionDispatch::Routing::PolymorphicRoutes
|
46
|
+
# include ActionDispatch::Routing::UrlFor
|
47
|
+
#
|
48
|
+
# sig { params(args: T.untyped).returns(String) }
|
49
|
+
# def edit_index_path(*args); end
|
50
|
+
#
|
51
|
+
# sig { params(args: T.untyped).returns(String) }
|
52
|
+
# def index_path(*args); end
|
53
|
+
#
|
54
|
+
# sig { params(args: T.untyped).returns(String) }
|
55
|
+
# def new_index_path(*args); end
|
56
|
+
# end
|
57
|
+
# ~~~
|
58
|
+
#
|
59
|
+
# ~~~rbi
|
60
|
+
# # generated_url_helpers_module.rbi
|
61
|
+
# # typed: true
|
62
|
+
# module GeneratedUrlHelpersModule
|
63
|
+
# include ActionDispatch::Routing::PolymorphicRoutes
|
64
|
+
# include ActionDispatch::Routing::UrlFor
|
65
|
+
#
|
66
|
+
# sig { params(args: T.untyped).returns(String) }
|
67
|
+
# def edit_index_url(*args); end
|
68
|
+
#
|
69
|
+
# sig { params(args: T.untyped).returns(String) }
|
70
|
+
# def index_url(*args); end
|
71
|
+
#
|
72
|
+
# sig { params(args: T.untyped).returns(String) }
|
73
|
+
# def new_index_url(*args); end
|
74
|
+
# end
|
75
|
+
# ~~~
|
76
|
+
#
|
77
|
+
# ~~~rbi
|
78
|
+
# # post.rbi
|
79
|
+
# # typed: true
|
80
|
+
# class Post
|
81
|
+
# include GeneratedPathHelpersModule
|
82
|
+
# include GeneratedUrlHelpersModule
|
83
|
+
# end
|
84
|
+
# ~~~
|
16
85
|
class UrlHelpers < Base
|
17
86
|
extend T::Sig
|
18
87
|
|
@@ -29,13 +98,18 @@ module Tapioca
|
|
29
98
|
end
|
30
99
|
end
|
31
100
|
|
32
|
-
|
101
|
+
NON_DISCOVERABLE_INCLUDERS = T.let([
|
102
|
+
ActionDispatch::IntegrationTest,
|
103
|
+
ActionView::Helpers,
|
104
|
+
], T::Array[Module])
|
105
|
+
|
106
|
+
sig { override.returns(T::Enumerable[Module]) }
|
33
107
|
def gather_constants
|
34
108
|
Object.const_set(:GeneratedUrlHelpersModule, Rails.application.routes.named_routes.url_helpers_module)
|
35
109
|
Object.const_set(:GeneratedPathHelpersModule, Rails.application.routes.named_routes.path_helpers_module)
|
36
110
|
|
37
|
-
|
38
|
-
|
111
|
+
module_enumerator = T.cast(ObjectSpace.each_object(Module), T::Enumerator[Module])
|
112
|
+
constants = module_enumerator.select do |mod|
|
39
113
|
next unless Module.instance_method(:name).bind(mod).call
|
40
114
|
|
41
115
|
includes_helper?(mod, GeneratedUrlHelpersModule) ||
|
@@ -44,7 +118,7 @@ module Tapioca
|
|
44
118
|
includes_helper?(mod.singleton_class, GeneratedPathHelpersModule)
|
45
119
|
end
|
46
120
|
|
47
|
-
constants
|
121
|
+
constants.concat(NON_DISCOVERABLE_INCLUDERS)
|
48
122
|
end
|
49
123
|
|
50
124
|
private
|
@@ -67,8 +141,11 @@ module Tapioca
|
|
67
141
|
|
68
142
|
sig { params(mod: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module), helper_module: Module).void }
|
69
143
|
def create_mixins_for(mod, constant, helper_module)
|
70
|
-
|
71
|
-
|
144
|
+
include_helper = constant.ancestors.include?(helper_module) || NON_DISCOVERABLE_INCLUDERS.include?(constant)
|
145
|
+
extend_helper = constant.singleton_class.ancestors.include?(helper_module)
|
146
|
+
|
147
|
+
mod.create_include(T.must(helper_module.name)) if include_helper
|
148
|
+
mod.create_extend(T.must(helper_module.name)) if extend_helper
|
72
149
|
end
|
73
150
|
|
74
151
|
sig { params(mod: Module, helper: Module).returns(T::Boolean) }
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
# typed: strict
|
3
3
|
|
4
|
-
|
4
|
+
require 'spoom'
|
5
5
|
|
6
6
|
module Tapioca
|
7
7
|
module Compilers
|
@@ -15,7 +15,7 @@ module Tapioca
|
|
15
15
|
|
16
16
|
sig { returns(String) }
|
17
17
|
def compile
|
18
|
-
config =
|
18
|
+
config = Spoom::Sorbet::Config.parse_file(@sorbet_path)
|
19
19
|
files = collect_files(config)
|
20
20
|
files.flat_map do |file|
|
21
21
|
collect_requires(file).reject do |req|
|
@@ -28,7 +28,7 @@ module Tapioca
|
|
28
28
|
|
29
29
|
private
|
30
30
|
|
31
|
-
sig { params(config:
|
31
|
+
sig { params(config: Spoom::Sorbet::Config).returns(T::Array[String]) }
|
32
32
|
def collect_files(config)
|
33
33
|
config.paths.flat_map do |path|
|
34
34
|
path = (Pathname.new(@sorbet_path) / "../.." / path).cleanpath
|
@@ -49,7 +49,7 @@ module Tapioca
|
|
49
49
|
end.compact
|
50
50
|
end
|
51
51
|
|
52
|
-
sig { params(config:
|
52
|
+
sig { params(config: Spoom::Sorbet::Config, file: String).returns(T::Boolean) }
|
53
53
|
def file_ignored_by_sorbet?(config, file)
|
54
54
|
config.ignore.any? do |path|
|
55
55
|
Regexp.new(Regexp.escape(path)) =~ file
|
@@ -29,7 +29,7 @@ module Tapioca
|
|
29
29
|
def generate
|
30
30
|
symbols
|
31
31
|
.sort
|
32
|
-
.map(
|
32
|
+
.map { |symbol| generate_from_symbol(symbol) }
|
33
33
|
.compact
|
34
34
|
.join("\n\n")
|
35
35
|
.concat("\n")
|
@@ -383,8 +383,18 @@ module Tapioca
|
|
383
383
|
indented("include(#{qualified_name_of(mod)})")
|
384
384
|
end.join("\n")
|
385
385
|
|
386
|
-
|
387
|
-
|
386
|
+
ancestors = singleton_class_of(constant).ancestors
|
387
|
+
extends_as_concern = ancestors.any? do |mod|
|
388
|
+
qualified_name_of(mod) == "::ActiveSupport::Concern"
|
389
|
+
end
|
390
|
+
class_methods_module = resolve_constant("#{name_of(constant)}::ClassMethods")
|
391
|
+
|
392
|
+
mixed_in_module = if extends_as_concern && Module === class_methods_module
|
393
|
+
class_methods_module
|
394
|
+
else
|
395
|
+
dynamic_extends.find do |mod|
|
396
|
+
mod != constant && public_module?(mod)
|
397
|
+
end
|
388
398
|
end
|
389
399
|
|
390
400
|
return result if mixed_in_module.nil?
|
@@ -494,20 +504,46 @@ module Tapioca
|
|
494
504
|
return if symbol_ignored?(symbol_name) && !method_in_gem?(method)
|
495
505
|
|
496
506
|
signature = signature_of(method)
|
497
|
-
method = signature.method if signature
|
507
|
+
method = T.let(signature.method, UnboundMethod) if signature
|
498
508
|
|
499
509
|
method_name = method.name.to_s
|
500
510
|
return unless valid_method_name?(method_name)
|
501
511
|
return if struct_method?(constant, method_name)
|
502
512
|
return if method_name.start_with?("__t_props_generated_")
|
503
513
|
|
504
|
-
|
505
|
-
|
506
|
-
|
514
|
+
parameters = T.let(method.parameters, T::Array[[Symbol, T.nilable(Symbol)]])
|
515
|
+
|
516
|
+
sanitized_parameters = parameters.map do |type, name|
|
517
|
+
unless name
|
518
|
+
# For attr_writer methods, Sorbet signatures have the name
|
519
|
+
# of the method (without the trailing = sign) as the name of
|
520
|
+
# the only parameter. So, if the parameter does not have a name
|
521
|
+
# then the replacement name should be the name of the method
|
522
|
+
# (minus trailing =) if and only if there is a signature for the
|
523
|
+
# method and the parameter is required and there is a single
|
524
|
+
# parameter and the signature also defines a single parameter and
|
525
|
+
# the name of the method ends with a = character.
|
526
|
+
writer_method_with_sig = (
|
527
|
+
signature && type == :req &&
|
528
|
+
parameters.size == 1 &&
|
529
|
+
signature.arg_types.size == 1 &&
|
530
|
+
method_name[-1] == "="
|
531
|
+
)
|
532
|
+
|
533
|
+
name = if writer_method_with_sig
|
534
|
+
T.must(method_name[0...-1]).to_sym
|
535
|
+
else
|
536
|
+
:_
|
537
|
+
end
|
538
|
+
end
|
507
539
|
|
508
540
|
# Sanitize param names
|
509
541
|
name = name.to_s.gsub(/[^a-zA-Z0-9_]/, '_')
|
510
542
|
|
543
|
+
[type, name]
|
544
|
+
end
|
545
|
+
|
546
|
+
parameter_list = sanitized_parameters.map do |type, name|
|
511
547
|
case type
|
512
548
|
when :req
|
513
549
|
name
|
@@ -526,26 +562,31 @@ module Tapioca
|
|
526
562
|
end
|
527
563
|
end.join(', ')
|
528
564
|
|
529
|
-
|
565
|
+
parameter_list = "(#{parameter_list})" if parameter_list != ""
|
566
|
+
signature_str = indented(compile_signature(signature, sanitized_parameters)) if signature
|
530
567
|
|
531
|
-
signature_str = indented(compile_signature(signature)) if signature
|
532
568
|
[
|
533
569
|
signature_str,
|
534
|
-
indented("def #{method_name}#{
|
570
|
+
indented("def #{method_name}#{parameter_list}; end"),
|
535
571
|
].compact.join("\n")
|
536
572
|
end
|
537
573
|
|
538
574
|
TYPE_PARAMETER_MATCHER = /T\.type_parameter\(:?([[:word:]]+)\)/
|
539
575
|
|
540
|
-
sig { params(signature: T.untyped).returns(String) }
|
541
|
-
def compile_signature(signature)
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
576
|
+
sig { params(signature: T.untyped, parameters: T::Array[[Symbol, String]]).returns(String) }
|
577
|
+
def compile_signature(signature, parameters)
|
578
|
+
parameter_types = T.let(signature.arg_types.to_h, T::Hash[Symbol, T::Types::Base])
|
579
|
+
parameter_types.merge!(signature.kwarg_types)
|
580
|
+
parameter_types[signature.rest_name] = signature.rest_type if signature.has_rest
|
581
|
+
parameter_types[signature.keyrest_name] = signature.keyrest_type if signature.has_keyrest
|
582
|
+
parameter_types[signature.block_name] = signature.block_type if signature.block_name
|
583
|
+
|
584
|
+
params = parameters.map do |_, name|
|
585
|
+
type = parameter_types[name.to_sym]
|
586
|
+
"#{name}: #{type}"
|
587
|
+
end.join(", ")
|
546
588
|
|
547
|
-
|
548
|
-
returns = signature.return_type.to_s
|
589
|
+
returns = type_of(signature.return_type)
|
549
590
|
|
550
591
|
type_parameters = (params + returns).scan(TYPE_PARAMETER_MATCHER).flatten.uniq.map { |p| ":#{p}" }.join(", ")
|
551
592
|
type_parameters = ".type_parameters(#{type_parameters})" unless type_parameters.empty?
|
@@ -764,6 +805,11 @@ module Tapioca
|
|
764
805
|
nil
|
765
806
|
end
|
766
807
|
|
808
|
+
sig { params(constant: Module).returns(String) }
|
809
|
+
def type_of(constant)
|
810
|
+
constant.to_s.gsub(/\bAttachedClass\b/, "T.attached_class")
|
811
|
+
end
|
812
|
+
|
767
813
|
sig { params(constant: Module, other: BasicObject).returns(T::Boolean).checked(:never) }
|
768
814
|
def are_equal?(constant, other)
|
769
815
|
BasicObject.instance_method(:equal?).bind(constant).call(other)
|
data/lib/tapioca/gemfile.rb
CHANGED
@@ -28,7 +28,7 @@ module Tapioca
|
|
28
28
|
sig { returns(T::Array[Gem]) }
|
29
29
|
def dependencies
|
30
30
|
@dependencies ||= begin
|
31
|
-
specs = definition.specs.to_a
|
31
|
+
specs = definition.locked_gems.specs.to_a
|
32
32
|
|
33
33
|
definition
|
34
34
|
.resolve
|
@@ -79,17 +79,18 @@ module Tapioca
|
|
79
79
|
extend(T::Sig)
|
80
80
|
|
81
81
|
IGNORED_GEMS = T.let(%w{
|
82
|
-
sorbet sorbet-static sorbet-runtime
|
82
|
+
sorbet sorbet-static sorbet-runtime
|
83
83
|
}.freeze, T::Array[String])
|
84
84
|
|
85
85
|
sig { returns(String) }
|
86
|
-
attr_reader :full_gem_path
|
86
|
+
attr_reader :full_gem_path, :version
|
87
87
|
|
88
88
|
sig { params(spec: Spec).void }
|
89
89
|
def initialize(spec)
|
90
90
|
@spec = T.let(spec, Tapioca::Gemfile::Spec)
|
91
91
|
real_gem_path = to_realpath(@spec.full_gem_path)
|
92
92
|
@full_gem_path = T.let(real_gem_path, String)
|
93
|
+
@version = T.let(version_string, String)
|
93
94
|
end
|
94
95
|
|
95
96
|
sig { params(gemfile_dir: String).returns(T::Boolean) }
|
@@ -109,11 +110,6 @@ module Tapioca
|
|
109
110
|
@spec.name
|
110
111
|
end
|
111
112
|
|
112
|
-
sig { returns(::Gem::Version) }
|
113
|
-
def version
|
114
|
-
@spec.version
|
115
|
-
end
|
116
|
-
|
117
113
|
sig { returns(String) }
|
118
114
|
def rbi_file_name
|
119
115
|
"#{name}@#{version}.rbi"
|
@@ -121,11 +117,38 @@ module Tapioca
|
|
121
117
|
|
122
118
|
sig { params(path: String).returns(T::Boolean) }
|
123
119
|
def contains_path?(path)
|
124
|
-
to_realpath(path).start_with?(full_gem_path)
|
120
|
+
to_realpath(path).start_with?(full_gem_path) || has_parent_gemspec?(path)
|
125
121
|
end
|
126
122
|
|
127
123
|
private
|
128
124
|
|
125
|
+
sig { returns(String) }
|
126
|
+
def version_string
|
127
|
+
version = @spec.version.to_s
|
128
|
+
version += "-#{@spec.source.revision}" if Bundler::Source::Git === @spec.source
|
129
|
+
version
|
130
|
+
end
|
131
|
+
|
132
|
+
sig { params(path: String).returns(T::Boolean) }
|
133
|
+
def has_parent_gemspec?(path)
|
134
|
+
# For some Git installed gems the location of the loaded file can
|
135
|
+
# be different from the gem path as indicated by the spec file
|
136
|
+
#
|
137
|
+
# To compensate for these cases, we walk up the directory hierarchy
|
138
|
+
# from the given file and try to match a <gem-name.gemspec> file in
|
139
|
+
# one of those folders to see if the path really belongs in the given gem
|
140
|
+
# or not.
|
141
|
+
return false unless Bundler::Source::Git === @spec.source
|
142
|
+
parent = Pathname.new(path)
|
143
|
+
|
144
|
+
until parent.root?
|
145
|
+
parent = parent.parent.expand_path
|
146
|
+
return true if parent.join("#{name}.gemspec").file?
|
147
|
+
end
|
148
|
+
|
149
|
+
false
|
150
|
+
end
|
151
|
+
|
129
152
|
sig { params(path: T.any(String, Pathname)).returns(String) }
|
130
153
|
def to_realpath(path)
|
131
154
|
path_string = path.to_s
|
data/lib/tapioca/generator.rb
CHANGED
@@ -162,6 +162,11 @@ module Tapioca
|
|
162
162
|
|
163
163
|
private
|
164
164
|
|
165
|
+
EMPTY_RBI_COMMENT = <<~CONTENT
|
166
|
+
# THIS IS AN EMPTY RBI FILE.
|
167
|
+
# see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
|
168
|
+
CONTENT
|
169
|
+
|
165
170
|
sig { returns(Gemfile) }
|
166
171
|
def bundle
|
167
172
|
@bundle ||= Gemfile.new
|
@@ -422,20 +427,25 @@ module Tapioca
|
|
422
427
|
say("Compiling #{gem_name}, this may take a few seconds... ")
|
423
428
|
|
424
429
|
strictness = config.typed_overrides[gem.name] || "true"
|
425
|
-
|
430
|
+
rbi_body_content = compiler.compile(gem)
|
426
431
|
content = String.new
|
427
432
|
content << rbi_header(
|
428
433
|
config.generate_command,
|
429
434
|
reason: "types exported from the `#{gem.name}` gem",
|
430
435
|
strictness: strictness
|
431
436
|
)
|
432
|
-
content << compiler.compile(gem)
|
433
437
|
|
434
438
|
FileUtils.mkdir_p(config.outdir)
|
435
439
|
filename = config.outpath / gem.rbi_file_name
|
436
|
-
File.write(filename.to_s, content)
|
437
440
|
|
438
|
-
|
441
|
+
if rbi_body_content.strip.empty?
|
442
|
+
content << EMPTY_RBI_COMMENT
|
443
|
+
say("Done (empty output)", :yellow)
|
444
|
+
else
|
445
|
+
content << rbi_body_content
|
446
|
+
say("Done", :green)
|
447
|
+
end
|
448
|
+
File.write(filename.to_s, content)
|
439
449
|
|
440
450
|
Pathname.glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
|
441
451
|
remove(file) unless file.basename.to_s == gem.rbi_file_name
|
data/lib/tapioca/loader.rb
CHANGED
@@ -99,6 +99,16 @@ module Tapioca
|
|
99
99
|
|
100
100
|
sig { void }
|
101
101
|
def eager_load_rails_app
|
102
|
+
if Object.const_defined?("ActiveSupport")
|
103
|
+
Object.const_get("ActiveSupport").run_load_hooks(
|
104
|
+
:before_eager_load,
|
105
|
+
Object.const_get("Rails").application
|
106
|
+
)
|
107
|
+
end
|
108
|
+
if Object.const_defined?("Zeitwerk::Loader")
|
109
|
+
zeitwerk_loader = Object.const_get("Zeitwerk::Loader")
|
110
|
+
zeitwerk_loader.eager_load_all
|
111
|
+
end
|
102
112
|
Object.const_get("Rails").autoloaders.each(&:eager_load)
|
103
113
|
end
|
104
114
|
|
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.4.
|
4
|
+
version: 0.4.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ufuk Kayserilioglu
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: exe
|
13
13
|
cert_chain: []
|
14
|
-
date: 2020-
|
14
|
+
date: 2020-09-15 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: pry
|
@@ -69,6 +69,20 @@ dependencies:
|
|
69
69
|
- - ">="
|
70
70
|
- !ruby/object:Gem::Version
|
71
71
|
version: 2.1.0
|
72
|
+
- !ruby/object:Gem::Dependency
|
73
|
+
name: spoom
|
74
|
+
requirement: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
type: :runtime
|
80
|
+
prerelease: false
|
81
|
+
version_requirements: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
72
86
|
- !ruby/object:Gem::Dependency
|
73
87
|
name: thor
|
74
88
|
requirement: !ruby/object:Gem::Requirement
|
@@ -127,7 +141,6 @@ files:
|
|
127
141
|
- lib/tapioca/gemfile.rb
|
128
142
|
- lib/tapioca/generator.rb
|
129
143
|
- lib/tapioca/loader.rb
|
130
|
-
- lib/tapioca/sorbet_config_parser.rb
|
131
144
|
- lib/tapioca/version.rb
|
132
145
|
homepage: https://github.com/Shopify/tapioca
|
133
146
|
licenses:
|
@@ -142,7 +155,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
155
|
requirements:
|
143
156
|
- - ">="
|
144
157
|
- !ruby/object:Gem::Version
|
145
|
-
version: 2.
|
158
|
+
version: '2.4'
|
146
159
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
160
|
requirements:
|
148
161
|
- - ">="
|
@@ -1,77 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
module Tapioca
|
5
|
-
class SorbetConfig
|
6
|
-
extend T::Sig
|
7
|
-
|
8
|
-
sig { returns(T::Array[String]) }
|
9
|
-
attr_reader :paths, :ignore
|
10
|
-
|
11
|
-
sig { void }
|
12
|
-
def initialize
|
13
|
-
@paths = T.let([], T::Array[String])
|
14
|
-
@ignore = T.let([], T::Array[String])
|
15
|
-
end
|
16
|
-
|
17
|
-
class << self
|
18
|
-
extend T::Sig
|
19
|
-
|
20
|
-
sig { params(sorbet_config_path: String).returns(SorbetConfig) }
|
21
|
-
def parse_file(sorbet_config_path)
|
22
|
-
parse_string(File.read(sorbet_config_path))
|
23
|
-
end
|
24
|
-
|
25
|
-
sig { params(sorbet_config: String).returns(SorbetConfig) }
|
26
|
-
def parse_string(sorbet_config)
|
27
|
-
config = SorbetConfig.new
|
28
|
-
ignore = T.let(false, T::Boolean)
|
29
|
-
skip = T.let(false, T::Boolean)
|
30
|
-
sorbet_config.each_line do |line|
|
31
|
-
line = line.strip
|
32
|
-
case line
|
33
|
-
when /^--ignore$/
|
34
|
-
ignore = true
|
35
|
-
next
|
36
|
-
when /^--ignore=/
|
37
|
-
config.ignore << parse_option(line)
|
38
|
-
next
|
39
|
-
when /^--file$/
|
40
|
-
next
|
41
|
-
when /^--file=/
|
42
|
-
config.paths << parse_option(line)
|
43
|
-
next
|
44
|
-
when /^--dir$/
|
45
|
-
next
|
46
|
-
when /^--dir=/
|
47
|
-
config.paths << parse_option(line)
|
48
|
-
next
|
49
|
-
when /^--.*=/
|
50
|
-
next
|
51
|
-
when /^--/
|
52
|
-
skip = true
|
53
|
-
when /^-.*=?/
|
54
|
-
next
|
55
|
-
else
|
56
|
-
if ignore
|
57
|
-
config.ignore << line
|
58
|
-
ignore = false
|
59
|
-
elsif skip
|
60
|
-
skip = false
|
61
|
-
else
|
62
|
-
config.paths << line
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
config
|
67
|
-
end
|
68
|
-
|
69
|
-
private
|
70
|
-
|
71
|
-
sig { params(line: String).returns(String) }
|
72
|
-
def parse_option(line)
|
73
|
-
T.must(line.split("=").last).strip
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|