tapioca 0.4.2 → 0.4.7

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: 46bdcc628673ddc597495e267b471e3a6f4c705b8f8d8d32dffba17c69470c21
4
- data.tar.gz: 4a2e597d03ea9354835e73f52034d088b1c15ac65f445d0f6963bbbca09c2b23
3
+ metadata.gz: a2ebe6488f101318e156ba0025de299975fce0d348f05ae9cd7bd38cf77ffe28
4
+ data.tar.gz: 3e22db95ccff12b1915f7f783e5669400cb5381078c49f370565da655459d62b
5
5
  SHA512:
6
- metadata.gz: 51c134a7948c262721672f61105e08e1e31146b70d982fac0944b0d42be91d5e2210a110775596d11704e60eac68d6b2e27c016587777c658fab2b64131f7a1e
7
- data.tar.gz: 1f048066d59bc2cc08a62fb93de96afdb7146d8e1c987c2a98677edd4920b08896096f9beaa658466ebd54be729c2bf6947ef1c409ab7329dac5ac27f7847dbc
6
+ metadata.gz: cb2d0ac2308cd8c44159a737e92090965dfa0ec616aaf83a6ad4b3cea69348bb12d61ecdce7f527db78d75c129fec19e765845ea475c375a74efcc240e1d2f25
7
+ data.tar.gz: d4c98b1c97946544c1032f13e920e77a1a62d61ad00e4f6522bef129690b68d2db07d7c08af74f9f7a245c3a771299534fa4a1cf2d464deaf03821e9180b73f6
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
@@ -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
 
@@ -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
- yield
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
@@ -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
 
@@ -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
@@ -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
  # ~~~
@@ -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
- sig { override.returns(T::Enumerable[T.untyped]) }
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
- constants = ObjectSpace.each_object(Module).select do |mod|
38
- mod = T.cast(mod, T.class_of(Module))
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 << ActionDispatch::IntegrationTest
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
- mod.create_include(T.must(helper_module.name)) if constant.ancestors.include?(helper_module)
71
- mod.create_extend(T.must(helper_module.name)) if constant.singleton_class.ancestors.include?(helper_module)
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,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,7 +28,7 @@ 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
@@ -49,7 +49,7 @@ module Tapioca
49
49
  end.compact
50
50
  end
51
51
 
52
- sig { params(config: SorbetConfig, file: String).returns(T::Boolean) }
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
@@ -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
 
@@ -383,8 +383,18 @@ module Tapioca
383
383
  indented("include(#{qualified_name_of(mod)})")
384
384
  end.join("\n")
385
385
 
386
- mixed_in_module = dynamic_extends.find do |mod|
387
- mod != constant && public_module?(mod)
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
- params = T.let(method.parameters, T::Array[T::Array[Symbol]])
505
- parameters = params.map do |(type, name)|
506
- name ||= :_
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
- parameters = "(#{parameters})" if parameters != ""
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}#{parameters}; end"),
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
- params = signature.arg_types
543
- params += signature.kwarg_types.to_a
544
- params << [signature.rest_name, signature.rest_type] if signature.has_rest
545
- params << [signature.block_name, signature.block_type] if signature.block_name
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
- params = params.compact.map { |name, type| "#{name}: #{type}" }.join(", ")
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)
@@ -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
@@ -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
 
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Tapioca
5
- VERSION = "0.4.2"
5
+ VERSION = "0.4.7"
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.2
4
+ version: 0.4.7
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-18 00:00:00.000000000 Z
14
+ date: 2020-10-07 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