tapioca 0.5.1 → 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/lib/tapioca/cli.rb +54 -139
  4. data/lib/tapioca/compilers/dsl/active_model_secure_password.rb +101 -0
  5. data/lib/tapioca/compilers/dsl/active_record_enum.rb +1 -1
  6. data/lib/tapioca/compilers/dsl/active_record_fixtures.rb +86 -0
  7. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +41 -33
  8. data/lib/tapioca/compilers/dsl/active_support_concern.rb +0 -2
  9. data/lib/tapioca/compilers/dsl/base.rb +12 -0
  10. data/lib/tapioca/compilers/dsl/identity_cache.rb +0 -1
  11. data/lib/tapioca/compilers/dsl/mixed_in_class_attributes.rb +74 -0
  12. data/lib/tapioca/compilers/dsl/smart_properties.rb +4 -4
  13. data/lib/tapioca/compilers/dsl_compiler.rb +7 -6
  14. data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +198 -0
  15. data/lib/tapioca/compilers/sorbet.rb +0 -1
  16. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +84 -153
  17. data/lib/tapioca/compilers/symbol_table_compiler.rb +5 -11
  18. data/lib/tapioca/config.rb +1 -0
  19. data/lib/tapioca/config_builder.rb +1 -0
  20. data/lib/tapioca/constant_locator.rb +7 -1
  21. data/lib/tapioca/gemfile.rb +11 -5
  22. data/lib/tapioca/generators/base.rb +61 -0
  23. data/lib/tapioca/generators/dsl.rb +362 -0
  24. data/lib/tapioca/generators/gem.rb +345 -0
  25. data/lib/tapioca/generators/init.rb +79 -0
  26. data/lib/tapioca/generators/require.rb +52 -0
  27. data/lib/tapioca/generators/todo.rb +76 -0
  28. data/lib/tapioca/generators.rb +9 -0
  29. data/lib/tapioca/internal.rb +1 -2
  30. data/lib/tapioca/loader.rb +2 -2
  31. data/lib/tapioca/rbi_ext/model.rb +44 -0
  32. data/lib/tapioca/reflection.rb +8 -1
  33. data/lib/tapioca/version.rb +1 -1
  34. data/lib/tapioca.rb +2 -0
  35. metadata +34 -12
  36. data/lib/tapioca/generator.rb +0 -717
@@ -0,0 +1,74 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "active_support/core_ext/class/attribute"
6
+ rescue LoadError
7
+ return
8
+ end
9
+
10
+ module Tapioca
11
+ module Compilers
12
+ module Dsl
13
+ # `Tapioca::Compilers::Dsl::MixedInClassAttributes` generates RBI files for modules that dynamically use
14
+ # `class_attribute` on classes.
15
+ #
16
+ # For example, given the following concern
17
+ #
18
+ # ~~~rb
19
+ # module Taggeable
20
+ # extend ActiveSupport::Concern
21
+ #
22
+ # included do
23
+ # class_attribute :tag
24
+ # end
25
+ # end
26
+ # ~~~
27
+ #
28
+ # this generator will produce the RBI file `taggeable.rbi` with the following content:
29
+ #
30
+ # ~~~rbi
31
+ # # typed: strong
32
+ #
33
+ # module Taggeable
34
+ # include GeneratedInstanceMethods
35
+ #
36
+ # mixes_in_class_methods GeneratedClassMethods
37
+ #
38
+ # module GeneratedClassMethods
39
+ # def tag; end
40
+ # def tag=(value); end
41
+ # def tag?; end
42
+ # end
43
+ #
44
+ # module GeneratedInstanceMethods
45
+ # def tag; end
46
+ # def tag=(value); end
47
+ # def tag?; end
48
+ # end
49
+ # end
50
+ # ~~~
51
+ class MixedInClassAttributes < Base
52
+ extend T::Sig
53
+
54
+ sig { override.params(root: RBI::Tree, constant: Module).void }
55
+ def decorate(root, constant)
56
+ mixin_compiler = DynamicMixinCompiler.new(constant)
57
+ return if mixin_compiler.empty_attributes?
58
+
59
+ root.create_path(constant) do |mod|
60
+ mixin_compiler.compile_class_attributes(mod)
61
+ end
62
+ end
63
+
64
+ sig { override.returns(T::Enumerable[Module]) }
65
+ def gather_constants
66
+ # Select all non-anonymous modules that have overridden Module.included
67
+ all_modules.select do |mod|
68
+ !mod.is_a?(Class) && name_of(mod) && Tapioca::Reflection.method_of(mod, :included).owner != Module
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -84,10 +84,10 @@ module Tapioca
84
84
 
85
85
  sig { override.returns(T::Enumerable[Module]) }
86
86
  def gather_constants
87
- all_classes.select do |c|
88
- c < ::SmartProperties
89
- end.reject do |c|
90
- name_of(c).nil? || c == ::SmartProperties::Validations::Ancestor
87
+ all_modules.select do |c|
88
+ name_of(c) &&
89
+ c != ::SmartProperties::Validations::Ancestor &&
90
+ c < ::SmartProperties && ::SmartProperties::ClassMethods === c
91
91
  end
92
92
  end
93
93
 
@@ -34,7 +34,7 @@ module Tapioca
34
34
  @error_handler = T.let(error_handler || $stderr.method(:puts), T.proc.params(error: String).void)
35
35
  end
36
36
 
37
- sig { params(blk: T.proc.params(constant: Module, rbi: String).void).void }
37
+ sig { params(blk: T.proc.params(constant: Module, rbi: RBI::File).void).void }
38
38
  def run(&blk)
39
39
  constants_to_process = gather_constants(requested_constants)
40
40
 
@@ -51,6 +51,10 @@ module Tapioca
51
51
 
52
52
  blk.call(constant, rbi)
53
53
  end
54
+
55
+ generators.flat_map(&:errors).each do |msg|
56
+ report_error(msg)
57
+ end
54
58
  end
55
59
 
56
60
  private
@@ -77,7 +81,7 @@ module Tapioca
77
81
  constants
78
82
  end
79
83
 
80
- sig { params(constant: Module).returns(T.nilable(String)) }
84
+ sig { params(constant: Module).returns(T.nilable(RBI::File)) }
81
85
  def rbi_for_constant(constant)
82
86
  file = RBI::File.new(strictness: "true")
83
87
 
@@ -88,10 +92,7 @@ module Tapioca
88
92
 
89
93
  return if file.root.empty?
90
94
 
91
- file.root.nest_non_public_methods!
92
- file.root.group_nodes!
93
- file.root.sort_nodes!
94
- file.string
95
+ file
95
96
  end
96
97
 
97
98
  sig { params(error: String).returns(T.noreturn) }
@@ -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