tapioca 0.5.0 → 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,198 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ class DynamicMixinCompiler
5
+ extend T::Sig
6
+ include Tapioca::Reflection
7
+
8
+ sig { returns(T::Array[Module]) }
9
+ attr_reader :dynamic_extends, :dynamic_includes
10
+
11
+ sig { returns(T::Array[Symbol]) }
12
+ attr_reader :class_attribute_readers, :class_attribute_writers, :class_attribute_predicates
13
+
14
+ sig { returns(T::Array[Symbol]) }
15
+ attr_reader :instance_attribute_readers, :instance_attribute_writers, :instance_attribute_predicates
16
+
17
+ sig { params(constant: Module).void }
18
+ def initialize(constant)
19
+ @constant = constant
20
+ mixins_from_modules = {}.compare_by_identity
21
+ class_attribute_readers = T.let([], T::Array[Symbol])
22
+ class_attribute_writers = T.let([], T::Array[Symbol])
23
+ class_attribute_predicates = T.let([], T::Array[Symbol])
24
+
25
+ instance_attribute_readers = T.let([], T::Array[Symbol])
26
+ instance_attribute_writers = T.let([], T::Array[Symbol])
27
+ instance_attribute_predicates = T.let([], T::Array[Symbol])
28
+
29
+ Class.new do
30
+ # Override the `self.include` method
31
+ define_singleton_method(:include) do |mod|
32
+ # Take a snapshot of the list of singleton class ancestors
33
+ # before the actual include
34
+ before = singleton_class.ancestors
35
+ # Call the actual `include` method with the supplied module
36
+ super(mod).tap do
37
+ # Take a snapshot of the list of singleton class ancestors
38
+ # after the actual include
39
+ after = singleton_class.ancestors
40
+ # The difference is the modules that are added to the list
41
+ # of ancestors of the singleton class. Those are all the
42
+ # modules that were `extend`ed due to the `include` call.
43
+ #
44
+ # We record those modules on our lookup table keyed by
45
+ # the included module with the values being all the modules
46
+ # that that module pulls into the singleton class.
47
+ #
48
+ # We need to reverse the order, since the extend order should
49
+ # be the inverse of the ancestor order. That is, earlier
50
+ # extended modules would be later in the ancestor chain.
51
+ mixins_from_modules[mod] = (after - before).reverse!
52
+ end
53
+ rescue Exception # rubocop:disable Lint/RescueException
54
+ # this is a best effort, bail if we can't perform this
55
+ end
56
+
57
+ define_singleton_method(:class_attribute) do |*attrs, **kwargs|
58
+ class_attribute_readers.concat(attrs)
59
+ class_attribute_writers.concat(attrs)
60
+
61
+ instance_predicate = kwargs.fetch(:instance_predicate, true)
62
+ instance_accessor = kwargs.fetch(:instance_accessor, true)
63
+ instance_reader = kwargs.fetch(:instance_reader, instance_accessor)
64
+ instance_writer = kwargs.fetch(:instance_writer, instance_accessor)
65
+
66
+ if instance_reader
67
+ instance_attribute_readers.concat(attrs)
68
+ end
69
+
70
+ if instance_writer
71
+ instance_attribute_writers.concat(attrs)
72
+ end
73
+
74
+ if instance_predicate
75
+ class_attribute_predicates.concat(attrs)
76
+
77
+ if instance_reader
78
+ instance_attribute_predicates.concat(attrs)
79
+ end
80
+ end
81
+
82
+ super(*attrs, **kwargs) if defined?(super)
83
+ end
84
+
85
+ # rubocop:disable Style/MissingRespondToMissing
86
+ T::Sig::WithoutRuntime.sig { params(symbol: Symbol, args: T.untyped).returns(T.untyped) }
87
+ def method_missing(symbol, *args)
88
+ # We need this here so that we can handle any random instance
89
+ # method calls on the fake including class that may be done by
90
+ # the included module during the `self.included` hook.
91
+ end
92
+
93
+ class << self
94
+ extend T::Sig
95
+
96
+ T::Sig::WithoutRuntime.sig { params(symbol: Symbol, args: T.untyped).returns(T.untyped) }
97
+ def method_missing(symbol, *args)
98
+ # Similarly, we need this here so that we can handle any
99
+ # random class method calls on the fake including class
100
+ # that may be done by the included module during the
101
+ # `self.included` hook.
102
+ end
103
+ end
104
+ # rubocop:enable Style/MissingRespondToMissing
105
+ end.include(constant)
106
+
107
+ # The value that corresponds to the original included constant
108
+ # is the list of all dynamically extended modules because of that
109
+ # constant. We grab that value by deleting the key for the original
110
+ # constant.
111
+ @dynamic_extends = T.let(mixins_from_modules.delete(constant) || [], T::Array[Module])
112
+
113
+ # Since we deleted the original constant from the list of keys, all
114
+ # the keys that remain are the ones that are dynamically included modules
115
+ # during the include of the original constant.
116
+ @dynamic_includes = T.let(mixins_from_modules.keys, T::Array[Module])
117
+
118
+ @class_attribute_readers = T.let(class_attribute_readers, T::Array[Symbol])
119
+ @class_attribute_writers = T.let(class_attribute_writers, T::Array[Symbol])
120
+ @class_attribute_predicates = T.let(class_attribute_predicates, T::Array[Symbol])
121
+
122
+ @instance_attribute_readers = T.let(instance_attribute_readers, T::Array[Symbol])
123
+ @instance_attribute_writers = T.let(instance_attribute_writers, T::Array[Symbol])
124
+ @instance_attribute_predicates = T.let(instance_attribute_predicates, T::Array[Symbol])
125
+ end
126
+
127
+ sig { returns(T::Boolean) }
128
+ def empty_attributes?
129
+ @class_attribute_readers.empty? && @class_attribute_writers.empty?
130
+ end
131
+
132
+ sig { params(tree: RBI::Tree).void }
133
+ def compile_class_attributes(tree)
134
+ return if empty_attributes?
135
+
136
+ # Create a synthetic module to hold the generated class methods
137
+ tree << RBI::Module.new("GeneratedClassMethods") do |mod|
138
+ class_attribute_readers.each do |attribute|
139
+ mod << RBI::Method.new(attribute.to_s)
140
+ end
141
+
142
+ class_attribute_writers.each do |attribute|
143
+ mod << RBI::Method.new("#{attribute}=") do |method|
144
+ method << RBI::Param.new("value")
145
+ end
146
+ end
147
+
148
+ class_attribute_predicates.each do |attribute|
149
+ mod << RBI::Method.new("#{attribute}?")
150
+ end
151
+ end
152
+
153
+ # Create a synthetic module to hold the generated instance methods
154
+ tree << RBI::Module.new("GeneratedInstanceMethods") do |mod|
155
+ instance_attribute_readers.each do |attribute|
156
+ mod << RBI::Method.new(attribute.to_s)
157
+ end
158
+
159
+ instance_attribute_writers.each do |attribute|
160
+ mod << RBI::Method.new("#{attribute}=") do |method|
161
+ method << RBI::Param.new("value")
162
+ end
163
+ end
164
+
165
+ instance_attribute_predicates.each do |attribute|
166
+ mod << RBI::Method.new("#{attribute}?")
167
+ end
168
+ end
169
+
170
+ # Add a mixes_in_class_methods and include for the generated modules
171
+ tree << RBI::MixesInClassMethods.new("GeneratedClassMethods")
172
+ tree << RBI::Include.new("GeneratedInstanceMethods")
173
+ end
174
+
175
+ sig { params(tree: RBI::Tree).returns([T::Array[Module], T::Array[Module]]) }
176
+ def compile_mixes_in_class_methods(tree)
177
+ includes = dynamic_includes.select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
178
+ includes.each do |mod|
179
+ qname = qualified_name_of(mod)
180
+ tree << RBI::Include.new(T.must(qname))
181
+ end
182
+
183
+ # If we can generate multiple mixes_in_class_methods, then we want to use all dynamic extends that are not the
184
+ # constant itself
185
+ mixed_in_class_methods = dynamic_extends.select { |mod| mod != @constant }
186
+ return [[], []] if mixed_in_class_methods.empty?
187
+
188
+ mixed_in_class_methods.each do |mod|
189
+ qualified_name = qualified_name_of(mod)
190
+ next if qualified_name.nil? || qualified_name.empty?
191
+ tree << RBI::MixesInClassMethods.new(qualified_name)
192
+ end
193
+
194
+ [mixed_in_class_methods, includes]
195
+ rescue
196
+ [[], []] # silence errors
197
+ end
198
+ end
@@ -18,7 +18,6 @@ module Tapioca
18
18
  EXE_PATH_ENV_VAR = "TAPIOCA_SORBET_EXE"
19
19
 
20
20
  FEATURE_REQUIREMENTS = T.let({
21
- mixes_in_class_methods_multiple_args: Gem::Requirement.new("> 0.5.6200"),
22
21
  }.freeze, T::Hash[Symbol, Gem::Requirement])
23
22
 
24
23
  class << self
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "pathname"
@@ -10,36 +10,47 @@ module Tapioca
10
10
  extend(T::Sig)
11
11
  include(Reflection)
12
12
 
13
- IGNORED_SYMBOLS = ["YAML", "MiniTest", "Mutex"]
14
-
15
- attr_reader(:gem, :indent)
16
-
17
- sig { params(gem: Gemfile::GemSpec, indent: Integer).void }
18
- def initialize(gem, indent = 0)
13
+ IGNORED_SYMBOLS = T.let(["YAML", "MiniTest", "Mutex"], T::Array[String])
14
+ IGNORED_COMMENTS = T.let([
15
+ ":doc:",
16
+ ":nodoc:",
17
+ "typed:",
18
+ "frozen_string_literal:",
19
+ "encoding:",
20
+ "warn_indent:",
21
+ "shareable_constant_value:",
22
+ "rubocop:",
23
+ ], T::Array[String])
24
+
25
+ sig { returns(Gemfile::GemSpec) }
26
+ attr_reader :gem
27
+
28
+ sig { returns(Integer) }
29
+ attr_reader :indent
30
+
31
+ sig { params(gem: Gemfile::GemSpec, indent: Integer, include_doc: T::Boolean).void }
32
+ def initialize(gem, indent = 0, include_doc = false)
19
33
  @gem = gem
20
34
  @indent = indent
21
- @seen = Set.new
22
- @alias_namespace ||= Set.new
35
+ @seen = T.let(Set.new, T::Set[String])
36
+ @alias_namespace = T.let(Set.new, T::Set[String])
23
37
  @symbol_queue = T.let(symbols.sort.dup, T::Array[String])
24
- end
25
-
26
- sig { returns(String) }
27
- def generate
28
- rbi = RBI::Tree.new
38
+ @symbols = T.let(nil, T.nilable(T::Set[String]))
39
+ @include_doc = include_doc
29
40
 
30
- generate_from_symbol(rbi, T.must(@symbol_queue.shift)) until @symbol_queue.empty?
41
+ gem.parse_yard_docs if include_doc
42
+ end
31
43
 
32
- rbi.nest_singleton_methods!
33
- rbi.nest_non_public_methods!
34
- rbi.group_nodes!
35
- rbi.sort_nodes!
36
- rbi.string
44
+ sig { params(rbi: RBI::File).void }
45
+ def generate(rbi)
46
+ generate_from_symbol(rbi.root, T.must(@symbol_queue.shift)) until @symbol_queue.empty?
37
47
  end
38
48
 
39
49
  private
40
50
 
51
+ sig { params(name: T.nilable(String)).void }
41
52
  def add_to_symbol_queue(name)
42
- @symbol_queue << name unless symbols.include?(name) || symbol_ignored?(name)
53
+ @symbol_queue << name unless name.nil? || symbols.include?(name) || symbol_ignored?(name)
43
54
  end
44
55
 
45
56
  sig { returns(T::Set[String]) }
@@ -154,28 +165,33 @@ module Tapioca
154
165
  name_of(klass)
155
166
  end
156
167
 
168
+ comments = documentation_comments(name)
169
+
157
170
  if klass_name == "T::Private::Types::TypeAlias"
158
- tree << RBI::Const.new(name, "T.type_alias { #{T.unsafe(value).aliased_type} }")
171
+ constant = RBI::Const.new(name, "T.type_alias { #{T.unsafe(value).aliased_type} }", comments: comments)
172
+ tree << constant
159
173
  return
160
174
  end
161
175
 
162
176
  return if klass_name&.start_with?("T::Types::", "T::Private::")
163
177
 
164
178
  type_name = klass_name || "T.untyped"
179
+ constant = RBI::Const.new(name, "T.let(T.unsafe(nil), #{type_name})", comments: comments)
165
180
 
166
- tree << RBI::Const.new(name, "T.let(T.unsafe(nil), #{type_name})")
181
+ tree << constant
167
182
  end
168
183
 
169
184
  sig { params(tree: RBI::Tree, name: String, constant: Module).void }
170
185
  def compile_module(tree, name, constant)
171
186
  return unless defined_in_gem?(constant, strict: false)
172
187
 
188
+ comments = documentation_comments(name)
173
189
  scope =
174
190
  if constant.is_a?(Class)
175
191
  superclass = compile_superclass(constant)
176
- RBI::Class.new(name, superclass_name: superclass)
192
+ RBI::Class.new(name, superclass_name: superclass, comments: comments)
177
193
  else
178
- RBI::Module.new(name)
194
+ RBI::Module.new(name, comments: comments)
179
195
  end
180
196
 
181
197
  compile_body(scope, name, constant)
@@ -193,9 +209,22 @@ module Tapioca
193
209
  compile_methods(tree, name, constant)
194
210
  compile_module_helpers(tree, constant)
195
211
  compile_mixins(tree, constant)
196
- compile_mixes_in_class_methods(tree, constant)
197
212
  compile_props(tree, constant)
198
213
  compile_enums(tree, constant)
214
+ compile_dynamic_mixins(tree, constant)
215
+ end
216
+
217
+ sig { params(tree: RBI::Tree, constant: Module).void }
218
+ def compile_dynamic_mixins(tree, constant)
219
+ return if constant.is_a?(Class)
220
+
221
+ mixin_compiler = DynamicMixinCompiler.new(constant)
222
+ mixin_compiler.compile_class_attributes(tree)
223
+ dynamic_extends, dynamic_includes = mixin_compiler.compile_mixes_in_class_methods(tree)
224
+
225
+ (dynamic_includes + dynamic_extends).each do |mod|
226
+ add_to_symbol_queue(name_of(mod))
227
+ end
199
228
  end
200
229
 
201
230
  sig { params(tree: RBI::Tree, constant: Module).void }
@@ -391,130 +420,6 @@ module Tapioca
391
420
  end
392
421
  end
393
422
 
394
- sig { params(constant: Module).returns([T::Array[Module], T::Array[Module]]) }
395
- def collect_dynamic_mixins_of(constant)
396
- mixins_from_modules = {}.compare_by_identity
397
-
398
- Class.new do
399
- # Override the `self.include` method
400
- define_singleton_method(:include) do |mod|
401
- # Take a snapshot of the list of singleton class ancestors
402
- # before the actual include
403
- before = singleton_class.ancestors
404
- # Call the actual `include` method with the supplied module
405
- include_result = super(mod)
406
- # Take a snapshot of the list of singleton class ancestors
407
- # after the actual include
408
- after = singleton_class.ancestors
409
- # The difference is the modules that are added to the list
410
- # of ancestors of the singleton class. Those are all the
411
- # modules that were `extend`ed due to the `include` call.
412
- #
413
- # We record those modules on our lookup table keyed by
414
- # the included module with the values being all the modules
415
- # that that module pulls into the singleton class.
416
- #
417
- # We need to reverse the order, since the extend order should
418
- # be the inverse of the ancestor order. That is, earlier
419
- # extended modules would be later in the ancestor chain.
420
- mixins_from_modules[mod] = (after - before).reverse!
421
-
422
- include_result
423
- rescue Exception # rubocop:disable Lint/RescueException
424
- # this is a best effort, bail if we can't perform this
425
- end
426
-
427
- # rubocop:disable Style/MissingRespondToMissing
428
- def method_missing(symbol, *args)
429
- # We need this here so that we can handle any random instance
430
- # method calls on the fake including class that may be done by
431
- # the included module during the `self.included` hook.
432
- end
433
-
434
- class << self
435
- def method_missing(symbol, *args)
436
- # Similarly, we need this here so that we can handle any
437
- # random class method calls on the fake including class
438
- # that may be done by the included module during the
439
- # `self.included` hook.
440
- end
441
- end
442
- # rubocop:enable Style/MissingRespondToMissing
443
- end.include(constant)
444
-
445
- [
446
- # The value that corresponds to the original included constant
447
- # is the list of all dynamically extended modules because of that
448
- # constant. We grab that value by deleting the key for the original
449
- # constant.
450
- T.must(mixins_from_modules.delete(constant)),
451
- # Since we deleted the original constant from the list of keys, all
452
- # the keys that remain are the ones that are dynamically included modules
453
- # during the include of the original constant.
454
- mixins_from_modules.keys,
455
- ]
456
- end
457
-
458
- sig { params(constant: Module, dynamic_extends: T::Array[Module]).returns(T::Array[Module]) }
459
- def collect_mixed_in_class_methods(constant, dynamic_extends)
460
- if Tapioca::Compilers::Sorbet.supports?(:mixes_in_class_methods_multiple_args)
461
- # If we can generate multiple mixes_in_class_methods, then
462
- # we want to use all dynamic extends that are not the constant itself
463
- return dynamic_extends.select { |mod| mod != constant }
464
- end
465
-
466
- # For older Sorbet version, we do an explicit check for an AS::Concern
467
- # related ClassMethods module.
468
- ancestors = singleton_class_of(constant).ancestors
469
- extends_as_concern = ancestors.any? do |mod|
470
- qualified_name_of(mod) == "::ActiveSupport::Concern"
471
- end
472
- class_methods_module = resolve_constant("#{name_of(constant)}::ClassMethods")
473
-
474
- mixed_in_module = if extends_as_concern && Module === class_methods_module
475
- # If this module is a concern and the ClassMethods module exists
476
- # then, we prefer to generate a mixes_in_class_methods call for
477
- # that module only, since we only have a single shot.
478
- class_methods_module
479
- else
480
- # Otherwise, we use the first dynamic extend module that is not
481
- # the constant itself. We don't have a better heuristic in the
482
- # absence of being able to supply multiple arguments.
483
- dynamic_extends.find { |mod| mod != constant }
484
- end
485
-
486
- Array(mixed_in_module)
487
- end
488
-
489
- sig { params(tree: RBI::Tree, constant: Module).void }
490
- def compile_mixes_in_class_methods(tree, constant)
491
- return if constant.is_a?(Class)
492
-
493
- dynamic_extends, dynamic_includes = collect_dynamic_mixins_of(constant)
494
-
495
- dynamic_includes
496
- .select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
497
- .map do |mod|
498
- add_to_symbol_queue(name_of(mod))
499
-
500
- qname = qualified_name_of(mod)
501
- tree << RBI::Include.new(T.must(qname))
502
- end
503
-
504
- mixed_in_class_methods = collect_mixed_in_class_methods(constant, dynamic_extends)
505
- return if mixed_in_class_methods.empty?
506
-
507
- mixed_in_class_methods.each do |mod|
508
- add_to_symbol_queue(name_of(mod))
509
-
510
- qualified_name = qualified_name_of(mod)
511
- next if qualified_name.nil? || qualified_name.empty?
512
- tree << RBI::MixesInClassMethods.new(qualified_name)
513
- end
514
- rescue
515
- nil # silence errors
516
- end
517
-
518
423
  sig { params(tree: RBI::Tree, name: String, constant: Module).void }
519
424
  def compile_methods(tree, name, constant)
520
425
  compile_method(
@@ -630,7 +535,15 @@ module Tapioca
630
535
  [type, name]
631
536
  end
632
537
 
633
- rbi_method = RBI::Method.new(method_name, is_singleton: constant.singleton_class?, visibility: visibility)
538
+ separator = constant.singleton_class? ? "." : "#"
539
+ comments = documentation_comments("#{symbol_name}#{separator}#{method_name}")
540
+ rbi_method = RBI::Method.new(
541
+ method_name,
542
+ is_singleton: constant.singleton_class?,
543
+ visibility: visibility,
544
+ comments: comments
545
+ )
546
+
634
547
  rbi_method.sigs << compile_signature(signature, sanitized_parameters) if signature
635
548
 
636
549
  sanitized_parameters.each do |type, name|
@@ -710,8 +623,10 @@ module Tapioca
710
623
  SymbolLoader.ignore_symbol?(symbol_name)
711
624
  end
712
625
 
713
- SPECIAL_METHOD_NAMES = ["!", "~", "+@", "**", "-@", "*", "/", "%", "+", "-", "<<", ">>", "&", "|", "^", "<",
714
- "<=", "=>", ">", ">=", "==", "===", "!=", "=~", "!~", "<=>", "[]", "[]=", "`"]
626
+ SPECIAL_METHOD_NAMES = T.let([
627
+ "!", "~", "+@", "**", "-@", "*", "/", "%", "+", "-", "<<", ">>", "&", "|", "^",
628
+ "<", "<=", "=>", ">", ">=", "==", "===", "!=", "=~", "!~", "<=>", "[]", "[]=", "`"
629
+ ], T::Array[String])
715
630
 
716
631
  sig { params(name: String).returns(T::Boolean) }
717
632
  def valid_method_name?(name)
@@ -770,6 +685,7 @@ module Tapioca
770
685
  @seen.include?(name)
771
686
  end
772
687
 
688
+ sig { params(constant: Module).returns(T.nilable(UnboundMethod)) }
773
689
  def initialize_method_for(constant)
774
690
  constant.instance_method(:initialize)
775
691
  rescue
@@ -840,6 +756,21 @@ module Tapioca
840
756
 
841
757
  name_of(target)
842
758
  end
759
+
760
+ sig { params(name: String).returns(T::Array[RBI::Comment]) }
761
+ def documentation_comments(name)
762
+ return [] unless @include_doc
763
+
764
+ yard_docs = YARD::Registry.at(name)
765
+ return [] unless yard_docs
766
+
767
+ docstring = yard_docs.docstring
768
+ return [] if /(copyright|license)/i.match?(docstring)
769
+
770
+ docstring.lines
771
+ .reject { |line| IGNORED_COMMENTS.any? { |comment| line.include?(comment) } }
772
+ .map! { |line| RBI::Comment.new(line) }
773
+ end
843
774
  end
844
775
  end
845
776
  end
@@ -6,17 +6,11 @@ module Tapioca
6
6
  class SymbolTableCompiler
7
7
  extend(T::Sig)
8
8
 
9
- sig do
10
- params(
11
- gem: Gemfile::GemSpec,
12
- indent: Integer
13
- ).returns(String)
14
- end
15
- def compile(
16
- gem,
17
- indent = 0
18
- )
19
- Tapioca::Compilers::SymbolTable::SymbolGenerator.new(gem, indent).generate
9
+ sig { params(gem: Gemfile::GemSpec, rbi: RBI::File, indent: Integer, include_docs: T::Boolean).void }
10
+ def compile(gem, rbi, indent = 0, include_docs = false)
11
+ Tapioca::Compilers::SymbolTable::SymbolGenerator
12
+ .new(gem, indent, include_docs)
13
+ .generate(rbi)
20
14
  end
21
15
  end
22
16
  end
@@ -14,6 +14,7 @@ module Tapioca
14
14
  const(:todos_path, String)
15
15
  const(:generators, T::Array[String])
16
16
  const(:file_header, T::Boolean, default: true)
17
+ const(:doc, T::Boolean, default: false)
17
18
 
18
19
  sig { returns(Pathname) }
19
20
  def outpath
@@ -67,6 +67,7 @@ module Tapioca
67
67
  "todos_path" => Config::DEFAULT_TODOSPATH,
68
68
  "generators" => [],
69
69
  "file_header" => true,
70
+ "doc" => false,
70
71
  }.freeze, T::Hash[String, T.untyped])
71
72
  end
72
73
  end
@@ -2,6 +2,8 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "bundler"
5
+ require "logger"
6
+ require "yard-sorbet"
5
7
 
6
8
  module Tapioca
7
9
  class Gemfile
@@ -9,10 +11,7 @@ module Tapioca
9
11
 
10
12
  Spec = T.type_alias do
11
13
  T.any(
12
- T.all(
13
- ::Bundler::StubSpecification,
14
- ::Bundler::RemoteSpecification
15
- ),
14
+ ::Bundler::StubSpecification,
16
15
  ::Gem::Specification
17
16
  )
18
17
  end
@@ -116,7 +115,9 @@ module Tapioca
116
115
  sig { returns(T::Array[Pathname]) }
117
116
  def files
118
117
  if default_gem?
119
- @spec.files.map do |file|
118
+ # `Bundler::RemoteSpecification` delegates missing methods to
119
+ # `Gem::Specification`, so `files` actually always exists on spec.
120
+ T.unsafe(@spec).files.map do |file|
120
121
  ruby_lib_dir.join(file)
121
122
  end
122
123
  else
@@ -145,6 +146,11 @@ module Tapioca
145
146
  end
146
147
  end
147
148
 
149
+ sig { void }
150
+ def parse_yard_docs
151
+ files.each { |path| YARD.parse(path.to_s, [], Logger::Severity::FATAL) }
152
+ end
153
+
148
154
  private
149
155
 
150
156
  sig { returns(T::Boolean) }
@@ -0,0 +1,61 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # TODO: Remove me when logging logic has been abstracted.
5
+ require "thor"
6
+
7
+ module Tapioca
8
+ module Generators
9
+ class Base
10
+ extend T::Sig
11
+ extend T::Helpers
12
+
13
+ class FileWriter < Thor
14
+ include Thor::Actions
15
+ end
16
+
17
+ # TODO: Remove me when logging logic has been abstracted
18
+ include Thor::Base
19
+
20
+ abstract!
21
+
22
+ sig { params(default_command: String, file_writer: Thor::Actions).void }
23
+ def initialize(default_command:, file_writer: FileWriter.new)
24
+ @file_writer = file_writer
25
+ @default_command = default_command
26
+ end
27
+
28
+ sig { abstract.void }
29
+ def generate; end
30
+
31
+ private
32
+
33
+ # TODO: Remove me when logging logic has been abstracted
34
+ sig { params(message: String, color: T.any(Symbol, T::Array[Symbol])).void }
35
+ def say_error(message = "", *color)
36
+ force_new_line = (message.to_s !~ /( |\t)\Z/)
37
+ # NOTE: This is a hack. We're no longer subclassing from Thor::Shell::Color
38
+ # so we no longer have access to the prepare_message call.
39
+ # We should update this to remove this.
40
+ buffer = shell.send(:prepare_message, *T.unsafe([message, *T.unsafe(color)]))
41
+ buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
42
+
43
+ $stderr.print(buffer)
44
+ $stderr.flush
45
+ end
46
+
47
+ sig do
48
+ params(
49
+ path: T.any(String, Pathname),
50
+ content: String,
51
+ force: T::Boolean,
52
+ skip: T::Boolean,
53
+ verbose: T::Boolean
54
+ ).void
55
+ end
56
+ def create_file(path, content, force: true, skip: false, verbose: true)
57
+ @file_writer.create_file(path, force: force, skip: skip, verbose: verbose) { content }
58
+ end
59
+ end
60
+ end
61
+ end