tapioca 0.4.13 → 0.4.18

Sign up to get free protection for your applications and to get access to all the features.
@@ -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"