ruby-next-core 0.15.3 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +127 -54
  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 +10 -2
  7. data/lib/.rbnext/2.1/ruby-next/language.rb +59 -12
  8. data/lib/.rbnext/2.3/ruby-next/commands/nextify.rb +83 -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/pattern_matching.rb +2 -2
  13. data/lib/.rbnext/2.3/ruby-next/language/rewriters/base.rb +6 -32
  14. data/lib/.rbnext/2.3/ruby-next/utils.rb +3 -22
  15. data/lib/.rbnext/2.6/ruby-next/core/data.rb +163 -0
  16. data/lib/.rbnext/2.7/ruby-next/core/data.rb +163 -0
  17. data/lib/.rbnext/2.7/ruby-next/core.rb +10 -2
  18. data/lib/.rbnext/2.7/ruby-next/language/paco_parsers/string_literals.rb +109 -0
  19. data/lib/.rbnext/2.7/ruby-next/language/rewriters/2.7/pattern_matching.rb +2 -2
  20. data/lib/.rbnext/2.7/ruby-next/language/rewriters/text.rb +132 -0
  21. data/lib/.rbnext/3.2/ruby-next/commands/base.rb +55 -0
  22. data/lib/.rbnext/3.2/ruby-next/language/rewriters/2.7/pattern_matching.rb +1095 -0
  23. data/lib/.rbnext/3.2/ruby-next/rubocop.rb +210 -0
  24. data/lib/ruby-next/commands/nextify.rb +85 -3
  25. data/lib/ruby-next/config.rb +29 -2
  26. data/lib/ruby-next/core/data.rb +163 -0
  27. data/lib/ruby-next/core/matchdata/deconstruct.rb +9 -0
  28. data/lib/ruby-next/core/matchdata/deconstruct_keys.rb +20 -0
  29. data/lib/ruby-next/core/matchdata/named_captures.rb +11 -0
  30. data/lib/ruby-next/core/refinement/import.rb +44 -36
  31. data/lib/ruby-next/core/time/deconstruct_keys.rb +30 -0
  32. data/lib/ruby-next/core.rb +10 -2
  33. data/lib/ruby-next/irb.rb +2 -2
  34. data/lib/ruby-next/language/bootsnap.rb +2 -25
  35. data/lib/ruby-next/language/eval.rb +4 -4
  36. data/lib/ruby-next/language/paco_parser.rb +7 -0
  37. data/lib/ruby-next/language/paco_parsers/base.rb +47 -0
  38. data/lib/ruby-next/language/paco_parsers/comments.rb +26 -0
  39. data/lib/ruby-next/language/paco_parsers/string_literals.rb +109 -0
  40. data/lib/ruby-next/language/parser.rb +31 -6
  41. data/lib/ruby-next/language/rewriters/3.0/args_forward_leading.rb +2 -2
  42. data/lib/ruby-next/language/rewriters/3.1/oneline_pattern_parensless.rb +1 -1
  43. data/lib/ruby-next/language/rewriters/3.1/shorthand_hash.rb +2 -1
  44. data/lib/ruby-next/language/rewriters/3.2/anonymous_restargs.rb +104 -0
  45. data/lib/ruby-next/language/rewriters/abstract.rb +57 -0
  46. data/lib/ruby-next/language/rewriters/base.rb +6 -32
  47. data/lib/ruby-next/language/rewriters/edge/it_param.rb +58 -0
  48. data/lib/ruby-next/language/rewriters/edge.rb +12 -0
  49. data/lib/ruby-next/language/rewriters/proposed/bind_vars_pattern.rb +3 -0
  50. data/lib/ruby-next/language/rewriters/proposed/method_reference.rb +9 -20
  51. data/lib/ruby-next/language/rewriters/text.rb +132 -0
  52. data/lib/ruby-next/language/runtime.rb +9 -86
  53. data/lib/ruby-next/language/setup.rb +5 -2
  54. data/lib/ruby-next/language/unparser.rb +5 -0
  55. data/lib/ruby-next/language.rb +59 -12
  56. data/lib/ruby-next/pry.rb +1 -1
  57. data/lib/ruby-next/rubocop.rb +2 -0
  58. data/lib/ruby-next/utils.rb +3 -22
  59. data/lib/ruby-next/version.rb +1 -1
  60. data/lib/uby-next.rb +2 -2
  61. metadata +63 -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
@@ -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,18 +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
27
65
 
28
66
  ensure_rbnext!
67
+
68
+ log stats.report
29
69
  end
30
70
 
31
71
  def parse!(args)
32
72
  print_help = false
33
73
  print_rewriters = false
34
74
  rewriter_names = []
75
+ custom_rewriters = []
35
76
  @single_version = false
77
+ @overwrite = false
36
78
 
37
79
  optparser = base_parser do |opts|
38
80
  opts.banner = "Usage: ruby-next nextify DIRECTORY_OR_FILE [options]"
@@ -49,11 +91,17 @@ module RubyNext
49
91
  @single_version = true
50
92
  end
51
93
 
94
+ opts.on("--overwrite", "Overwrite original file") do
95
+ @overwrite = true
96
+ end
97
+
52
98
  opts.on("--edge", "Enable edge (master) Ruby features") do |val|
99
+ ENV["RUBY_NEXT_EDGE"] = val.to_s
53
100
  require "ruby-next/language/rewriters/edge"
54
101
  end
55
102
 
56
103
  opts.on("--proposed", "Enable proposed/experimental Ruby features") do |val|
104
+ ENV["RUBY_NEXT_PROPOSED"] = val.to_s
57
105
  require "ruby-next/language/rewriters/proposed"
58
106
  end
59
107
 
@@ -76,6 +124,10 @@ module RubyNext
76
124
  rewriter_names << val
77
125
  end
78
126
 
127
+ opts.on("--import-rewriter=REWRITERS...", "Specify paths to load custom rewritiers") do |val|
128
+ custom_rewriters << val
129
+ end
130
+
79
131
  opts.on("-h", "--help", "Print help") do
80
132
  print_help = true
81
133
  end
@@ -90,9 +142,14 @@ module RubyNext
90
142
  exit 0
91
143
  end
92
144
 
145
+ # Load custom rewriters
146
+ custom_rewriters.each do |path|
147
+ Kernel.load path
148
+ end
149
+
93
150
  if print_rewriters
94
151
  Language.rewriters.each do |rewriter|
95
- $stdout.puts "#{rewriter::NAME} (\"#{rewriter::SYNTAX_PROBE}\")"
152
+ $stdout.puts "#{rewriter::NAME} (\"#{rewriter::SYNTAX_PROBE}\")#{rewriter.unsupported_syntax? ? " (unsupported)" : ""}"
96
153
  end
97
154
  exit 0
98
155
  end
@@ -119,6 +176,11 @@ module RubyNext
119
176
  end
120
177
  end
121
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
+
122
184
  @paths =
123
185
  if File.directory?(lib_path)
124
186
  Dir[File.join(lib_path, "**/*.rb")]
@@ -132,6 +194,8 @@ module RubyNext
132
194
  private
133
195
 
134
196
  def transpile(path, contents, version: min_version)
197
+ stats.scan!
198
+
135
199
  rewriters = specified_rewriters || Language.rewriters.select { |rw| rw.unsupported_version?(version) }
136
200
 
137
201
  context = Language::TransformContext.new
@@ -157,12 +221,20 @@ module RubyNext
157
221
  end
158
222
 
159
223
  def save(contents, path, version)
224
+ stats.transpiled!
225
+
160
226
  return $stdout.puts(contents) if stdout?
161
227
 
162
228
  paths = [Pathname.new(path).relative_path_from(Pathname.new(lib_path))]
163
229
 
164
230
  paths.unshift(version.segments[0..1].join(".")) unless single_version?
165
231
 
232
+ if overwrite?
233
+ overwrite_file_content!(path: path, contents: contents)
234
+
235
+ return
236
+ end
237
+
166
238
  next_path =
167
239
  if next_dir_path.end_with?(".rb")
168
240
  out_path
@@ -179,6 +251,14 @@ module RubyNext
179
251
  log "Generated: #{next_path}"
180
252
  end
181
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
+
182
262
  def remove_rbnext!
183
263
  return if CLI.dry_run? || stdout?
184
264
 
@@ -195,12 +275,14 @@ module RubyNext
195
275
 
196
276
  return if next_dir_path.end_with?(".rb")
197
277
 
278
+ return if overwrite?
279
+
198
280
  FileUtils.mkdir_p next_dir_path
199
281
  File.write(File.join(next_dir_path, ".keep"), "")
200
282
  end
201
283
 
202
284
  def next_dir_path
203
- @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)
204
286
  end
205
287
 
206
288
  def stdout?
@@ -10,16 +10,18 @@ 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
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).
23
25
  if defined?(TruffleRuby)
24
26
  def current_ruby_version
25
27
  "2.6.5"
@@ -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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyNext::Core.patch MatchData, method: :named_captures, version: "3.3", supported: "a".match(/a/).method(:named_captures).arity != 0, core_ext: :prepend do
4
+ <<-'RUBY'
5
+ def named_captures(symbolize_names: false)
6
+ return super() unless symbolize_names
7
+
8
+ super().transform_keys!(&:to_sym)
9
+ end
10
+ RUBY
11
+ end