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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +127 -54
- 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 +59 -12
- data/lib/.rbnext/2.3/ruby-next/commands/nextify.rb +83 -3
- data/lib/.rbnext/2.3/ruby-next/config.rb +79 -0
- data/lib/.rbnext/2.3/ruby-next/core/data.rb +163 -0
- data/lib/.rbnext/2.3/ruby-next/language/eval.rb +4 -4
- 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 +163 -0
- data/lib/.rbnext/2.7/ruby-next/core/data.rb +163 -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 +85 -3
- data/lib/ruby-next/config.rb +29 -2
- data/lib/ruby-next/core/data.rb +163 -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/eval.rb +4 -4
- 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 +31 -6
- 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.1/shorthand_hash.rb +2 -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 +59 -12
- 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 +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
|
-
|
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 ||=
|
285
|
+
@next_dir_path ||= out_path || File.join(lib_path, RUBY_NEXT_DIR)
|
204
286
|
end
|
205
287
|
|
206
288
|
def stdout?
|
data/lib/ruby-next/config.rb
CHANGED
@@ -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 =>
|
13
|
+
3 => 4
|
14
14
|
}.freeze
|
15
15
|
|
16
|
-
LATEST_VERSION = [3,
|
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,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
|