ruby-next-core 0.14.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -0
  3. data/README.md +163 -56
  4. data/bin/mspec +11 -0
  5. data/lib/.rbnext/2.1/ruby-next/commands/nextify.rb +295 -0
  6. data/lib/.rbnext/2.1/ruby-next/core.rb +12 -4
  7. data/lib/.rbnext/2.1/ruby-next/language.rb +62 -12
  8. data/lib/.rbnext/2.3/ruby-next/commands/nextify.rb +97 -3
  9. data/lib/.rbnext/2.3/ruby-next/config.rb +79 -0
  10. data/lib/.rbnext/2.3/ruby-next/core/data.rb +163 -0
  11. data/lib/.rbnext/2.3/ruby-next/language/eval.rb +4 -4
  12. data/lib/.rbnext/2.3/ruby-next/language/rewriters/2.7/args_forward.rb +134 -0
  13. data/lib/.rbnext/2.3/ruby-next/language/rewriters/2.7/pattern_matching.rb +122 -47
  14. data/lib/.rbnext/2.3/ruby-next/language/rewriters/base.rb +6 -32
  15. data/lib/.rbnext/2.3/ruby-next/utils.rb +3 -22
  16. data/lib/.rbnext/2.6/ruby-next/core/data.rb +163 -0
  17. data/lib/.rbnext/2.7/ruby-next/core/data.rb +163 -0
  18. data/lib/.rbnext/2.7/ruby-next/core.rb +12 -4
  19. data/lib/.rbnext/2.7/ruby-next/language/paco_parsers/string_literals.rb +109 -0
  20. data/lib/.rbnext/2.7/ruby-next/language/rewriters/2.7/pattern_matching.rb +1095 -0
  21. data/lib/.rbnext/2.7/ruby-next/language/rewriters/text.rb +132 -0
  22. data/lib/.rbnext/3.2/ruby-next/commands/base.rb +55 -0
  23. data/lib/.rbnext/3.2/ruby-next/language/rewriters/2.7/pattern_matching.rb +1095 -0
  24. data/lib/.rbnext/3.2/ruby-next/rubocop.rb +210 -0
  25. data/lib/ruby-next/cli.rb +10 -15
  26. data/lib/ruby-next/commands/nextify.rb +99 -3
  27. data/lib/ruby-next/config.rb +31 -4
  28. data/lib/ruby-next/core/data.rb +163 -0
  29. data/lib/ruby-next/core/matchdata/deconstruct.rb +9 -0
  30. data/lib/ruby-next/core/matchdata/deconstruct_keys.rb +20 -0
  31. data/lib/ruby-next/core/matchdata/named_captures.rb +11 -0
  32. data/lib/ruby-next/core/proc/compose.rb +0 -1
  33. data/lib/ruby-next/core/refinement/import.rb +44 -36
  34. data/lib/ruby-next/core/time/deconstruct_keys.rb +30 -0
  35. data/lib/ruby-next/core.rb +11 -3
  36. data/lib/ruby-next/irb.rb +24 -0
  37. data/lib/ruby-next/language/bootsnap.rb +2 -25
  38. data/lib/ruby-next/language/eval.rb +4 -4
  39. data/lib/ruby-next/language/paco_parser.rb +7 -0
  40. data/lib/ruby-next/language/paco_parsers/base.rb +47 -0
  41. data/lib/ruby-next/language/paco_parsers/comments.rb +26 -0
  42. data/lib/ruby-next/language/paco_parsers/string_literals.rb +109 -0
  43. data/lib/ruby-next/language/parser.rb +31 -6
  44. data/lib/ruby-next/language/rewriters/2.5/rescue_within_block.rb +41 -0
  45. data/lib/ruby-next/language/rewriters/2.7/args_forward.rb +57 -0
  46. data/lib/ruby-next/language/rewriters/2.7/pattern_matching.rb +120 -45
  47. data/lib/ruby-next/language/rewriters/3.0/args_forward_leading.rb +2 -2
  48. data/lib/ruby-next/language/rewriters/3.1/oneline_pattern_parensless.rb +1 -1
  49. data/lib/ruby-next/language/rewriters/3.1/shorthand_hash.rb +2 -1
  50. data/lib/ruby-next/language/rewriters/3.2/anonymous_restargs.rb +104 -0
  51. data/lib/ruby-next/language/rewriters/abstract.rb +57 -0
  52. data/lib/ruby-next/language/rewriters/base.rb +6 -32
  53. data/lib/ruby-next/language/rewriters/edge/it_param.rb +58 -0
  54. data/lib/ruby-next/language/rewriters/edge.rb +12 -0
  55. data/lib/ruby-next/language/rewriters/proposed/bind_vars_pattern.rb +3 -0
  56. data/lib/ruby-next/language/rewriters/proposed/method_reference.rb +9 -20
  57. data/lib/ruby-next/language/rewriters/text.rb +132 -0
  58. data/lib/ruby-next/language/runtime.rb +9 -86
  59. data/lib/ruby-next/language/setup.rb +5 -2
  60. data/lib/ruby-next/language/unparser.rb +5 -0
  61. data/lib/ruby-next/language.rb +62 -12
  62. data/lib/ruby-next/pry.rb +90 -0
  63. data/lib/ruby-next/rubocop.rb +2 -0
  64. data/lib/ruby-next/utils.rb +3 -22
  65. data/lib/ruby-next/version.rb +1 -1
  66. data/lib/uby-next/irb.rb +3 -0
  67. data/lib/uby-next/pry.rb +3 -0
  68. data/lib/uby-next.rb +2 -2
  69. metadata +70 -10
@@ -182,11 +182,10 @@ module RubyNext
182
182
  end
183
183
  end
184
184
 
185
- # rubocop:disable Style/MethodMissingSuper
186
185
  # rubocop:disable Style/MissingRespondToMissing
187
186
  class Noop < Base
188
187
  # Return node itself, no memoization
189
- def method_missing(mid, node, *)
188
+ def method_missing(mid, node, *__rest__)
190
189
  node
191
190
  end
192
191
  end
@@ -243,6 +242,7 @@ module RubyNext
243
242
 
244
243
  @deconstructed_keys = {}
245
244
  @predicates = Predicates::CaseIn.new
245
+ @lvars = []
246
246
 
247
247
  matchee_ast =
248
248
  s(:begin, s(:lvasgn, MATCHEE, node.children[0]))
@@ -272,6 +272,7 @@ module RubyNext
272
272
 
273
273
  @deconstructed_keys = {}
274
274
  @predicates = Predicates::Noop.new
275
+ @lvars = []
275
276
 
276
277
  matchee =
277
278
  s(:begin, s(:lvasgn, MATCHEE, node.children[0]))
@@ -282,10 +283,12 @@ module RubyNext
282
283
  arr: MATCHEE_ARR,
283
284
  hash: MATCHEE_HASH
284
285
  ) do
285
- send(
286
- :"#{node.children[1].type}_clause",
287
- node.children[1]
288
- ).then do |node|
286
+ with_declared_locals do
287
+ send(
288
+ :"#{node.children[1].type}_clause",
289
+ node.children[1]
290
+ )
291
+ end.then do |node|
289
292
  s(:begin,
290
293
  s(:or,
291
294
  node,
@@ -311,6 +314,7 @@ module RubyNext
311
314
 
312
315
  @deconstructed_keys = {}
313
316
  @predicates = Predicates::Noop.new
317
+ @lvars = []
314
318
 
315
319
  matchee =
316
320
  s(:begin, s(:lvasgn, MATCHEE, node.children[0]))
@@ -321,10 +325,12 @@ module RubyNext
321
325
  arr: MATCHEE_ARR,
322
326
  hash: MATCHEE_HASH
323
327
  ) do
324
- send(
325
- :"#{node.children[1].type}_clause",
326
- node.children[1]
327
- )
328
+ with_declared_locals do
329
+ send(
330
+ :"#{node.children[1].type}_clause",
331
+ node.children[1]
332
+ )
333
+ end
328
334
  end
329
335
 
330
336
  node.updated(
@@ -395,13 +401,15 @@ module RubyNext
395
401
  def build_when_clause(clause)
396
402
  predicates.reset!
397
403
  [
398
- with_guard(
399
- send(
400
- :"#{clause.children[0].type}_clause",
401
- clause.children[0]
402
- ),
403
- clause.children[1] # guard
404
- ),
404
+ with_declared_locals do
405
+ with_guard(
406
+ send(
407
+ :"#{clause.children[0].type}_clause",
408
+ clause.children[0]
409
+ ),
410
+ clause.children[1] # guard
411
+ )
412
+ end,
405
413
  process(clause.children[2] || s(:nil)) # expression
406
414
  ].then do |children|
407
415
  s(:when, *children)
@@ -417,15 +425,15 @@ module RubyNext
417
425
  s(:begin,
418
426
  s(:and,
419
427
  node,
420
- send(:"#{pattern.type}_clause", pattern)))
428
+ send(:"#{pattern.type}_clause", pattern, right)))
421
429
  end
422
430
  end
423
431
 
424
- def match_alt_clause(node)
432
+ def match_alt_clause(node, matchee = s(:lvar, locals[:matchee]))
425
433
  children = locals.with(ALTERNATION_MARKER => true) do
426
434
  node.children.map.with_index do |child, i|
427
435
  predicates.terminate! if i == 1
428
- send :"#{child.type}_clause", child
436
+ send :"#{child.type}_clause", child, matchee
429
437
  end
430
438
  end
431
439
  s(:begin, s(:or, *children))
@@ -442,7 +450,7 @@ module RubyNext
442
450
  var = node.children[0]
443
451
  return s(:true) if var == :_
444
452
 
445
- check_match_var_alternation!(var) unless var.is_a?(::Parser::AST::Node)
453
+ check_match_var_alternation!(var)
446
454
 
447
455
  s(:begin,
448
456
  s(:or,
@@ -541,11 +549,10 @@ module RubyNext
541
549
  def array_find(head, *nodes, tail)
542
550
  index = s(:lvar, :__i__)
543
551
 
544
- match_vars = []
545
-
546
552
  head_match =
547
553
  unless head.children.empty?
548
- match_vars << build_var_assignment(head.children[0].children[0])
554
+ # we only need to call this to track the lvar usage
555
+ build_var_assignment(head.children[0].children[0])
549
556
 
550
557
  arr_take = s(:send,
551
558
  s(:lvar, locals[:arr]),
@@ -557,16 +564,19 @@ module RubyNext
557
564
 
558
565
  tail_match =
559
566
  unless tail.children.empty?
560
- match_vars << build_var_assignment(tail.children[0].children[0])
567
+ # we only need to call this to track the lvar usage
568
+ build_var_assignment(tail.children[0].children[0])
561
569
 
562
570
  match_var_clause(tail.children[0], arr_slice(index + nodes.size, -1))
563
571
  end
564
572
 
565
573
  nodes.each do |node|
566
574
  if node.type == :match_var
567
- match_vars << build_var_assignment(node.children[0])
575
+ # we only need to call this to track the lvar usage
576
+ build_var_assignment(node.children[0])
568
577
  elsif node.type == :match_as
569
- match_vars << build_var_assignment(node.children[1].children[0])
578
+ # we only need to call this to track the lvar usage
579
+ build_var_assignment(node.children[1].children[0])
570
580
  end
571
581
  end
572
582
 
@@ -594,19 +604,7 @@ module RubyNext
594
604
  s(:args,
595
605
  s(:arg, :_),
596
606
  s(:arg, :__i__)),
597
- pattern).then do |block|
598
- next block if match_vars.empty?
599
-
600
- # We need to declare match vars outside of `find` block
601
- locals_declare = s(:begin, s(:masgn,
602
- s(:mlhs, *match_vars),
603
- s(:nil)))
604
-
605
- s(:begin,
606
- s(:or,
607
- locals_declare,
608
- block))
609
- end
607
+ pattern)
610
608
  end
611
609
 
612
610
  def array_match_rest(index, node, *tail)
@@ -649,6 +647,14 @@ module RubyNext
649
647
  end
650
648
  end
651
649
 
650
+ def find_pattern_array_element(node, index)
651
+ element = arr_item_at(index)
652
+ locals.with(arr: locals[:arr, index]) do
653
+ predicates.push :"i#{index}"
654
+ find_pattern_clause(node, element).tap { predicates.pop }
655
+ end
656
+ end
657
+
652
658
  def hash_pattern_array_element(node, index)
653
659
  element = arr_item_at(index)
654
660
  locals.with(hash: locals[:arr, index]) do
@@ -657,6 +663,14 @@ module RubyNext
657
663
  end
658
664
  end
659
665
 
666
+ def const_pattern_array_element(node, index)
667
+ element = arr_item_at(index)
668
+ locals.with(arr: locals[:arr, index]) do
669
+ predicates.push :"i#{index}"
670
+ const_pattern_clause(node, element).tap { predicates.pop }
671
+ end
672
+ end
673
+
660
674
  def match_alt_array_element(node, index)
661
675
  children = node.children.map do |child, i|
662
676
  send :"#{child.type}_array_element", child, index
@@ -665,11 +679,19 @@ module RubyNext
665
679
  end
666
680
 
667
681
  def match_var_array_element(node, index)
668
- match_var_clause(node, arr_item_at(index))
682
+ element = arr_item_at(index)
683
+ locals.with(arr: locals[:arr, index]) do
684
+ predicates.push :"i#{index}"
685
+ match_var_clause(node, element).tap { predicates.pop }
686
+ end
669
687
  end
670
688
 
671
689
  def match_as_array_element(node, index)
672
- match_as_clause(node, arr_item_at(index))
690
+ element = arr_item_at(index)
691
+ locals.with(arr: locals[:arr, index]) do
692
+ predicates.push :"i#{index}"
693
+ match_as_clause(node, element).tap { predicates.pop }
694
+ end
673
695
  end
674
696
 
675
697
  def pin_array_element(node, index)
@@ -829,6 +851,24 @@ module RubyNext
829
851
  end
830
852
  end
831
853
 
854
+ def find_pattern_hash_element(node, key)
855
+ element = hash_value_at(key)
856
+ key_index = deconstructed_key(key)
857
+ locals.with(arr: locals[:hash, key_index]) do
858
+ predicates.push :"k#{key_index}"
859
+ find_pattern_clause(node, element).tap { predicates.pop }
860
+ end
861
+ end
862
+
863
+ def const_pattern_hash_element(node, key)
864
+ element = hash_value_at(key)
865
+ key_index = deconstructed_key(key)
866
+ locals.with(hash: locals[:hash, key_index]) do
867
+ predicates.push :"k#{key_index}"
868
+ const_pattern_clause(node, element).tap { predicates.pop }
869
+ end
870
+ end
871
+
832
872
  def hash_element(head, *tail)
833
873
  send("#{head.type}_hash_element", head).then do |node|
834
874
  next node if tail.empty?
@@ -869,12 +909,22 @@ module RubyNext
869
909
  end
870
910
 
871
911
  def match_as_hash_element(node, key)
872
- match_as_clause(node, hash_value_at(key))
912
+ element = hash_value_at(key)
913
+ key_index = deconstructed_key(key)
914
+ locals.with(hash: locals[:hash, key_index]) do
915
+ predicates.push :"k#{key_index}"
916
+ match_as_clause(node, element).tap { predicates.pop }
917
+ end
873
918
  end
874
919
 
875
920
  def match_var_hash_element(node, key = nil)
876
921
  key ||= node.children[0]
877
- match_var_clause(node, hash_value_at(key))
922
+ element = hash_value_at(key)
923
+ key_index = deconstructed_key(key)
924
+ locals.with(hash: locals[:hash, key_index]) do
925
+ predicates.push :"k#{key_index}"
926
+ match_var_clause(node, element).tap { predicates.pop }
927
+ end
878
928
  end
879
929
 
880
930
  def match_nil_pattern_hash_element(node, _key = nil)
@@ -948,6 +998,24 @@ module RubyNext
948
998
  end
949
999
  end
950
1000
 
1001
+ def with_declared_locals
1002
+ lvars.clear
1003
+ node = yield
1004
+
1005
+ return node if lvars.empty?
1006
+
1007
+ # We need to declare match lvars outside of the outer `find` block,
1008
+ # so we do that for that whole pattern
1009
+ locals_declare = s(:begin, s(:masgn,
1010
+ s(:mlhs, *lvars.uniq.map { |_1| s(:lvasgn, _1) }),
1011
+ s(:nil)))
1012
+
1013
+ s(:begin,
1014
+ s(:or,
1015
+ locals_declare,
1016
+ node))
1017
+ end
1018
+
951
1019
  def no_matching_pattern
952
1020
  raise_error(
953
1021
  :NoMatchingPatternError,
@@ -967,7 +1035,7 @@ module RubyNext
967
1035
  s(:send, node, :respond_to?, mid.to_ast_node)
968
1036
  end
969
1037
 
970
- def respond_to_missing?(mid, *)
1038
+ def respond_to_missing?(mid, *__rest__)
971
1039
  return true if mid.to_s.match?(/_(clause|array_element)/)
972
1040
  super
973
1041
  end
@@ -982,13 +1050,17 @@ module RubyNext
982
1050
 
983
1051
  private
984
1052
 
985
- attr_reader :deconstructed_keys, :predicates
1053
+ attr_reader :deconstructed_keys, :predicates, :lvars
986
1054
 
987
1055
  # Raise SyntaxError if match-var is used within alternation
988
1056
  # https://github.com/ruby/ruby/blob/672213ef1ca2b71312084057e27580b340438796/compile.c#L5900
989
1057
  def check_match_var_alternation!(name)
990
1058
  return unless locals.key?(ALTERNATION_MARKER)
991
1059
 
1060
+ if name.is_a?(::Parser::AST::Node)
1061
+ raise ::SyntaxError, "illegal variable in alternative pattern (#{name.children.first})"
1062
+ end
1063
+
992
1064
  return if name.start_with?("_")
993
1065
 
994
1066
  raise ::SyntaxError, "illegal variable in alternative pattern (#{name})"
@@ -1008,7 +1080,10 @@ module RubyNext
1008
1080
 
1009
1081
  # Value could be omitted for mass assignment
1010
1082
  def build_var_assignment(var, value = nil)
1011
- return s(:lvasgn, *[var, value].compact) unless var.is_a?(::Parser::AST::Node)
1083
+ unless var.is_a?(::Parser::AST::Node)
1084
+ lvars << var
1085
+ return s(:lvasgn, *[var, value].compact)
1086
+ end
1012
1087
 
1013
1088
  asign_type = :"#{var.type.to_s[0]}vasgn"
1014
1089
 
@@ -12,7 +12,7 @@ module RubyNext
12
12
 
13
13
  MSG
14
14
 
15
- class Base < ::Parser::TreeRewriter
15
+ class Base < Abstract
16
16
  class LocalsTracker
17
17
  using(Module.new do
18
18
  refine ::Parser::AST::Node do
@@ -64,39 +64,15 @@ module RubyNext
64
64
  end
65
65
  end
66
66
 
67
- class << self
68
- # Returns true if the syntax is supported
69
- # by the current Ruby (performs syntax check, not version check)
70
- def unsupported_syntax?
71
- save_verbose, $VERBOSE = $VERBOSE, nil
72
- eval_mid = Kernel.respond_to?(:eval_without_ruby_next) ? :eval_without_ruby_next : :eval
73
- Kernel.send eval_mid, self::SYNTAX_PROBE, nil, __FILE__, __LINE__
74
- false
75
- rescue SyntaxError, StandardError
76
- true
77
- ensure
78
- $VERBOSE = save_verbose
79
- end
80
-
81
- # Returns true if the syntax is supported
82
- # by the specified version
83
- def unsupported_version?(version)
84
- self::MIN_SUPPORTED_VERSION > version
85
- end
86
-
87
- private
67
+ attr_reader :locals
88
68
 
89
- def transform(source)
90
- Language.transform(source, rewriters: [self], using: false)
91
- end
69
+ def self.ast?
70
+ true
92
71
  end
93
72
 
94
- attr_reader :locals
95
-
96
- def initialize(context)
97
- @context = context
73
+ def initialize(*args)
98
74
  @locals = LocalsTracker.new
99
- super()
75
+ super
100
76
  end
101
77
 
102
78
  def s(type, *children)
@@ -144,8 +120,6 @@ module RubyNext
144
120
 
145
121
  Unparser.unparse(ast).chomp
146
122
  end
147
-
148
- attr_reader :context
149
123
  end
150
124
  end
151
125
  end
@@ -4,28 +4,6 @@ module RubyNext
4
4
  module Utils
5
5
  module_function
6
6
 
7
- if $LOAD_PATH.respond_to?(:resolve_feature_path)
8
- def resolve_feature_path(feature)
9
- ((((__safe_lvar__ = $LOAD_PATH.resolve_feature_path(feature)) || true) && (!__safe_lvar__.nil? || nil)) && __safe_lvar__.last)
10
- rescue LoadError
11
- end
12
- else
13
- def resolve_feature_path(path)
14
- if File.file?(relative = File.expand_path(path))
15
- path = relative
16
- end
17
-
18
- path = "#{path}.rb" if File.extname(path).empty?
19
-
20
- return path if Pathname.new(path).absolute?
21
-
22
- $LOAD_PATH.find do |lp|
23
- lpath = File.join(lp, path)
24
- return File.realpath(lpath) if File.file?(lpath)
25
- end
26
- end
27
- end
28
-
29
7
  def source_with_lines(source, path)
30
8
  source.lines.map.with_index do |line, i|
31
9
  "#{(i + 1).to_s.rjust(4)}: #{line}"
@@ -36,6 +14,7 @@ module RubyNext
36
14
 
37
15
  # Returns true if modules refinement is supported in current version
38
16
  def refine_modules?
17
+ save_verbose, $VERBOSE = $VERBOSE, nil
39
18
  @refine_modules ||=
40
19
  begin
41
20
  # Make sure that including modules within refinements works
@@ -59,6 +38,8 @@ module RubyNext
59
38
  true
60
39
  rescue TypeError, NoMethodError
61
40
  false
41
+ ensure
42
+ $VERBOSE = save_verbose
62
43
  end
63
44
  end
64
45
  end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The code below originates from https://github.com/saturnflyer/polyfill-data
4
+
5
+ if !Object.const_defined?(:Data) || !Data.respond_to?(:define)
6
+
7
+ # Drop legacy Data class
8
+ begin
9
+ Object.send(:remove_const, :Data)
10
+ rescue
11
+ nil
12
+ end
13
+
14
+ class Data < Object
15
+ using RubyNext
16
+
17
+ class << self
18
+ undef_method :new
19
+ attr_reader :members
20
+ end
21
+
22
+ def self.define(*args, &block)
23
+ raise ArgumentError if args.any?(/=/)
24
+ if block
25
+ mod = Module.new
26
+ mod.define_singleton_method(:_) do |klass|
27
+ klass.class_eval(&block)
28
+ end
29
+ arity_converter = mod.method(:_)
30
+ end
31
+ klass = ::Class.new(self)
32
+
33
+ klass.instance_variable_set(:@members, args.map(&:to_sym).freeze)
34
+
35
+ klass.define_singleton_method(:new) do |*new_args, **new_kwargs, &block|
36
+ init_kwargs = if new_args.any?
37
+ raise ArgumentError, "unknown arguments #{new_args[members.size..-1].join(", ")}" if new_args.size > members.size
38
+ members.take(new_args.size).zip(new_args).to_h
39
+ else
40
+ new_kwargs
41
+ end
42
+
43
+ allocate.tap do |instance|
44
+ instance.send(:initialize, **init_kwargs, &block)
45
+ end.freeze
46
+ end
47
+
48
+ class << klass
49
+ alias_method :[], :new
50
+ undef_method :define
51
+ end
52
+
53
+ args.each do |arg|
54
+ if klass.method_defined?(arg)
55
+ raise ArgumentError, "duplicate member #{arg}"
56
+ end
57
+ klass.define_method(arg) do
58
+ @attributes[arg]
59
+ end
60
+ end
61
+
62
+ if arity_converter
63
+ klass.class_eval(&arity_converter)
64
+ end
65
+
66
+ klass
67
+ end
68
+
69
+ def self.inherited(subclass)
70
+ subclass.instance_variable_set(:@members, members)
71
+ end
72
+
73
+ def members
74
+ self.class.members
75
+ end
76
+
77
+ def initialize(**kwargs)
78
+ kwargs_size = kwargs.size
79
+ members_size = members.size
80
+
81
+ if kwargs_size > members_size
82
+ extras = kwargs.except(*members).keys
83
+
84
+ if extras.size > 1
85
+ raise ArgumentError, "unknown keywords: #{extras.map { |_1| ":#{_1}" }.join(", ")}"
86
+ else
87
+ raise ArgumentError, "unknown keyword: :#{extras.first}"
88
+ end
89
+ elsif kwargs_size < members_size
90
+ missing = members.select { |k| !kwargs.include?(k) }
91
+
92
+ if missing.size > 1
93
+ raise ArgumentError, "missing keywords: #{missing.map { |_1| ":#{_1}" }.join(", ")}"
94
+ else
95
+ raise ArgumentError, "missing keyword: :#{missing.first}"
96
+ end
97
+ end
98
+
99
+ @attributes = members.map { |m| [m, kwargs[m]] }.to_h
100
+ end
101
+
102
+ def deconstruct
103
+ @attributes.values
104
+ end
105
+
106
+ def deconstruct_keys(array)
107
+ raise TypeError unless array.is_a?(Array) || array.nil?
108
+ return @attributes if array&.first.nil?
109
+
110
+ @attributes.slice(*array)
111
+ end
112
+
113
+ def to_h(&block)
114
+ @attributes.to_h(&block)
115
+ end
116
+
117
+ def hash
118
+ to_h.hash
119
+ end
120
+
121
+ def eql?(other)
122
+ self.class == other.class && hash == other.hash
123
+ end
124
+
125
+ def ==(other)
126
+ self.class == other.class && to_h == other.to_h
127
+ end
128
+
129
+ def inspect
130
+ attribute_markers = @attributes.map do |key, value|
131
+ insect_key = key.to_s.start_with?("@") ? ":#{key}" : key
132
+ "#{insect_key}=#{value}"
133
+ end.join(", ")
134
+
135
+ display = ["data", self.class.name, attribute_markers].compact.join(" ")
136
+
137
+ "#<#{display}>"
138
+ end
139
+ alias_method :to_s, :inspect
140
+
141
+ def with(**kwargs)
142
+ return self if kwargs.empty?
143
+
144
+ self.class.new(**@attributes.merge(kwargs))
145
+ end
146
+
147
+ private
148
+
149
+ def marshal_dump
150
+ @attributes
151
+ end
152
+
153
+ def marshal_load(attributes)
154
+ @attributes = attributes
155
+ freeze
156
+ end
157
+
158
+ def initialize_copy(source)
159
+ super.freeze
160
+ end
161
+ end
162
+
163
+ end