ruby-next-core 0.8.0 → 0.10.0
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 +47 -0
- data/README.md +85 -11
- data/bin/transform +9 -1
- data/lib/.rbnext/2.3/ruby-next/commands/core_ext.rb +167 -0
- data/lib/.rbnext/2.3/ruby-next/commands/nextify.rb +198 -0
- data/lib/.rbnext/2.3/ruby-next/language/eval.rb +66 -0
- data/lib/.rbnext/2.3/ruby-next/language/rewriters/base.rb +121 -0
- data/lib/.rbnext/2.3/ruby-next/language/rewriters/endless_range.rb +63 -0
- data/lib/.rbnext/2.3/ruby-next/language/rewriters/pattern_matching.rb +944 -0
- data/lib/.rbnext/2.3/ruby-next/utils.rb +65 -0
- data/lib/ruby-next.rb +8 -5
- data/lib/ruby-next/cli.rb +2 -2
- data/lib/ruby-next/commands/core_ext.rb +2 -2
- data/lib/ruby-next/commands/nextify.rb +64 -22
- data/lib/ruby-next/core.rb +39 -22
- data/lib/ruby-next/core/array/deconstruct.rb +9 -9
- data/lib/ruby-next/core/array/difference_union_intersection.rb +12 -12
- data/lib/ruby-next/core/constants/no_matching_pattern_error.rb +3 -3
- data/lib/ruby-next/core/enumerable/filter.rb +8 -8
- data/lib/ruby-next/core/enumerable/filter_map.rb +25 -25
- data/lib/ruby-next/core/enumerable/tally.rb +7 -7
- data/lib/ruby-next/core/enumerator/produce.rb +12 -12
- data/lib/ruby-next/core/hash/deconstruct_keys.rb +9 -9
- data/lib/ruby-next/core/hash/except.rb +11 -0
- data/lib/ruby-next/core/hash/merge.rb +8 -8
- data/lib/ruby-next/core/kernel/then.rb +2 -2
- data/lib/ruby-next/core/proc/compose.rb +11 -11
- data/lib/ruby-next/core/string/split.rb +6 -6
- data/lib/ruby-next/core/struct/deconstruct.rb +2 -2
- data/lib/ruby-next/core/struct/deconstruct_keys.rb +17 -17
- data/lib/ruby-next/core/symbol/end_with.rb +4 -4
- data/lib/ruby-next/core/symbol/start_with.rb +4 -4
- data/lib/ruby-next/core/time/ceil.rb +6 -5
- data/lib/ruby-next/core/time/floor.rb +4 -4
- data/lib/ruby-next/core/unboundmethod/bind_call.rb +4 -4
- data/lib/ruby-next/core_ext.rb +2 -2
- data/lib/ruby-next/language.rb +31 -5
- data/lib/ruby-next/language/eval.rb +10 -8
- data/lib/ruby-next/language/proposed.rb +3 -0
- data/lib/ruby-next/language/rewriters/args_forward.rb +24 -20
- data/lib/ruby-next/language/rewriters/base.rb +2 -2
- data/lib/ruby-next/language/rewriters/endless_method.rb +26 -3
- data/lib/ruby-next/language/rewriters/endless_range.rb +1 -0
- data/lib/ruby-next/language/rewriters/find_pattern.rb +44 -0
- data/lib/ruby-next/language/rewriters/method_reference.rb +2 -1
- data/lib/ruby-next/language/rewriters/numbered_params.rb +1 -0
- data/lib/ruby-next/language/rewriters/pattern_matching.rb +105 -13
- data/lib/ruby-next/language/rewriters/right_hand_assignment.rb +2 -1
- data/lib/ruby-next/language/rewriters/runtime.rb +6 -0
- data/lib/ruby-next/language/rewriters/runtime/dir.rb +32 -0
- data/lib/ruby-next/language/rewriters/safe_navigation.rb +87 -0
- data/lib/ruby-next/language/rewriters/shorthand_hash.rb +47 -0
- data/lib/ruby-next/language/rewriters/squiggly_heredoc.rb +36 -0
- data/lib/ruby-next/language/runtime.rb +3 -2
- data/lib/ruby-next/logging.rb +1 -1
- data/lib/ruby-next/rubocop.rb +15 -9
- data/lib/ruby-next/setup_self.rb +22 -0
- data/lib/ruby-next/utils.rb +30 -0
- data/lib/ruby-next/version.rb +1 -1
- data/lib/uby-next.rb +8 -4
- metadata +22 -7
@@ -0,0 +1,198 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
require "ruby-next/language"
|
7
|
+
|
8
|
+
module RubyNext
|
9
|
+
module Commands
|
10
|
+
class Nextify < Base
|
11
|
+
using RubyNext
|
12
|
+
|
13
|
+
attr_reader :lib_path, :paths, :out_path, :min_version, :single_version, :specified_rewriters
|
14
|
+
|
15
|
+
def run
|
16
|
+
log "RubyNext core strategy: #{RubyNext::Core.strategy}"
|
17
|
+
log "RubyNext transpile mode: #{RubyNext::Language.mode}"
|
18
|
+
|
19
|
+
remove_rbnext!
|
20
|
+
|
21
|
+
@min_version ||= MIN_SUPPORTED_VERSION
|
22
|
+
|
23
|
+
paths.each do |path|
|
24
|
+
contents = File.read(path)
|
25
|
+
transpile path, contents
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def parse!(args)
|
30
|
+
print_help = false
|
31
|
+
print_rewriters = false
|
32
|
+
rewriter_names = []
|
33
|
+
@single_version = false
|
34
|
+
|
35
|
+
optparser = base_parser do |opts|
|
36
|
+
opts.banner = "Usage: ruby-next nextify DIRECTORY_OR_FILE [options]"
|
37
|
+
|
38
|
+
opts.on("-o", "--output=OUTPUT", "Specify output directory or file or stdout") do |val|
|
39
|
+
@out_path = val
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on("--min-version=VERSION", "Specify the minimum Ruby version to support") do |val|
|
43
|
+
@min_version = Gem::Version.new(val)
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on("--single-version", "Only create one version of a file (for the earliest Ruby version)") do
|
47
|
+
@single_version = true
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on("--edge", "Enable edge (master) Ruby features") do |val|
|
51
|
+
require "ruby-next/language/edge"
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on("--proposed", "Enable proposed/experimental Ruby features") do |val|
|
55
|
+
require "ruby-next/language/proposed"
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on(
|
59
|
+
"--transpile-mode=MODE",
|
60
|
+
"Transpiler mode (ast or rewrite). Default: ast"
|
61
|
+
) do |val|
|
62
|
+
Language.mode = val.to_sym
|
63
|
+
end
|
64
|
+
|
65
|
+
opts.on("--[no-]refine", "Do not inject `using RubyNext`") do |val|
|
66
|
+
Core.strategy = :core_ext unless val
|
67
|
+
end
|
68
|
+
|
69
|
+
opts.on("--list-rewriters", "List available rewriters") do |val|
|
70
|
+
print_rewriters = true
|
71
|
+
end
|
72
|
+
|
73
|
+
opts.on("--rewrite=REWRITERS...", "Specify particular Ruby features to rewrite") do |val|
|
74
|
+
rewriter_names << val
|
75
|
+
end
|
76
|
+
|
77
|
+
opts.on("-h", "--help", "Print help") do
|
78
|
+
print_help = true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
optparser.parse!(args)
|
83
|
+
|
84
|
+
@lib_path = args[0]
|
85
|
+
|
86
|
+
if print_help
|
87
|
+
$stdout.puts optparser.help
|
88
|
+
exit 0
|
89
|
+
end
|
90
|
+
|
91
|
+
if print_rewriters
|
92
|
+
Language.rewriters.each do |rewriter|
|
93
|
+
$stdout.puts "#{rewriter::NAME} (\"#{rewriter::SYNTAX_PROBE}\")"
|
94
|
+
end
|
95
|
+
exit 0
|
96
|
+
end
|
97
|
+
|
98
|
+
unless ((!lib_path.nil?) || nil) && lib_path.then(&File.method(:exist?))
|
99
|
+
$stdout.puts "Path not found: #{lib_path}"
|
100
|
+
$stdout.puts optparser.help
|
101
|
+
exit 2
|
102
|
+
end
|
103
|
+
|
104
|
+
if rewriter_names.any? && min_version
|
105
|
+
$stdout.puts "--rewrite cannot be used with --min-version simultaneously"
|
106
|
+
exit 2
|
107
|
+
end
|
108
|
+
|
109
|
+
@specified_rewriters =
|
110
|
+
if rewriter_names.any?
|
111
|
+
begin
|
112
|
+
Language.select_rewriters(*rewriter_names)
|
113
|
+
rescue Language::RewriterNotFoundError => error
|
114
|
+
$stdout.puts error.message
|
115
|
+
$stdout.puts "Try --list-rewriters to see list of available rewriters"
|
116
|
+
exit 2
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
@paths =
|
121
|
+
if File.directory?(lib_path)
|
122
|
+
Dir[File.join(lib_path, "**/*.rb")]
|
123
|
+
elsif File.file?(lib_path)
|
124
|
+
[lib_path].tap do |_|
|
125
|
+
@lib_path = File.dirname(lib_path)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def transpile(path, contents, version: min_version)
|
133
|
+
rewriters = specified_rewriters || Language.rewriters.select { |rw| rw.unsupported_version?(version) }
|
134
|
+
|
135
|
+
context = Language::TransformContext.new
|
136
|
+
|
137
|
+
new_contents = Language.transform contents, context: context, rewriters: rewriters
|
138
|
+
|
139
|
+
return unless context.dirty?
|
140
|
+
|
141
|
+
versions = context.sorted_versions
|
142
|
+
version = versions.shift
|
143
|
+
|
144
|
+
# First, store already transpiled contents in the minimum required version dir
|
145
|
+
save new_contents, path, version
|
146
|
+
|
147
|
+
return if versions.empty? || single_version?
|
148
|
+
|
149
|
+
# Then, generate the source code for the next version
|
150
|
+
transpile path, contents, version: version
|
151
|
+
end
|
152
|
+
|
153
|
+
def save(contents, path, version)
|
154
|
+
return $stdout.puts(contents) if stdout?
|
155
|
+
|
156
|
+
paths = [Pathname.new(path).relative_path_from(Pathname.new(lib_path))]
|
157
|
+
|
158
|
+
paths.unshift(version.segments[0..1].join(".")) unless single_version?
|
159
|
+
|
160
|
+
next_path =
|
161
|
+
if next_dir_path.end_with?(".rb")
|
162
|
+
out_path
|
163
|
+
else
|
164
|
+
File.join(next_dir_path, *paths)
|
165
|
+
end
|
166
|
+
|
167
|
+
unless CLI.dry_run?
|
168
|
+
FileUtils.mkdir_p File.dirname(next_path)
|
169
|
+
|
170
|
+
File.write(next_path, contents)
|
171
|
+
end
|
172
|
+
|
173
|
+
log "Generated: #{next_path}"
|
174
|
+
end
|
175
|
+
|
176
|
+
def remove_rbnext!
|
177
|
+
return if CLI.dry_run? || stdout?
|
178
|
+
|
179
|
+
return unless File.directory?(next_dir_path)
|
180
|
+
|
181
|
+
log "Remove old files: #{next_dir_path}"
|
182
|
+
FileUtils.rm_r(next_dir_path)
|
183
|
+
end
|
184
|
+
|
185
|
+
def next_dir_path
|
186
|
+
@next_dir_path ||= (out_path || File.join(lib_path, RUBY_NEXT_DIR))
|
187
|
+
end
|
188
|
+
|
189
|
+
def stdout?
|
190
|
+
out_path == "stdout"
|
191
|
+
end
|
192
|
+
|
193
|
+
def single_version?
|
194
|
+
single_version || specified_rewriters
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyNext
|
4
|
+
module Language
|
5
|
+
module KernelEval
|
6
|
+
if Utils.refine_modules?
|
7
|
+
refine Kernel do
|
8
|
+
def eval(source, bind = nil, *args)
|
9
|
+
new_source = ::RubyNext::Language::Runtime.transform(
|
10
|
+
source,
|
11
|
+
using: ((!bind.nil?) || nil) && bind.receiver == TOPLEVEL_BINDING.receiver || ((!(((!bind.nil?) || nil) && bind.receiver).nil?) || nil) && (((!bind.nil?) || nil) && bind.receiver).is_a?(Module)
|
12
|
+
)
|
13
|
+
RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
|
14
|
+
super new_source, bind, *args
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module InstanceEval # :nodoc:
|
21
|
+
refine Object do
|
22
|
+
def instance_eval(*args, &block)
|
23
|
+
return super(*args, &block) if block_given?
|
24
|
+
|
25
|
+
source = args.shift
|
26
|
+
new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
|
27
|
+
RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
|
28
|
+
super new_source, *args
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module ClassEval
|
34
|
+
refine Module do
|
35
|
+
def module_eval(*args, &block)
|
36
|
+
return super(*args, &block) if block_given?
|
37
|
+
|
38
|
+
source = args.shift
|
39
|
+
new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
|
40
|
+
RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
|
41
|
+
super new_source, *args
|
42
|
+
end
|
43
|
+
|
44
|
+
def class_eval(*args, &block)
|
45
|
+
return super(*args, &block) if block_given?
|
46
|
+
|
47
|
+
source = args.shift
|
48
|
+
new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
|
49
|
+
RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
|
50
|
+
super new_source, *args
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Refinements for `eval`-like methods.
|
56
|
+
# Transpiling eval is only possible if we do not use local from the binding,
|
57
|
+
# because we cannot access the binding of caller (without non-production ready hacks).
|
58
|
+
#
|
59
|
+
# This module is meant mainly for testing purposes.
|
60
|
+
module Eval
|
61
|
+
include InstanceEval
|
62
|
+
include ClassEval
|
63
|
+
include KernelEval
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyNext
|
4
|
+
module Language
|
5
|
+
module Rewriters
|
6
|
+
using RubyNext
|
7
|
+
|
8
|
+
CUSTOM_PARSER_REQUIRED = <<-MSG
|
9
|
+
The %s feature is not a part of the latest stable Ruby release
|
10
|
+
and is not supported by your Parser gem version.
|
11
|
+
Use RubyNext's parser to use it: https://github.com/ruby-next/parser
|
12
|
+
|
13
|
+
MSG
|
14
|
+
|
15
|
+
class Base < ::Parser::TreeRewriter
|
16
|
+
class LocalsTracker
|
17
|
+
attr_reader :stacks
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@stacks = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def with(**locals)
|
24
|
+
stacks << locals
|
25
|
+
yield.tap { stacks.pop }
|
26
|
+
end
|
27
|
+
|
28
|
+
def [](name, suffix = nil)
|
29
|
+
fetch(name).then do |name|
|
30
|
+
next name unless suffix
|
31
|
+
:"#{name}#{suffix}__"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def key?(name)
|
36
|
+
!!fetch(name) { false } # rubocop:disable Style/RedundantFetchBlock
|
37
|
+
end
|
38
|
+
|
39
|
+
def fetch(name)
|
40
|
+
ind = -1
|
41
|
+
|
42
|
+
loop do
|
43
|
+
break stacks[ind][name] if stacks[ind].key?(name)
|
44
|
+
ind -= 1
|
45
|
+
break if stacks[ind].nil?
|
46
|
+
end.then do |name|
|
47
|
+
next name unless name.nil?
|
48
|
+
|
49
|
+
return yield if block_given?
|
50
|
+
raise ArgumentError, "Local var not found in scope: #{name}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class << self
|
56
|
+
# Returns true if the syntax is supported
|
57
|
+
# by the current Ruby (performs syntax check, not version check)
|
58
|
+
def unsupported_syntax?
|
59
|
+
save_verbose, $VERBOSE = $VERBOSE, nil
|
60
|
+
eval_mid = Kernel.respond_to?(:eval_without_ruby_next) ? :eval_without_ruby_next : :eval
|
61
|
+
Kernel.send eval_mid, self::SYNTAX_PROBE, nil, __FILE__, __LINE__
|
62
|
+
false
|
63
|
+
rescue SyntaxError, NameError
|
64
|
+
true
|
65
|
+
ensure
|
66
|
+
$VERBOSE = save_verbose
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns true if the syntax is supported
|
70
|
+
# by the specified version
|
71
|
+
def unsupported_version?(version)
|
72
|
+
self::MIN_SUPPORTED_VERSION > version
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def transform(source)
|
78
|
+
Language.transform(source, rewriters: [self], using: false)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
attr_reader :locals
|
83
|
+
|
84
|
+
def initialize(context)
|
85
|
+
@context = context
|
86
|
+
@locals = LocalsTracker.new
|
87
|
+
super()
|
88
|
+
end
|
89
|
+
|
90
|
+
def s(type, *children)
|
91
|
+
::Parser::AST::Node.new(type, children)
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def replace(range, ast)
|
97
|
+
((!@source_rewriter.nil?) || nil) && @source_rewriter.replace(range, unparse(ast))
|
98
|
+
end
|
99
|
+
|
100
|
+
def remove(range)
|
101
|
+
((!@source_rewriter.nil?) || nil) && @source_rewriter.remove(range)
|
102
|
+
end
|
103
|
+
|
104
|
+
def insert_after(range, ast)
|
105
|
+
((!@source_rewriter.nil?) || nil) && @source_rewriter.insert_after(range, unparse(ast))
|
106
|
+
end
|
107
|
+
|
108
|
+
def insert_before(range, ast)
|
109
|
+
((!@source_rewriter.nil?) || nil) && @source_rewriter.insert_before(range, unparse(ast))
|
110
|
+
end
|
111
|
+
|
112
|
+
def unparse(ast)
|
113
|
+
return ast if ast.is_a?(String)
|
114
|
+
Unparser.unparse(ast)
|
115
|
+
end
|
116
|
+
|
117
|
+
attr_reader :context
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyNext
|
4
|
+
module Language
|
5
|
+
module Rewriters
|
6
|
+
class EndlessRange < Base
|
7
|
+
NAME = "endless-range"
|
8
|
+
SYNTAX_PROBE = "[0, 1][1..]"
|
9
|
+
MIN_SUPPORTED_VERSION = Gem::Version.new("2.6.0")
|
10
|
+
|
11
|
+
def on_index(node)
|
12
|
+
@current_index = node
|
13
|
+
new_index = process(node.children.last)
|
14
|
+
return unless new_index != node.children.last
|
15
|
+
|
16
|
+
node.updated(
|
17
|
+
nil,
|
18
|
+
[
|
19
|
+
node.children.first,
|
20
|
+
new_index
|
21
|
+
]
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_erange(node)
|
26
|
+
return unless node.children.last.nil?
|
27
|
+
|
28
|
+
context.track! self
|
29
|
+
|
30
|
+
new_end =
|
31
|
+
if index_arg?(node)
|
32
|
+
s(:int, -1)
|
33
|
+
else
|
34
|
+
s(:const,
|
35
|
+
s(:const,
|
36
|
+
s(:cbase), :Float),
|
37
|
+
:INFINITY)
|
38
|
+
end
|
39
|
+
|
40
|
+
replace(node.loc.expression, "#{node.children.first.loc.expression.source}..#{unparse(new_end)}")
|
41
|
+
|
42
|
+
node.updated(
|
43
|
+
:irange,
|
44
|
+
[
|
45
|
+
node.children.first,
|
46
|
+
new_end
|
47
|
+
]
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
alias_method :on_irange, :on_erange
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
attr_reader :current_index
|
56
|
+
|
57
|
+
def index_arg?(node)
|
58
|
+
((!(((!current_index.nil?) || nil) && current_index.children).nil?) || nil) && (((!current_index.nil?) || nil) && current_index.children).include?(node)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|