tapioca 0.4.1 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91a976cddc1429c3b08568b39d8fae5fe8d4d54a8b8b2b04deebb75f475987e8
4
- data.tar.gz: 45074066696bd5c838ab09523980fd9c148faeb1b9f6ee800e8752e636edbd1a
3
+ metadata.gz: dd111409d415b9a17a11cd2d1a5d6d60fdab411934fc5bd1ade87b4bee9e27a3
4
+ data.tar.gz: 6cc495adc75090725de7f8cb873672e804452c16931d5a995c543827ab6400cb
5
5
  SHA512:
6
- metadata.gz: 1646cf3ec4957442bf0895a636bfc684e00f0d88e2252502d119fd3f4b4073e41b773ce0cbe8f725766d0c82bae7f67b92381a32a123b6fa2067ddd6a9e2c8d2
7
- data.tar.gz: 33f8dae8437b44c2bb2e7d4a1bb483adc11c0ae58322b62fa97a879698ba3d9abab4f198d38b7a8433e57f32f64df5b39ce1b5d8a13ebe37b011dae2d179d926
6
+ metadata.gz: 993e27b7960ce9830d58098a3a965214342821d87c3753e3b6a72ab8d9d33e86c1a5f454141cd2f19b73565ecfe9f3454c7c7d45825256c27c02ccd2d2396780
7
+ data.tar.gz: bfeb2377b5a5b059ce4897cbe1b7e39cef0657d799479ffd75c646897236928cd7fe317fd9cf9a49597b8dd28fc7657e90d9386eb82927cc9cdbb806203c4538
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")
data/README.md CHANGED
@@ -8,6 +8,8 @@ As yet, no gem exports type information in a consumable format and it would be a
8
8
 
9
9
  When you run `tapioca sync` in a project, `tapioca` loads all the gems that are in your dependency list from the Gemfile into memory. It then performs runtime introspection on the loaded types to understand their structure and generates an appropriate RBI file for each gem with a versioned filename.
10
10
 
11
+ ## Manual gem requires
12
+
11
13
  For gems that have a normal default `require` and load all of their constants through such a require, everything works seamlessly. However, for gems that are marked as `require: false` in the Gemfile, or for gems that export optionally loaded types via different requires, where a single require does not load the whole gem code into memory, `tapioca` will not be able to load some of the types into memory and, thus, won't be able to generate complete RBIs for them. For this reason, we need to keep a small external file named `sorbet/tapioca/require.rb` that is executed after all the gems in the Gemfile have been required and before generation of gem RBIs have started. This file is responsible for adding the requires for additional files from gems, which are not covered by the default require.
12
14
 
13
15
  For example, suppose you are using the class `BetterHtml::Parser` exported from the `better_html` gem. Just doing a `require "better_html"` (which is the default require) does not load that type:
@@ -95,6 +97,12 @@ Command: `tapioca todo`
95
97
 
96
98
  This will generate the file `sorbet/rbi/todo.rbi` defining all unresolved constants as empty modules.
97
99
 
100
+ ### Generate DSL RBI files
101
+
102
+ Command: `tapioca dsl [constant...]`
103
+
104
+ This will generate DSL RBIs for specified constants (or for all handled constants, if a constant name is not supplied). You can read about DSL RBI generators supplied by `tapioca` in [the manual](manual/generators.md).
105
+
98
106
  ### Flags
99
107
 
100
108
  - `--prerequire [file]`: A file to be required before `Bundler.require` is called.
@@ -103,10 +111,6 @@ This will generate the file `sorbet/rbi/todo.rbi` defining all unresolved consta
103
111
  - `--generate-command [command]`: The command to run to regenerate RBI files (used in header comment of the RBI files), defaults to the current command.
104
112
  - `--typed-overrides [gem:level]`: Overrides typed sigils for generated gem RBIs for gem `gem` to level `level` (`level` can be one of `ignore`, `false`, `true`, `strict`, or `strong`, see [the Sorbet docs](https://sorbet.org/docs/static#file-level-granularity-strictness-levels) for more details).
105
113
 
106
- ### Strong typing option for ActiveRecord column methods
107
-
108
- `tapioca` gives you the option to generate stricter type signatures for your ActiveRecord column types. By default, methods generated for columns that are defined in the schema have signatures of T.untyped. However, if the object extends a module with name StrongTypeGeneration, tapioca will generate stricter signatures that follow closely with the types defined in the schema. Expectation is the StrongTypeGeneration module you define in your application won't allow objects to be initialized with "bad state". It will check all the attributes that are not nillable to ensure they are not nil.
109
-
110
114
  ## Contributing
111
115
 
112
116
  Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/tapioca. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](https://github.com/Shopify/tapioca/blob/master/CODE_OF_CONDUCT.md) code of conduct.
data/Rakefile CHANGED
@@ -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
@@ -44,8 +44,8 @@ module Tapioca
44
44
  end
45
45
  create_file(Config::DEFAULT_POSTREQUIRE, skip: true) do
46
46
  <<~CONTENT
47
- # frozen_string_literal: true
48
47
  # typed: false
48
+ # frozen_string_literal: true
49
49
 
50
50
  # Add your extra requires here
51
51
  CONTENT
@@ -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
  #
@@ -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,
@@ -227,20 +223,6 @@ module Tapioca
227
223
  )
228
224
  end
229
225
 
230
- sig do
231
- params(
232
- constant: T.class_of(ActiveRecord::Base),
233
- reflection: ReflectionType
234
- ).returns(T::Boolean)
235
- end
236
- def belongs_to_and_required?(constant, reflection)
237
- return false unless constant.table_exists?
238
- return false unless reflection.belongs_to?
239
- column_definition = constant.columns_hash[reflection.foreign_key.to_s]
240
-
241
- !column_definition.nil? && !column_definition.null
242
- end
243
-
244
226
  sig do
245
227
  params(
246
228
  constant: T.class_of(ActiveRecord::Base),
@@ -17,6 +17,17 @@ module Tapioca
17
17
  # responsible for defining the attribute methods that would be created for the columns that
18
18
  # are defined in the Active Record model.
19
19
  #
20
+ # **Note:** This generator, by default, generates weak signatures for column methods and treats each
21
+ # column to be `T.untyped`. This is done on purpose to ensure that the nilability of Active Record
22
+ # columns do not make it hard for existing code to adopt gradual typing. It is possible, however, to
23
+ # generate stricter type signatures for your ActiveRecord column types. If your ActiveRecord model extends
24
+ # a module with name `StrongTypeGeneration`, this generator will generate stricter signatures that follow
25
+ # closely with the types defined in the schema.
26
+ #
27
+ # The `StrongTypeGeneration` module you define in your application should add an `after_initialize` callback
28
+ # to the model and ensure that all the non-nilable attributes of the model are actually initialized with non-`nil`
29
+ # values.
30
+ #
20
31
  # For example, with the following model class:
21
32
  #
22
33
  # ~~~rb
@@ -196,25 +207,25 @@ module Tapioca
196
207
  klass,
197
208
  "#{attribute_name}_before_last_save",
198
209
  methods_to_add,
199
- return_type: getter_type
210
+ return_type: as_nilable_type(getter_type)
200
211
  )
201
212
  add_method(
202
213
  klass,
203
214
  "#{attribute_name}_change_to_be_saved",
204
215
  methods_to_add,
205
- return_type: "[#{getter_type}, #{getter_type}]"
216
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])"
206
217
  )
207
218
  add_method(
208
219
  klass,
209
220
  "#{attribute_name}_in_database",
210
221
  methods_to_add,
211
- return_type: getter_type
222
+ return_type: as_nilable_type(getter_type)
212
223
  )
213
224
  add_method(
214
225
  klass,
215
226
  "saved_change_to_#{attribute_name}",
216
227
  methods_to_add,
217
- return_type: "[#{getter_type}, #{getter_type}]"
228
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])"
218
229
  )
219
230
  add_method(
220
231
  klass,
@@ -235,7 +246,7 @@ module Tapioca
235
246
  klass,
236
247
  "#{attribute_name}_change",
237
248
  methods_to_add,
238
- return_type: "[#{getter_type}, #{getter_type}]"
249
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])"
239
250
  )
240
251
  add_method(
241
252
  klass,
@@ -252,13 +263,13 @@ module Tapioca
252
263
  klass,
253
264
  "#{attribute_name}_was",
254
265
  methods_to_add,
255
- return_type: getter_type
266
+ return_type: as_nilable_type(getter_type)
256
267
  )
257
268
  add_method(
258
269
  klass,
259
270
  "#{attribute_name}_previous_change",
260
271
  methods_to_add,
261
- return_type: "[#{getter_type}, #{getter_type}]"
272
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])"
262
273
  )
263
274
  add_method(
264
275
  klass,
@@ -270,7 +281,7 @@ module Tapioca
270
281
  klass,
271
282
  "#{attribute_name}_previously_was",
272
283
  methods_to_add,
273
- return_type: getter_type
284
+ return_type: as_nilable_type(getter_type)
274
285
  )
275
286
  add_method(
276
287
  klass,
@@ -349,9 +360,9 @@ module Tapioca
349
360
  !(constant.singleton_class < Object.const_get(:StrongTypeGeneration))
350
361
  end
351
362
 
352
- sig { params(column_type: Module).returns(String) }
363
+ sig { params(column_type: Object).returns(String) }
353
364
  def handle_unknown_type(column_type)
354
- return "T.untyped" unless column_type < ActiveModel::Type::Value
365
+ return "T.untyped" unless ActiveModel::Type::Value === column_type
355
366
 
356
367
  lookup_return_type_of_method(column_type, :deserialize) ||
357
368
  lookup_return_type_of_method(column_type, :cast) ||
@@ -359,19 +370,33 @@ module Tapioca
359
370
  "T.untyped"
360
371
  end
361
372
 
362
- sig { params(column_type: Module, method: Symbol).returns(T.nilable(String)) }
373
+ sig { params(column_type: ActiveModel::Type::Value, method: Symbol).returns(T.nilable(String)) }
363
374
  def lookup_return_type_of_method(column_type, method)
364
- signature = T::Private::Methods.signature_for_method(column_type.instance_method(method))
375
+ signature = T::Private::Methods.signature_for_method(column_type.method(method))
365
376
  return unless signature
366
377
 
367
- return_type = signature.return_type.to_s
368
- return_type if return_type != "<VOID>" && return_type != "<NOT-TYPED>"
378
+ return_type = signature.return_type
379
+ return if T::Types::Simple === return_type && T::Generic === return_type.raw_type
380
+ return if return_type == T::Private::Types::Void || return_type == T::Private::Types::NotTyped
381
+
382
+ return_type.to_s
369
383
  end
370
384
 
371
- sig { params(column_type: Module, method: Symbol).returns(T.nilable(String)) }
385
+ sig { params(column_type: ActiveModel::Type::Value, method: Symbol).returns(T.nilable(String)) }
372
386
  def lookup_arg_type_of_method(column_type, method)
373
- signature = T::Private::Methods.signature_for_method(column_type.instance_method(method))
374
- signature.arg_types.first.last.to_s if signature
387
+ signature = T::Private::Methods.signature_for_method(column_type.method(method))
388
+ return unless signature
389
+
390
+ arg_type = signature.arg_types.first.last
391
+ return if T::Types::Simple === arg_type && T::Generic === arg_type.raw_type
392
+
393
+ arg_type.to_s
394
+ end
395
+
396
+ sig { params(type: String).returns(String) }
397
+ def as_nilable_type(type)
398
+ return type if type.start_with?("T.nilable(")
399
+ "T.nilable(#{type})"
375
400
  end
376
401
  end
377
402
  end
@@ -16,7 +16,7 @@ module Tapioca
16
16
  module Compilers
17
17
  module Dsl
18
18
  # `Tapioca::Compilers::DSL::ActiveRecordIdentityCache` generates RBI files for ActiveRecord models
19
- # that use `include IdentityCache`
19
+ # that use `include IdentityCache`.
20
20
  # `IdentityCache` is a blob level caching solution to plug into ActiveRecord. (see https://github.com/Shopify/identity_cache).
21
21
  #
22
22
  # For example, with the following ActiveRecord class:
@@ -61,7 +61,6 @@ module Tapioca
61
61
  # def fetch_by_title_and_review_date(title, review_date, includes: nil); end
62
62
  # end
63
63
  # ~~~
64
-
65
64
  class ActiveRecordIdentityCache < Base
66
65
  extend T::Sig
67
66
 
@@ -127,7 +126,7 @@ module Tapioca
127
126
  if returns_collection
128
127
  COLLECTION_TYPE.call(cache_type)
129
128
  else
130
- "::#{cache_type}"
129
+ "T.nilable(::#{cache_type})"
131
130
  end
132
131
  rescue ArgumentError
133
132
  "T.untyped"
@@ -37,7 +37,7 @@ module Tapioca
37
37
  # module Post::GeneratedRelationMethods
38
38
  # sig { params(args: T.untyped, blk: T.untyped).returns(T.untyped) }
39
39
  # def private_kind(*args, &blk); end
40
-
40
+ #
41
41
  # sig { params(args: T.untyped, blk: T.untyped).returns(T.untyped) }
42
42
  # def public_kind(*args, &blk); end
43
43
  # end
@@ -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
@@ -1,6 +1,8 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "parlour"
5
+
4
6
  module Tapioca
5
7
  module Compilers
6
8
  module Dsl
@@ -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,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  # typed: strict
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
@@ -29,7 +29,7 @@ module Tapioca
29
29
  def generate
30
30
  symbols
31
31
  .sort
32
- .map(&method(:generate_from_symbol))
32
+ .map { |symbol| generate_from_symbol(symbol) }
33
33
  .compact
34
34
  .join("\n\n")
35
35
  .concat("\n")
@@ -383,8 +383,18 @@ module Tapioca
383
383
  indented("include(#{qualified_name_of(mod)})")
384
384
  end.join("\n")
385
385
 
386
- 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)
@@ -28,7 +28,7 @@ module Tapioca
28
28
  sig { returns(T::Array[Gem]) }
29
29
  def dependencies
30
30
  @dependencies ||= begin
31
- specs = definition.specs.to_a
31
+ specs = definition.locked_gems.specs.to_a
32
32
 
33
33
  definition
34
34
  .resolve
@@ -79,17 +79,18 @@ module Tapioca
79
79
  extend(T::Sig)
80
80
 
81
81
  IGNORED_GEMS = T.let(%w{
82
- sorbet sorbet-static sorbet-runtime tapioca
82
+ sorbet sorbet-static sorbet-runtime
83
83
  }.freeze, T::Array[String])
84
84
 
85
85
  sig { returns(String) }
86
- attr_reader :full_gem_path
86
+ attr_reader :full_gem_path, :version
87
87
 
88
88
  sig { params(spec: Spec).void }
89
89
  def initialize(spec)
90
90
  @spec = T.let(spec, Tapioca::Gemfile::Spec)
91
91
  real_gem_path = to_realpath(@spec.full_gem_path)
92
92
  @full_gem_path = T.let(real_gem_path, String)
93
+ @version = T.let(version_string, String)
93
94
  end
94
95
 
95
96
  sig { params(gemfile_dir: String).returns(T::Boolean) }
@@ -109,11 +110,6 @@ module Tapioca
109
110
  @spec.name
110
111
  end
111
112
 
112
- sig { returns(::Gem::Version) }
113
- def version
114
- @spec.version
115
- end
116
-
117
113
  sig { returns(String) }
118
114
  def rbi_file_name
119
115
  "#{name}@#{version}.rbi"
@@ -121,11 +117,38 @@ module Tapioca
121
117
 
122
118
  sig { params(path: String).returns(T::Boolean) }
123
119
  def contains_path?(path)
124
- to_realpath(path).start_with?(full_gem_path)
120
+ to_realpath(path).start_with?(full_gem_path) || has_parent_gemspec?(path)
125
121
  end
126
122
 
127
123
  private
128
124
 
125
+ sig { returns(String) }
126
+ def version_string
127
+ version = @spec.version.to_s
128
+ version += "-#{@spec.source.revision}" if Bundler::Source::Git === @spec.source
129
+ version
130
+ end
131
+
132
+ sig { params(path: String).returns(T::Boolean) }
133
+ def has_parent_gemspec?(path)
134
+ # For some Git installed gems the location of the loaded file can
135
+ # be different from the gem path as indicated by the spec file
136
+ #
137
+ # To compensate for these cases, we walk up the directory hierarchy
138
+ # from the given file and try to match a <gem-name.gemspec> file in
139
+ # one of those folders to see if the path really belongs in the given gem
140
+ # or not.
141
+ return false unless Bundler::Source::Git === @spec.source
142
+ parent = Pathname.new(path)
143
+
144
+ until parent.root?
145
+ parent = parent.parent.expand_path
146
+ return true if parent.join("#{name}.gemspec").file?
147
+ end
148
+
149
+ false
150
+ end
151
+
129
152
  sig { params(path: T.any(String, Pathname)).returns(String) }
130
153
  def to_realpath(path)
131
154
  path_string = path.to_s
@@ -162,6 +162,11 @@ module Tapioca
162
162
 
163
163
  private
164
164
 
165
+ EMPTY_RBI_COMMENT = <<~CONTENT
166
+ # THIS IS AN EMPTY RBI FILE.
167
+ # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
168
+ CONTENT
169
+
165
170
  sig { returns(Gemfile) }
166
171
  def bundle
167
172
  @bundle ||= Gemfile.new
@@ -422,20 +427,25 @@ module Tapioca
422
427
  say("Compiling #{gem_name}, this may take a few seconds... ")
423
428
 
424
429
  strictness = config.typed_overrides[gem.name] || "true"
425
-
430
+ rbi_body_content = compiler.compile(gem)
426
431
  content = String.new
427
432
  content << rbi_header(
428
433
  config.generate_command,
429
434
  reason: "types exported from the `#{gem.name}` gem",
430
435
  strictness: strictness
431
436
  )
432
- content << compiler.compile(gem)
433
437
 
434
438
  FileUtils.mkdir_p(config.outdir)
435
439
  filename = config.outpath / gem.rbi_file_name
436
- File.write(filename.to_s, content)
437
440
 
438
- say("Done", :green)
441
+ if rbi_body_content.strip.empty?
442
+ content << EMPTY_RBI_COMMENT
443
+ say("Done (empty output)", :yellow)
444
+ else
445
+ content << rbi_body_content
446
+ say("Done", :green)
447
+ end
448
+ File.write(filename.to_s, content)
439
449
 
440
450
  Pathname.glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
441
451
  remove(file) unless file.basename.to_s == gem.rbi_file_name
@@ -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.1"
5
+ VERSION = "0.4.6"
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.1
4
+ version: 0.4.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ufuk Kayserilioglu
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: exe
13
13
  cert_chain: []
14
- date: 2020-07-27 00:00:00.000000000 Z
14
+ date: 2020-09-15 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: pry
@@ -69,6 +69,20 @@ dependencies:
69
69
  - - ">="
70
70
  - !ruby/object:Gem::Version
71
71
  version: 2.1.0
72
+ - !ruby/object:Gem::Dependency
73
+ name: spoom
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ type: :runtime
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
72
86
  - !ruby/object:Gem::Dependency
73
87
  name: thor
74
88
  requirement: !ruby/object:Gem::Requirement
@@ -127,7 +141,6 @@ files:
127
141
  - lib/tapioca/gemfile.rb
128
142
  - lib/tapioca/generator.rb
129
143
  - lib/tapioca/loader.rb
130
- - lib/tapioca/sorbet_config_parser.rb
131
144
  - lib/tapioca/version.rb
132
145
  homepage: https://github.com/Shopify/tapioca
133
146
  licenses:
@@ -142,7 +155,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
142
155
  requirements:
143
156
  - - ">="
144
157
  - !ruby/object:Gem::Version
145
- version: 2.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