tapioca 0.4.13 → 0.4.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -63,7 +63,7 @@ module Tapioca
63
63
 
64
64
  content = String.new
65
65
  content << rbi_header(
66
- config.generate_command,
66
+ "#{Config::DEFAULT_COMMAND} require",
67
67
  reason: "explicit gem requires",
68
68
  strictness: "false"
69
69
  )
@@ -76,8 +76,8 @@ module Tapioca
76
76
  say("Done", :green)
77
77
 
78
78
  say("All requires from this application have been written to #{name}.", [:green, :bold])
79
- cmd = set_color("tapioca sync", :yellow, :bold)
80
- say("Please review changes and commit them, then run #{cmd}.", [:green, :bold])
79
+ cmd = set_color("#{Config::DEFAULT_COMMAND} sync", :yellow, :bold)
80
+ say("Please review changes and commit them, then run `#{cmd}`.", [:green, :bold])
81
81
  end
82
82
 
83
83
  sig { void }
@@ -99,7 +99,7 @@ module Tapioca
99
99
 
100
100
  content = String.new
101
101
  content << rbi_header(
102
- config.generate_command,
102
+ "#{Config::DEFAULT_COMMAND} todo",
103
103
  reason: "unresolved constants",
104
104
  strictness: "false"
105
105
  )
@@ -116,14 +116,26 @@ module Tapioca
116
116
  say("Please review changes and commit them.", [:green, :bold])
117
117
  end
118
118
 
119
- sig { params(requested_constants: T::Array[String]).void }
120
- def build_dsl(requested_constants)
119
+ sig do
120
+ params(
121
+ requested_constants: T::Array[String],
122
+ should_verify: T::Boolean,
123
+ ).void
124
+ end
125
+ def build_dsl(requested_constants, should_verify: false)
121
126
  load_application(eager_load: requested_constants.empty?)
122
127
  load_dsl_generators
123
128
 
124
- say("Compiling DSL RBI files...")
129
+ if should_verify
130
+ say("Checking for out-of-date RBIs...")
131
+ else
132
+ say("Compiling DSL RBI files...")
133
+ end
125
134
  say("")
126
135
 
136
+ outpath = should_verify ? Dir.mktmpdir : config.outpath
137
+ rbi_files_to_purge = existing_rbi_filenames(requested_constants)
138
+
127
139
  compiler = Compilers::DslCompiler.new(
128
140
  requested_constants: constantize(requested_constants),
129
141
  requested_generators: config.generators,
@@ -133,14 +145,21 @@ module Tapioca
133
145
  )
134
146
 
135
147
  compiler.run do |constant, contents|
136
- compile_dsl_rbi(constant, contents)
148
+ filename = compile_dsl_rbi(constant, contents, outpath: Pathname.new(outpath))
149
+ rbi_files_to_purge.delete(filename) if filename
137
150
  end
138
-
139
151
  say("")
140
- say("Done", :green)
141
152
 
142
- say("All operations performed in working directory.", [:green, :bold])
143
- say("Please review changes and commit them.", [:green, :bold])
153
+ if should_verify
154
+ perform_dsl_verification(outpath)
155
+ else
156
+ purge_stale_dsl_rbi_files(rbi_files_to_purge)
157
+
158
+ say("Done", :green)
159
+
160
+ say("All operations performed in working directory.", [:green, :bold])
161
+ say("Please review changes and commit them.", [:green, :bold])
162
+ end
144
163
  end
145
164
 
146
165
  sig { void }
@@ -192,6 +211,10 @@ module Tapioca
192
211
  exit(1)
193
212
  end
194
213
  say(" Done", :green)
214
+ unless bundle.missing_specs.empty?
215
+ say(" completed with missing specs: ")
216
+ say(bundle.missing_specs.join(', '), :yellow)
217
+ end
195
218
  puts
196
219
  end
197
220
 
@@ -202,7 +225,7 @@ module Tapioca
202
225
  say_error("If you populated ", :yellow)
203
226
  say_error("#{file} ", :bold, :blue)
204
227
  say_error("with ", :yellow)
205
- say_error("tapioca require", :bold, :blue)
228
+ say_error("`#{Config::DEFAULT_COMMAND} require`", :bold, :blue)
206
229
  say_error("you should probably review it and remove the faulty line.", :yellow)
207
230
  end
208
231
 
@@ -249,13 +272,38 @@ module Tapioca
249
272
 
250
273
  sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) }
251
274
  def constantize(constant_names)
252
- constant_names.map do |name|
275
+ constant_map = constant_names.map do |name|
253
276
  begin
254
- name.constantize
277
+ [name, name.constantize]
255
278
  rescue NameError
256
- nil
279
+ [name, nil]
280
+ end
281
+ end.to_h
282
+
283
+ unprocessable_constants = constant_map.select { |_, v| v.nil? }
284
+ unless unprocessable_constants.empty?
285
+ unprocessable_constants.each do |name, _|
286
+ say("Error: Cannot find constant '#{name}'", :red)
287
+ remove(dsl_rbi_filename(name))
288
+ end
289
+
290
+ exit(1)
291
+ end
292
+
293
+ constant_map.values
294
+ end
295
+
296
+ sig { params(requested_constants: T::Array[String], path: Pathname).returns(T::Set[Pathname]) }
297
+ def existing_rbi_filenames(requested_constants, path: config.outpath)
298
+ filenames = if requested_constants.empty?
299
+ Pathname.glob(path / "**/*.rbi")
300
+ else
301
+ requested_constants.map do |constant_name|
302
+ dsl_rbi_filename(constant_name)
257
303
  end
258
- end.compact
304
+ end
305
+
306
+ filenames.to_set
259
307
  end
260
308
 
261
309
  sig { returns(T::Hash[String, String]) }
@@ -273,6 +321,11 @@ module Tapioca
273
321
  .to_h
274
322
  end
275
323
 
324
+ sig { params(constant_name: String).returns(Pathname) }
325
+ def dsl_rbi_filename(constant_name)
326
+ config.outpath / "#{constant_name.underscore}.rbi"
327
+ end
328
+
276
329
  sig { params(gem_name: String, version: String).returns(Pathname) }
277
330
  def gem_rbi_filename(gem_name, version)
278
331
  config.outpath / "#{gem_name}@#{version}.rbi"
@@ -312,6 +365,7 @@ module Tapioca
312
365
 
313
366
  sig { params(filename: Pathname).void }
314
367
  def remove(filename)
368
+ return unless filename.exist?
315
369
  say("-- Removing: #{filename}")
316
370
  filename.unlink
317
371
  end
@@ -430,7 +484,7 @@ module Tapioca
430
484
  rbi_body_content = compiler.compile(gem)
431
485
  content = String.new
432
486
  content << rbi_header(
433
- config.generate_command,
487
+ "#{Config::DEFAULT_COMMAND} sync",
434
488
  reason: "types exported from the `#{gem.name}` gem",
435
489
  strictness: strictness
436
490
  )
@@ -447,23 +501,22 @@ module Tapioca
447
501
  end
448
502
  File.write(filename.to_s, content)
449
503
 
450
- Pathname.glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
504
+ T.unsafe(Pathname).glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
451
505
  remove(file) unless file.basename.to_s == gem.rbi_file_name
452
506
  end
453
507
  end
454
508
 
455
- sig { params(constant: Module, contents: String).void }
456
- def compile_dsl_rbi(constant, contents)
509
+ sig { params(constant: Module, contents: String, outpath: Pathname).returns(T.nilable(Pathname)) }
510
+ def compile_dsl_rbi(constant, contents, outpath: config.outpath)
457
511
  return if contents.nil?
458
512
 
459
- command = format(config.generate_command, constant.name)
460
513
  constant_name = Module.instance_method(:name).bind(constant).call
461
514
  rbi_name = constant_name.underscore + ".rbi"
462
- filename = config.outpath / rbi_name
515
+ filename = outpath / rbi_name
463
516
 
464
517
  out = String.new
465
518
  out << rbi_header(
466
- command,
519
+ "#{Config::DEFAULT_COMMAND} dsl #{constant_name}",
467
520
  reason: "dynamic methods in `#{constant.name}`"
468
521
  )
469
522
  out << contents
@@ -472,6 +525,56 @@ module Tapioca
472
525
  File.write(filename, out)
473
526
  say("Wrote: ", [:green])
474
527
  say(filename)
528
+
529
+ filename
530
+ end
531
+
532
+ sig { params(tmp_dir: Pathname).returns(T.nilable(String)) }
533
+ def verify_dsl_rbi(tmp_dir:)
534
+ existing_rbis = existing_rbi_filenames([]).sort
535
+ new_rbis = existing_rbi_filenames([], path: tmp_dir).grep_v(/gem|shim/).sort
536
+
537
+ return "New file(s) introduced." if existing_rbis.length != new_rbis.length
538
+
539
+ desynced_files = []
540
+
541
+ (0..existing_rbis.length - 1).each do |i|
542
+ desynced_files << new_rbis[i] unless FileUtils.identical?(existing_rbis[i], new_rbis[i])
543
+ end
544
+
545
+ unless desynced_files.empty?
546
+ filenames = desynced_files.map { |f| f.to_s.sub!(tmp_dir.to_s, "sorbet/rbi/dsl") }.join("\n - ")
547
+
548
+ return "File(s) updated:\n - #{filenames}"
549
+ end
550
+
551
+ nil
552
+ end
553
+
554
+ sig { params(dir: String).void }
555
+ def perform_dsl_verification(dir)
556
+ if (error = verify_dsl_rbi(tmp_dir: Pathname.new(dir)))
557
+ say("RBI files are out-of-date, please run `#{Config::DEFAULT_COMMAND} dsl` to update.")
558
+ say("Reason: ", [:red])
559
+ say(error)
560
+ exit(1)
561
+ else
562
+ say("Nothing to do, all RBIs are up-to-date.")
563
+ end
564
+ ensure
565
+ FileUtils.remove_entry(dir)
566
+ end
567
+
568
+ sig { params(files: T::Set[Pathname]).void }
569
+ def purge_stale_dsl_rbi_files(files)
570
+ if files.any?
571
+ say("Removing stale RBI files...")
572
+
573
+ files.sort.each do |filename|
574
+ remove(filename)
575
+ end
576
+ say("")
577
+ end
475
578
  end
476
579
  end
477
580
  end
@@ -0,0 +1,170 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ # This class is responsible for storing and looking up information related to generic types.
6
+ #
7
+ # The class stores 2 different kinds of data, in two separate lookup tables:
8
+ # 1. a lookup of generic type instances by name: `@generic_instances`
9
+ # 2. a lookup of type variable serializer by constant and type variable
10
+ # instance: `@type_variables`
11
+ #
12
+ # By storing the above data, we can cheaply query each constant against this registry
13
+ # to see if it declares any generic type variables. This becomes a simple lookup in the
14
+ # `@type_variables` hash table with the given constant.
15
+ #
16
+ # If there is no entry, then we can cheaply know that we can skip generic type
17
+ # information generation for this type.
18
+ #
19
+ # On the other hand, if we get a result, then the result will be a hash of type
20
+ # variable to type variable serializers. This allows us to associate type variables
21
+ # to the constant names that represent them, easily.
22
+ module GenericTypeRegistry
23
+ @generic_instances = T.let(
24
+ {},
25
+ T::Hash[String, Module]
26
+ )
27
+
28
+ @type_variables = T.let(
29
+ {},
30
+ T::Hash[Integer, T::Hash[Integer, String]]
31
+ )
32
+
33
+ class << self
34
+ extend T::Sig
35
+
36
+ # This method is responsible for building the name of the instantiated concrete type
37
+ # and cloning the given constant so that we can return a type that is the same
38
+ # as the current type but is a different instance and has a different name method.
39
+ #
40
+ # We cache those cloned instances by their name in `@generic_instances`, so that
41
+ # we don't keep instantiating a new type every single time it is referenced.
42
+ # For example, `[Foo[Integer], Foo[Integer], Foo[Integer], Foo[String]]` will only
43
+ # result in 2 clones (1 for `Foo[Integer]` and another for `Foo[String]`) and
44
+ # 2 hash lookups (for the other two `Foo[Integer]`s).
45
+ #
46
+ # This method returns the created or cached clone of the constant.
47
+ sig { params(constant: T.untyped, types: T.untyped).returns(Module) }
48
+ def register_type(constant, types)
49
+ # Build the name of the instantiated generic type,
50
+ # something like `"Foo[X, Y, Z]"`
51
+ type_list = types.map { |type| T::Utils.coerce(type).name }.join(", ")
52
+ name = "#{name_of(constant)}[#{type_list}]"
53
+
54
+ # Create a clone of the constant with an overridden `name`
55
+ # method that returns the name we constructed above.
56
+ #
57
+ # Also, we try to memoize the clone based on the name, so that
58
+ # we don't have to keep recreating clones all the time.
59
+ @generic_instances[name] ||= constant.clone.tap do |clone|
60
+ clone.define_singleton_method(:name) { name }
61
+ end
62
+ end
63
+
64
+ sig do
65
+ params(
66
+ constant: T.untyped,
67
+ type_member: T::Types::TypeVariable,
68
+ fixed: T.untyped,
69
+ lower: T.untyped,
70
+ upper: T.untyped
71
+ ).void
72
+ end
73
+ def register_type_member(constant, type_member, fixed, lower, upper)
74
+ register_type_variable(constant, :type_member, type_member, fixed, lower, upper)
75
+ end
76
+
77
+ sig do
78
+ params(
79
+ constant: T.untyped,
80
+ type_template: T::Types::TypeVariable,
81
+ fixed: T.untyped,
82
+ lower: T.untyped,
83
+ upper: T.untyped
84
+ ).void
85
+ end
86
+ def register_type_template(constant, type_template, fixed, lower, upper)
87
+ register_type_variable(constant, :type_template, type_template, fixed, lower, upper)
88
+ end
89
+
90
+ sig { params(constant: Module).returns(T.nilable(T::Hash[Integer, String])) }
91
+ def lookup_type_variables(constant)
92
+ @type_variables[object_id_of(constant)]
93
+ end
94
+
95
+ private
96
+
97
+ # This method is called from intercepted calls to `type_member` and `type_template`.
98
+ # We get passed all the arguments to those methods, as well as the `T::Types::TypeVariable`
99
+ # instance generated by the Sorbet defined `type_member`/`type_template` call on `T::Generic`.
100
+ #
101
+ # This method creates a `String` with that data and stores it in the
102
+ # `@type_variables` lookup table, keyed by the `constant` and `type_variable`.
103
+ #
104
+ # Finally, the original `type_variable` is returned from this method, so that the caller
105
+ # can return it from the original methods as well.
106
+ sig do
107
+ params(
108
+ constant: T.untyped,
109
+ type_variable_type: T.enum([:type_member, :type_template]),
110
+ type_variable: T::Types::TypeVariable,
111
+ fixed: T.untyped,
112
+ lower: T.untyped,
113
+ upper: T.untyped
114
+ ).void
115
+ end
116
+ # rubocop:disable Metrics/ParameterLists
117
+ def register_type_variable(constant, type_variable_type, type_variable, fixed, lower, upper)
118
+ # rubocop:enable Metrics/ParameterLists
119
+ type_variables = lookup_or_initialize_type_variables(constant)
120
+
121
+ type_variables[object_id_of(type_variable)] = serialize_type_variable(
122
+ type_variable_type,
123
+ type_variable.variance,
124
+ fixed,
125
+ lower,
126
+ upper
127
+ )
128
+ end
129
+
130
+ sig { params(constant: Module).returns(T::Hash[Integer, String]) }
131
+ def lookup_or_initialize_type_variables(constant)
132
+ @type_variables[object_id_of(constant)] ||= {}
133
+ end
134
+
135
+ sig do
136
+ params(
137
+ type_variable_type: Symbol,
138
+ variance: Symbol,
139
+ fixed: T.untyped,
140
+ lower: T.untyped,
141
+ upper: T.untyped
142
+ ).returns(String)
143
+ end
144
+ def serialize_type_variable(type_variable_type, variance, fixed, lower, upper)
145
+ parts = []
146
+ parts << ":#{variance}" unless variance == :invariant
147
+ parts << "fixed: #{fixed}" if fixed
148
+ parts << "lower: #{lower}" unless lower == T.untyped
149
+ parts << "upper: #{upper}" unless upper == BasicObject
150
+
151
+ parameters = parts.join(", ")
152
+
153
+ serialized = T.let(type_variable_type.to_s, String)
154
+ serialized += "(#{parameters})" unless parameters.empty?
155
+
156
+ serialized
157
+ end
158
+
159
+ sig { params(constant: Module).returns(T.nilable(String)) }
160
+ def name_of(constant)
161
+ Module.instance_method(:name).bind(constant).call
162
+ end
163
+
164
+ sig { params(object: BasicObject).returns(Integer) }
165
+ def object_id_of(object)
166
+ Object.instance_method(:object_id).bind(object).call
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,21 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "tapioca"
5
+ require "tapioca/loader"
6
+ require "tapioca/constant_locator"
7
+ require "tapioca/generic_type_registry"
8
+ require "tapioca/sorbet_ext/generic_name_patch"
9
+ require "tapioca/config"
10
+ require "tapioca/config_builder"
11
+ require "tapioca/generator"
12
+ require "tapioca/cli"
13
+ require "tapioca/cli/main"
14
+ require "tapioca/gemfile"
15
+ require "tapioca/compilers/sorbet"
16
+ require "tapioca/compilers/requires_compiler"
17
+ require "tapioca/compilers/symbol_table_compiler"
18
+ require "tapioca/compilers/symbol_table/symbol_generator"
19
+ require "tapioca/compilers/symbol_table/symbol_loader"
20
+ require "tapioca/compilers/todos_compiler"
21
+ require "tapioca/compilers/dsl_compiler"