tapioca 0.4.18 → 0.4.23

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.
@@ -0,0 +1,18 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ class String
5
+ extend T::Sig
6
+
7
+ sig { returns(String) }
8
+ def underscore
9
+ return self unless /[A-Z-]|::/.match?(self)
10
+
11
+ word = to_s.gsub("::", "/")
12
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
13
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
14
+ word.tr!("-", "_")
15
+ word.downcase!
16
+ word
17
+ end
18
+ end
@@ -108,8 +108,14 @@ module Tapioca
108
108
 
109
109
  sig { returns(T::Array[Pathname]) }
110
110
  def files
111
- @spec.full_require_paths.flat_map do |path|
112
- Pathname.glob((Pathname.new(path) / "**/*.rb").to_s)
111
+ if default_gem?
112
+ @spec.files.map do |file|
113
+ ruby_lib_dir.join(file)
114
+ end
115
+ else
116
+ @spec.full_require_paths.flat_map do |path|
117
+ Pathname.glob((Pathname.new(path) / "**/*.rb").to_s)
118
+ end
113
119
  end
114
120
  end
115
121
 
@@ -125,11 +131,25 @@ module Tapioca
125
131
 
126
132
  sig { params(path: String).returns(T::Boolean) }
127
133
  def contains_path?(path)
128
- to_realpath(path).start_with?(full_gem_path) || has_parent_gemspec?(path)
134
+ if default_gem?
135
+ files.any? { |file| file.to_s == to_realpath(path) }
136
+ else
137
+ to_realpath(path).start_with?(full_gem_path) || has_parent_gemspec?(path)
138
+ end
129
139
  end
130
140
 
131
141
  private
132
142
 
143
+ sig { returns(T::Boolean) }
144
+ def default_gem?
145
+ @spec.respond_to?(:default_gem?) && @spec.default_gem?
146
+ end
147
+
148
+ sig { returns(Pathname) }
149
+ def ruby_lib_dir
150
+ Pathname.new(RbConfig::CONFIG["rubylibdir"])
151
+ end
152
+
133
153
  sig { returns(String) }
134
154
  def version_string
135
155
  version = @spec.version.to_s
@@ -176,7 +196,7 @@ module Tapioca
176
196
 
177
197
  sig { returns(T::Boolean) }
178
198
  def gem_in_bundle_path?
179
- full_gem_path.start_with?(Bundler.bundle_path.to_s)
199
+ full_gem_path.start_with?(Bundler.bundle_path.to_s, Bundler.app_cache.to_s)
180
200
  end
181
201
  end
182
202
  end
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'pathname'
5
5
  require 'thor'
6
+ require "tapioca/core_ext/string"
6
7
 
7
8
  module Tapioca
8
9
  class Generator < ::Thor::Shell::Color
@@ -120,9 +121,10 @@ module Tapioca
120
121
  params(
121
122
  requested_constants: T::Array[String],
122
123
  should_verify: T::Boolean,
124
+ quiet: T::Boolean
123
125
  ).void
124
126
  end
125
- def build_dsl(requested_constants, should_verify: false)
127
+ def build_dsl(requested_constants, should_verify: false, quiet: false)
126
128
  load_application(eager_load: requested_constants.empty?)
127
129
  load_dsl_generators
128
130
 
@@ -133,7 +135,7 @@ module Tapioca
133
135
  end
134
136
  say("")
135
137
 
136
- outpath = should_verify ? Dir.mktmpdir : config.outpath
138
+ outpath = should_verify ? Pathname.new(Dir.mktmpdir) : config.outpath
137
139
  rbi_files_to_purge = existing_rbi_filenames(requested_constants)
138
140
 
139
141
  compiler = Compilers::DslCompiler.new(
@@ -145,8 +147,18 @@ module Tapioca
145
147
  )
146
148
 
147
149
  compiler.run do |constant, contents|
148
- filename = compile_dsl_rbi(constant, contents, outpath: Pathname.new(outpath))
149
- rbi_files_to_purge.delete(filename) if filename
150
+ constant_name = Module.instance_method(:name).bind(constant).call
151
+
152
+ filename = compile_dsl_rbi(
153
+ constant_name,
154
+ contents,
155
+ outpath: outpath,
156
+ quiet: should_verify || quiet
157
+ )
158
+
159
+ if filename
160
+ rbi_files_to_purge.delete(filename)
161
+ end
150
162
  end
151
163
  say("")
152
164
 
@@ -274,7 +286,7 @@ module Tapioca
274
286
  def constantize(constant_names)
275
287
  constant_map = constant_names.map do |name|
276
288
  begin
277
- [name, name.constantize]
289
+ [name, Object.const_get(name)]
278
290
  rescue NameError
279
291
  [name, nil]
280
292
  end
@@ -506,60 +518,101 @@ module Tapioca
506
518
  end
507
519
  end
508
520
 
509
- sig { params(constant: Module, contents: String, outpath: Pathname).returns(T.nilable(Pathname)) }
510
- def compile_dsl_rbi(constant, contents, outpath: config.outpath)
521
+ sig do
522
+ params(constant_name: String, contents: String, outpath: Pathname, quiet: T::Boolean)
523
+ .returns(T.nilable(Pathname))
524
+ end
525
+ def compile_dsl_rbi(constant_name, contents, outpath: config.outpath, quiet: false)
511
526
  return if contents.nil?
512
527
 
513
- constant_name = Module.instance_method(:name).bind(constant).call
514
528
  rbi_name = constant_name.underscore + ".rbi"
515
529
  filename = outpath / rbi_name
516
530
 
517
531
  out = String.new
518
532
  out << rbi_header(
519
533
  "#{Config::DEFAULT_COMMAND} dsl #{constant_name}",
520
- reason: "dynamic methods in `#{constant.name}`"
534
+ reason: "dynamic methods in `#{constant_name}`"
521
535
  )
522
536
  out << contents
523
537
 
524
538
  FileUtils.mkdir_p(File.dirname(filename))
525
539
  File.write(filename, out)
526
- say("Wrote: ", [:green])
527
- say(filename)
540
+
541
+ unless quiet
542
+ say("Wrote: ", [:green])
543
+ say(filename)
544
+ end
528
545
 
529
546
  filename
530
547
  end
531
548
 
532
- sig { params(tmp_dir: Pathname).returns(T.nilable(String)) }
549
+ sig { params(tmp_dir: Pathname).returns(T::Hash[String, Symbol]) }
533
550
  def verify_dsl_rbi(tmp_dir:)
534
- existing_rbis = existing_rbi_filenames([]).sort
535
- new_rbis = existing_rbi_filenames([], path: tmp_dir).grep_v(/gem|shim/).sort
551
+ diff = {}
536
552
 
537
- return "New file(s) introduced." if existing_rbis.length != new_rbis.length
553
+ existing_rbis = rbi_files_in(config.outpath)
554
+ new_rbis = rbi_files_in(tmp_dir)
538
555
 
539
- desynced_files = []
556
+ added_files = (new_rbis - existing_rbis)
540
557
 
541
- (0..existing_rbis.length - 1).each do |i|
542
- desynced_files << new_rbis[i] unless FileUtils.identical?(existing_rbis[i], new_rbis[i])
558
+ added_files.each do |file|
559
+ diff[file] = :added
543
560
  end
544
561
 
545
- unless desynced_files.empty?
546
- filenames = desynced_files.map { |f| f.to_s.sub!(tmp_dir.to_s, "sorbet/rbi/dsl") }.join("\n - ")
562
+ removed_files = (existing_rbis - new_rbis)
547
563
 
548
- return "File(s) updated:\n - #{filenames}"
564
+ removed_files.each do |file|
565
+ diff[file] = :removed
549
566
  end
550
567
 
551
- nil
568
+ common_files = (existing_rbis & new_rbis)
569
+
570
+ changed_files = common_files.map do |filename|
571
+ filename unless FileUtils.identical?(config.outpath / filename, tmp_dir / filename)
572
+ end.compact
573
+
574
+ changed_files.each do |file|
575
+ diff[file] = :changed
576
+ end
577
+
578
+ diff
579
+ end
580
+
581
+ sig { params(cause: Symbol, files: T::Array[String]).returns(String) }
582
+ def build_error_for_files(cause, files)
583
+ filenames = files.map do |file|
584
+ config.outpath / file
585
+ end.join("\n - ")
586
+
587
+ " File(s) #{cause}:\n - #{filenames}"
588
+ end
589
+
590
+ sig { params(path: Pathname).returns(T::Array[Pathname]) }
591
+ def rbi_files_in(path)
592
+ Pathname.glob(path / "**/*.rbi").map do |file|
593
+ file.relative_path_from(path)
594
+ end.sort
552
595
  end
553
596
 
554
- sig { params(dir: String).void }
597
+ sig { params(dir: Pathname).void }
555
598
  def perform_dsl_verification(dir)
556
- if (error = verify_dsl_rbi(tmp_dir: Pathname.new(dir)))
557
- say("RBI files are out-of-date, please run `#{Config::DEFAULT_COMMAND} dsl` to update.")
558
- say("Reason: ", [:red])
559
- say(error)
560
- exit(1)
561
- else
599
+ diff = verify_dsl_rbi(tmp_dir: dir)
600
+
601
+ if diff.empty?
562
602
  say("Nothing to do, all RBIs are up-to-date.")
603
+ else
604
+ say("RBI files are out-of-date. In your development environment, please run:", :green)
605
+ say(" `#{Config::DEFAULT_COMMAND} dsl`", [:green, :bold])
606
+ say("Once it is complete, be sure to commit and push any changes", :green)
607
+
608
+ say("")
609
+
610
+ say("Reason:", [:red])
611
+ diff.group_by(&:last).sort.each do |cause, diff_for_cause|
612
+ say(build_error_for_files(cause, diff_for_cause.map(&:first)))
613
+ end
614
+
615
+ exit(1)
563
616
  end
564
617
  ensure
565
618
  FileUtils.remove_entry(dir)
@@ -51,14 +51,12 @@ module Tapioca
51
51
  type_list = types.map { |type| T::Utils.coerce(type).name }.join(", ")
52
52
  name = "#{name_of(constant)}[#{type_list}]"
53
53
 
54
- # Create a clone of the constant with an overridden `name`
54
+ # Create a generic type with an overridden `name`
55
55
  # method that returns the name we constructed above.
56
56
  #
57
- # Also, we try to memoize the clone based on the name, so that
58
- # we don't have to keep recreating clones all the time.
59
- @generic_instances[name] ||= constant.clone.tap do |clone|
60
- clone.define_singleton_method(:name) { name }
61
- end
57
+ # Also, we try to memoize the generic type based on the name, so that
58
+ # we don't have to keep recreating them all the time.
59
+ @generic_instances[name] ||= create_generic_type(constant, name)
62
60
  end
63
61
 
64
62
  sig do
@@ -94,6 +92,31 @@ module Tapioca
94
92
 
95
93
  private
96
94
 
95
+ sig { params(constant: Module, name: String).returns(Module) }
96
+ def create_generic_type(constant, name)
97
+ generic_type = case constant
98
+ when Class
99
+ # For classes, we want to create a subclass, so that an instance of
100
+ # the generic class `Foo[Bar]` is still a `Foo`. That is:
101
+ # `Foo[Bar].new.is_a?(Foo)` should be true, which isn't the case
102
+ # if we just clone the class. But subclassing works just fine.
103
+ create_safe_subclass(constant)
104
+ else
105
+ # This can only be a module and it is fine to just clone modules
106
+ # since they can't have instances and will not have `is_a?` relationships.
107
+ # Moreover, we never `include`/`extend` any generic modules into the
108
+ # ancestor tree, so this doesn't become a problem with checking the
109
+ # instance of a class being `is_a?` of a module type.
110
+ constant.clone
111
+ end
112
+
113
+ # Let's set the `name` method to return the proper generic name
114
+ generic_type.define_singleton_method(:name) { name }
115
+
116
+ # Return the generic type we created
117
+ generic_type
118
+ end
119
+
97
120
  # This method is called from intercepted calls to `type_member` and `type_template`.
98
121
  # We get passed all the arguments to those methods, as well as the `T::Types::TypeVariable`
99
122
  # instance generated by the Sorbet defined `type_member`/`type_template` call on `T::Generic`.
@@ -127,6 +150,35 @@ module Tapioca
127
150
  )
128
151
  end
129
152
 
153
+ sig { params(constant: Class).returns(Class) }
154
+ def create_safe_subclass(constant)
155
+ # Lookup the "inherited" class method
156
+ inherited_method = constant.method(:inherited)
157
+ # and the module that defines it
158
+ owner = inherited_method.owner
159
+
160
+ # If no one has overriden the inherited method yet, just subclass
161
+ return Class.new(constant) if Class == owner
162
+
163
+ begin
164
+ # Otherwise, some inherited method could be preventing us
165
+ # from creating subclasses, so let's override it and rescue
166
+ owner.send(:define_method, :inherited) do |s|
167
+ begin
168
+ inherited_method.call(s)
169
+ rescue
170
+ # Ignoring errors
171
+ end
172
+ end
173
+
174
+ # return a subclass
175
+ Class.new(constant)
176
+ ensure
177
+ # Reinstate the original inherited method back.
178
+ owner.send(:define_method, :inherited, inherited_method)
179
+ end
180
+ end
181
+
130
182
  sig { params(constant: Module).returns(T::Hash[Integer, String]) }
131
183
  def lookup_or_initialize_type_variables(constant)
132
184
  @type_variables[object_id_of(constant)] ||= {}
@@ -6,12 +6,20 @@ require "tapioca/loader"
6
6
  require "tapioca/constant_locator"
7
7
  require "tapioca/generic_type_registry"
8
8
  require "tapioca/sorbet_ext/generic_name_patch"
9
+ require "tapioca/sorbet_ext/fixed_hash_patch"
9
10
  require "tapioca/config"
10
11
  require "tapioca/config_builder"
11
12
  require "tapioca/generator"
12
13
  require "tapioca/cli"
13
14
  require "tapioca/cli/main"
14
15
  require "tapioca/gemfile"
16
+ require "tapioca/rbi/model"
17
+ require "tapioca/rbi/visitor"
18
+ require "tapioca/rbi/rewriters/nest_singleton_methods"
19
+ require "tapioca/rbi/rewriters/nest_non_public_methods"
20
+ require "tapioca/rbi/rewriters/group_nodes"
21
+ require "tapioca/rbi/rewriters/sort_nodes"
22
+ require "tapioca/rbi/printer"
15
23
  require "tapioca/compilers/sorbet"
16
24
  require "tapioca/compilers/requires_compiler"
17
25
  require "tapioca/compilers/symbol_table_compiler"
@@ -0,0 +1,405 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module RBI
6
+ class Node
7
+ extend T::Sig
8
+ extend T::Helpers
9
+
10
+ abstract!
11
+
12
+ sig { returns(T.nilable(Tree)) }
13
+ attr_accessor :parent_tree
14
+
15
+ sig { void }
16
+ def initialize
17
+ @parent_tree = nil
18
+ end
19
+
20
+ sig { void }
21
+ def detach
22
+ tree = parent_tree
23
+ return unless tree
24
+ tree.nodes.delete(self)
25
+ self.parent_tree = nil
26
+ end
27
+ end
28
+
29
+ class Tree < Node
30
+ extend T::Sig
31
+
32
+ sig { returns(T::Array[Node]) }
33
+ attr_reader :nodes
34
+
35
+ sig { void }
36
+ def initialize
37
+ super()
38
+ @nodes = T.let([], T::Array[Node])
39
+ end
40
+
41
+ sig { params(node: Node).void }
42
+ def <<(node)
43
+ node.parent_tree = self
44
+ @nodes << node
45
+ end
46
+
47
+ sig { returns(T::Boolean) }
48
+ def empty?
49
+ nodes.empty?
50
+ end
51
+ end
52
+
53
+ # Scopes
54
+
55
+ class Scope < Tree
56
+ extend T::Helpers
57
+
58
+ abstract!
59
+ end
60
+
61
+ class Module < Scope
62
+ extend T::Sig
63
+
64
+ sig { returns(String) }
65
+ attr_accessor :name
66
+
67
+ sig { params(name: String).void }
68
+ def initialize(name)
69
+ super()
70
+ @name = name
71
+ end
72
+ end
73
+
74
+ class Class < Scope
75
+ extend T::Sig
76
+
77
+ sig { returns(String) }
78
+ attr_accessor :name
79
+
80
+ sig { returns(T.nilable(String)) }
81
+ attr_accessor :superclass_name
82
+
83
+ sig { params(name: String, superclass_name: T.nilable(String)).void }
84
+ def initialize(name, superclass_name: nil)
85
+ super()
86
+ @name = name
87
+ @superclass_name = superclass_name
88
+ end
89
+ end
90
+
91
+ class SingletonClass < Scope
92
+ extend T::Sig
93
+
94
+ sig { void }
95
+ def initialize
96
+ super()
97
+ end
98
+ end
99
+
100
+ # Consts
101
+
102
+ class Const < Node
103
+ extend T::Sig
104
+
105
+ sig { returns(String) }
106
+ attr_reader :name, :value
107
+
108
+ sig { params(name: String, value: String).void }
109
+ def initialize(name, value)
110
+ super()
111
+ @name = name
112
+ @value = value
113
+ end
114
+ end
115
+
116
+ # Methods and args
117
+
118
+ class Method < Node
119
+ extend T::Sig
120
+
121
+ sig { returns(String) }
122
+ attr_accessor :name
123
+
124
+ sig { returns(T::Array[Param]) }
125
+ attr_reader :params
126
+
127
+ sig { returns(T::Boolean) }
128
+ attr_accessor :is_singleton
129
+
130
+ sig { returns(Visibility) }
131
+ attr_accessor :visibility
132
+
133
+ sig { returns(T::Array[Sig]) }
134
+ attr_accessor :sigs
135
+
136
+ sig do
137
+ params(
138
+ name: String,
139
+ params: T::Array[Param],
140
+ is_singleton: T::Boolean,
141
+ visibility: Visibility,
142
+ sigs: T::Array[Sig]
143
+ ).void
144
+ end
145
+ def initialize(name, params: [], is_singleton: false, visibility: Visibility::Public, sigs: [])
146
+ super()
147
+ @name = name
148
+ @params = params
149
+ @is_singleton = is_singleton
150
+ @visibility = visibility
151
+ @sigs = sigs
152
+ end
153
+
154
+ sig { params(param: Param).void }
155
+ def <<(param)
156
+ @params << param
157
+ end
158
+ end
159
+
160
+ class Param < Node
161
+ extend T::Sig
162
+
163
+ sig { returns(String) }
164
+ attr_reader :name
165
+
166
+ sig { params(name: String).void }
167
+ def initialize(name)
168
+ super()
169
+ @name = name
170
+ end
171
+ end
172
+
173
+ class OptParam < Param
174
+ extend T::Sig
175
+
176
+ sig { returns(String) }
177
+ attr_reader :value
178
+
179
+ sig { params(name: String, value: String).void }
180
+ def initialize(name, value)
181
+ super(name)
182
+ @value = value
183
+ end
184
+ end
185
+
186
+ class RestParam < Param; end
187
+ class KwParam < Param; end
188
+ class KwOptParam < OptParam; end
189
+ class KwRestParam < Param; end
190
+ class BlockParam < Param; end
191
+
192
+ # Mixins
193
+
194
+ class Mixin < Node
195
+ extend T::Sig
196
+ extend T::Helpers
197
+
198
+ abstract!
199
+
200
+ sig { returns(String) }
201
+ attr_reader :name
202
+
203
+ sig { params(name: String).void }
204
+ def initialize(name)
205
+ super()
206
+ @name = name
207
+ end
208
+ end
209
+
210
+ class Include < Mixin; end
211
+ class Extend < Mixin; end
212
+
213
+ # Visibility
214
+
215
+ class Visibility < Node
216
+ extend T::Sig
217
+ extend T::Helpers
218
+
219
+ abstract!
220
+
221
+ sig { returns(Symbol) }
222
+ attr_reader :visibility
223
+
224
+ sig { params(visibility: Symbol).void }
225
+ def initialize(visibility)
226
+ super()
227
+ @visibility = visibility
228
+ end
229
+
230
+ sig { returns(T::Boolean) }
231
+ def public?
232
+ visibility == :public
233
+ end
234
+
235
+ Public = T.let(Visibility.new(:public), Visibility)
236
+ Protected = T.let(Visibility.new(:protected), Visibility)
237
+ Private = T.let(Visibility.new(:private), Visibility)
238
+ end
239
+
240
+ # Sorbet's sigs
241
+
242
+ class Sig < Node
243
+ extend T::Sig
244
+
245
+ sig { returns(T::Array[SigParam]) }
246
+ attr_reader :params
247
+
248
+ sig { returns(T.nilable(String)) }
249
+ attr_accessor :return_type
250
+
251
+ sig { returns(T::Boolean) }
252
+ attr_accessor :is_abstract, :is_override, :is_overridable
253
+
254
+ sig { returns(T::Array[String]) }
255
+ attr_reader :type_params
256
+
257
+ sig do
258
+ params(
259
+ params: T::Array[SigParam],
260
+ return_type: T.nilable(String),
261
+ is_abstract: T::Boolean,
262
+ is_override: T::Boolean,
263
+ is_overridable: T::Boolean,
264
+ type_params: T::Array[String]
265
+ ).void
266
+ end
267
+ def initialize(
268
+ params: [],
269
+ return_type: nil,
270
+ is_abstract: false,
271
+ is_override: false,
272
+ is_overridable: false,
273
+ type_params: []
274
+ )
275
+ super()
276
+ @params = params
277
+ @return_type = return_type
278
+ @is_abstract = is_abstract
279
+ @is_override = is_override
280
+ @is_overridable = is_overridable
281
+ @type_params = type_params
282
+ end
283
+
284
+ sig { params(param: SigParam).void }
285
+ def <<(param)
286
+ @params << param
287
+ end
288
+ end
289
+
290
+ class SigParam < Node
291
+ extend T::Sig
292
+
293
+ sig { returns(String) }
294
+ attr_reader :name, :type
295
+
296
+ sig { params(name: String, type: String).void }
297
+ def initialize(name, type)
298
+ super()
299
+ @name = name
300
+ @type = type
301
+ end
302
+ end
303
+
304
+ # Sorbet's T::Struct
305
+
306
+ class TStruct < Class
307
+ extend T::Sig
308
+
309
+ sig { params(name: String).void }
310
+ def initialize(name)
311
+ super(name, superclass_name: "::T::Struct")
312
+ end
313
+ end
314
+
315
+ class TStructField < Node
316
+ extend T::Sig
317
+ extend T::Helpers
318
+
319
+ abstract!
320
+
321
+ sig { returns(String) }
322
+ attr_accessor :name, :type
323
+
324
+ sig { returns(T.nilable(String)) }
325
+ attr_accessor :default
326
+
327
+ sig do
328
+ params(
329
+ name: String,
330
+ type: String,
331
+ default: T.nilable(String)
332
+ ).void
333
+ end
334
+ def initialize(name, type, default: nil)
335
+ super()
336
+ @name = name
337
+ @type = type
338
+ @default = default
339
+ end
340
+ end
341
+
342
+ class TStructProp < TStructField; end
343
+ class TStructConst < TStructField; end
344
+
345
+ # Sorbet's T::Enum
346
+
347
+ class TEnum < Class
348
+ extend T::Sig
349
+
350
+ sig { params(name: String).void }
351
+ def initialize(name)
352
+ super(name, superclass_name: "::T::Enum")
353
+ end
354
+ end
355
+
356
+ class TEnumBlock < Node
357
+ extend T::Sig
358
+
359
+ sig { returns(T::Array[String]) }
360
+ attr_reader :names
361
+
362
+ sig { params(names: T::Array[String]).void }
363
+ def initialize(names = [])
364
+ super()
365
+ @names = names
366
+ end
367
+
368
+ sig { returns(T::Boolean) }
369
+ def empty?
370
+ names.empty?
371
+ end
372
+ end
373
+
374
+ # Sorbet's misc.
375
+
376
+ class Helper < Node
377
+ extend T::Helpers
378
+
379
+ sig { returns(String) }
380
+ attr_reader :name
381
+
382
+ sig { params(name: String).void }
383
+ def initialize(name)
384
+ super()
385
+ @name = name
386
+ end
387
+ end
388
+
389
+ class TypeMember < Node
390
+ extend T::Sig
391
+
392
+ sig { returns(String) }
393
+ attr_reader :name, :value
394
+
395
+ sig { params(name: String, value: String).void }
396
+ def initialize(name, value)
397
+ super()
398
+ @name = name
399
+ @value = value
400
+ end
401
+ end
402
+
403
+ class MixesInClassMethods < Mixin; end
404
+ end
405
+ end