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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ffc5e922b8cd11f0667bfe31aa52ffc1087d954e0de34fbf97bcd54f65f522df
4
- data.tar.gz: 25b67e05f26e1c6eb9e1e9723d1d6ed853986e658f389da74203349b99fca1c2
3
+ metadata.gz: f225f517eb2bb6e6d7aa6910bb42a35280b57ad8223dbd8bc255ce92377c6eb3
4
+ data.tar.gz: f3672f181cbcd71b9c8056c815e77313d75fcb2a78d2be5dbad3d2e51e35de39
5
5
  SHA512:
6
- metadata.gz: 1334f75cb44c365c0dd075e2aa52c65bd4b685634376a2b06db9de2c6d3d809997957568f6a14006ccb984cf845e267bf75c027906f20ee9546643e9f968462f
7
- data.tar.gz: adad8cf61d2810cb823739616c77cbf4ac2d88a88d2fac60c84c837764cdd3940376815ee446b931e2ebe9ab41ac2b48daeb519db4892f680f9aeb2fa6888db4
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
- [![Build Status](https://travis-ci.org/Shopify/tapioca.svg?branch=master)](https://travis-ci.org/Shopify/tapioca)
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
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
5
+ Dir['tasks/**/*.rake'].each { |t| load t }
5
6
 
6
7
  Rake.application.options.trace = false
7
8
 
@@ -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
@@ -42,6 +42,7 @@ module Tapioca
42
42
  # # ...
43
43
  # end
44
44
  # end
45
+ # ~~~
45
46
  #
46
47
  # this generator will produce an RBI file `user_controller.rbi` with the following content:
47
48
  #
@@ -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(T.nilable(::User)) }
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(T.nilable(::Category)) }
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(T.nilable(::User)) }
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(T.nilable(::User)) }
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(T.nilable(::Category)) }
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(T.nilable(::Category)) }
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 = if belongs_to_and_required?(constant, reflection)
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: association_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: association_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: association_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
@@ -86,8 +86,6 @@ module Tapioca
86
86
  # def saved_change_to_reviewed; end
87
87
  # end
88
88
  # ~~~
89
- # end
90
-
91
89
  class ActiveRecordTypedStore < Base
92
90
  extend T::Sig
93
91
 
@@ -34,7 +34,7 @@ module Tapioca
34
34
  # # ...
35
35
  # end
36
36
  # end
37
- # ~~~rb
37
+ # ~~~
38
38
  #
39
39
  # this generator will produce an RBI file with the following content:
40
40
  # ~~~rbi
@@ -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), i|
96
- name ||= :_
97
- name = name.to_s.gsub(/&|\*/, '_') # avoid incorrect names from `delegate`
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: method_types[i])
104
+ ::Parlour::RbiGenerator::Parameter.new(name, type: method_type)
101
105
  when :opt
102
- ::Parlour::RbiGenerator::Parameter.new(name, type: method_types[i], default: 'T.unsafe(nil)')
106
+ ::Parlour::RbiGenerator::Parameter.new(name, type: method_type, default: 'T.unsafe(nil)')
103
107
  when :rest
104
- ::Parlour::RbiGenerator::Parameter.new("*#{name}", type: method_types[i])
108
+ ::Parlour::RbiGenerator::Parameter.new("*#{name}", type: method_type)
105
109
  when :keyreq
106
- ::Parlour::RbiGenerator::Parameter.new("#{name}:", type: method_types[i])
110
+ ::Parlour::RbiGenerator::Parameter.new("#{name}:", type: method_type)
107
111
  when :key
108
- ::Parlour::RbiGenerator::Parameter.new("#{name}:", type: method_types[i], default: 'T.unsafe(nil)')
112
+ ::Parlour::RbiGenerator::Parameter.new("#{name}:", type: method_type, default: 'T.unsafe(nil)')
109
113
  when :keyrest
110
- ::Parlour::RbiGenerator::Parameter.new("**#{name}", type: method_types[i])
114
+ ::Parlour::RbiGenerator::Parameter.new("**#{name}", type: method_type)
111
115
  when :block
112
- ::Parlour::RbiGenerator::Parameter.new("&#{name}", type: method_types[i])
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
- # add_file("cart.proto", :syntax => :proto3) do
23
- # add_message "MyCart" do
24
- # optional :shop_id, :int32, 1
25
- # optional :customer_id, :int64, 2
26
- # optional :number_value, :double, 3
27
- # optional :string_value, :string, 4
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(Proc) }
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 proc { true } if requested_generators.empty?
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
- require_relative '../sorbet_config_parser'
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 = SorbetConfig.parse_file(@sorbet_path)
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: SorbetConfig).returns(T::Array[String]) }
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
- file_ignored_by_sorbet?(config, file)
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: SorbetConfig, file: String).returns(T::Boolean) }
53
- def file_ignored_by_sorbet?(config, file)
54
- config.ignore.any? do |path|
55
- Regexp.new(Regexp.escape(path)) =~ file
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
  require 'shellwords'
@@ -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
- indented("#{method} :#{name}, #{type}")
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
- mixed_in_module = dynamic_extends.find do |mod|
387
- mod != constant && public_module?(mod)
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
- params = T.let(method.parameters, T::Array[T::Array[Symbol]])
505
- parameters = params.map do |(type, name)|
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 = signature &&
516
- type == :req &&
517
- params.size == 1 &&
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
- parameters = "(#{parameters})" if parameters != ""
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}#{parameters}; end"),
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
- params = signature.arg_types
563
- params += signature.kwarg_types.to_a
564
- params << [signature.rest_name, signature.rest_type] if signature.has_rest
565
- params << [signature.block_name, signature.block_type] if signature.block_name
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 = params.compact.map { |name, type| "#{name}: #{type}" }.join(", ")
568
- returns = signature.return_type.to_s
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,5 +1,5 @@
1
- # frozen_string_literal: true
2
1
  # typed: true
2
+ # frozen_string_literal: true
3
3
 
4
4
  require 'json'
5
5
  require 'tempfile'
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
1
  # typed: strong
2
+ # frozen_string_literal: true
3
3
 
4
4
  module Tapioca
5
5
  module Compilers
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
1
  # typed: strong
2
+ # frozen_string_literal: true
3
3
 
4
4
  module Tapioca
5
5
  module Compilers
@@ -1,7 +1,9 @@
1
- # typed: false
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
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
1
  # typed: strict
2
+ # frozen_string_literal: true
3
3
 
4
4
  require "bundler"
5
5
 
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
1
  # typed: strict
2
+ # frozen_string_literal: true
3
3
 
4
4
  require 'pathname'
5
5
  require 'thor'
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
1
  # typed: strict
2
+ # frozen_string_literal: true
3
3
 
4
4
  module Tapioca
5
5
  class Loader
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Tapioca
5
- VERSION = "0.4.3"
5
+ VERSION = "0.4.8"
6
6
  end
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.3
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-08-19 00:00:00.000000000 Z
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.3.7
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