tapioca 0.4.3 → 0.4.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +3 -0
- data/README.md +7 -5
- data/Rakefile +1 -0
- data/lib/tapioca/cli.rb +9 -2
- data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +1 -0
- data/lib/tapioca/compilers/dsl/active_record_associations.rb +10 -28
- data/lib/tapioca/compilers/dsl/active_record_columns.rb +25 -8
- data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +3 -4
- 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 +14 -10
- data/lib/tapioca/compilers/dsl/protobuf.rb +8 -8
- data/lib/tapioca/compilers/dsl/url_helpers.rb +68 -0
- data/lib/tapioca/compilers/dsl_compiler.rb +5 -5
- data/lib/tapioca/compilers/requires_compiler.rb +37 -9
- data/lib/tapioca/compilers/sorbet.rb +1 -1
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +57 -24
- data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +1 -1
- data/lib/tapioca/compilers/symbol_table_compiler.rb +1 -1
- data/lib/tapioca/compilers/todos_compiler.rb +1 -1
- data/lib/tapioca/core_ext/class.rb +8 -3
- data/lib/tapioca/gemfile.rb +1 -1
- data/lib/tapioca/generator.rb +1 -1
- data/lib/tapioca/loader.rb +1 -1
- 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: f225f517eb2bb6e6d7aa6910bb42a35280b57ad8223dbd8bc255ce92377c6eb3
|
4
|
+
data.tar.gz: f3672f181cbcd71b9c8056c815e77313d75fcb2a78d2be5dbad3d2e51e35de39
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dcd0b9ebf36c2a467bb2e88cb91b1f0634cb772a3a5cf29f7dd3b716ddc3c4fd2ea4dd84b671df243255cc15651deb157d149578dde07f3f7eb26f0a8814a297
|
7
|
+
data.tar.gz: df7355fe19f05210797993286ead0f653a0f5b5f89263f469e8afece16d39026bde0cec1a0b266256ab236f43c27e95bd2912be3a9be74877ca6ca28037e09d7
|
data/Gemfile
CHANGED
@@ -11,6 +11,7 @@ group(:deployment, :development) do
|
|
11
11
|
end
|
12
12
|
|
13
13
|
gem("bundler", "~> 1.17")
|
14
|
+
gem("yard", "~> 0.9.25")
|
14
15
|
gem("pry-byebug")
|
15
16
|
gem("minitest")
|
16
17
|
gem("minitest-hooks")
|
@@ -33,3 +34,5 @@ group(:development, :test) do
|
|
33
34
|
gem("activeresource", "~> 5.1", require: false)
|
34
35
|
gem("google-protobuf", "~>3.12.0", require: false)
|
35
36
|
end
|
37
|
+
|
38
|
+
gem "rubocop-sorbet", ">= 0.4.1"
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Tapioca
|
2
2
|
|
3
|
-
|
3
|
+
![Build Status](https://github.com/Shopify/tapioca/workflows/CI/badge.svg)
|
4
4
|
|
5
5
|
Tapioca is a library used to generate RBI (Ruby interface) files for use with [Sorbet](https://sorbet.org). RBI files provide the structure (classes, modules, methods, parameters) of the gem/library to Sorbet to assist with typechecking.
|
6
6
|
|
@@ -97,6 +97,12 @@ Command: `tapioca todo`
|
|
97
97
|
|
98
98
|
This will generate the file `sorbet/rbi/todo.rbi` defining all unresolved constants as empty modules.
|
99
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
|
+
|
100
106
|
### Flags
|
101
107
|
|
102
108
|
- `--prerequire [file]`: A file to be required before `Bundler.require` is called.
|
@@ -105,10 +111,6 @@ This will generate the file `sorbet/rbi/todo.rbi` defining all unresolved consta
|
|
105
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.
|
106
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).
|
107
113
|
|
108
|
-
### Strong typing option for ActiveRecord column methods
|
109
|
-
|
110
|
-
`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.
|
111
|
-
|
112
114
|
## Contributing
|
113
115
|
|
114
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/cli.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
1
|
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'thor'
|
5
5
|
|
@@ -34,6 +34,8 @@ module Tapioca
|
|
34
34
|
banner: "gem:level [gem:level ...]",
|
35
35
|
desc: "Overrides for typed sigils for generated gem RBIs"
|
36
36
|
|
37
|
+
map T.unsafe(%w[--version -v] => :__print_version)
|
38
|
+
|
37
39
|
desc "init", "initializes folder structure"
|
38
40
|
def init
|
39
41
|
create_file(Config::SORBET_CONFIG, skip: true) do
|
@@ -44,8 +46,8 @@ module Tapioca
|
|
44
46
|
end
|
45
47
|
create_file(Config::DEFAULT_POSTREQUIRE, skip: true) do
|
46
48
|
<<~CONTENT
|
47
|
-
# frozen_string_literal: true
|
48
49
|
# typed: false
|
50
|
+
# frozen_string_literal: true
|
49
51
|
|
50
52
|
# Add your extra requires here
|
51
53
|
CONTENT
|
@@ -92,6 +94,11 @@ module Tapioca
|
|
92
94
|
end
|
93
95
|
end
|
94
96
|
|
97
|
+
desc "--version, -v", "show version"
|
98
|
+
def __print_version
|
99
|
+
puts "Tapioca v#{Tapioca::VERSION}"
|
100
|
+
end
|
101
|
+
|
95
102
|
no_commands do
|
96
103
|
def self.exit_on_failure?
|
97
104
|
true
|
@@ -45,10 +45,10 @@ module Tapioca
|
|
45
45
|
# sig { params(value: T.nilable(::User)).void }
|
46
46
|
# def author=(value); end
|
47
47
|
#
|
48
|
-
# sig { params(args: T.untyped, blk: T.untyped).returns(
|
48
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(::User) }
|
49
49
|
# def build_author(*args, &blk); end
|
50
50
|
#
|
51
|
-
# sig { params(args: T.untyped, blk: T.untyped).returns(
|
51
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(::Category) }
|
52
52
|
# def build_category(*args, &blk); end
|
53
53
|
#
|
54
54
|
# sig { returns(T.nilable(::Category)) }
|
@@ -69,16 +69,16 @@ module Tapioca
|
|
69
69
|
# sig { params(value: T::Enumerable[::Comment]).void }
|
70
70
|
# def comments=(value); end
|
71
71
|
#
|
72
|
-
# sig { params(args: T.untyped, blk: T.untyped).returns(
|
72
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(::User) }
|
73
73
|
# def create_author(*args, &blk); end
|
74
74
|
#
|
75
|
-
# sig { params(args: T.untyped, blk: T.untyped).returns(
|
75
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(::User) }
|
76
76
|
# def create_author!(*args, &blk); end
|
77
77
|
#
|
78
|
-
# sig { params(args: T.untyped, blk: T.untyped).returns(
|
78
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(::Category) }
|
79
79
|
# def create_category(*args, &blk); end
|
80
80
|
#
|
81
|
-
# sig { params(args: T.untyped, blk: T.untyped).returns(
|
81
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(::Category) }
|
82
82
|
# def create_category!(*args, &blk); end
|
83
83
|
#
|
84
84
|
# sig { returns(T.nilable(::User)) }
|
@@ -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,
|
@@ -164,7 +160,7 @@ module Tapioca
|
|
164
160
|
Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
|
165
161
|
Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
|
166
162
|
],
|
167
|
-
return_type:
|
163
|
+
return_type: association_class
|
168
164
|
)
|
169
165
|
create_method(
|
170
166
|
klass,
|
@@ -173,7 +169,7 @@ module Tapioca
|
|
173
169
|
Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
|
174
170
|
Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
|
175
171
|
],
|
176
|
-
return_type:
|
172
|
+
return_type: association_class
|
177
173
|
)
|
178
174
|
create_method(
|
179
175
|
klass,
|
@@ -182,7 +178,7 @@ module Tapioca
|
|
182
178
|
Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
|
183
179
|
Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
|
184
180
|
],
|
185
|
-
return_type:
|
181
|
+
return_type: association_class
|
186
182
|
)
|
187
183
|
end
|
188
184
|
end
|
@@ -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,
|
@@ -381,6 +392,12 @@ module Tapioca
|
|
381
392
|
|
382
393
|
arg_type.to_s
|
383
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})"
|
400
|
+
end
|
384
401
|
end
|
385
402
|
end
|
386
403
|
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
|
|
@@ -109,7 +108,7 @@ module Tapioca
|
|
109
108
|
sig { override.returns(T::Enumerable[Module]) }
|
110
109
|
def gather_constants
|
111
110
|
::ActiveRecord::Base.descendants.select do |klass|
|
112
|
-
klass < IdentityCache
|
111
|
+
klass < IdentityCache::WithoutPrimaryIndex
|
113
112
|
end
|
114
113
|
end
|
115
114
|
|
@@ -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
|
@@ -92,24 +92,28 @@ module Tapioca
|
|
92
92
|
method_def = signature.nil? ? method_def : signature.method
|
93
93
|
method_types = parameters_types_from_signature(method_def, signature)
|
94
94
|
|
95
|
-
method_def.parameters.each_with_index.map do |(type, name),
|
96
|
-
|
97
|
-
|
95
|
+
method_def.parameters.each_with_index.map do |(type, name), index|
|
96
|
+
fallback_arg_name = "_arg#{index}"
|
97
|
+
|
98
|
+
name ||= fallback_arg_name
|
99
|
+
name = name.to_s.gsub(/&|\*/, fallback_arg_name) # avoid incorrect names from `delegate`
|
100
|
+
method_type = method_types[index]
|
101
|
+
|
98
102
|
case type
|
99
103
|
when :req
|
100
|
-
::Parlour::RbiGenerator::Parameter.new(name, type:
|
104
|
+
::Parlour::RbiGenerator::Parameter.new(name, type: method_type)
|
101
105
|
when :opt
|
102
|
-
::Parlour::RbiGenerator::Parameter.new(name, type:
|
106
|
+
::Parlour::RbiGenerator::Parameter.new(name, type: method_type, default: 'T.unsafe(nil)')
|
103
107
|
when :rest
|
104
|
-
::Parlour::RbiGenerator::Parameter.new("*#{name}", type:
|
108
|
+
::Parlour::RbiGenerator::Parameter.new("*#{name}", type: method_type)
|
105
109
|
when :keyreq
|
106
|
-
::Parlour::RbiGenerator::Parameter.new("#{name}:", type:
|
110
|
+
::Parlour::RbiGenerator::Parameter.new("#{name}:", type: method_type)
|
107
111
|
when :key
|
108
|
-
::Parlour::RbiGenerator::Parameter.new("#{name}:", type:
|
112
|
+
::Parlour::RbiGenerator::Parameter.new("#{name}:", type: method_type, default: 'T.unsafe(nil)')
|
109
113
|
when :keyrest
|
110
|
-
::Parlour::RbiGenerator::Parameter.new("**#{name}", type:
|
114
|
+
::Parlour::RbiGenerator::Parameter.new("**#{name}", type: method_type)
|
111
115
|
when :block
|
112
|
-
::Parlour::RbiGenerator::Parameter.new("&#{name}", type:
|
116
|
+
::Parlour::RbiGenerator::Parameter.new("&#{name}", type: method_type)
|
113
117
|
else
|
114
118
|
raise "Unknown type `#{type}`."
|
115
119
|
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
|
# ~~~
|
@@ -14,6 +14,74 @@ end
|
|
14
14
|
module Tapioca
|
15
15
|
module Compilers
|
16
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
|
+
# ~~~
|
17
85
|
class UrlHelpers < Base
|
18
86
|
extend T::Sig
|
19
87
|
|
@@ -1,5 +1,5 @@
|
|
1
|
+
# typed: strict
|
1
2
|
# frozen_string_literal: true
|
2
|
-
# typed: true
|
3
3
|
|
4
4
|
require "tapioca/compilers/dsl/base"
|
5
5
|
|
@@ -30,7 +30,7 @@ module Tapioca
|
|
30
30
|
T::Enumerable[Dsl::Base]
|
31
31
|
)
|
32
32
|
@requested_constants = requested_constants
|
33
|
-
@error_handler = error_handler || $stderr.method(:puts)
|
33
|
+
@error_handler = T.let(error_handler || $stderr.method(:puts), T.proc.params(error: String).void)
|
34
34
|
end
|
35
35
|
|
36
36
|
sig { params(blk: T.proc.params(constant: Module, rbi: String).void).void }
|
@@ -54,9 +54,9 @@ module Tapioca
|
|
54
54
|
|
55
55
|
private
|
56
56
|
|
57
|
-
sig { params(requested_generators: T::Array[String]).returns(
|
57
|
+
sig { params(requested_generators: T::Array[String]).returns(T.proc.params(klass: Class).returns(T::Boolean)) }
|
58
58
|
def generator_filter(requested_generators)
|
59
|
-
return
|
59
|
+
return ->(_klass) { true } if requested_generators.empty?
|
60
60
|
|
61
61
|
generators = requested_generators.map(&:downcase)
|
62
62
|
|
@@ -70,7 +70,7 @@ module Tapioca
|
|
70
70
|
def gather_generators(requested_generators)
|
71
71
|
generator_filter = generator_filter(requested_generators)
|
72
72
|
|
73
|
-
Dsl::Base.descendants.select(&generator_filter).map(&:new)
|
73
|
+
T.cast(Dsl::Base.descendants.select(&generator_filter).map(&:new), T::Enumerable[Dsl::Base])
|
74
74
|
end
|
75
75
|
|
76
76
|
sig { params(requested_constants: T::Array[Module]).returns(T::Set[Module]) }
|
@@ -1,7 +1,7 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
1
|
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
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,13 +28,14 @@ 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
|
35
35
|
if path.directory?
|
36
36
|
Dir.glob("#{path}/**/*.rb", File::FNM_EXTGLOB).reject do |file|
|
37
|
-
|
37
|
+
relative_file_path = Pathname.new(file).relative_path_from(path)
|
38
|
+
file_ignored_by_sorbet?(config, relative_file_path)
|
38
39
|
end
|
39
40
|
else
|
40
41
|
[path.to_s]
|
@@ -49,13 +50,40 @@ module Tapioca
|
|
49
50
|
end.compact
|
50
51
|
end
|
51
52
|
|
52
|
-
sig { params(config:
|
53
|
-
def file_ignored_by_sorbet?(config,
|
54
|
-
|
55
|
-
|
53
|
+
sig { params(config: Spoom::Sorbet::Config, file_path: Pathname).returns(T::Boolean) }
|
54
|
+
def file_ignored_by_sorbet?(config, file_path)
|
55
|
+
file_path_parts = path_parts(file_path)
|
56
|
+
|
57
|
+
config.ignore.any? do |ignore|
|
58
|
+
# Sorbet --ignore matching method:
|
59
|
+
# ---
|
60
|
+
# Ignores input files that contain the given
|
61
|
+
# string in their paths (relative to the input
|
62
|
+
# path passed to Sorbet).
|
63
|
+
#
|
64
|
+
# Strings beginning with / match against the
|
65
|
+
# prefix of these relative paths; others are
|
66
|
+
# substring matchs.
|
67
|
+
|
68
|
+
# Matches must be against whole folder and file
|
69
|
+
# names, so `foo` matches `/foo/bar.rb` and
|
70
|
+
# `/bar/foo/baz.rb` but not `/foo.rb` or
|
71
|
+
# `/foo2/bar.rb`.
|
72
|
+
ignore_parts = path_parts(Pathname.new(ignore))
|
73
|
+
file_path_part_sequences = file_path_parts.each_cons(ignore_parts.size)
|
74
|
+
# if ignore string begins with /, we only want the first sequence to match
|
75
|
+
file_path_part_sequences = [file_path_part_sequences.first].to_enum if ignore.start_with?("/")
|
76
|
+
|
77
|
+
# we need to match whole segments
|
78
|
+
file_path_part_sequences.include?(ignore_parts)
|
56
79
|
end
|
57
80
|
end
|
58
81
|
|
82
|
+
sig { params(path: Pathname).returns(T::Array[String]) }
|
83
|
+
def path_parts(path)
|
84
|
+
T.unsafe(path).descend.map { |part| part.basename.to_s }
|
85
|
+
end
|
86
|
+
|
59
87
|
sig { params(files: T::Enumerable[String], name: String).returns(T::Boolean) }
|
60
88
|
def name_in_project?(files, name)
|
61
89
|
files.any? do |file|
|
@@ -1,5 +1,5 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
1
|
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'pathname'
|
5
5
|
|
@@ -209,7 +209,11 @@ module Tapioca
|
|
209
209
|
method = "const" if prop.fetch(:immutable, false)
|
210
210
|
type = prop.fetch(:type_object, "T.untyped")
|
211
211
|
|
212
|
-
|
212
|
+
if prop.key?(:default)
|
213
|
+
indented("#{method} :#{name}, #{type}, default: T.unsafe(nil)")
|
214
|
+
else
|
215
|
+
indented("#{method} :#{name}, #{type}")
|
216
|
+
end
|
213
217
|
end.join("\n")
|
214
218
|
end
|
215
219
|
|
@@ -383,8 +387,18 @@ module Tapioca
|
|
383
387
|
indented("include(#{qualified_name_of(mod)})")
|
384
388
|
end.join("\n")
|
385
389
|
|
386
|
-
|
387
|
-
|
390
|
+
ancestors = singleton_class_of(constant).ancestors
|
391
|
+
extends_as_concern = ancestors.any? do |mod|
|
392
|
+
qualified_name_of(mod) == "::ActiveSupport::Concern"
|
393
|
+
end
|
394
|
+
class_methods_module = resolve_constant("#{name_of(constant)}::ClassMethods")
|
395
|
+
|
396
|
+
mixed_in_module = if extends_as_concern && Module === class_methods_module
|
397
|
+
class_methods_module
|
398
|
+
else
|
399
|
+
dynamic_extends.find do |mod|
|
400
|
+
mod != constant && public_module?(mod)
|
401
|
+
end
|
388
402
|
end
|
389
403
|
|
390
404
|
return result if mixed_in_module.nil?
|
@@ -494,15 +508,18 @@ module Tapioca
|
|
494
508
|
return if symbol_ignored?(symbol_name) && !method_in_gem?(method)
|
495
509
|
|
496
510
|
signature = signature_of(method)
|
497
|
-
method = signature.method if signature
|
511
|
+
method = T.let(signature.method, UnboundMethod) if signature
|
498
512
|
|
499
513
|
method_name = method.name.to_s
|
500
514
|
return unless valid_method_name?(method_name)
|
501
515
|
return if struct_method?(constant, method_name)
|
502
516
|
return if method_name.start_with?("__t_props_generated_")
|
503
517
|
|
504
|
-
|
505
|
-
|
518
|
+
parameters = T.let(method.parameters, T::Array[[Symbol, T.nilable(Symbol)]])
|
519
|
+
|
520
|
+
sanitized_parameters = parameters.each_with_index.map do |(type, name), index|
|
521
|
+
fallback_arg_name = "_arg#{index}"
|
522
|
+
|
506
523
|
unless name
|
507
524
|
# For attr_writer methods, Sorbet signatures have the name
|
508
525
|
# of the method (without the trailing = sign) as the name of
|
@@ -512,22 +529,27 @@ module Tapioca
|
|
512
529
|
# method and the parameter is required and there is a single
|
513
530
|
# parameter and the signature also defines a single parameter and
|
514
531
|
# the name of the method ends with a = character.
|
515
|
-
writer_method_with_sig =
|
516
|
-
type == :req &&
|
517
|
-
|
532
|
+
writer_method_with_sig = (
|
533
|
+
signature && type == :req &&
|
534
|
+
parameters.size == 1 &&
|
518
535
|
signature.arg_types.size == 1 &&
|
519
536
|
method_name[-1] == "="
|
537
|
+
)
|
520
538
|
|
521
539
|
name = if writer_method_with_sig
|
522
|
-
method_name[0...-1].to_sym
|
540
|
+
T.must(method_name[0...-1]).to_sym
|
523
541
|
else
|
524
|
-
|
542
|
+
fallback_arg_name
|
525
543
|
end
|
526
544
|
end
|
527
545
|
|
528
546
|
# Sanitize param names
|
529
|
-
name = name.to_s.gsub(/[^a-zA-Z0-9_]/,
|
547
|
+
name = name.to_s.gsub(/[^a-zA-Z0-9_]/, fallback_arg_name)
|
548
|
+
|
549
|
+
[type, name]
|
550
|
+
end
|
530
551
|
|
552
|
+
parameter_list = sanitized_parameters.map do |type, name|
|
531
553
|
case type
|
532
554
|
when :req
|
533
555
|
name
|
@@ -546,26 +568,31 @@ module Tapioca
|
|
546
568
|
end
|
547
569
|
end.join(', ')
|
548
570
|
|
549
|
-
|
571
|
+
parameter_list = "(#{parameter_list})" if parameter_list != ""
|
572
|
+
signature_str = indented(compile_signature(signature, sanitized_parameters)) if signature
|
550
573
|
|
551
|
-
signature_str = indented(compile_signature(signature)) if signature
|
552
574
|
[
|
553
575
|
signature_str,
|
554
|
-
indented("def #{method_name}#{
|
576
|
+
indented("def #{method_name}#{parameter_list}; end"),
|
555
577
|
].compact.join("\n")
|
556
578
|
end
|
557
579
|
|
558
580
|
TYPE_PARAMETER_MATCHER = /T\.type_parameter\(:?([[:word:]]+)\)/
|
559
581
|
|
560
|
-
sig { params(signature: T.untyped).returns(String) }
|
561
|
-
def compile_signature(signature)
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
582
|
+
sig { params(signature: T.untyped, parameters: T::Array[[Symbol, String]]).returns(String) }
|
583
|
+
def compile_signature(signature, parameters)
|
584
|
+
parameter_types = T.let(signature.arg_types.to_h, T::Hash[Symbol, T::Types::Base])
|
585
|
+
parameter_types.merge!(signature.kwarg_types)
|
586
|
+
parameter_types[signature.rest_name] = signature.rest_type if signature.has_rest
|
587
|
+
parameter_types[signature.keyrest_name] = signature.keyrest_type if signature.has_keyrest
|
588
|
+
parameter_types[signature.block_name] = signature.block_type if signature.block_name
|
566
589
|
|
567
|
-
params =
|
568
|
-
|
590
|
+
params = parameters.map do |_, name|
|
591
|
+
type = parameter_types[name.to_sym]
|
592
|
+
"#{name}: #{type}"
|
593
|
+
end.join(", ")
|
594
|
+
|
595
|
+
returns = type_of(signature.return_type)
|
569
596
|
|
570
597
|
type_parameters = (params + returns).scan(TYPE_PARAMETER_MATCHER).flatten.uniq.map { |p| ":#{p}" }.join(", ")
|
571
598
|
type_parameters = ".type_parameters(#{type_parameters})" unless type_parameters.empty?
|
@@ -591,6 +618,7 @@ module Tapioca
|
|
591
618
|
signature_body = signature_body
|
592
619
|
.gsub(".returns(<VOID>)", ".void")
|
593
620
|
.gsub("<NOT-TYPED>", "T.untyped")
|
621
|
+
.gsub(".params()", "")
|
594
622
|
.gsub(TYPE_PARAMETER_MATCHER, "T.type_parameter(:\\1)")[1..-1]
|
595
623
|
|
596
624
|
"sig { #{signature_body} }"
|
@@ -784,6 +812,11 @@ module Tapioca
|
|
784
812
|
nil
|
785
813
|
end
|
786
814
|
|
815
|
+
sig { params(constant: Module).returns(String) }
|
816
|
+
def type_of(constant)
|
817
|
+
constant.to_s.gsub(/\bAttachedClass\b/, "T.attached_class")
|
818
|
+
end
|
819
|
+
|
787
820
|
sig { params(constant: Module, other: BasicObject).returns(T::Boolean).checked(:never) }
|
788
821
|
def are_equal?(constant, other)
|
789
822
|
BasicObject.instance_method(:equal?).bind(constant).call(other)
|
@@ -1,7 +1,9 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
class Class
|
5
|
+
extend T::Sig
|
6
|
+
|
5
7
|
# Returns an array with all classes that are < than its receiver.
|
6
8
|
#
|
7
9
|
# class C; end
|
@@ -15,9 +17,12 @@ class Class
|
|
15
17
|
#
|
16
18
|
# class D < C; end
|
17
19
|
# C.descendants # => [B, A, D]
|
20
|
+
sig { returns(T::Array[Class]) }
|
18
21
|
def descendants
|
19
|
-
ObjectSpace.each_object(singleton_class).reject do |k|
|
20
|
-
k.singleton_class? || k == self
|
22
|
+
result = ObjectSpace.each_object(singleton_class).reject do |k|
|
23
|
+
T.cast(k, Module).singleton_class? || k == self
|
21
24
|
end
|
25
|
+
|
26
|
+
T.cast(result, T::Array[Class])
|
22
27
|
end
|
23
28
|
end
|
data/lib/tapioca/gemfile.rb
CHANGED
data/lib/tapioca/generator.rb
CHANGED
data/lib/tapioca/loader.rb
CHANGED
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.8
|
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-11-05 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
|