tapioca 0.6.4 → 0.7.2

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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -2
  3. data/README.md +27 -15
  4. data/Rakefile +10 -14
  5. data/lib/tapioca/cli.rb +65 -80
  6. data/lib/tapioca/{generators/base.rb → commands/command.rb} +16 -9
  7. data/lib/tapioca/{generators → commands}/dsl.rb +59 -45
  8. data/lib/tapioca/{generators → commands}/gem.rb +93 -30
  9. data/lib/tapioca/{generators → commands}/init.rb +9 -13
  10. data/lib/tapioca/{generators → commands}/require.rb +8 -10
  11. data/lib/tapioca/commands/todo.rb +86 -0
  12. data/lib/tapioca/commands.rb +13 -0
  13. data/lib/tapioca/dsl/compiler.rb +185 -0
  14. data/lib/tapioca/{compilers/dsl → dsl/compilers}/aasm.rb +12 -9
  15. data/lib/tapioca/{compilers/dsl → dsl/compilers}/action_controller_helpers.rb +13 -20
  16. data/lib/tapioca/{compilers/dsl → dsl/compilers}/action_mailer.rb +10 -8
  17. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_job.rb +11 -9
  18. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_model_attributes.rb +13 -11
  19. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_model_secure_password.rb +10 -12
  20. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_associations.rb +28 -34
  21. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_columns.rb +18 -16
  22. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_enum.rb +14 -12
  23. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_fixtures.rb +12 -8
  24. data/lib/tapioca/dsl/compilers/active_record_relations.rb +712 -0
  25. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_scope.rb +21 -20
  26. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_typed_store.rb +11 -16
  27. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_resource.rb +10 -8
  28. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_storage.rb +14 -10
  29. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_support_concern.rb +19 -14
  30. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_support_current_attributes.rb +16 -21
  31. data/lib/tapioca/{compilers/dsl → dsl/compilers}/config.rb +11 -9
  32. data/lib/tapioca/{compilers/dsl → dsl/compilers}/frozen_record.rb +13 -11
  33. data/lib/tapioca/{compilers/dsl → dsl/compilers}/identity_cache.rb +23 -22
  34. data/lib/tapioca/{compilers/dsl → dsl/compilers}/mixed_in_class_attributes.rb +12 -10
  35. data/lib/tapioca/{compilers/dsl → dsl/compilers}/protobuf.rb +22 -10
  36. data/lib/tapioca/{compilers/dsl → dsl/compilers}/rails_generators.rb +12 -13
  37. data/lib/tapioca/{compilers/dsl → dsl/compilers}/sidekiq_worker.rb +14 -13
  38. data/lib/tapioca/{compilers/dsl → dsl/compilers}/smart_properties.rb +11 -9
  39. data/lib/tapioca/{compilers/dsl → dsl/compilers}/state_machines.rb +12 -10
  40. data/lib/tapioca/{compilers/dsl → dsl/compilers}/url_helpers.rb +20 -15
  41. data/lib/tapioca/dsl/compilers.rb +31 -0
  42. data/lib/tapioca/{compilers/dsl → dsl}/extensions/frozen_record.rb +2 -2
  43. data/lib/tapioca/dsl/helpers/active_record_column_type_helper.rb +114 -0
  44. data/lib/tapioca/dsl/helpers/active_record_constants_helper.rb +29 -0
  45. data/lib/tapioca/{compilers/dsl → dsl/helpers}/param_helper.rb +6 -3
  46. data/lib/tapioca/dsl/pipeline.rb +169 -0
  47. data/lib/tapioca/gem/events.rb +120 -0
  48. data/lib/tapioca/gem/listeners/base.rb +48 -0
  49. data/lib/tapioca/gem/listeners/dynamic_mixins.rb +32 -0
  50. data/lib/tapioca/gem/listeners/methods.rb +183 -0
  51. data/lib/tapioca/gem/listeners/mixins.rb +101 -0
  52. data/lib/tapioca/gem/listeners/remove_empty_payload_scopes.rb +21 -0
  53. data/lib/tapioca/gem/listeners/sorbet_enums.rb +26 -0
  54. data/lib/tapioca/gem/listeners/sorbet_helpers.rb +29 -0
  55. data/lib/tapioca/gem/listeners/sorbet_props.rb +33 -0
  56. data/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb +23 -0
  57. data/lib/tapioca/gem/listeners/sorbet_signatures.rb +79 -0
  58. data/lib/tapioca/gem/listeners/sorbet_type_variables.rb +51 -0
  59. data/lib/tapioca/gem/listeners/subconstants.rb +37 -0
  60. data/lib/tapioca/gem/listeners/yard_doc.rb +96 -0
  61. data/lib/tapioca/gem/listeners.rb +16 -0
  62. data/lib/tapioca/gem/pipeline.rb +365 -0
  63. data/lib/tapioca/helpers/cli_helper.rb +7 -0
  64. data/lib/tapioca/helpers/config_helper.rb +5 -8
  65. data/lib/tapioca/helpers/shims_helper.rb +87 -0
  66. data/lib/tapioca/helpers/signatures_helper.rb +17 -0
  67. data/lib/tapioca/helpers/sorbet_helper.rb +57 -0
  68. data/lib/tapioca/helpers/test/dsl_compiler.rb +118 -0
  69. data/lib/tapioca/helpers/test/isolation.rb +1 -1
  70. data/lib/tapioca/helpers/test/template.rb +13 -2
  71. data/lib/tapioca/helpers/type_variable_helper.rb +43 -0
  72. data/lib/tapioca/internal.rb +18 -10
  73. data/lib/tapioca/rbi_ext/model.rb +14 -50
  74. data/lib/tapioca/rbi_formatter.rb +37 -0
  75. data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +227 -0
  76. data/lib/tapioca/runtime/generic_type_registry.rb +168 -0
  77. data/lib/tapioca/runtime/loader.rb +123 -0
  78. data/lib/tapioca/runtime/reflection.rb +157 -0
  79. data/lib/tapioca/runtime/trackers/autoload.rb +72 -0
  80. data/lib/tapioca/runtime/trackers/constant_definition.rb +44 -0
  81. data/lib/tapioca/runtime/trackers/mixin.rb +80 -0
  82. data/lib/tapioca/runtime/trackers/required_ancestor.rb +50 -0
  83. data/lib/tapioca/{trackers.rb → runtime/trackers.rb} +4 -3
  84. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +69 -34
  85. data/lib/tapioca/sorbet_ext/name_patch.rb +7 -1
  86. data/lib/tapioca/{compilers → static}/requires_compiler.rb +2 -2
  87. data/lib/tapioca/static/symbol_loader.rb +83 -0
  88. data/lib/tapioca/static/symbol_table_parser.rb +63 -0
  89. data/lib/tapioca/version.rb +1 -1
  90. data/lib/tapioca.rb +2 -7
  91. metadata +83 -62
  92. data/lib/tapioca/compilers/dsl/active_record_relations.rb +0 -720
  93. data/lib/tapioca/compilers/dsl/base.rb +0 -195
  94. data/lib/tapioca/compilers/dsl/helper/active_record_constants.rb +0 -27
  95. data/lib/tapioca/compilers/dsl_compiler.rb +0 -134
  96. data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +0 -223
  97. data/lib/tapioca/compilers/sorbet.rb +0 -59
  98. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +0 -780
  99. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +0 -90
  100. data/lib/tapioca/compilers/symbol_table_compiler.rb +0 -17
  101. data/lib/tapioca/compilers/todos_compiler.rb +0 -32
  102. data/lib/tapioca/generators/todo.rb +0 -76
  103. data/lib/tapioca/generators.rb +0 -9
  104. data/lib/tapioca/generic_type_registry.rb +0 -164
  105. data/lib/tapioca/helpers/active_record_column_type_helper.rb +0 -108
  106. data/lib/tapioca/loader.rb +0 -119
  107. data/lib/tapioca/reflection.rb +0 -151
  108. data/lib/tapioca/trackers/autoload.rb +0 -70
  109. data/lib/tapioca/trackers/constant_definition.rb +0 -42
  110. data/lib/tapioca/trackers/mixin.rb +0 -78
@@ -7,15 +7,18 @@ module Tapioca
7
7
  module Helpers
8
8
  module Test
9
9
  module Template
10
- include Kernel
11
10
  extend T::Sig
11
+ extend T::Helpers
12
+
13
+ requires_ancestor { Kernel }
14
+
12
15
  ERB_SUPPORTS_KVARGS = T.let(
13
16
  ::ERB.instance_method(:initialize).parameters.assoc(:key), T.nilable([Symbol, Symbol])
14
17
  )
15
18
 
16
19
  sig { params(selector: String).returns(T::Boolean) }
17
20
  def ruby_version(selector)
18
- Gem::Requirement.new(selector).satisfied_by?(Gem::Version.new(RUBY_VERSION))
21
+ ::Gem::Requirement.new(selector).satisfied_by?(::Gem::Version.new(RUBY_VERSION))
19
22
  end
20
23
 
21
24
  sig { params(src: String).returns(String) }
@@ -28,6 +31,14 @@ module Tapioca
28
31
 
29
32
  erb.result(binding)
30
33
  end
34
+
35
+ sig { params(str: String, indent: Integer).returns(String) }
36
+ def indented(str, indent)
37
+ str.lines.map! do |line|
38
+ next line if line.chomp.empty?
39
+ (" " * indent) + line
40
+ end.join
41
+ end
31
42
  end
32
43
  end
33
44
  end
@@ -0,0 +1,43 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module TypeVariableHelper
6
+ extend T::Sig
7
+ extend SorbetHelper
8
+
9
+ sig do
10
+ params(
11
+ type: String,
12
+ variance: Symbol,
13
+ fixed: T.nilable(String),
14
+ upper: T.nilable(String),
15
+ lower: T.nilable(String)
16
+ ).returns(String)
17
+ end
18
+ def self.serialize_type_variable(type, variance, fixed, upper, lower)
19
+ variance = nil if variance == :invariant
20
+
21
+ bounds = []
22
+ bounds << "fixed: #{fixed}" if fixed
23
+ bounds << "lower: #{lower}" if lower
24
+ bounds << "upper: #{upper}" if upper
25
+
26
+ parameters = []
27
+ block = []
28
+
29
+ parameters << ":#{variance}" if variance
30
+
31
+ if sorbet_supports?(:type_variable_block_syntax)
32
+ block = bounds
33
+ else
34
+ parameters.concat(bounds)
35
+ end
36
+
37
+ serialized = type.dup
38
+ serialized << "(#{parameters.join(", ")})" unless parameters.empty?
39
+ serialized << " { { #{block.join(", ")} } }" unless block.empty?
40
+ serialized
41
+ end
42
+ end
43
+ end
@@ -2,20 +2,28 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "tapioca"
5
- require "tapioca/loader"
5
+ require "tapioca/runtime/reflection"
6
+ require "tapioca/runtime/trackers"
7
+ require "tapioca/runtime/dynamic_mixin_compiler"
8
+ require "tapioca/runtime/loader"
9
+ require "tapioca/helpers/sorbet_helper"
10
+ require "tapioca/helpers/type_variable_helper"
6
11
  require "tapioca/sorbet_ext/generic_name_patch"
7
12
  require "tapioca/sorbet_ext/fixed_hash_patch"
8
- require "tapioca/generic_type_registry"
13
+ require "tapioca/runtime/generic_type_registry"
9
14
  require "tapioca/helpers/cli_helper"
10
15
  require "tapioca/helpers/config_helper"
11
- require "tapioca/generators"
16
+ require "tapioca/helpers/signatures_helper"
17
+ require "tapioca/helpers/shims_helper"
18
+ require "tapioca/commands"
12
19
  require "tapioca/cli"
13
20
  require "tapioca/gemfile"
14
21
  require "tapioca/executor"
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"
22
+ require "tapioca/static/symbol_table_parser"
23
+ require "tapioca/static/symbol_loader"
24
+ require "tapioca/gem/events"
25
+ require "tapioca/gem/listeners"
26
+ require "tapioca/gem/pipeline"
27
+ require "tapioca/dsl/compiler"
28
+ require "tapioca/dsl/pipeline"
29
+ require "tapioca/static/requires_compiler"
@@ -1,59 +1,13 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "rbi"
5
-
6
4
  module RBI
7
- class File
8
- extend T::Sig
9
-
10
- sig { returns(String) }
11
- def transformed_string
12
- transform_rbi!
13
- string
14
- end
15
-
16
- sig { void }
17
- def transform_rbi!
18
- root.nest_singleton_methods!
19
- root.nest_non_public_methods!
20
- root.group_nodes!
21
- root.sort_nodes!
22
- end
23
-
24
- sig do
25
- params(
26
- command: String,
27
- reason: T.nilable(String),
28
- display_heading: T::Boolean
29
- ).void
30
- end
31
- def set_file_header(command, reason: nil, display_heading: true)
32
- return unless display_heading
33
- comments << RBI::Comment.new("DO NOT EDIT MANUALLY")
34
- comments << RBI::Comment.new("This is an autogenerated file for #{reason}.") unless reason.nil?
35
- comments << RBI::Comment.new("Please instead update this file by running `#{command}`.")
36
- end
37
-
38
- sig { void }
39
- def set_empty_body_content
40
- comments << RBI::BlankLine.new unless comments.empty?
41
- comments << RBI::Comment.new("THIS IS AN EMPTY RBI FILE.")
42
- comments << RBI::Comment.new("see https://github.com/Shopify/tapioca/wiki/Manual-Gem-Requires")
43
- end
44
-
45
- sig { returns(T::Boolean) }
46
- def empty?
47
- root.empty?
48
- end
49
- end
50
-
51
5
  class Tree
52
6
  extend T::Sig
53
7
 
54
8
  sig { params(constant: ::Module, block: T.nilable(T.proc.params(scope: Scope).void)).void }
55
9
  def create_path(constant, &block)
56
- constant_name = Tapioca::Reflection.name_of(constant)
10
+ constant_name = Tapioca::Runtime::Reflection.name_of(constant)
57
11
  raise "given constant does not have a name" unless constant_name
58
12
 
59
13
  instance = ::Module.const_get(constant_name)
@@ -107,8 +61,18 @@ module RBI
107
61
  create_node(RBI::MixesInClassMethods.new(name))
108
62
  end
109
63
 
110
- sig { params(name: String, value: String).void }
111
- def create_type_member(name, value: "type_member")
64
+ sig do
65
+ params(
66
+ name: String,
67
+ type: String,
68
+ variance: Symbol,
69
+ fixed: T.nilable(String),
70
+ upper: T.nilable(String),
71
+ lower: T.nilable(String)
72
+ ).void
73
+ end
74
+ def create_type_variable(name, type:, variance: :invariant, fixed: nil, upper: nil, lower: nil)
75
+ value = Tapioca::TypeVariableHelper.serialize_type_variable(type, variance, fixed, upper, lower)
112
76
  create_node(RBI::TypeMember.new(name, value))
113
77
  end
114
78
 
@@ -137,7 +101,7 @@ module RBI
137
101
 
138
102
  SPECIAL_METHOD_NAMES = T.let(
139
103
  ["!", "~", "+@", "**", "-@", "*", "/", "%", "+", "-", "<<", ">>", "&", "|", "^", "<", "<=", "=>", ">", ">=",
140
- "==", "===", "!=", "=~", "!~", "<=>", "[]", "[]=", "`"].freeze,
104
+ "==", "===", "!=", "=~", "!~", "<=>", "[]", "[]=", "`",].freeze,
141
105
  T::Array[String]
142
106
  )
143
107
 
@@ -0,0 +1,37 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ class RBIFormatter < RBI::Formatter
6
+ extend T::Sig
7
+
8
+ sig do
9
+ params(
10
+ file: RBI::File,
11
+ command: String,
12
+ reason: T.nilable(String)
13
+ ).void
14
+ end
15
+ def write_header!(file, command, reason: nil)
16
+ file.comments << RBI::Comment.new("DO NOT EDIT MANUALLY")
17
+ file.comments << RBI::Comment.new("This is an autogenerated file for #{reason}.") unless reason.nil?
18
+ file.comments << RBI::Comment.new("Please instead update this file by running `#{command}`.")
19
+ end
20
+
21
+ sig { params(file: RBI::File).void }
22
+ def write_empty_body_comment!(file)
23
+ file.comments << RBI::BlankLine.new unless file.comments.empty?
24
+ file.comments << RBI::Comment.new("THIS IS AN EMPTY RBI FILE.")
25
+ file.comments << RBI::Comment.new("see https://github.com/Shopify/tapioca/wiki/Manual-Gem-Requires")
26
+ end
27
+ end
28
+
29
+ DEFAULT_RBI_FORMATTER = T.let(RBIFormatter.new(
30
+ add_sig_templates: false,
31
+ group_nodes: true,
32
+ max_line_length: nil,
33
+ nest_singleton_methods: true,
34
+ nest_non_public_methods: true,
35
+ sort_nodes: true
36
+ ), RBIFormatter)
37
+ end
@@ -0,0 +1,227 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Runtime
6
+ class DynamicMixinCompiler
7
+ extend T::Sig
8
+ include Runtime::Reflection
9
+
10
+ sig { returns(T::Array[Module]) }
11
+ attr_reader :dynamic_extends, :dynamic_includes
12
+
13
+ sig { returns(T::Array[Symbol]) }
14
+ attr_reader :class_attribute_readers, :class_attribute_writers, :class_attribute_predicates
15
+
16
+ sig { returns(T::Array[Symbol]) }
17
+ attr_reader :instance_attribute_readers, :instance_attribute_writers, :instance_attribute_predicates
18
+
19
+ sig { params(constant: Module).void }
20
+ def initialize(constant)
21
+ @constant = constant
22
+ mixins_from_modules = {}.compare_by_identity
23
+ class_attribute_readers = T.let([], T::Array[Symbol])
24
+ class_attribute_writers = T.let([], T::Array[Symbol])
25
+ class_attribute_predicates = T.let([], T::Array[Symbol])
26
+
27
+ instance_attribute_readers = T.let([], T::Array[Symbol])
28
+ instance_attribute_writers = T.let([], T::Array[Symbol])
29
+ instance_attribute_predicates = T.let([], T::Array[Symbol])
30
+
31
+ Class.new do
32
+ # Override the `self.include` method
33
+ define_singleton_method(:include) do |mod|
34
+ # Take a snapshot of the list of singleton class ancestors
35
+ # before the actual include
36
+ before = singleton_class.ancestors
37
+ # Call the actual `include` method with the supplied module
38
+ super(mod).tap do
39
+ # Take a snapshot of the list of singleton class ancestors
40
+ # after the actual include
41
+ after = singleton_class.ancestors
42
+ # The difference is the modules that are added to the list
43
+ # of ancestors of the singleton class. Those are all the
44
+ # modules that were `extend`ed due to the `include` call.
45
+ #
46
+ # We record those modules on our lookup table keyed by
47
+ # the included module with the values being all the modules
48
+ # that that module pulls into the singleton class.
49
+ #
50
+ # We need to reverse the order, since the extend order should
51
+ # be the inverse of the ancestor order. That is, earlier
52
+ # extended modules would be later in the ancestor chain.
53
+ mixins_from_modules[mod] = (after - before).reverse!
54
+ end
55
+ rescue Exception # rubocop:disable Lint/RescueException
56
+ # this is a best effort, bail if we can't perform this
57
+ end
58
+
59
+ define_singleton_method(:class_attribute) do |*attrs, **kwargs|
60
+ class_attribute_readers.concat(attrs)
61
+ class_attribute_writers.concat(attrs)
62
+
63
+ instance_predicate = kwargs.fetch(:instance_predicate, true)
64
+ instance_accessor = kwargs.fetch(:instance_accessor, true)
65
+ instance_reader = kwargs.fetch(:instance_reader, instance_accessor)
66
+ instance_writer = kwargs.fetch(:instance_writer, instance_accessor)
67
+
68
+ if instance_reader
69
+ instance_attribute_readers.concat(attrs)
70
+ end
71
+
72
+ if instance_writer
73
+ instance_attribute_writers.concat(attrs)
74
+ end
75
+
76
+ if instance_predicate
77
+ class_attribute_predicates.concat(attrs)
78
+
79
+ if instance_reader
80
+ instance_attribute_predicates.concat(attrs)
81
+ end
82
+ end
83
+
84
+ super(*attrs, **kwargs) if defined?(super)
85
+ end
86
+
87
+ # rubocop:disable Style/MissingRespondToMissing
88
+ T::Sig::WithoutRuntime.sig { params(symbol: Symbol, args: T.untyped).returns(T.untyped) }
89
+ def method_missing(symbol, *args)
90
+ # We need this here so that we can handle any random instance
91
+ # method calls on the fake including class that may be done by
92
+ # the included module during the `self.included` hook.
93
+ end
94
+
95
+ class << self
96
+ extend T::Sig
97
+
98
+ T::Sig::WithoutRuntime.sig { params(symbol: Symbol, args: T.untyped).returns(T.untyped) }
99
+ def method_missing(symbol, *args)
100
+ # Similarly, we need this here so that we can handle any
101
+ # random class method calls on the fake including class
102
+ # that may be done by the included module during the
103
+ # `self.included` hook.
104
+ end
105
+ end
106
+ # rubocop:enable Style/MissingRespondToMissing
107
+ end.include(constant)
108
+
109
+ # The value that corresponds to the original included constant
110
+ # is the list of all dynamically extended modules because of that
111
+ # constant. We grab that value by deleting the key for the original
112
+ # constant.
113
+ @dynamic_extends = T.let(mixins_from_modules.delete(constant) || [], T::Array[Module])
114
+
115
+ # Since we deleted the original constant from the list of keys, all
116
+ # the keys that remain are the ones that are dynamically included modules
117
+ # during the include of the original constant.
118
+ @dynamic_includes = T.let(mixins_from_modules.keys, T::Array[Module])
119
+
120
+ @class_attribute_readers = T.let(class_attribute_readers, T::Array[Symbol])
121
+ @class_attribute_writers = T.let(class_attribute_writers, T::Array[Symbol])
122
+ @class_attribute_predicates = T.let(class_attribute_predicates, T::Array[Symbol])
123
+
124
+ @instance_attribute_readers = T.let(instance_attribute_readers, T::Array[Symbol])
125
+ @instance_attribute_writers = T.let(instance_attribute_writers, T::Array[Symbol])
126
+ @instance_attribute_predicates = T.let(instance_attribute_predicates, T::Array[Symbol])
127
+ end
128
+
129
+ sig { returns(T::Boolean) }
130
+ def empty_attributes?
131
+ @class_attribute_readers.empty? && @class_attribute_writers.empty?
132
+ end
133
+
134
+ sig { params(tree: RBI::Tree).void }
135
+ def compile_class_attributes(tree)
136
+ return if empty_attributes?
137
+
138
+ # Create a synthetic module to hold the generated class methods
139
+ tree << RBI::Module.new("GeneratedClassMethods") do |mod|
140
+ class_attribute_readers.each do |attribute|
141
+ mod << RBI::Method.new(attribute.to_s)
142
+ end
143
+
144
+ class_attribute_writers.each do |attribute|
145
+ mod << RBI::Method.new("#{attribute}=") do |method|
146
+ method << RBI::Param.new("value")
147
+ end
148
+ end
149
+
150
+ class_attribute_predicates.each do |attribute|
151
+ mod << RBI::Method.new("#{attribute}?")
152
+ end
153
+ end
154
+
155
+ # Create a synthetic module to hold the generated instance methods
156
+ tree << RBI::Module.new("GeneratedInstanceMethods") do |mod|
157
+ instance_attribute_readers.each do |attribute|
158
+ mod << RBI::Method.new(attribute.to_s)
159
+ end
160
+
161
+ instance_attribute_writers.each do |attribute|
162
+ mod << RBI::Method.new("#{attribute}=") do |method|
163
+ method << RBI::Param.new("value")
164
+ end
165
+ end
166
+
167
+ instance_attribute_predicates.each do |attribute|
168
+ mod << RBI::Method.new("#{attribute}?")
169
+ end
170
+ end
171
+
172
+ # Add a mixes_in_class_methods and include for the generated modules
173
+ tree << RBI::MixesInClassMethods.new("GeneratedClassMethods")
174
+ tree << RBI::Include.new("GeneratedInstanceMethods")
175
+ end
176
+
177
+ sig { params(tree: RBI::Tree).returns([T::Array[Module], T::Array[Module]]) }
178
+ def compile_mixes_in_class_methods(tree)
179
+ includes = dynamic_includes.map do |mod|
180
+ qname = qualified_name_of(mod)
181
+
182
+ next if qname.nil? || qname.empty?
183
+ next if filtered_mixin?(qname)
184
+
185
+ tree << RBI::Include.new(qname)
186
+
187
+ mod
188
+ end.compact
189
+
190
+ # If we can generate multiple mixes_in_class_methods, then we want to use all dynamic extends that are not the
191
+ # constant itself
192
+ mixed_in_class_methods = dynamic_extends.select do |mod|
193
+ mod != @constant && !module_included_by_another_dynamic_extend?(mod, dynamic_extends)
194
+ end
195
+
196
+ return [[], []] if mixed_in_class_methods.empty?
197
+
198
+ mixed_in_class_methods.each do |mod|
199
+ qualified_name = qualified_name_of(mod)
200
+
201
+ next if qualified_name.nil? || qualified_name.empty?
202
+ next if filtered_mixin?(qualified_name)
203
+
204
+ tree << RBI::MixesInClassMethods.new(qualified_name)
205
+ end
206
+
207
+ [mixed_in_class_methods, includes]
208
+ rescue
209
+ [[], []] # silence errors
210
+ end
211
+
212
+ sig { params(mod: Module, dynamic_extends: T::Array[Module]).returns(T::Boolean) }
213
+ def module_included_by_another_dynamic_extend?(mod, dynamic_extends)
214
+ dynamic_extends.any? do |dynamic_extend|
215
+ mod != dynamic_extend && ancestors_of(dynamic_extend).include?(mod)
216
+ end
217
+ end
218
+
219
+ sig { params(qualified_mixin_name: String).returns(T::Boolean) }
220
+ def filtered_mixin?(qualified_mixin_name)
221
+ # filter T:: namespace mixins that aren't T::Props
222
+ # T::Props and subconstants have semantic value
223
+ qualified_mixin_name.start_with?("::T::") && !qualified_mixin_name.start_with?("::T::Props")
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,168 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Runtime
6
+ # This class is responsible for storing and looking up information related to generic types.
7
+ #
8
+ # The class stores 2 different kinds of data, in two separate lookup tables:
9
+ # 1. a lookup of generic type instances by name: `@generic_instances`
10
+ # 2. a lookup of type variable serializer by constant and type variable
11
+ # instance: `@type_variables`
12
+ #
13
+ # By storing the above data, we can cheaply query each constant against this registry
14
+ # to see if it declares any generic type variables. This becomes a simple lookup in the
15
+ # `@type_variables` hash table with the given constant.
16
+ #
17
+ # If there is no entry, then we can cheaply know that we can skip generic type
18
+ # information generation for this type.
19
+ #
20
+ # On the other hand, if we get a result, then the result will be a hash of type
21
+ # variable to type variable serializers. This allows us to associate type variables
22
+ # to the constant names that represent them, easily.
23
+ module GenericTypeRegistry
24
+ @generic_instances = T.let(
25
+ {},
26
+ T::Hash[String, Module]
27
+ )
28
+
29
+ @type_variables = T.let(
30
+ {}.compare_by_identity,
31
+ T::Hash[Module, T::Array[TypeVariableModule]]
32
+ )
33
+
34
+ class << self
35
+ extend T::Sig
36
+
37
+ # This method is responsible for building the name of the instantiated concrete type
38
+ # and cloning the given constant so that we can return a type that is the same
39
+ # as the current type but is a different instance and has a different name method.
40
+ #
41
+ # We cache those cloned instances by their name in `@generic_instances`, so that
42
+ # we don't keep instantiating a new type every single time it is referenced.
43
+ # For example, `[Foo[Integer], Foo[Integer], Foo[Integer], Foo[String]]` will only
44
+ # result in 2 clones (1 for `Foo[Integer]` and another for `Foo[String]`) and
45
+ # 2 hash lookups (for the other two `Foo[Integer]`s).
46
+ #
47
+ # This method returns the created or cached clone of the constant.
48
+ sig { params(constant: T.untyped, types: T.untyped).returns(Module) }
49
+ def register_type(constant, types)
50
+ # Build the name of the instantiated generic type,
51
+ # something like `"Foo[X, Y, Z]"`
52
+ type_list = types.map { |type| T::Utils.coerce(type).name }.join(", ")
53
+ name = "#{Reflection.name_of(constant)}[#{type_list}]"
54
+
55
+ # Create a generic type with an overridden `name`
56
+ # method that returns the name we constructed above.
57
+ #
58
+ # Also, we try to memoize the generic type based on the name, so that
59
+ # we don't have to keep recreating them all the time.
60
+ @generic_instances[name] ||= create_generic_type(constant, name)
61
+ end
62
+
63
+ sig { params(instance: Object).returns(T::Boolean) }
64
+ def generic_type_instance?(instance)
65
+ @generic_instances.values.any? { |generic_type| generic_type === instance }
66
+ end
67
+
68
+ sig { params(constant: Module).returns(T.nilable(T::Array[TypeVariableModule])) }
69
+ def lookup_type_variables(constant)
70
+ @type_variables[constant]
71
+ end
72
+
73
+ # This method is called from intercepted calls to `type_member` and `type_template`.
74
+ # We get passed all the arguments to those methods, as well as the `T::Types::TypeVariable`
75
+ # instance generated by the Sorbet defined `type_member`/`type_template` call on `T::Generic`.
76
+ #
77
+ # This method creates a `String` with that data and stores it in the
78
+ # `@type_variables` lookup table, keyed by the `constant` and `type_variable`.
79
+ #
80
+ # Finally, the original `type_variable` is returned from this method, so that the caller
81
+ # can return it from the original methods as well.
82
+ sig do
83
+ params(
84
+ constant: T.untyped,
85
+ type_variable: TypeVariableModule,
86
+ ).void
87
+ end
88
+ def register_type_variable(constant, type_variable)
89
+ type_variables = lookup_or_initialize_type_variables(constant)
90
+
91
+ type_variables << type_variable
92
+ end
93
+
94
+ private
95
+
96
+ sig { params(constant: Module, name: String).returns(Module) }
97
+ def create_generic_type(constant, name)
98
+ generic_type = case constant
99
+ when Class
100
+ # For classes, we want to create a subclass, so that an instance of
101
+ # the generic class `Foo[Bar]` is still a `Foo`. That is:
102
+ # `Foo[Bar].new.is_a?(Foo)` should be true, which isn't the case
103
+ # if we just clone the class. But subclassing works just fine.
104
+ create_safe_subclass(constant)
105
+ else
106
+ # This can only be a module and it is fine to just clone modules
107
+ # since they can't have instances and will not have `is_a?` relationships.
108
+ # Moreover, we never `include`/`extend` any generic modules into the
109
+ # ancestor tree, so this doesn't become a problem with checking the
110
+ # instance of a class being `is_a?` of a module type.
111
+ constant.clone
112
+ end
113
+
114
+ # Let's set the `name` and `to_s` methods to return the proper generic name
115
+ name_proc = -> { name }
116
+ generic_type.define_singleton_method(:name, name_proc)
117
+ generic_type.define_singleton_method(:to_s, name_proc)
118
+
119
+ # We need to define a `<=` method on the cloned constant, so that Sorbet
120
+ # can do covariance/contravariance checks on the type variables.
121
+ #
122
+ # Normally, we would be doing proper covariance/contravariance checks here, but
123
+ # that is not necessary, since we are not implementing a runtime type checker
124
+ # here. It is just enough for the checks to pass, so that we can serialize the
125
+ # signatures, assuming the sigs were well-formed.
126
+ #
127
+ # So we act like all subtype checks pass.
128
+ generic_type.define_singleton_method(:<=) { |_| true }
129
+
130
+ # Return the generic type we created
131
+ generic_type
132
+ end
133
+
134
+ sig { params(constant: Class).returns(Class) }
135
+ def create_safe_subclass(constant)
136
+ # Lookup the "inherited" class method
137
+ inherited_method = constant.method(:inherited)
138
+ # and the module that defines it
139
+ owner = inherited_method.owner
140
+
141
+ # If no one has overriden the inherited method yet, just subclass
142
+ return Class.new(constant) if Class == owner
143
+
144
+ begin
145
+ # Otherwise, some inherited method could be preventing us
146
+ # from creating subclasses, so let's override it and rescue
147
+ owner.send(:define_method, :inherited) do |s|
148
+ inherited_method.call(s)
149
+ rescue
150
+ # Ignoring errors
151
+ end
152
+
153
+ # return a subclass
154
+ Class.new(constant)
155
+ ensure
156
+ # Reinstate the original inherited method back.
157
+ owner.send(:define_method, :inherited, inherited_method)
158
+ end
159
+ end
160
+
161
+ sig { params(constant: Module).returns(T::Array[TypeVariableModule]) }
162
+ def lookup_or_initialize_type_variables(constant)
163
+ @type_variables[constant] ||= []
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end