ruby-next-core 0.15.3 → 1.0.0.rc.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +118 -48
- data/bin/mspec +11 -0
- data/lib/.rbnext/2.1/ruby-next/commands/nextify.rb +295 -0
- data/lib/.rbnext/2.1/ruby-next/core.rb +10 -2
- data/lib/.rbnext/2.1/ruby-next/language.rb +54 -10
- data/lib/.rbnext/2.3/ruby-next/commands/nextify.rb +82 -2
- data/lib/.rbnext/2.3/ruby-next/config.rb +79 -0
- data/lib/.rbnext/2.3/ruby-next/core/data.rb +159 -0
- data/lib/.rbnext/2.3/ruby-next/language/rewriters/2.7/pattern_matching.rb +2 -2
- data/lib/.rbnext/2.3/ruby-next/language/rewriters/base.rb +6 -32
- data/lib/.rbnext/2.3/ruby-next/utils.rb +3 -22
- data/lib/.rbnext/2.6/ruby-next/core/data.rb +159 -0
- data/lib/.rbnext/2.7/ruby-next/core/data.rb +159 -0
- data/lib/.rbnext/2.7/ruby-next/core.rb +10 -2
- data/lib/.rbnext/2.7/ruby-next/language/paco_parsers/string_literals.rb +109 -0
- data/lib/.rbnext/2.7/ruby-next/language/rewriters/2.7/pattern_matching.rb +2 -2
- data/lib/.rbnext/2.7/ruby-next/language/rewriters/text.rb +132 -0
- data/lib/.rbnext/3.2/ruby-next/commands/base.rb +55 -0
- data/lib/.rbnext/3.2/ruby-next/language/rewriters/2.7/pattern_matching.rb +1095 -0
- data/lib/.rbnext/3.2/ruby-next/rubocop.rb +210 -0
- data/lib/ruby-next/commands/nextify.rb +84 -2
- data/lib/ruby-next/config.rb +27 -0
- data/lib/ruby-next/core/data.rb +159 -0
- data/lib/ruby-next/core/matchdata/deconstruct.rb +9 -0
- data/lib/ruby-next/core/matchdata/deconstruct_keys.rb +20 -0
- data/lib/ruby-next/core/matchdata/named_captures.rb +11 -0
- data/lib/ruby-next/core/refinement/import.rb +44 -36
- data/lib/ruby-next/core/time/deconstruct_keys.rb +30 -0
- data/lib/ruby-next/core.rb +10 -2
- data/lib/ruby-next/irb.rb +2 -2
- data/lib/ruby-next/language/bootsnap.rb +2 -25
- data/lib/ruby-next/language/paco_parser.rb +7 -0
- data/lib/ruby-next/language/paco_parsers/base.rb +47 -0
- data/lib/ruby-next/language/paco_parsers/comments.rb +26 -0
- data/lib/ruby-next/language/paco_parsers/string_literals.rb +109 -0
- data/lib/ruby-next/language/parser.rb +24 -2
- data/lib/ruby-next/language/rewriters/3.0/args_forward_leading.rb +2 -2
- data/lib/ruby-next/language/rewriters/3.1/oneline_pattern_parensless.rb +1 -1
- data/lib/ruby-next/language/rewriters/3.2/anonymous_restargs.rb +104 -0
- data/lib/ruby-next/language/rewriters/abstract.rb +57 -0
- data/lib/ruby-next/language/rewriters/base.rb +6 -32
- data/lib/ruby-next/language/rewriters/edge/it_param.rb +58 -0
- data/lib/ruby-next/language/rewriters/edge.rb +12 -0
- data/lib/ruby-next/language/rewriters/proposed/bind_vars_pattern.rb +3 -0
- data/lib/ruby-next/language/rewriters/proposed/method_reference.rb +9 -20
- data/lib/ruby-next/language/rewriters/text.rb +132 -0
- data/lib/ruby-next/language/runtime.rb +9 -86
- data/lib/ruby-next/language/setup.rb +5 -2
- data/lib/ruby-next/language/unparser.rb +5 -0
- data/lib/ruby-next/language.rb +54 -10
- data/lib/ruby-next/pry.rb +1 -1
- data/lib/ruby-next/rubocop.rb +2 -0
- data/lib/ruby-next/utils.rb +3 -22
- data/lib/ruby-next/version.rb +1 -1
- data/lib/uby-next.rb +2 -2
- metadata +65 -12
@@ -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
|
-
|
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,6 +275,8 @@ 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
|
data/lib/ruby-next/config.rb
CHANGED
@@ -20,6 +20,8 @@ module RubyNext
|
|
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,159 @@
|
|
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 members
|
70
|
+
self.class.members
|
71
|
+
end
|
72
|
+
|
73
|
+
def initialize(**kwargs)
|
74
|
+
kwargs_size = kwargs.size
|
75
|
+
members_size = members.size
|
76
|
+
|
77
|
+
if kwargs_size > members_size
|
78
|
+
extras = kwargs.except(*members).keys
|
79
|
+
|
80
|
+
if extras.size > 1
|
81
|
+
raise ArgumentError, "unknown keywords: #{extras.map { ":#{_1}" }.join(", ")}"
|
82
|
+
else
|
83
|
+
raise ArgumentError, "unknown keyword: :#{extras.first}"
|
84
|
+
end
|
85
|
+
elsif kwargs_size < members_size
|
86
|
+
missing = members.select { |k| !kwargs.include?(k) }
|
87
|
+
|
88
|
+
if missing.size > 1
|
89
|
+
raise ArgumentError, "missing keywords: #{missing.map { ":#{_1}" }.join(", ")}"
|
90
|
+
else
|
91
|
+
raise ArgumentError, "missing keyword: :#{missing.first}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
@attributes = members.map { |m| [m, kwargs[m]] }.to_h
|
96
|
+
end
|
97
|
+
|
98
|
+
def deconstruct
|
99
|
+
@attributes.values
|
100
|
+
end
|
101
|
+
|
102
|
+
def deconstruct_keys(array)
|
103
|
+
raise TypeError unless array.is_a?(Array) || array.nil?
|
104
|
+
return @attributes if array&.first.nil?
|
105
|
+
|
106
|
+
@attributes.slice(*array)
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_h(&block)
|
110
|
+
@attributes.to_h(&block)
|
111
|
+
end
|
112
|
+
|
113
|
+
def hash
|
114
|
+
to_h.hash
|
115
|
+
end
|
116
|
+
|
117
|
+
def eql?(other)
|
118
|
+
self.class == other.class && hash == other.hash
|
119
|
+
end
|
120
|
+
|
121
|
+
def ==(other)
|
122
|
+
self.class == other.class && to_h == other.to_h
|
123
|
+
end
|
124
|
+
|
125
|
+
def inspect
|
126
|
+
attribute_markers = @attributes.map do |key, value|
|
127
|
+
insect_key = key.to_s.start_with?("@") ? ":#{key}" : key
|
128
|
+
"#{insect_key}=#{value}"
|
129
|
+
end.join(", ")
|
130
|
+
|
131
|
+
display = ["data", self.class.name, attribute_markers].compact.join(" ")
|
132
|
+
|
133
|
+
"#<#{display}>"
|
134
|
+
end
|
135
|
+
alias_method :to_s, :inspect
|
136
|
+
|
137
|
+
def with(**kwargs)
|
138
|
+
return self if kwargs.empty?
|
139
|
+
|
140
|
+
self.class.new(**@attributes.merge(kwargs))
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def marshal_dump
|
146
|
+
@attributes
|
147
|
+
end
|
148
|
+
|
149
|
+
def marshal_load(attributes)
|
150
|
+
@attributes = attributes
|
151
|
+
freeze
|
152
|
+
end
|
153
|
+
|
154
|
+
def initialize_copy(source)
|
155
|
+
super.freeze
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
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
|