tapioca 0.4.17 → 0.4.22

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.
@@ -8,7 +8,6 @@ module Tapioca
8
8
  const(:outdir, String)
9
9
  const(:prerequire, T.nilable(String))
10
10
  const(:postrequire, String)
11
- const(:generate_command, String)
12
11
  const(:exclude, T::Array[String])
13
12
  const(:typed_overrides, T::Hash[String, String])
14
13
  const(:todos_path, String)
@@ -27,6 +26,7 @@ module Tapioca
27
26
  TAPIOCA_PATH = T.let("#{SORBET_PATH}/tapioca", String)
28
27
  TAPIOCA_CONFIG = T.let("#{TAPIOCA_PATH}/config.yml", String)
29
28
 
29
+ DEFAULT_COMMAND = T.let("bin/tapioca", String)
30
30
  DEFAULT_POSTREQUIRE = T.let("#{TAPIOCA_PATH}/require.rb", String)
31
31
  DEFAULT_RBIDIR = T.let("#{SORBET_PATH}/rbi", String)
32
32
  DEFAULT_DSLDIR = T.let("#{DEFAULT_RBIDIR}/dsl", String)
@@ -10,9 +10,13 @@ module Tapioca
10
10
 
11
11
  sig { params(command: Symbol, options: T::Hash[String, T.untyped]).returns(Config) }
12
12
  def from_options(command, options)
13
- Config.from_hash(
14
- merge_options(default_options(command), config_options, options)
15
- )
13
+ merged_options = merge_options(default_options(command), config_options, options)
14
+
15
+ puts(<<~MSG) if merged_options.include?("generate_command")
16
+ DEPRECATION: The `-c` and `--cmd` flags will be removed in a future release.
17
+ MSG
18
+
19
+ Config.from_hash(merged_options)
16
20
  end
17
21
 
18
22
  private
@@ -40,14 +44,6 @@ module Tapioca
40
44
  DEFAULT_OPTIONS.merge("outdir" => default_outdir)
41
45
  end
42
46
 
43
- sig { returns(String) }
44
- def default_command
45
- command = File.basename($PROGRAM_NAME)
46
- args = ARGV.join(" ")
47
-
48
- "#{command} #{args}".strip
49
- end
50
-
51
47
  sig { params(options: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
52
48
  def merge_options(*options)
53
49
  options.each_with_object({}) do |option, result|
@@ -65,7 +61,6 @@ module Tapioca
65
61
  DEFAULT_OPTIONS = T.let({
66
62
  "postrequire" => Config::DEFAULT_POSTREQUIRE,
67
63
  "outdir" => nil,
68
- "generate_command" => default_command,
69
64
  "exclude" => [],
70
65
  "typed_overrides" => Config::DEFAULT_OVERRIDES,
71
66
  "todos_path" => Config::DEFAULT_TODOSPATH,
@@ -0,0 +1,18 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ class String
5
+ extend T::Sig
6
+
7
+ sig { returns(String) }
8
+ def underscore
9
+ return self unless /[A-Z-]|::/.match?(self)
10
+
11
+ word = to_s.gsub("::", "/")
12
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
13
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
14
+ word.tr!("-", "_")
15
+ word.downcase!
16
+ word
17
+ end
18
+ end
@@ -176,7 +176,7 @@ module Tapioca
176
176
 
177
177
  sig { returns(T::Boolean) }
178
178
  def gem_in_bundle_path?
179
- full_gem_path.start_with?(Bundler.bundle_path.to_s)
179
+ full_gem_path.start_with?(Bundler.bundle_path.to_s, Bundler.app_cache.to_s)
180
180
  end
181
181
  end
182
182
  end
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'pathname'
5
5
  require 'thor'
6
+ require "tapioca/core_ext/string"
6
7
 
7
8
  module Tapioca
8
9
  class Generator < ::Thor::Shell::Color
@@ -63,7 +64,7 @@ module Tapioca
63
64
 
64
65
  content = String.new
65
66
  content << rbi_header(
66
- config.generate_command,
67
+ "#{Config::DEFAULT_COMMAND} require",
67
68
  reason: "explicit gem requires",
68
69
  strictness: "false"
69
70
  )
@@ -76,8 +77,8 @@ module Tapioca
76
77
  say("Done", :green)
77
78
 
78
79
  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])
80
+ cmd = set_color("#{Config::DEFAULT_COMMAND} sync", :yellow, :bold)
81
+ say("Please review changes and commit them, then run `#{cmd}`.", [:green, :bold])
81
82
  end
82
83
 
83
84
  sig { void }
@@ -99,7 +100,7 @@ module Tapioca
99
100
 
100
101
  content = String.new
101
102
  content << rbi_header(
102
- config.generate_command,
103
+ "#{Config::DEFAULT_COMMAND} todo",
103
104
  reason: "unresolved constants",
104
105
  strictness: "false"
105
106
  )
@@ -116,16 +117,27 @@ module Tapioca
116
117
  say("Please review changes and commit them.", [:green, :bold])
117
118
  end
118
119
 
119
- sig { params(requested_constants: T::Array[String]).void }
120
- def build_dsl(requested_constants)
120
+ sig do
121
+ params(
122
+ requested_constants: T::Array[String],
123
+ should_verify: T::Boolean,
124
+ quiet: T::Boolean
125
+ ).void
126
+ end
127
+ def build_dsl(requested_constants, should_verify: false, quiet: false)
121
128
  load_application(eager_load: requested_constants.empty?)
122
129
  load_dsl_generators
123
130
 
124
- rbi_files_to_purge = existing_rbi_filenames(requested_constants)
125
-
126
- say("Compiling DSL RBI files...")
131
+ if should_verify
132
+ say("Checking for out-of-date RBIs...")
133
+ else
134
+ say("Compiling DSL RBI files...")
135
+ end
127
136
  say("")
128
137
 
138
+ outpath = should_verify ? Pathname.new(Dir.mktmpdir) : config.outpath
139
+ rbi_files_to_purge = existing_rbi_filenames(requested_constants)
140
+
129
141
  compiler = Compilers::DslCompiler.new(
130
142
  requested_constants: constantize(requested_constants),
131
143
  requested_generators: config.generators,
@@ -135,24 +147,31 @@ module Tapioca
135
147
  )
136
148
 
137
149
  compiler.run do |constant, contents|
138
- filename = compile_dsl_rbi(constant, contents)
139
- rbi_files_to_purge.delete(filename) if filename
140
- end
150
+ constant_name = Module.instance_method(:name).bind(constant).call
141
151
 
142
- unless rbi_files_to_purge.empty?
143
- say("")
144
- say("Removing stale RBI files...")
152
+ filename = compile_dsl_rbi(
153
+ constant_name,
154
+ contents,
155
+ outpath: outpath,
156
+ quiet: should_verify || quiet
157
+ )
145
158
 
146
- rbi_files_to_purge.sort.each do |filename|
147
- remove(filename)
159
+ if filename
160
+ rbi_files_to_purge.delete(filename)
148
161
  end
149
162
  end
150
-
151
163
  say("")
152
- say("Done", :green)
153
164
 
154
- say("All operations performed in working directory.", [:green, :bold])
155
- say("Please review changes and commit them.", [:green, :bold])
165
+ if should_verify
166
+ perform_dsl_verification(outpath)
167
+ else
168
+ purge_stale_dsl_rbi_files(rbi_files_to_purge)
169
+
170
+ say("Done", :green)
171
+
172
+ say("All operations performed in working directory.", [:green, :bold])
173
+ say("Please review changes and commit them.", [:green, :bold])
174
+ end
156
175
  end
157
176
 
158
177
  sig { void }
@@ -218,7 +237,7 @@ module Tapioca
218
237
  say_error("If you populated ", :yellow)
219
238
  say_error("#{file} ", :bold, :blue)
220
239
  say_error("with ", :yellow)
221
- say_error("tapioca require", :bold, :blue)
240
+ say_error("`#{Config::DEFAULT_COMMAND} require`", :bold, :blue)
222
241
  say_error("you should probably review it and remove the faulty line.", :yellow)
223
242
  end
224
243
 
@@ -267,7 +286,7 @@ module Tapioca
267
286
  def constantize(constant_names)
268
287
  constant_map = constant_names.map do |name|
269
288
  begin
270
- [name, name.constantize]
289
+ [name, Object.const_get(name)]
271
290
  rescue NameError
272
291
  [name, nil]
273
292
  end
@@ -286,10 +305,10 @@ module Tapioca
286
305
  constant_map.values
287
306
  end
288
307
 
289
- sig { params(requested_constants: T::Array[String]).returns(T::Set[Pathname]) }
290
- def existing_rbi_filenames(requested_constants)
308
+ sig { params(requested_constants: T::Array[String], path: Pathname).returns(T::Set[Pathname]) }
309
+ def existing_rbi_filenames(requested_constants, path: config.outpath)
291
310
  filenames = if requested_constants.empty?
292
- Pathname.glob(config.outpath / "**/*.rbi")
311
+ Pathname.glob(path / "**/*.rbi")
293
312
  else
294
313
  requested_constants.map do |constant_name|
295
314
  dsl_rbi_filename(constant_name)
@@ -477,7 +496,7 @@ module Tapioca
477
496
  rbi_body_content = compiler.compile(gem)
478
497
  content = String.new
479
498
  content << rbi_header(
480
- config.generate_command,
499
+ "#{Config::DEFAULT_COMMAND} sync",
481
500
  reason: "types exported from the `#{gem.name}` gem",
482
501
  strictness: strictness
483
502
  )
@@ -494,33 +513,121 @@ module Tapioca
494
513
  end
495
514
  File.write(filename.to_s, content)
496
515
 
497
- Pathname.glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
516
+ T.unsafe(Pathname).glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
498
517
  remove(file) unless file.basename.to_s == gem.rbi_file_name
499
518
  end
500
519
  end
501
520
 
502
- sig { params(constant: Module, contents: String).returns(T.nilable(Pathname)) }
503
- def compile_dsl_rbi(constant, contents)
521
+ sig do
522
+ params(constant_name: String, contents: String, outpath: Pathname, quiet: T::Boolean)
523
+ .returns(T.nilable(Pathname))
524
+ end
525
+ def compile_dsl_rbi(constant_name, contents, outpath: config.outpath, quiet: false)
504
526
  return if contents.nil?
505
527
 
506
- command = format(config.generate_command, constant.name)
507
- constant_name = Module.instance_method(:name).bind(constant).call
508
528
  rbi_name = constant_name.underscore + ".rbi"
509
- filename = config.outpath / rbi_name
529
+ filename = outpath / rbi_name
510
530
 
511
531
  out = String.new
512
532
  out << rbi_header(
513
- command,
514
- reason: "dynamic methods in `#{constant.name}`"
533
+ "#{Config::DEFAULT_COMMAND} dsl #{constant_name}",
534
+ reason: "dynamic methods in `#{constant_name}`"
515
535
  )
516
536
  out << contents
517
537
 
518
538
  FileUtils.mkdir_p(File.dirname(filename))
519
539
  File.write(filename, out)
520
- say("Wrote: ", [:green])
521
- say(filename)
540
+
541
+ unless quiet
542
+ say("Wrote: ", [:green])
543
+ say(filename)
544
+ end
522
545
 
523
546
  filename
524
547
  end
548
+
549
+ sig { params(tmp_dir: Pathname).returns(T::Hash[String, Symbol]) }
550
+ def verify_dsl_rbi(tmp_dir:)
551
+ diff = {}
552
+
553
+ existing_rbis = rbi_files_in(config.outpath)
554
+ new_rbis = rbi_files_in(tmp_dir)
555
+
556
+ added_files = (new_rbis - existing_rbis)
557
+
558
+ added_files.each do |file|
559
+ diff[file] = :added
560
+ end
561
+
562
+ removed_files = (existing_rbis - new_rbis)
563
+
564
+ removed_files.each do |file|
565
+ diff[file] = :removed
566
+ end
567
+
568
+ common_files = (existing_rbis & new_rbis)
569
+
570
+ changed_files = common_files.map do |filename|
571
+ filename unless FileUtils.identical?(config.outpath / filename, tmp_dir / filename)
572
+ end.compact
573
+
574
+ changed_files.each do |file|
575
+ diff[file] = :changed
576
+ end
577
+
578
+ diff
579
+ end
580
+
581
+ sig { params(cause: Symbol, files: T::Array[String]).returns(String) }
582
+ def build_error_for_files(cause, files)
583
+ filenames = files.map do |file|
584
+ config.outpath / file
585
+ end.join("\n - ")
586
+
587
+ " File(s) #{cause}:\n - #{filenames}"
588
+ end
589
+
590
+ sig { params(path: Pathname).returns(T::Array[Pathname]) }
591
+ def rbi_files_in(path)
592
+ Pathname.glob(path / "**/*.rbi").map do |file|
593
+ file.relative_path_from(path)
594
+ end.sort
595
+ end
596
+
597
+ sig { params(dir: Pathname).void }
598
+ def perform_dsl_verification(dir)
599
+ diff = verify_dsl_rbi(tmp_dir: dir)
600
+
601
+ if diff.empty?
602
+ say("Nothing to do, all RBIs are up-to-date.")
603
+ else
604
+ say("RBI files are out-of-date. In your development environment, please run:", :green)
605
+ say(" `#{Config::DEFAULT_COMMAND} dsl`", [:green, :bold])
606
+ say("Once it is complete, be sure to commit and push any changes", :green)
607
+
608
+ say("")
609
+
610
+ say("Reason:", [:red])
611
+ diff.group_by(&:last).sort.each do |cause, diff_for_cause|
612
+ say(build_error_for_files(cause, diff_for_cause.map(&:first)))
613
+ end
614
+
615
+ exit(1)
616
+ end
617
+ ensure
618
+ FileUtils.remove_entry(dir)
619
+ end
620
+
621
+ sig { params(files: T::Set[Pathname]).void }
622
+ def purge_stale_dsl_rbi_files(files)
623
+ if files.any?
624
+ say("Removing stale RBI files...")
625
+
626
+ files.sort.each do |filename|
627
+ remove(filename)
628
+ end
629
+ say("")
630
+ end
631
+ end
525
632
  end
526
633
  end
@@ -0,0 +1,222 @@
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 generic type with an overridden `name`
55
+ # method that returns the name we constructed above.
56
+ #
57
+ # Also, we try to memoize the generic type based on the name, so that
58
+ # we don't have to keep recreating them all the time.
59
+ @generic_instances[name] ||= create_generic_type(constant, name)
60
+ end
61
+
62
+ sig do
63
+ params(
64
+ constant: T.untyped,
65
+ type_member: T::Types::TypeVariable,
66
+ fixed: T.untyped,
67
+ lower: T.untyped,
68
+ upper: T.untyped
69
+ ).void
70
+ end
71
+ def register_type_member(constant, type_member, fixed, lower, upper)
72
+ register_type_variable(constant, :type_member, type_member, fixed, lower, upper)
73
+ end
74
+
75
+ sig do
76
+ params(
77
+ constant: T.untyped,
78
+ type_template: T::Types::TypeVariable,
79
+ fixed: T.untyped,
80
+ lower: T.untyped,
81
+ upper: T.untyped
82
+ ).void
83
+ end
84
+ def register_type_template(constant, type_template, fixed, lower, upper)
85
+ register_type_variable(constant, :type_template, type_template, fixed, lower, upper)
86
+ end
87
+
88
+ sig { params(constant: Module).returns(T.nilable(T::Hash[Integer, String])) }
89
+ def lookup_type_variables(constant)
90
+ @type_variables[object_id_of(constant)]
91
+ end
92
+
93
+ private
94
+
95
+ sig { params(constant: Module, name: String).returns(Module) }
96
+ def create_generic_type(constant, name)
97
+ generic_type = case constant
98
+ when Class
99
+ # For classes, we want to create a subclass, so that an instance of
100
+ # the generic class `Foo[Bar]` is still a `Foo`. That is:
101
+ # `Foo[Bar].new.is_a?(Foo)` should be true, which isn't the case
102
+ # if we just clone the class. But subclassing works just fine.
103
+ create_safe_subclass(constant)
104
+ else
105
+ # This can only be a module and it is fine to just clone modules
106
+ # since they can't have instances and will not have `is_a?` relationships.
107
+ # Moreover, we never `include`/`extend` any generic modules into the
108
+ # ancestor tree, so this doesn't become a problem with checking the
109
+ # instance of a class being `is_a?` of a module type.
110
+ constant.clone
111
+ end
112
+
113
+ # Let's set the `name` method to return the proper generic name
114
+ generic_type.define_singleton_method(:name) { name }
115
+
116
+ # Return the generic type we created
117
+ generic_type
118
+ end
119
+
120
+ # This method is called from intercepted calls to `type_member` and `type_template`.
121
+ # We get passed all the arguments to those methods, as well as the `T::Types::TypeVariable`
122
+ # instance generated by the Sorbet defined `type_member`/`type_template` call on `T::Generic`.
123
+ #
124
+ # This method creates a `String` with that data and stores it in the
125
+ # `@type_variables` lookup table, keyed by the `constant` and `type_variable`.
126
+ #
127
+ # Finally, the original `type_variable` is returned from this method, so that the caller
128
+ # can return it from the original methods as well.
129
+ sig do
130
+ params(
131
+ constant: T.untyped,
132
+ type_variable_type: T.enum([:type_member, :type_template]),
133
+ type_variable: T::Types::TypeVariable,
134
+ fixed: T.untyped,
135
+ lower: T.untyped,
136
+ upper: T.untyped
137
+ ).void
138
+ end
139
+ # rubocop:disable Metrics/ParameterLists
140
+ def register_type_variable(constant, type_variable_type, type_variable, fixed, lower, upper)
141
+ # rubocop:enable Metrics/ParameterLists
142
+ type_variables = lookup_or_initialize_type_variables(constant)
143
+
144
+ type_variables[object_id_of(type_variable)] = serialize_type_variable(
145
+ type_variable_type,
146
+ type_variable.variance,
147
+ fixed,
148
+ lower,
149
+ upper
150
+ )
151
+ end
152
+
153
+ sig { params(constant: Class).returns(Class) }
154
+ def create_safe_subclass(constant)
155
+ # Lookup the "inherited" class method
156
+ inherited_method = constant.method(:inherited)
157
+ # and the module that defines it
158
+ owner = inherited_method.owner
159
+
160
+ # If no one has overriden the inherited method yet, just subclass
161
+ return Class.new(constant) if Class == owner
162
+
163
+ begin
164
+ # Otherwise, some inherited method could be preventing us
165
+ # from creating subclasses, so let's override it and rescue
166
+ owner.send(:define_method, :inherited) do |s|
167
+ begin
168
+ inherited_method.call(s)
169
+ rescue
170
+ # Ignoring errors
171
+ end
172
+ end
173
+
174
+ # return a subclass
175
+ Class.new(constant)
176
+ ensure
177
+ # Reinstate the original inherited method back.
178
+ owner.send(:define_method, :inherited, inherited_method)
179
+ end
180
+ end
181
+
182
+ sig { params(constant: Module).returns(T::Hash[Integer, String]) }
183
+ def lookup_or_initialize_type_variables(constant)
184
+ @type_variables[object_id_of(constant)] ||= {}
185
+ end
186
+
187
+ sig do
188
+ params(
189
+ type_variable_type: Symbol,
190
+ variance: Symbol,
191
+ fixed: T.untyped,
192
+ lower: T.untyped,
193
+ upper: T.untyped
194
+ ).returns(String)
195
+ end
196
+ def serialize_type_variable(type_variable_type, variance, fixed, lower, upper)
197
+ parts = []
198
+ parts << ":#{variance}" unless variance == :invariant
199
+ parts << "fixed: #{fixed}" if fixed
200
+ parts << "lower: #{lower}" unless lower == T.untyped
201
+ parts << "upper: #{upper}" unless upper == BasicObject
202
+
203
+ parameters = parts.join(", ")
204
+
205
+ serialized = T.let(type_variable_type.to_s, String)
206
+ serialized += "(#{parameters})" unless parameters.empty?
207
+
208
+ serialized
209
+ end
210
+
211
+ sig { params(constant: Module).returns(T.nilable(String)) }
212
+ def name_of(constant)
213
+ Module.instance_method(:name).bind(constant).call
214
+ end
215
+
216
+ sig { params(object: BasicObject).returns(Integer) }
217
+ def object_id_of(object)
218
+ Object.instance_method(:object_id).bind(object).call
219
+ end
220
+ end
221
+ end
222
+ end