tapioca 0.6.4 → 0.7.2

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