tapioca 0.4.15 → 0.4.20

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: 41fd25f2497122191c01ef7e9376d6d56e070e9ec5e7626fd1ace153a1b01fd5
4
- data.tar.gz: 390bdd0023f086be720f722d530c00cee4eafd603052259331a353c4fa0d3a9b
3
+ metadata.gz: 567eff35f7ee24b52dafcd45ceb40ccaf042b6e0661c13cc5dcb87228c21c792
4
+ data.tar.gz: 0c5143d8407d5583ccd80edbb047fb0b7500b2de8bba1717723a92f97994c045
5
5
  SHA512:
6
- metadata.gz: 1f25290fb6e4544329b79b7eb20191126cddf1e9707f1aeb98b60d1dd24ea1f9c7df525ccfb5fd2b50fb12929b773b9adf9143b98da859903c29cdf3125484f6
7
- data.tar.gz: 8c7adbe3d3881118487945b46638d5b0bdd29d732424f071c7efb39048db80c7cdd9601bc51e306742aaeae02b002661204e5c18eeb8162c138507269a794c01
6
+ metadata.gz: d82321ab3b2516a4410825a6dfe4b253d7cccf49abc8c62db0b2a8cdbe13f064aa43df759108685ba06c36073c7a59cfda4ff361cdad1bfc736d7e557ae3ada6
7
+ data.tar.gz: 0b9bd1ff91ca97cbbb1922e9915a8d046421ff40f3cd2774b4518c979f44635507b48fcce93a904770b1a65e7214a9897ce5444ce26ee6d532523a595e24ec96
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ > :warning: **Note**: This software is currently under active development. The API and interface should be considered unstable until a v1.0.0 release.
2
+
1
3
  # Tapioca
2
4
 
3
5
  ![Build Status](https://github.com/Shopify/tapioca/workflows/CI/badge.svg)
@@ -14,7 +16,7 @@ For gems that have a normal default `require` and load all of their constants th
14
16
 
15
17
  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:
16
18
 
17
- ```ruby
19
+ ```shell
18
20
  $ bundle exec pry
19
21
  [1] pry(main)> require 'better_html'
20
22
  => true
@@ -110,7 +112,7 @@ This will generate DSL RBIs for specified constants (or for all handled constant
110
112
  - `--prerequire [file]`: A file to be required before `Bundler.require` is called.
111
113
  - `--postrequire [file]`: A file to be required after `Bundler.require` is called.
112
114
  - `--out [directory]`: The output directory for generated RBI files, default to `sorbet/rbi/gems`.
113
- - `--generate-command [command]`: The command to run to regenerate RBI files (used in header comment of the RBI files), defaults to the current command.
115
+ - `--generate-command [command]`: **[DEPRECATED]** The command to run to regenerate RBI files (used in header comment of the RBI files), defaults to the current command.
114
116
  - `--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).
115
117
 
116
118
  ## Contributing
data/exe/tapioca CHANGED
@@ -1,6 +1,21 @@
1
1
  #! /usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "../lib/tapioca"
4
+ require 'sorbet-runtime'
5
5
 
6
- Tapioca::Cli.start(ARGV)
6
+ begin
7
+ T::Configuration.default_checked_level = :never
8
+ # Suppresses errors caused by T.cast, T.let, T.must, etc.
9
+ T::Configuration.inline_type_error_handler = ->(*) {}
10
+ # Suppresses errors caused by incorrect parameter ordering
11
+ T::Configuration.sig_validation_error_handler = ->(*) {}
12
+ rescue
13
+ # Need this rescue so that if another gem has
14
+ # already set the checked level by the time we
15
+ # get to it, we don't fail outright.
16
+ nil
17
+ end
18
+
19
+ require_relative "../lib/tapioca/internal"
20
+
21
+ Tapioca::Cli::Main.start(ARGV)
data/lib/tapioca.rb CHANGED
@@ -17,31 +17,5 @@ module Tapioca
17
17
  class Error < StandardError; end
18
18
  end
19
19
 
20
- begin
21
- T::Configuration.default_checked_level = :never
22
- # Suppresses errors caused by T.cast, T.let, T.must, etc.
23
- T::Configuration.inline_type_error_handler = ->(*) {}
24
- # Suppresses errors caused by incorrect parameter ordering
25
- T::Configuration.sig_validation_error_handler = ->(*) {}
26
- rescue
27
- # Need this rescue so that if another gem has
28
- # already set the checked level by the time we
29
- # get to it, we don't fail outright.
30
- nil
31
- end
32
-
33
- require "tapioca/loader"
34
- require "tapioca/constant_locator"
35
- require "tapioca/config"
36
- require "tapioca/config_builder"
37
- require "tapioca/generator"
38
- require "tapioca/cli"
39
- require "tapioca/gemfile"
40
- require "tapioca/compilers/sorbet"
41
- require "tapioca/compilers/requires_compiler"
42
- require "tapioca/compilers/symbol_table_compiler"
43
- require "tapioca/compilers/symbol_table/symbol_generator"
44
- require "tapioca/compilers/symbol_table/symbol_loader"
45
- require "tapioca/compilers/todos_compiler"
46
- require "tapioca/compilers/dsl_compiler"
20
+ require "tapioca/compilers/dsl/base"
47
21
  require "tapioca/version"
data/lib/tapioca/cli.rb CHANGED
@@ -4,112 +4,5 @@
4
4
  require 'thor'
5
5
 
6
6
  module Tapioca
7
- class Cli < Thor
8
- include(Thor::Actions)
9
-
10
- class_option :prerequire,
11
- aliases: ["--pre", "-b"],
12
- banner: "file",
13
- desc: "A file to be required before Bundler.require is called"
14
- class_option :postrequire,
15
- aliases: ["--post", "-a"],
16
- banner: "file",
17
- desc: "A file to be required after Bundler.require is called"
18
- class_option :outdir,
19
- aliases: ["--out", "-o"],
20
- banner: "directory",
21
- desc: "The output directory for generated RBI files"
22
- class_option :generate_command,
23
- aliases: ["--cmd", "-c"],
24
- banner: "command",
25
- desc: "The command to run to regenerate RBI files"
26
- class_option :exclude,
27
- aliases: ["-x"],
28
- type: :array,
29
- banner: "gem [gem ...]",
30
- desc: "Excludes the given gem(s) from RBI generation"
31
- class_option :typed_overrides,
32
- aliases: ["--typed", "-t"],
33
- type: :hash,
34
- banner: "gem:level [gem:level ...]",
35
- desc: "Overrides for typed sigils for generated gem RBIs"
36
-
37
- map T.unsafe(%w[--version -v] => :__print_version)
38
-
39
- desc "init", "initializes folder structure"
40
- def init
41
- create_file(Config::SORBET_CONFIG, skip: true) do
42
- <<~CONTENT
43
- --dir
44
- .
45
- CONTENT
46
- end
47
- create_file(Config::DEFAULT_POSTREQUIRE, skip: true) do
48
- <<~CONTENT
49
- # typed: false
50
- # frozen_string_literal: true
51
-
52
- # Add your extra requires here
53
- CONTENT
54
- end
55
- end
56
-
57
- desc "require", "generate the list of files to be required by tapioca"
58
- def require
59
- Tapioca.silence_warnings do
60
- generator.build_requires
61
- end
62
- end
63
-
64
- desc "todo", "generate the list of unresolved constants"
65
- def todo
66
- Tapioca.silence_warnings do
67
- generator.build_todos
68
- end
69
- end
70
-
71
- desc "dsl [constant...]", "generate RBIs for dynamic methods"
72
- option :generators,
73
- type: :array,
74
- aliases: ["--gen", "-g"],
75
- banner: "generator [generator ...]",
76
- desc: "Only run supplied DSL generators"
77
- def dsl(*constants)
78
- Tapioca.silence_warnings do
79
- generator.build_dsl(constants)
80
- end
81
- end
82
-
83
- desc "generate [gem...]", "generate RBIs from gems"
84
- def generate(*gems)
85
- Tapioca.silence_warnings do
86
- generator.build_gem_rbis(gems)
87
- end
88
- end
89
-
90
- desc "sync", "sync RBIs to Gemfile"
91
- def sync
92
- Tapioca.silence_warnings do
93
- generator.sync_rbis_with_gemfile
94
- end
95
- end
96
-
97
- desc "--version, -v", "show version"
98
- def __print_version
99
- puts "Tapioca v#{Tapioca::VERSION}"
100
- end
101
-
102
- no_commands do
103
- def self.exit_on_failure?
104
- true
105
- end
106
-
107
- def generator
108
- current_command = T.must(current_command_chain.first)
109
- @generator ||= Generator.new(
110
- ConfigBuilder.from_options(current_command, options)
111
- )
112
- end
113
- end
114
- end
7
+ module Cli; end
115
8
  end
@@ -0,0 +1,146 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Cli
6
+ class Main < Thor
7
+ include(Thor::Actions)
8
+
9
+ class_option :prerequire,
10
+ aliases: ["--pre", "-b"],
11
+ banner: "file",
12
+ desc: "A file to be required before Bundler.require is called"
13
+ class_option :postrequire,
14
+ aliases: ["--post", "-a"],
15
+ banner: "file",
16
+ desc: "A file to be required after Bundler.require is called"
17
+ class_option :outdir,
18
+ aliases: ["--out", "-o"],
19
+ banner: "directory",
20
+ desc: "The output directory for generated RBI files"
21
+ class_option :generate_command,
22
+ aliases: ["--cmd", "-c"],
23
+ banner: "command",
24
+ desc: "The command to run to regenerate RBI files"
25
+ class_option :exclude,
26
+ aliases: ["-x"],
27
+ type: :array,
28
+ banner: "gem [gem ...]",
29
+ desc: "Excludes the given gem(s) from RBI generation"
30
+ class_option :typed_overrides,
31
+ aliases: ["--typed", "-t"],
32
+ type: :hash,
33
+ banner: "gem:level [gem:level ...]",
34
+ desc: "Overrides for typed sigils for generated gem RBIs"
35
+
36
+ map T.unsafe(%w[--version -v] => :__print_version)
37
+
38
+ desc "init", "initializes folder structure"
39
+ def init
40
+ create_config
41
+ create_post_require
42
+ generate_binstub
43
+ end
44
+
45
+ desc "require", "generate the list of files to be required by tapioca"
46
+ def require
47
+ Tapioca.silence_warnings do
48
+ generator.build_requires
49
+ end
50
+ end
51
+
52
+ desc "todo", "generate the list of unresolved constants"
53
+ def todo
54
+ Tapioca.silence_warnings do
55
+ generator.build_todos
56
+ end
57
+ end
58
+
59
+ desc "dsl [constant...]", "generate RBIs for dynamic methods"
60
+ option :generators,
61
+ type: :array,
62
+ aliases: ["--gen", "-g"],
63
+ banner: "generator [generator ...]",
64
+ desc: "Only run supplied DSL generators"
65
+ option :verify,
66
+ type: :boolean,
67
+ default: false,
68
+ desc: "Verifies RBIs are up-to-date"
69
+ option :quiet,
70
+ aliases: ["-q"],
71
+ type: :boolean,
72
+ desc: "Supresses file creation output"
73
+ def dsl(*constants)
74
+ Tapioca.silence_warnings do
75
+ generator.build_dsl(constants, should_verify: options[:verify], quiet: options[:quiet])
76
+ end
77
+ end
78
+
79
+ desc "generate [gem...]", "generate RBIs from gems"
80
+ def generate(*gems)
81
+ Tapioca.silence_warnings do
82
+ generator.build_gem_rbis(gems)
83
+ end
84
+ end
85
+
86
+ desc "sync", "sync RBIs to Gemfile"
87
+ def sync
88
+ Tapioca.silence_warnings do
89
+ generator.sync_rbis_with_gemfile
90
+ end
91
+ end
92
+
93
+ desc "--version, -v", "show version"
94
+ def __print_version
95
+ puts "Tapioca v#{Tapioca::VERSION}"
96
+ end
97
+
98
+ private
99
+
100
+ def create_config
101
+ create_file(Config::SORBET_CONFIG, skip: true) do
102
+ <<~CONTENT
103
+ --dir
104
+ .
105
+ CONTENT
106
+ end
107
+ end
108
+
109
+ def create_post_require
110
+ create_file(Config::DEFAULT_POSTREQUIRE, skip: true) do
111
+ <<~CONTENT
112
+ # typed: false
113
+ # frozen_string_literal: true
114
+
115
+ # Add your extra requires here
116
+ CONTENT
117
+ end
118
+ end
119
+
120
+ def generate_binstub
121
+ bin_stub_exists = File.exist?("bin/tapioca")
122
+ installer = Bundler::Installer.new(Bundler.root, Bundler.definition)
123
+ spec = Bundler.definition.specs.find { |s| s.name == "tapioca" }
124
+ installer.generate_bundler_executable_stubs(spec, { force: true })
125
+ if bin_stub_exists
126
+ shell.say_status(:force, "bin/tapioca", :yellow)
127
+ else
128
+ shell.say_status(:create, "bin/tapioca", :green)
129
+ end
130
+ end
131
+
132
+ no_commands do
133
+ def self.exit_on_failure?
134
+ true
135
+ end
136
+
137
+ def generator
138
+ current_command = T.must(current_command_chain.first)
139
+ @generator ||= Generator.new(
140
+ ConfigBuilder.from_options(current_command, options)
141
+ )
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -371,7 +371,6 @@ module Tapioca
371
371
  return unless signature
372
372
 
373
373
  return_type = signature.return_type
374
- return if T::Types::Simple === return_type && T::Generic === return_type.raw_type
375
374
  return if return_type == T::Private::Types::Void || return_type == T::Private::Types::NotTyped
376
375
 
377
376
  return_type.to_s
@@ -382,10 +381,11 @@ module Tapioca
382
381
  signature = T::Private::Methods.signature_for_method(column_type.method(method))
383
382
  return unless signature
384
383
 
385
- arg_type = signature.arg_types.first.last
386
- return if T::Types::Simple === arg_type && T::Generic === arg_type.raw_type
384
+ # Arg types is an array [name, type] entries, so we desctructure the type of
385
+ # first argument to get the first argument type
386
+ _, first_argument_type = signature.arg_types.first
387
387
 
388
- arg_type.to_s
388
+ first_argument_type.to_s
389
389
  end
390
390
 
391
391
  sig { params(type: String).returns(String) }
@@ -60,7 +60,7 @@ module Tapioca
60
60
 
61
61
  model.create_module(module_name) do |mod|
62
62
  scope_method_names.each do |scope_method|
63
- generate_scope_method(scope_method, mod)
63
+ generate_scope_method(scope_method.to_s, mod)
64
64
  end
65
65
  end
66
66
 
@@ -123,7 +123,7 @@ module Tapioca
123
123
  # Compile a Ruby method return type into a Parlour type
124
124
  sig do
125
125
  params(method_def: T.any(Method, UnboundMethod))
126
- .returns(String)
126
+ .returns(T.nilable(String))
127
127
  end
128
128
  def compile_method_return_type_to_parlour(method_def)
129
129
  signature = T::Private::Methods.signature_for_method(method_def)
@@ -89,7 +89,7 @@ module Tapioca
89
89
  class UrlHelpers < Base
90
90
  extend T::Sig
91
91
 
92
- sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module)).void }
92
+ sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: Module).void }
93
93
  def decorate(root, constant)
94
94
  case constant
95
95
  when GeneratedPathHelpersModule.singleton_class, GeneratedUrlHelpersModule.singleton_class
@@ -127,7 +127,7 @@ module Tapioca
127
127
 
128
128
  private
129
129
 
130
- sig { params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module)).void }
130
+ sig { params(root: Parlour::RbiGenerator::Namespace, constant: Module).void }
131
131
  def generate_module_for(root, constant)
132
132
  root.create_module(T.must(constant.name)) do |mod|
133
133
  mod.create_include("::ActionDispatch::Routing::UrlFor")
@@ -143,7 +143,7 @@ module Tapioca
143
143
  end
144
144
  end
145
145
 
146
- sig { params(mod: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module), helper_module: Module).void }
146
+ sig { params(mod: Parlour::RbiGenerator::Namespace, constant: Module, helper_module: Module).void }
147
147
  def create_mixins_for(mod, constant, helper_module)
148
148
  include_helper = constant.ancestors.include?(helper_module) || NON_DISCOVERABLE_INCLUDERS.include?(constant)
149
149
  extend_helper = constant.singleton_class.ancestors.include?(helper_module)
@@ -8,6 +8,7 @@ module Tapioca
8
8
  module Compilers
9
9
  module Sorbet
10
10
  SORBET = Pathname.new(Gem::Specification.find_by_name("sorbet-static").full_gem_path) / "libexec" / "sorbet"
11
+ EXE_PATH_ENV_VAR = "TAPIOCA_SORBET_EXE"
11
12
 
12
13
  class << self
13
14
  extend(T::Sig)
@@ -26,7 +27,9 @@ module Tapioca
26
27
 
27
28
  sig { returns(String) }
28
29
  def sorbet_path
29
- SORBET.to_s.shellescape
30
+ sorbet_path = ENV.fetch(EXE_PATH_ENV_VAR, SORBET)
31
+ sorbet_path = SORBET if sorbet_path.empty?
32
+ sorbet_path.to_s.shellescape
30
33
  end
31
34
  end
32
35
  end
@@ -74,9 +74,15 @@ module Tapioca
74
74
  compile(symbol, constant)
75
75
  end
76
76
 
77
- sig { params(symbol: String, inherit: T::Boolean).returns(BasicObject).checked(:never) }
78
- def resolve_constant(symbol, inherit: false)
79
- Object.const_get(symbol, inherit)
77
+ sig do
78
+ params(
79
+ symbol: String,
80
+ inherit: T::Boolean,
81
+ namespace: Module
82
+ ).returns(BasicObject).checked(:never)
83
+ end
84
+ def resolve_constant(symbol, inherit: false, namespace: Object)
85
+ namespace.const_get(symbol, inherit)
80
86
  rescue NameError, LoadError, RuntimeError, ArgumentError, TypeError
81
87
  nil
82
88
  end
@@ -95,6 +101,7 @@ module Tapioca
95
101
  return if alias_namespaced?(name)
96
102
  return if seen?(name)
97
103
  return unless parent_declares_constant?(name)
104
+ return if T::Enum === constant # T::Enum instances are defined via `compile_enums`
98
105
 
99
106
  mark_seen(name)
100
107
  compile_constant(name, constant)
@@ -141,9 +148,11 @@ module Tapioca
141
148
  def compile_object(name, value)
142
149
  return if symbol_ignored?(name)
143
150
  klass = class_of(value)
144
- return if name_of(klass)&.start_with?("T::Types::", "T::Private::")
151
+ klass_name = name_of(klass)
152
+
153
+ return if klass_name&.start_with?("T::Types::", "T::Private::")
145
154
 
146
- type_name = public_module?(klass) && name_of(klass) || "T.untyped"
155
+ type_name = public_module?(klass) && klass_name || "T.untyped"
147
156
  indented("#{name} = T.let(T.unsafe(nil), #{type_name})")
148
157
  end
149
158
 
@@ -180,15 +189,17 @@ module Tapioca
180
189
 
181
190
  [
182
191
  compile_module_helpers(constant),
192
+ compile_type_variables(constant),
183
193
  compile_mixins(constant),
184
194
  compile_mixes_in_class_methods(constant),
185
195
  compile_props(constant),
196
+ compile_enums(constant),
186
197
  methods,
187
- ].select { |b| b != "" }.join("\n\n")
198
+ ].select { |b| b && !b.empty? }.join("\n\n")
188
199
  end
189
200
  end
190
201
 
191
- sig { params(constant: Module).returns(String) }
202
+ sig { params(constant: Module).returns(T.nilable(String)) }
192
203
  def compile_module_helpers(constant)
193
204
  abstract_type = T::Private::Abstract::Data.get(constant, :abstract_type)
194
205
 
@@ -197,12 +208,14 @@ module Tapioca
197
208
  helpers << indented("final!") if T::Private::Final.final_module?(constant)
198
209
  helpers << indented("sealed!") if T::Private::Sealed.sealed_module?(constant)
199
210
 
211
+ return if helpers.empty?
212
+
200
213
  helpers.join("\n")
201
214
  end
202
215
 
203
- sig { params(constant: Module).returns(String) }
216
+ sig { params(constant: Module).returns(T.nilable(String)) }
204
217
  def compile_props(constant)
205
- return "" unless T::Props::ClassMethods === constant
218
+ return unless T::Props::ClassMethods === constant
206
219
 
207
220
  constant.props.map do |name, prop|
208
221
  method = "prop"
@@ -217,6 +230,23 @@ module Tapioca
217
230
  end.join("\n")
218
231
  end
219
232
 
233
+ sig { params(constant: Module).returns(T.nilable(String)) }
234
+ def compile_enums(constant)
235
+ return unless T::Enum > constant
236
+
237
+ enums = T.unsafe(constant).values.map do |enum_type|
238
+ enum_type.instance_variable_get(:@const_name).to_s
239
+ end
240
+
241
+ content = [
242
+ indented('enums do'),
243
+ *enums.map { |e| indented(" #{e} = new") }.join("\n"),
244
+ indented('end'),
245
+ ]
246
+
247
+ content.join("\n")
248
+ end
249
+
220
250
  sig { params(name: String, constant: Module).returns(T.nilable(String)) }
221
251
  def compile_subconstants(name, constant)
222
252
  output = constants_of(constant).sort.uniq.map do |constant_name|
@@ -231,11 +261,71 @@ module Tapioca
231
261
  compile(symbol, subconstant)
232
262
  end.compact
233
263
 
234
- return "" if output.empty?
264
+ return if output.empty?
235
265
 
236
266
  "\n" + output.join("\n\n")
237
267
  end
238
268
 
269
+ sig { params(constant: Module).returns(T.nilable(String)) }
270
+ def compile_type_variables(constant)
271
+ type_variables = compile_type_variable_declarations(constant)
272
+ singleton_class_type_variables = compile_type_variable_declarations(singleton_class_of(constant))
273
+
274
+ return if !type_variables && !singleton_class_type_variables
275
+
276
+ type_variables += "\n" if type_variables
277
+ singleton_class_type_variables += "\n" if singleton_class_type_variables
278
+
279
+ [
280
+ type_variables,
281
+ singleton_class_type_variables,
282
+ ].compact.join("\n").rstrip
283
+ end
284
+
285
+ sig { params(constant: Module).returns(T.nilable(String)) }
286
+ def compile_type_variable_declarations(constant)
287
+ with_indentation_for_constant(constant) do
288
+ # Try to find the type variables defined on this constant, bail if we can't
289
+ type_variables = GenericTypeRegistry.lookup_type_variables(constant)
290
+ return unless type_variables
291
+
292
+ # Create a map of subconstants (via their object ids) to their names.
293
+ # We need this later when we want to lookup the name of the registered type
294
+ # variable via the value of the type variable constant.
295
+ subconstant_to_name_lookup = constants_of(constant).map do |constant_name|
296
+ [
297
+ object_id_of(resolve_constant(constant_name.to_s, namespace: constant)),
298
+ constant_name,
299
+ ]
300
+ end.to_h
301
+
302
+ # Map each type variable to its string representation.
303
+ #
304
+ # Each entry of `type_variables` maps an object_id to a String,
305
+ # and the order they are inserted into the hash is the order they should be
306
+ # defined in the source code.
307
+ #
308
+ # By looping over these entries and then getting the actual constant name
309
+ # from the `subconstant_to_name_lookup` we defined above, gives us all the
310
+ # information we need to serialize type variable definitions.
311
+ type_variable_declarations = type_variables.map do |type_variable_id, serialized_type_variable|
312
+ constant_name = subconstant_to_name_lookup[type_variable_id]
313
+ # Here, we know that constant_value will be an instance of
314
+ # T::Types::CustomTypeVariable, which knows how to serialize
315
+ # itself to a type_member/type_template
316
+ indented("#{constant_name} = #{serialized_type_variable}")
317
+ end.compact
318
+
319
+ return if type_variable_declarations.empty?
320
+
321
+ [
322
+ indented("extend T::Generic"),
323
+ "",
324
+ *type_variable_declarations,
325
+ ].compact.join("\n")
326
+ end
327
+ end
328
+
239
329
  sig { params(constant: Class).returns(String) }
240
330
  def compile_superclass(constant)
241
331
  superclass = T.let(nil, T.nilable(Class)) # rubocop:disable Lint/UselessAssignment
@@ -345,9 +435,13 @@ module Tapioca
345
435
  end
346
436
 
347
437
  define_singleton_method(:include) do |mod|
348
- before = singleton_class.ancestors
349
- super(mod).tap do
350
- mixins_from_modules[mod] = singleton_class.ancestors - before
438
+ begin
439
+ before = singleton_class.ancestors
440
+ super(mod).tap do
441
+ mixins_from_modules[mod] = singleton_class.ancestors - before
442
+ end
443
+ rescue Exception # rubocop:disable Lint/RescueException
444
+ # this is a best effort, bail if we can't perform this
351
445
  end
352
446
  end
353
447
 
@@ -408,29 +502,19 @@ module Tapioca
408
502
  instance_methods = compile_directly_owned_methods(name, constant)
409
503
  singleton_methods = compile_directly_owned_methods(name, singleton_class_of(constant))
410
504
 
411
- return if symbol_ignored?(name) && instance_methods.empty? && singleton_methods.empty?
505
+ return if symbol_ignored?(name) && !instance_methods && !singleton_methods
412
506
 
413
507
  [
414
- initialize_method || "",
508
+ initialize_method,
415
509
  instance_methods,
416
510
  singleton_methods,
417
- ].select { |b| b.strip != "" }.join("\n\n")
511
+ ].select { |b| b && !b.strip.empty? }.join("\n\n")
418
512
  end
419
513
 
420
- sig { params(module_name: String, mod: Module, for_visibility: T::Array[Symbol]).returns(String) }
514
+ sig { params(module_name: String, mod: Module, for_visibility: T::Array[Symbol]).returns(T.nilable(String)) }
421
515
  def compile_directly_owned_methods(module_name, mod, for_visibility = [:public, :protected, :private])
422
- indent_step = 0
423
- preamble = nil
424
- postamble = nil
425
-
426
- if mod.singleton_class?
427
- indent_step = 1
428
- preamble = indented("class << self")
429
- postamble = indented("end")
430
- end
431
-
432
- methods = with_indentation(indent_step) do
433
- method_names_by_visibility(mod)
516
+ with_indentation_for_constant(mod) do
517
+ methods = method_names_by_visibility(mod)
434
518
  .delete_if { |visibility, _method_list| !for_visibility.include?(visibility) }
435
519
  .flat_map do |visibility, method_list|
436
520
  compiled = method_list.sort!.map do |name|
@@ -447,16 +531,11 @@ module Tapioca
447
531
  compiled
448
532
  end
449
533
  .compact
450
- .join("\n")
451
- end
452
534
 
453
- return "" if methods.strip == ""
535
+ return if methods.empty?
454
536
 
455
- [
456
- preamble,
457
- methods,
458
- postamble,
459
- ].compact.join("\n")
537
+ methods.join("\n")
538
+ end
460
539
  end
461
540
 
462
541
  sig { params(mod: Module).returns(T::Hash[Symbol, T::Array[Symbol]]) }
@@ -636,6 +715,33 @@ module Tapioca
636
715
  @indent -= 2 * step
637
716
  end
638
717
 
718
+ sig do
719
+ params(
720
+ constant: Module,
721
+ blk: T.proc
722
+ .returns(T.nilable(String))
723
+ )
724
+ .returns(T.nilable(String))
725
+ end
726
+ def with_indentation_for_constant(constant, &blk)
727
+ step = if constant.singleton_class?
728
+ 1
729
+ else
730
+ 0
731
+ end
732
+
733
+ result = with_indentation(step, &blk)
734
+
735
+ return result unless result
736
+ return result unless constant.singleton_class?
737
+
738
+ [
739
+ indented("class << self"),
740
+ result,
741
+ indented("end"),
742
+ ].compact.join("\n")
743
+ end
744
+
639
745
  sig { params(str: String).returns(String) }
640
746
  def indented(str)
641
747
  " " * @indent + str
@@ -836,12 +942,12 @@ module Tapioca
836
942
  nil
837
943
  end
838
944
 
839
- sig { params(constant: Module).returns(String) }
945
+ sig { params(constant: T::Types::Base).returns(String) }
840
946
  def type_of(constant)
841
947
  constant.to_s.gsub(/\bAttachedClass\b/, "T.attached_class")
842
948
  end
843
949
 
844
- sig { params(object: Object).returns(T::Boolean).checked(:never) }
950
+ sig { params(object: BasicObject).returns(Integer).checked(:never) }
845
951
  def object_id_of(object)
846
952
  Object.instance_method(:object_id).bind(object).call
847
953
  end