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
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file contains patches to RuboCop to support
4
+ # edge features and fix some bugs with 2.7+ syntax
5
+
6
+ require "parser/ruby-next/version"
7
+ require "ruby-next/config"
8
+ require "ruby-next/language/parser"
9
+
10
+ module RuboCop
11
+ # Transform Ruby Next parser version to a float, e.g.: "2.8.0.1" => 2.801
12
+ RUBY_NEXT_VERSION = Parser::NEXT_VERSION.match(/(^\d+)\.(.+)$/)[1..-1].map { |part| part.delete(".") }.join(".").to_f
13
+
14
+ class TargetRuby
15
+ class RuboCopNextConfig < RuboCopConfig
16
+ private
17
+
18
+ def find_version
19
+ version = @config.for_all_cops["TargetRubyVersion"]
20
+ return unless version == "next"
21
+
22
+ RUBY_NEXT_VERSION
23
+ end
24
+ end
25
+
26
+ new_rubies = KNOWN_RUBIES + [RUBY_NEXT_VERSION]
27
+ remove_const :KNOWN_RUBIES
28
+ const_set :KNOWN_RUBIES, new_rubies
29
+
30
+ new_sources = [RuboCopNextConfig] + SOURCES
31
+ remove_const :SOURCES
32
+ const_set :SOURCES, new_sources
33
+ end
34
+ end
35
+
36
+ module RuboCop
37
+ class ProcessedSource
38
+ module ParserClassExt
39
+ def parser_class(version)
40
+ return super unless version == RUBY_NEXT_VERSION
41
+
42
+ require "parser/rubynext"
43
+ Parser::RubyNext
44
+ end
45
+ end
46
+
47
+ prepend ParserClassExt
48
+ end
49
+ end
50
+
51
+ # Let's make this file Ruby 2.2 compatible to avoid transpiling
52
+ # rubocop:disable Layout/HeredocIndentation
53
+ module RuboCop
54
+ module AST
55
+ module Traversal
56
+ # Fixed in https://github.com/rubocop-hq/rubocop/pull/7786
57
+ %i[case_match in_pattern find_pattern match_pattern match_pattern_p].each do |type|
58
+ next if method_defined?(:"on_#{type}")
59
+ module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
60
+ def on_#{type}(node)
61
+ node.children.each { |child| send(:"on_\#{child.type}", child) if child }
62
+ nil
63
+ end
64
+ RUBY
65
+ end
66
+ end
67
+
68
+ unless Builder.method_defined?(:match_pattern_p)
69
+ Builder.include RubyNext::Language::BuilderExt
70
+ end
71
+ end
72
+ end
73
+ # rubocop:enable Layout/HeredocIndentation
74
+
75
+ module RuboCop
76
+ module Cop
77
+ # Commissioner class is responsible for processing the AST and delegating
78
+ # work to the specified cops.
79
+ class Commissioner
80
+ def on_meth_ref(node)
81
+ trigger_responding_cops(:on_meth_ref, node)
82
+ end
83
+
84
+ unless method_defined?(:on_numblock)
85
+ def on_numblock(node)
86
+ children = node.children
87
+ child = children[0]
88
+ send(:"on_#{child.type}", child)
89
+ # children[1] is the number of parameters
90
+ return unless (child = children[2])
91
+
92
+ send(:"on_#{child.type}", child)
93
+ end
94
+ end
95
+
96
+ unless method_defined?(:on_def_e)
97
+ def on_def_e(node)
98
+ _name, _args_node, body_node = *node
99
+ send(:"on_#{body_node.type}", body_node)
100
+ end
101
+
102
+ def on_defs_e(node)
103
+ _definee_node, _name, _args_node, body_node = *node
104
+ send(:"on_#{body_node.type}", body_node)
105
+ end
106
+ end
107
+ end
108
+
109
+ Commissioner.prepend(Module.new do
110
+ # Ignore anonymous blocks
111
+ def on_block_pass(node)
112
+ return if node.children == [nil]
113
+
114
+ super
115
+ end
116
+
117
+ def on_blockarg(node)
118
+ return if node.children == [nil]
119
+
120
+ super
121
+ end
122
+ end)
123
+
124
+ module Layout
125
+ require "rubocop/cop/layout/assignment_indentation"
126
+
127
+ POTENTIAL_RIGHT_TYPES = %i[ivasgn lvasgn cvasgn gvasgn casgn masgn].freeze
128
+
129
+ AssignmentIndentation.prepend(Module.new do
130
+ def check_assignment(node, *__rest__)
131
+ return if rightward?(node)
132
+ super
133
+ end
134
+
135
+ private
136
+
137
+ def rightward?(node)
138
+ return unless POTENTIAL_RIGHT_TYPES.include?(node.type)
139
+
140
+ return unless node.loc.operator
141
+
142
+ assignee_loc =
143
+ if node.type == :masgn
144
+ node.children[0].loc.expression
145
+ else
146
+ node.loc.name
147
+ end
148
+
149
+ return false unless assignee_loc
150
+
151
+ assignee_loc.begin_pos > node.loc.operator.end_pos
152
+ end
153
+ end)
154
+
155
+ require "rubocop/cop/layout/empty_line_between_defs"
156
+ EmptyLineBetweenDefs.prepend(Module.new do
157
+ def def_end(node)
158
+ return super unless node.loc.end.nil?
159
+
160
+ node.loc.expression.line
161
+ end
162
+ end)
163
+
164
+ require "rubocop/cop/layout/space_after_colon"
165
+ SpaceAfterColon.prepend(Module.new do
166
+ def on_pair(node)
167
+ return if node.children[0].loc.last_column == node.children[1].loc.last_column
168
+
169
+ super(node)
170
+ end
171
+ end)
172
+ end
173
+
174
+ module Style
175
+ require "rubocop/cop/style/single_line_methods"
176
+ SingleLineMethods.prepend(Module.new do
177
+ def on_def(node)
178
+ return if node.loc.end.nil?
179
+ super
180
+ end
181
+
182
+ def on_defs(node)
183
+ return if node.loc.end.nil?
184
+ super
185
+ end
186
+ end)
187
+
188
+ require "rubocop/cop/style/def_with_parentheses"
189
+ DefWithParentheses.prepend(Module.new do
190
+ def on_def(node)
191
+ return if node.loc.end.nil?
192
+ super
193
+ end
194
+
195
+ def on_defs(node)
196
+ return if node.loc.end.nil?
197
+ super
198
+ end
199
+ end)
200
+
201
+ require "rubocop/cop/style/trailing_method_end_statement"
202
+ TrailingMethodEndStatement.prepend(Module.new do
203
+ def on_def(node)
204
+ return if node.loc.end.nil?
205
+ super
206
+ end
207
+ end)
208
+ end
209
+ end
210
+ end
data/lib/ruby-next/cli.rb CHANGED
@@ -24,9 +24,6 @@ module RubyNext
24
24
  "core_ext" => Commands::CoreExt
25
25
  }.freeze
26
26
 
27
- def initialize
28
- end
29
-
30
27
  def run(args = ARGV)
31
28
  maybe_print_version(args)
32
29
 
@@ -80,18 +77,16 @@ module RubyNext
80
77
  end
81
78
 
82
79
  def optparser
83
- @optparser ||= begin
84
- OptionParser.new do |opts|
85
- opts.banner = "Usage: ruby-next COMMAND [options]"
86
-
87
- opts.on("-v", "--version", "Print version") do
88
- $stdout.puts RubyNext::VERSION
89
- exit 0
90
- end
91
-
92
- opts.on("-h", "--help", "Print help") do
93
- @print_help = true
94
- end
80
+ @optparser ||= OptionParser.new do |opts|
81
+ opts.banner = "Usage: ruby-next COMMAND [options]"
82
+
83
+ opts.on("-v", "--version", "Print version") do
84
+ $stdout.puts RubyNext::VERSION
85
+ exit 0
86
+ end
87
+
88
+ opts.on("-h", "--help", "Print help") do
89
+ @print_help = true
95
90
  end
96
91
  end
97
92
  end
@@ -10,9 +10,45 @@ module RubyNext
10
10
  class Nextify < Base
11
11
  using RubyNext
12
12
 
13
- attr_reader :lib_path, :paths, :out_path, :min_version, :single_version, :specified_rewriters
13
+ class Stats
14
+ def initialize
15
+ @started_at = ::Process.clock_gettime(Process::CLOCK_MONOTONIC)
16
+ @files = 0
17
+ @scans = 0
18
+ @transpiled_files = 0
19
+ end
20
+
21
+ def file!
22
+ @files += 1
23
+ end
24
+
25
+ def scan!
26
+ @scans += 1
27
+ end
28
+
29
+ def transpiled!
30
+ @transpiled_files += 1
31
+ end
32
+
33
+ def report
34
+ <<~TXT
35
+
36
+ Files processed: #{@files}
37
+ Total scans: #{@scans}
38
+ Files transpiled: #{@transpiled_files}
39
+
40
+ Completed in #{::Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at}s
41
+ TXT
42
+ end
43
+ end
44
+
45
+ attr_reader :lib_path, :paths, :out_path, :min_version, :single_version, :specified_rewriters, :overwrite
46
+ alias_method :overwrite?, :overwrite
47
+ attr_reader :stats
14
48
 
15
49
  def run
50
+ @stats = Stats.new
51
+
16
52
  log "RubyNext core strategy: #{RubyNext::Core.strategy}"
17
53
  log "RubyNext transpile mode: #{RubyNext::Language.mode}"
18
54
 
@@ -21,16 +57,24 @@ module RubyNext
21
57
  @min_version ||= MIN_SUPPORTED_VERSION
22
58
 
23
59
  paths.each do |path|
60
+ stats.file!
61
+
24
62
  contents = File.read(path)
25
63
  transpile path, contents
26
64
  end
65
+
66
+ ensure_rbnext!
67
+
68
+ log stats.report
27
69
  end
28
70
 
29
71
  def parse!(args)
30
72
  print_help = false
31
73
  print_rewriters = false
32
74
  rewriter_names = []
75
+ custom_rewriters = []
33
76
  @single_version = false
77
+ @overwrite = false
34
78
 
35
79
  optparser = base_parser do |opts|
36
80
  opts.banner = "Usage: ruby-next nextify DIRECTORY_OR_FILE [options]"
@@ -47,11 +91,17 @@ module RubyNext
47
91
  @single_version = true
48
92
  end
49
93
 
94
+ opts.on("--overwrite", "Overwrite original file") do
95
+ @overwrite = true
96
+ end
97
+
50
98
  opts.on("--edge", "Enable edge (master) Ruby features") do |val|
99
+ ENV["RUBY_NEXT_EDGE"] = val.to_s
51
100
  require "ruby-next/language/rewriters/edge"
52
101
  end
53
102
 
54
103
  opts.on("--proposed", "Enable proposed/experimental Ruby features") do |val|
104
+ ENV["RUBY_NEXT_PROPOSED"] = val.to_s
55
105
  require "ruby-next/language/rewriters/proposed"
56
106
  end
57
107
 
@@ -74,6 +124,10 @@ module RubyNext
74
124
  rewriter_names << val
75
125
  end
76
126
 
127
+ opts.on("--import-rewriter=REWRITERS...", "Specify paths to load custom rewritiers") do |val|
128
+ custom_rewriters << val
129
+ end
130
+
77
131
  opts.on("-h", "--help", "Print help") do
78
132
  print_help = true
79
133
  end
@@ -88,9 +142,14 @@ module RubyNext
88
142
  exit 0
89
143
  end
90
144
 
145
+ # Load custom rewriters
146
+ custom_rewriters.each do |path|
147
+ Kernel.load path
148
+ end
149
+
91
150
  if print_rewriters
92
151
  Language.rewriters.each do |rewriter|
93
- $stdout.puts "#{rewriter::NAME} (\"#{rewriter::SYNTAX_PROBE}\")"
152
+ $stdout.puts "#{rewriter::NAME} (\"#{rewriter::SYNTAX_PROBE}\")#{rewriter.unsupported_syntax? ? " (unsupported)" : ""}"
94
153
  end
95
154
  exit 0
96
155
  end
@@ -117,6 +176,11 @@ module RubyNext
117
176
  end
118
177
  end
119
178
 
179
+ if overwrite? && !single_version?
180
+ $stdout.puts "--overwrite only works with --single-version or explcit rewritires specified (via --rewrite)"
181
+ exit 2
182
+ end
183
+
120
184
  @paths =
121
185
  if File.directory?(lib_path)
122
186
  Dir[File.join(lib_path, "**/*.rb")]
@@ -130,6 +194,8 @@ module RubyNext
130
194
  private
131
195
 
132
196
  def transpile(path, contents, version: min_version)
197
+ stats.scan!
198
+
133
199
  rewriters = specified_rewriters || Language.rewriters.select { |rw| rw.unsupported_version?(version) }
134
200
 
135
201
  context = Language::TransformContext.new
@@ -150,16 +216,25 @@ module RubyNext
150
216
  transpile path, contents, version: version
151
217
  rescue SyntaxError, StandardError => e
152
218
  warn "Failed to transpile #{path}: #{e.class} — #{e.message}"
219
+ warn e.backtrace.take(10).join("\n") if ENV["RUBY_NEXT_DEBUG"] == "1"
153
220
  exit 1
154
221
  end
155
222
 
156
223
  def save(contents, path, version)
224
+ stats.transpiled!
225
+
157
226
  return $stdout.puts(contents) if stdout?
158
227
 
159
228
  paths = [Pathname.new(path).relative_path_from(Pathname.new(lib_path))]
160
229
 
161
230
  paths.unshift(version.segments[0..1].join(".")) unless single_version?
162
231
 
232
+ if overwrite?
233
+ overwrite_file_content!(path: path, contents: contents)
234
+
235
+ return
236
+ end
237
+
163
238
  next_path =
164
239
  if next_dir_path.end_with?(".rb")
165
240
  out_path
@@ -176,6 +251,14 @@ module RubyNext
176
251
  log "Generated: #{next_path}"
177
252
  end
178
253
 
254
+ def overwrite_file_content!(path:, contents:)
255
+ unless CLI.dry_run?
256
+ File.write(path, contents)
257
+ end
258
+
259
+ log "Overwritten: #{path}"
260
+ end
261
+
179
262
  def remove_rbnext!
180
263
  return if CLI.dry_run? || stdout?
181
264
 
@@ -185,8 +268,21 @@ module RubyNext
185
268
  FileUtils.rm_r(next_dir_path)
186
269
  end
187
270
 
271
+ def ensure_rbnext!
272
+ return if CLI.dry_run? || stdout?
273
+
274
+ return if File.directory?(next_dir_path)
275
+
276
+ return if next_dir_path.end_with?(".rb")
277
+
278
+ return if overwrite?
279
+
280
+ FileUtils.mkdir_p next_dir_path
281
+ File.write(File.join(next_dir_path, ".keep"), "")
282
+ end
283
+
188
284
  def next_dir_path
189
- @next_dir_path ||= (out_path || File.join(lib_path, RUBY_NEXT_DIR))
285
+ @next_dir_path ||= out_path || File.join(lib_path, RUBY_NEXT_DIR)
190
286
  end
191
287
 
192
288
  def stdout?
@@ -10,17 +10,19 @@ module RubyNext
10
10
  # Defines last minor version for every major version
11
11
  LAST_MINOR_VERSIONS = {
12
12
  2 => 8, # 2.8 is required for backward compatibility: some gems already uses it
13
- 3 => 1
13
+ 3 => 4
14
14
  }.freeze
15
15
 
16
- LATEST_VERSION = [3, 1].freeze
16
+ LATEST_VERSION = [3, 4].freeze
17
17
 
18
18
  # A virtual version number used for proposed features
19
19
  NEXT_VERSION = "1995.next.0"
20
20
 
21
21
  class << self
22
- # TruffleRuby claims it's 2.7.2 compatible but...
23
- if defined?(TruffleRuby) && ::RUBY_VERSION =~ /^2\.7/
22
+ # TruffleRuby claims its RUBY_VERSION to be X.Y while not supporting all the features
23
+ # Currently (23.0.1), it still doesn't support pattern matching, although claims to be "like 3.1".
24
+ # So, we fallback to 2.6.5 (since we cannot use 2.7).
25
+ if defined?(TruffleRuby)
24
26
  def current_ruby_version
25
27
  "2.6.5"
26
28
  end
@@ -30,6 +32,15 @@ module RubyNext
30
32
  end
31
33
  end
32
34
 
35
+ # Returns true if we want to use edge syntax
36
+ def edge_syntax?
37
+ %w[y true 1].include?(ENV["RUBY_NEXT_EDGE"])
38
+ end
39
+
40
+ def proposed_syntax?
41
+ %w[y true 1].include?(ENV["RUBY_NEXT_PROPOSED"])
42
+ end
43
+
33
44
  def next_ruby_version(version = current_ruby_version)
34
45
  return if version == Gem::Version.new(NEXT_VERSION)
35
46
 
@@ -46,5 +57,21 @@ module RubyNext
46
57
 
47
58
  Gem::Version.new(nxt)
48
59
  end
60
+
61
+ # Load transpile settings from the RC file (nextify command flags)
62
+ def load_from_rc(path = ".rbnextrc")
63
+ return unless File.exist?(path)
64
+
65
+ require "yaml"
66
+
67
+ args = YAML.load_file(path)&.fetch("nextify", "")&.lines&.flat_map { |line| line.chomp.split(/\s+/) }
68
+
69
+ ENV["RUBY_NEXT_EDGE"] ||= "true" if args.delete("--edge")
70
+ ENV["RUBY_NEXT_PROPOSED"] ||= "true" if args.delete("--proposed")
71
+ ENV["RUBY_NEXT_TRANSPILE_MODE"] ||= "rewrite" if args.delete("--transpile-mode=rewrite")
72
+ ENV["RUBY_NEXT_TRANSPILE_MODE"] ||= "ast" if args.delete("--transpile-mode=ast")
73
+ end
49
74
  end
75
+
76
+ load_from_rc
50
77
  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..].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}" }.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}" }.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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyNext::Core.patch MatchData, method: :deconstruct, version: "3.2" do
4
+ <<-'RUBY'
5
+ def deconstruct
6
+ captures.map(&:to_str)
7
+ end
8
+ RUBY
9
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyNext::Core.patch MatchData, method: :deconstruct_keys, version: "3.2" do
4
+ <<-'RUBY'
5
+ def deconstruct_keys(keys)
6
+ raise TypeError, "wrong argument type #{keys.class} (expected Array)" if keys && !keys.is_a?(Array)
7
+
8
+ captured = named_captures.transform_keys!(&:to_sym)
9
+ return captured if keys.nil?
10
+
11
+ return {} if keys.size > captured.size
12
+
13
+ keys.each_with_object({}) do |k, acc|
14
+ raise TypeError, "wrong argument type #{k.class} (expected Symbol)" unless Symbol === k
15
+ return acc unless captured.key?(k)
16
+ acc[k] = self[k]
17
+ end
18
+ end
19
+ RUBY
20
+ end