ruby-next-core 0.10.5 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +25 -16
- data/lib/.rbnext/2.1/ruby-next/core.rb +206 -0
- data/lib/.rbnext/2.1/ruby-next/language.rb +227 -0
- data/lib/.rbnext/2.3/ruby-next/commands/nextify.rb +2 -2
- data/lib/.rbnext/2.3/ruby-next/language/eval.rb +4 -4
- data/lib/.rbnext/2.3/ruby-next/language/rewriters/base.rb +23 -5
- data/lib/.rbnext/2.3/ruby-next/language/rewriters/endless_range.rb +1 -1
- data/lib/.rbnext/2.3/ruby-next/language/rewriters/pattern_matching.rb +20 -17
- data/lib/.rbnext/2.3/ruby-next/utils.rb +1 -1
- data/lib/.rbnext/2.7/ruby-next/core.rb +206 -0
- data/lib/ruby-next/commands/nextify.rb +1 -1
- data/lib/ruby-next/config.rb +50 -0
- data/lib/ruby-next/core/array/deconstruct.rb +1 -1
- data/lib/ruby-next/core/array/intersect.rb +9 -0
- data/lib/ruby-next/core/constants/frozen_error.rb +15 -0
- data/lib/ruby-next/core/constants/no_matching_pattern_error.rb +1 -1
- data/lib/ruby-next/core/enumerable/tally.rb +46 -7
- data/lib/ruby-next/core/hash/deconstruct_keys.rb +1 -1
- data/lib/ruby-next/core/struct/deconstruct_keys.rb +3 -3
- data/lib/ruby-next/core.rb +8 -4
- data/lib/ruby-next/core_ext.rb +3 -1
- data/lib/ruby-next/language/bootsnap.rb +1 -1
- data/lib/ruby-next/language/edge.rb +0 -6
- data/lib/ruby-next/language/eval.rb +3 -3
- data/lib/ruby-next/language/parser.rb +16 -0
- data/lib/ruby-next/language/rewriters/args_forward.rb +14 -6
- data/lib/ruby-next/language/rewriters/args_forward_leading.rb +75 -0
- data/lib/ruby-next/language/rewriters/base.rb +19 -1
- data/lib/ruby-next/language/rewriters/in_pattern.rb +56 -0
- data/lib/ruby-next/language/rewriters/method_reference.rb +1 -1
- data/lib/ruby-next/language/rewriters/numeric_literals.rb +41 -0
- data/lib/ruby-next/language/rewriters/pattern_matching.rb +18 -15
- data/lib/ruby-next/language/rewriters/required_kwargs.rb +39 -0
- data/lib/ruby-next/language/rewriters/safe_navigation.rb +42 -29
- data/lib/ruby-next/language/rewriters/shorthand_hash.rb +1 -1
- data/lib/ruby-next/language/setup.rb +7 -4
- data/lib/ruby-next/language/unparser.rb +5 -0
- data/lib/ruby-next/language.rb +62 -44
- data/lib/ruby-next/rubocop.rb +9 -18
- data/lib/ruby-next/setup_self.rb +5 -3
- data/lib/ruby-next/version.rb +1 -1
- data/lib/ruby-next.rb +5 -38
- metadata +20 -18
- data/lib/.rbnext/2.3/ruby-next/language/rewriters/right_hand_assignment.rb +0 -117
- data/lib/ruby-next/language/rewriters/right_hand_assignment.rb +0 -117
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1e73900618b0d9da915699c40c610736e19919ac0d11bce7c7dc58e7d5ddb84b
|
4
|
+
data.tar.gz: b07917f44a763467f4bed145ae1224ba8aeb03eaba7924b2787c681df4b704f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb7bc9bc3416a8b6df9e99f346fb15c2c0ae0a9d63bba739adaa6c604110eaa753a5d7b00d98443400e92d7233fba7b4fda2a43e866ce85ef04ed46cd7e89a4c
|
7
|
+
data.tar.gz: 0b6410607425aaa0afb66309fa07739e72e3b8cbf94addf85d501f136755159db968225b6d1752a1bda77911592252d48e2d2c5421dae5798d5741ef6ec33616
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,46 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 0.13.0 (2021-09-27)
|
6
|
+
|
7
|
+
- Added `Enumerable#tally` with the resulting hash. ([@skryukov][])
|
8
|
+
|
9
|
+
- Added `Array#intersect?`. ([@skryukov][])
|
10
|
+
|
11
|
+
## 0.12.0 (2021-01-12)
|
12
|
+
|
13
|
+
- Added required keyword arguments rewriter. ([@palkan][])
|
14
|
+
|
15
|
+
Required kwargs were introduced in 2.1. Now we make them possible in 2.0.
|
16
|
+
|
17
|
+
- Added numeric literals rewriter. ([@palkan][])
|
18
|
+
|
19
|
+
Now it's possible to generate Ruby 2.0 compatible code from `2i + 1/3r`.
|
20
|
+
|
21
|
+
- Fixed several safe navigation (`&.`) bugs. ([@palkan][])
|
22
|
+
|
23
|
+
See [#68](https://github.com/ruby-next/ruby-next/issues/68) and [#69](https://github.com/ruby-next/ruby-next/issues/69).
|
24
|
+
|
25
|
+
## 0.11.1 (2020-12-28)
|
26
|
+
|
27
|
+
- Use separate _namespace_ for proposed features to avoid conflicts with new Ruby version. ([@palkan][])
|
28
|
+
|
29
|
+
Previously, we used the upcoming Ruby version number for proposed features (e.g., `3.0.0`), which broke
|
30
|
+
the load path setup, since transpiled files were not loaded anymore.
|
31
|
+
Now that's fixed by using a _virtual_ version number for proposals (`1995.next.0`).
|
32
|
+
|
33
|
+
## 0.11.0 (2020-12-24) 🎄
|
34
|
+
|
35
|
+
- Extended proposed shorthand Hash syntax to support kwargs as well. ([@palkan][])
|
36
|
+
|
37
|
+
You can try it: `x = 1; y = 2; foo(x:, y:) # => foo(x: x, y: y)`.
|
38
|
+
|
39
|
+
- **Rewrite mode is used by default in transpiler**. ([@palkan][])
|
40
|
+
|
41
|
+
- Move 3.0 features to stable features. ([@palkan][])
|
42
|
+
|
43
|
+
- Refactor `a => x` and `a in x` to comply with Ruby 3.0. ([@palkan][])
|
44
|
+
|
5
45
|
## 0.10.5 (2020-10-13)
|
6
46
|
|
7
47
|
- Fix Unparser 0.5.0 compatibility. ([@palkan][])
|
@@ -247,3 +287,4 @@ p a #=> 1
|
|
247
287
|
[@palkan]: https://github.com/palkan
|
248
288
|
[backports]: https://github.com/marcandre/backports
|
249
289
|
[@sl4vr]: https://github.com/sl4vr
|
290
|
+
[@skryukov]: https://github.com/skryukov
|
data/README.md
CHANGED
@@ -21,8 +21,20 @@ That's why Ruby Next implements the `master` features as fast as possible.
|
|
21
21
|
|
22
22
|
Read more about the motivation behind the Ruby Next in this post: [Ruby Next: Make all Rubies quack alike](https://evilmartians.com/chronicles/ruby-next-make-all-rubies-quack-alike).
|
23
23
|
|
24
|
-
<
|
25
|
-
<
|
24
|
+
<table style="border:none;">
|
25
|
+
<tr>
|
26
|
+
<td>
|
27
|
+
<a href="https://evilmartians.com/?utm_source=ruby-next">
|
28
|
+
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54">
|
29
|
+
</a>
|
30
|
+
</td>
|
31
|
+
<td>
|
32
|
+
<a href="http://www.digitalfukuoka.jp/topics/169">
|
33
|
+
<img src="http://www.digitalfukuoka.jp/javascripts/kcfinder/upload/images/excellence.jpg" width="200">
|
34
|
+
</a>
|
35
|
+
</td>
|
36
|
+
</tr>
|
37
|
+
</table>
|
26
38
|
|
27
39
|
## Posts
|
28
40
|
|
@@ -93,8 +105,7 @@ def greet(val) =
|
|
93
105
|
'👽'
|
94
106
|
end
|
95
107
|
|
96
|
-
greet(hello: 'martian')
|
97
|
-
puts greeting
|
108
|
+
puts greet(hello: 'martian')
|
98
109
|
"
|
99
110
|
|
100
111
|
=> 👽
|
@@ -179,17 +190,17 @@ In the AST mode, we parse the source code into AST, modifies this AST and **gene
|
|
179
190
|
|
180
191
|
In the rewrite mode, we apply changes to the source code itself, thus, keeping the original formatting of the unaffected code (in a similar way to RuboCop's autocorrect feature).
|
181
192
|
|
182
|
-
By default, we use the AST mode. That could likely change in the future when we collect enough feedback on the rewrite mode and fix potential bugs.
|
183
|
-
|
184
193
|
The main benefit of the rewrite mode is that it preserves the original code line numbers and layout, which is especially useful in debugging.
|
185
194
|
|
195
|
+
By default, we use the rewrite mode. If you found a bug with rewrite mode which is not reproducible in the AST mode, please, let us know.
|
196
|
+
|
186
197
|
You can change the transpiler mode:
|
187
198
|
|
188
199
|
- From code by setting `RubyNext::Language.mode = :ast` or `RubyNext::Language.mode = :rewrite`.
|
189
|
-
- Via environmental variable `RUBY_NEXT_TRANSPILE_MODE=
|
200
|
+
- Via environmental variable `RUBY_NEXT_TRANSPILE_MODE=ast`.
|
190
201
|
- Via CLI option ([see below](#cli)).
|
191
202
|
|
192
|
-
**NOTE:** For the time being, Unparser
|
203
|
+
**NOTE:** For the time being, Unparser doesn't support Ruby 3.0 AST nodes, so we always use rewrite mode in Ruby 3.0+.
|
193
204
|
|
194
205
|
## CLI
|
195
206
|
|
@@ -220,7 +231,7 @@ Usage: ruby-next nextify DIRECTORY_OR_FILE [options]
|
|
220
231
|
|
221
232
|
The behaviour depends on whether you transpile a single file or a directory:
|
222
233
|
|
223
|
-
- When transpiling a directory, the `.rbnext` subfolder is created within the target folder with subfolders for each supported Ruby versions (e.g., `.rbnext/2.6`, `.rbnext/2.7`). If you want to create only a single version (the smallest), you can also pass `--single-version` flag. In that case, no version directory is created (i.e., transpiled files go into `.rbnext`).
|
234
|
+
- When transpiling a directory, the `.rbnext` subfolder is created within the target folder with subfolders for each supported Ruby versions (e.g., `.rbnext/2.6`, `.rbnext/2.7`, `.rbnext/3.0`). If you want to create only a single version (the smallest), you can also pass `--single-version` flag. In that case, no version directory is created (i.e., transpiled files go into `.rbnext`).
|
224
235
|
|
225
236
|
- When transpiling a file and providing the output path as a _file_ path, only a single version is created. For example:
|
226
237
|
|
@@ -323,7 +334,7 @@ due to the way feature resolving works in Ruby (scanning the `$LOAD_PATH` and ha
|
|
323
334
|
|
324
335
|
If you're using [runtime mode](#runtime-usage) a long with `setup_gem_load_path` (e.g., in tests), the transpiled files are ignored (i.e., we do not modify `$LOAD_PATH`).
|
325
336
|
|
326
|
-
\* Ruby Next avoids storing duplicates; instead, only the code for the earlier version is created and is assumed to be used with other versions. For example, if the transpiled code is the same for Ruby 2.5 and Ruby 2.6, only the `.rbnext/2.7/path/to/file.rb` is kept. That's why multiple entries are added to the `$LOAD_PATH` (`.rbnext/2.6
|
337
|
+
\* Ruby Next avoids storing duplicates; instead, only the code for the earlier version is created and is assumed to be used with other versions. For example, if the transpiled code is the same for Ruby 2.5 and Ruby 2.6, only the `.rbnext/2.7/path/to/file.rb` is kept. That's why multiple entries are added to the `$LOAD_PATH` (`.rbnext/2.6`, `.rbnext/2.7`, and `.rbnext/3.0` in the specified order for Ruby 2.5, and `.rbnext/2.7` and `.rbnext/3.0` for Ruby 2.6).
|
327
338
|
|
328
339
|
### Transpiled files vs. VCS vs. installing from source
|
329
340
|
|
@@ -446,6 +457,8 @@ AllCops:
|
|
446
457
|
|
447
458
|
We currently provide support for Ruby 2.2, 2.3 and 2.4.
|
448
459
|
|
460
|
+
**NOTE:** By "support" here we mean using `ruby-next` CLI and runtime transpiling. Transpiled code may run on Ruby 2.0+.
|
461
|
+
|
449
462
|
Ruby Next itself relies on 2.5 features and contains polyfills only for version 2.5+ (and that won't change).
|
450
463
|
Thus, to make it work with <2.5 we need to backport some APIs ourselves.
|
451
464
|
|
@@ -509,17 +522,13 @@ require "ruby-next/language/runtime"
|
|
509
522
|
|
510
523
|
### Supported edge features
|
511
524
|
|
512
|
-
|
513
|
-
|
514
|
-
- Right-hand assignment (`13.divmod(5) => a,b`) ([#15921](https://bugs.ruby-lang.org/issues/15921)).
|
515
|
-
|
516
|
-
- Find pattern (`[0, 1, 2] in [*, 1 => a, *c]`) ([#16828](https://bugs.ruby-lang.org/issues/16828)).
|
525
|
+
`Array#intersect?` ([#15198](https://bugs.ruby-lang.org/issues/15198))
|
517
526
|
|
518
527
|
### Supported proposed features
|
519
528
|
|
520
529
|
- _Method reference_ operator (`.:`) ([#13581](https://bugs.ruby-lang.org/issues/13581)).
|
521
530
|
|
522
|
-
- Shorthand Hash notation (`data = {x, y}`) ([#15236](https://bugs.ruby-lang.org/issues/15236)).
|
531
|
+
- Shorthand Hash/kwarg notation (`data = {x, y}` or `foo(x:, y:)`) ([#15236](https://bugs.ruby-lang.org/issues/15236)).
|
523
532
|
|
524
533
|
## Contributing
|
525
534
|
|
@@ -0,0 +1,206 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
require "ruby-next/config"
|
6
|
+
require "ruby-next/utils"
|
7
|
+
|
8
|
+
module RubyNext
|
9
|
+
module Core
|
10
|
+
# Patch contains the extension implementation
|
11
|
+
# and meta information (e.g., Ruby version).
|
12
|
+
class Patch
|
13
|
+
attr_reader :refineables, :name, :mod, :method_name, :version, :body, :singleton, :core_ext, :supported, :native, :location
|
14
|
+
|
15
|
+
# Create a new patch for module/class (mod)
|
16
|
+
# with the specified uniq name
|
17
|
+
#
|
18
|
+
# `core_ext` defines the strategy for core extensions:
|
19
|
+
# - :patch — extend class directly
|
20
|
+
# - :prepend — extend class by prepending a module (e.g., when needs `super`)
|
21
|
+
def initialize(mod = nil, method: ::Kernel.raise(::ArgumentError, "missing keyword: method"), version: ::Kernel.raise(::ArgumentError, "missing keyword: version"), name: nil, supported: nil, native: nil, location: nil, refineable: mod, core_ext: :patch, singleton: nil)
|
22
|
+
@mod = mod
|
23
|
+
@method_name = method
|
24
|
+
@version = version
|
25
|
+
if method_name && mod
|
26
|
+
@supported = supported.nil? ? mod.method_defined?(method_name) : supported
|
27
|
+
# define whether running Ruby has a native implementation for this method
|
28
|
+
# for that, we check the source_location (which is nil for C defined methods)
|
29
|
+
@native = native.nil? ? (supported? && native_location?(mod.instance_method(method_name).source_location)) : native
|
30
|
+
end
|
31
|
+
@singleton = singleton
|
32
|
+
@refineables = Array(refineable)
|
33
|
+
@body = yield
|
34
|
+
@core_ext = core_ext
|
35
|
+
@location = location || build_location(caller_locations(1, 5))
|
36
|
+
@name = name || build_module_name
|
37
|
+
end
|
38
|
+
|
39
|
+
def prepend?
|
40
|
+
core_ext == :prepend
|
41
|
+
end
|
42
|
+
|
43
|
+
def core_ext?
|
44
|
+
!mod.nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
alias supported? supported
|
48
|
+
alias native? native
|
49
|
+
alias singleton? singleton
|
50
|
+
|
51
|
+
def to_module
|
52
|
+
Module.new.tap do |ext|
|
53
|
+
ext.module_eval(body, *location)
|
54
|
+
|
55
|
+
RubyNext::Core.const_set(name, ext)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def build_module_name
|
62
|
+
mod_name = singleton? ? singleton.name : mod.name
|
63
|
+
camelized_method_name = method_name.to_s.split("_").map(&:capitalize).join
|
64
|
+
|
65
|
+
"#{mod_name}#{camelized_method_name}".gsub(/\W/, "")
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_location(trace_locations)
|
69
|
+
# The caller_locations behaviour depends on implementaion,
|
70
|
+
# e.g. in JRuby https://github.com/jruby/jruby/issues/6055
|
71
|
+
while trace_locations.first.label != "patch"
|
72
|
+
trace_locations.shift
|
73
|
+
end
|
74
|
+
|
75
|
+
trace_location = trace_locations[1]
|
76
|
+
|
77
|
+
[trace_location.absolute_path, trace_location.lineno + 2]
|
78
|
+
end
|
79
|
+
|
80
|
+
def native_location?(location)
|
81
|
+
location.nil? || location.first.match?(/(<internal:|resource:\/truffleruby\/core)/)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Registry for patches
|
86
|
+
class Patches
|
87
|
+
attr_reader :extensions, :refined
|
88
|
+
|
89
|
+
def initialize
|
90
|
+
@names = Set.new
|
91
|
+
@extensions = Hash.new { |h, k| h[k] = [] }
|
92
|
+
@refined = Hash.new { |h, k| h[k] = [] }
|
93
|
+
end
|
94
|
+
|
95
|
+
# Register new patch
|
96
|
+
def <<(patch)
|
97
|
+
raise ArgumentError, "Patch already registered: #{patch.name}" if @names.include?(patch.name)
|
98
|
+
@names << patch.name
|
99
|
+
@extensions[patch.mod] << patch if patch.core_ext?
|
100
|
+
patch.refineables.each { |r| @refined[r] << patch } unless patch.native?
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class << self
|
105
|
+
STRATEGIES = %i[refine core_ext backports].freeze
|
106
|
+
|
107
|
+
attr_reader :strategy
|
108
|
+
|
109
|
+
def strategy=(val)
|
110
|
+
raise ArgumentError, "Unknown strategy: #{val}. Available: #{STRATEGIES.join(",")}" unless STRATEGIES.include?(val)
|
111
|
+
@strategy = val
|
112
|
+
end
|
113
|
+
|
114
|
+
def refine?
|
115
|
+
strategy == :refine
|
116
|
+
end
|
117
|
+
|
118
|
+
def core_ext?
|
119
|
+
strategy == :core_ext || strategy == :backports
|
120
|
+
end
|
121
|
+
|
122
|
+
def backports?
|
123
|
+
strategy == :backports
|
124
|
+
end
|
125
|
+
|
126
|
+
def patch(*__rest__, &__block__)
|
127
|
+
patches << Patch.new(*__rest__, &__block__)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Inject `using RubyNext` at the top of the source code
|
131
|
+
def inject!(contents)
|
132
|
+
if contents.frozen?
|
133
|
+
contents = contents.sub(/^(\s*[^#\s].*)/, 'using RubyNext;\1')
|
134
|
+
else
|
135
|
+
contents.sub!(/^(\s*[^#\s].*)/, 'using RubyNext;\1')
|
136
|
+
end
|
137
|
+
contents
|
138
|
+
end
|
139
|
+
|
140
|
+
def patches
|
141
|
+
@patches ||= Patches.new
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Use refinements by default
|
146
|
+
self.strategy = ENV.fetch("RUBY_NEXT_CORE_STRATEGY", "refine").to_sym
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
require "backports/2.5" if RubyNext::Core.backports?
|
151
|
+
|
152
|
+
require "ruby-next/core/kernel/then"
|
153
|
+
|
154
|
+
require "ruby-next/core/proc/compose"
|
155
|
+
|
156
|
+
require "ruby-next/core/enumerable/tally"
|
157
|
+
require "ruby-next/core/enumerable/filter"
|
158
|
+
require "ruby-next/core/enumerable/filter_map"
|
159
|
+
|
160
|
+
require "ruby-next/core/enumerator/produce"
|
161
|
+
|
162
|
+
require "ruby-next/core/array/difference_union_intersection"
|
163
|
+
|
164
|
+
require "ruby-next/core/hash/merge"
|
165
|
+
|
166
|
+
require "ruby-next/core/string/split"
|
167
|
+
|
168
|
+
require "ruby-next/core/symbol/start_with"
|
169
|
+
require "ruby-next/core/symbol/end_with"
|
170
|
+
|
171
|
+
require "ruby-next/core/unboundmethod/bind_call"
|
172
|
+
|
173
|
+
require "ruby-next/core/time/floor"
|
174
|
+
require "ruby-next/core/time/ceil"
|
175
|
+
|
176
|
+
# Core extensions required for pattern matching
|
177
|
+
# Required for pattern matching with refinements
|
178
|
+
unless defined?(NoMatchingPatternError)
|
179
|
+
class NoMatchingPatternError < StandardError
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
require "ruby-next/core/constants/no_matching_pattern_error"
|
184
|
+
require "ruby-next/core/constants/frozen_error"
|
185
|
+
require "ruby-next/core/array/deconstruct"
|
186
|
+
require "ruby-next/core/hash/deconstruct_keys"
|
187
|
+
require "ruby-next/core/struct/deconstruct"
|
188
|
+
require "ruby-next/core/struct/deconstruct_keys"
|
189
|
+
|
190
|
+
require "ruby-next/core/hash/except"
|
191
|
+
|
192
|
+
require "ruby-next/core/array/intersect"
|
193
|
+
|
194
|
+
# Generate refinements
|
195
|
+
RubyNext.module_eval do
|
196
|
+
RubyNext::Core.patches.refined.each do |mod, patches|
|
197
|
+
# Only refine modules when supported
|
198
|
+
next unless mod.is_a?(Class) || RubyNext::Utils.refine_modules?
|
199
|
+
|
200
|
+
refine mod do
|
201
|
+
patches.each do |patch|
|
202
|
+
module_eval(patch.body, *patch.location)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
gem "ruby-next-parser", ">= 2.8.0.3"
|
4
|
+
gem "unparser", ">= 0.4.7"
|
5
|
+
|
6
|
+
require "set"
|
7
|
+
|
8
|
+
require "ruby-next"
|
9
|
+
|
10
|
+
module RubyNext
|
11
|
+
# Language module contains tools to transpile newer Ruby syntax
|
12
|
+
# into an older one.
|
13
|
+
#
|
14
|
+
# It works the following way:
|
15
|
+
# - Takes a Ruby source code as input
|
16
|
+
# - Generates the AST using the edge parser (via the `parser` gem)
|
17
|
+
# - Pass this AST through the list of processors (one feature = one processor)
|
18
|
+
# - Each processor may modify the AST
|
19
|
+
# - Generates a transpiled source code from the transformed AST (via the `unparser` gem)
|
20
|
+
module Language
|
21
|
+
using RubyNext
|
22
|
+
|
23
|
+
require "ruby-next/language/parser"
|
24
|
+
require "ruby-next/language/unparser"
|
25
|
+
|
26
|
+
RewriterNotFoundError = Class.new(StandardError)
|
27
|
+
|
28
|
+
class TransformContext
|
29
|
+
attr_reader :versions, :use_ruby_next
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
# Minimum supported RubyNext version
|
33
|
+
@min_version = MIN_SUPPORTED_VERSION
|
34
|
+
@dirty = false
|
35
|
+
@versions = Set.new
|
36
|
+
@use_ruby_next = false
|
37
|
+
end
|
38
|
+
|
39
|
+
# Called by rewriter when it performs transfomrations
|
40
|
+
def track!(rewriter)
|
41
|
+
@dirty = true
|
42
|
+
versions << rewriter.class::MIN_SUPPORTED_VERSION
|
43
|
+
end
|
44
|
+
|
45
|
+
def use_ruby_next!
|
46
|
+
@use_ruby_next = true
|
47
|
+
end
|
48
|
+
|
49
|
+
alias use_ruby_next? use_ruby_next
|
50
|
+
|
51
|
+
def dirty?
|
52
|
+
@dirty == true
|
53
|
+
end
|
54
|
+
|
55
|
+
def min_version
|
56
|
+
versions.min
|
57
|
+
end
|
58
|
+
|
59
|
+
def sorted_versions
|
60
|
+
versions.to_a.sort
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class << self
|
65
|
+
attr_accessor :rewriters
|
66
|
+
attr_reader :watch_dirs
|
67
|
+
|
68
|
+
attr_accessor :strategy
|
69
|
+
|
70
|
+
MODES = %i[rewrite ast].freeze
|
71
|
+
|
72
|
+
attr_reader :mode
|
73
|
+
|
74
|
+
def mode=(val)
|
75
|
+
raise ArgumentError, "Unknown mode: #{val}. Available: #{MODES.join(",")}" unless MODES.include?(val)
|
76
|
+
@mode = val
|
77
|
+
end
|
78
|
+
|
79
|
+
def rewrite?
|
80
|
+
mode == :rewrite?
|
81
|
+
end
|
82
|
+
|
83
|
+
def ast?
|
84
|
+
mode == :ast
|
85
|
+
end
|
86
|
+
|
87
|
+
def runtime!
|
88
|
+
require "ruby-next/language/rewriters/runtime"
|
89
|
+
|
90
|
+
@runtime = true
|
91
|
+
end
|
92
|
+
|
93
|
+
def runtime?
|
94
|
+
@runtime
|
95
|
+
end
|
96
|
+
|
97
|
+
def transform(source, rewriters: self.rewriters, using: RubyNext::Core.refine?, context: TransformContext.new)
|
98
|
+
retried = 0
|
99
|
+
new_source = nil
|
100
|
+
begin
|
101
|
+
new_source =
|
102
|
+
if mode == :rewrite
|
103
|
+
rewrite(source, rewriters: rewriters, using: using, context: context)
|
104
|
+
else
|
105
|
+
regenerate(source, rewriters: rewriters, using: using, context: context)
|
106
|
+
end
|
107
|
+
rescue Unparser::UnknownNodeError => err
|
108
|
+
RubyNext.warn "Ruby Next fallbacks to \"rewrite\" transpiling mode since the version of Unparser you use doesn't support some syntax yet: #{err.message}.\n" \
|
109
|
+
"Try upgrading the Unparser or set transpiling mode to \"rewrite\" in case you use some edge or experimental syntax."
|
110
|
+
self.mode = :rewrite
|
111
|
+
retried += 1
|
112
|
+
retry unless retried > 1
|
113
|
+
raise
|
114
|
+
end
|
115
|
+
|
116
|
+
return new_source unless RubyNext::Core.refine?
|
117
|
+
return new_source unless using && context.use_ruby_next?
|
118
|
+
|
119
|
+
Core.inject! new_source.dup
|
120
|
+
end
|
121
|
+
|
122
|
+
def transformable?(path)
|
123
|
+
watch_dirs.any? { |dir| path.start_with?(dir) }
|
124
|
+
end
|
125
|
+
|
126
|
+
# Rewriters required for the current version
|
127
|
+
def current_rewriters
|
128
|
+
@current_rewriters ||= rewriters.select(&:unsupported_syntax?)
|
129
|
+
end
|
130
|
+
|
131
|
+
# This method guarantees that rewriters will be returned in order they defined in Language module
|
132
|
+
def select_rewriters(*names)
|
133
|
+
rewriters_delta = names - rewriters.map { |rewriter| rewriter::NAME }
|
134
|
+
if rewriters_delta.any?
|
135
|
+
raise RewriterNotFoundError, "Rewriters not found: #{rewriters_delta.join(",")}"
|
136
|
+
end
|
137
|
+
|
138
|
+
rewriters.select { |rewriter| names.include?(rewriter::NAME) }
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def regenerate(source, rewriters: ::Kernel.raise(::ArgumentError, "missing keyword: rewriters"), using: ::Kernel.raise(::ArgumentError, "missing keyword: using"), context: ::Kernel.raise(::ArgumentError, "missing keyword: context"))
|
144
|
+
parse_with_comments(source).then do |(ast, comments)|
|
145
|
+
rewriters.inject(ast) do |tree, rewriter|
|
146
|
+
rewriter.new(context).process(tree)
|
147
|
+
end.then do |new_ast|
|
148
|
+
next source unless context.dirty?
|
149
|
+
|
150
|
+
Unparser.unparse(new_ast, comments)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def rewrite(source, rewriters: ::Kernel.raise(::ArgumentError, "missing keyword: rewriters"), using: ::Kernel.raise(::ArgumentError, "missing keyword: using"), context: ::Kernel.raise(::ArgumentError, "missing keyword: context"))
|
156
|
+
rewriters.inject(source) do |src, rewriter|
|
157
|
+
buffer = Parser::Source::Buffer.new("<dynamic>")
|
158
|
+
buffer.source = src
|
159
|
+
|
160
|
+
rewriter.new(context).rewrite(buffer, parse(src))
|
161
|
+
end.then do |new_source|
|
162
|
+
next source unless context.dirty?
|
163
|
+
|
164
|
+
new_source
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
attr_writer :watch_dirs
|
169
|
+
end
|
170
|
+
|
171
|
+
self.rewriters = []
|
172
|
+
self.watch_dirs = %w[app lib spec test].map { |path| File.join(Dir.pwd, path) }
|
173
|
+
self.mode = ENV.fetch("RUBY_NEXT_TRANSPILE_MODE", "rewrite").to_sym
|
174
|
+
|
175
|
+
require "ruby-next/language/rewriters/base"
|
176
|
+
|
177
|
+
require "ruby-next/language/rewriters/squiggly_heredoc"
|
178
|
+
rewriters << Rewriters::SquigglyHeredoc
|
179
|
+
|
180
|
+
require "ruby-next/language/rewriters/safe_navigation"
|
181
|
+
rewriters << Rewriters::SafeNavigation
|
182
|
+
|
183
|
+
require "ruby-next/language/rewriters/numeric_literals"
|
184
|
+
rewriters << Rewriters::NumericLiterals
|
185
|
+
|
186
|
+
require "ruby-next/language/rewriters/required_kwargs"
|
187
|
+
rewriters << Rewriters::RequiredKwargs
|
188
|
+
|
189
|
+
require "ruby-next/language/rewriters/args_forward"
|
190
|
+
rewriters << Rewriters::ArgsForward
|
191
|
+
|
192
|
+
# Must be added after general args forward rewriter to become
|
193
|
+
# no-op in Ruby <2.7
|
194
|
+
require "ruby-next/language/rewriters/args_forward_leading"
|
195
|
+
rewriters << Rewriters::ArgsForwardLeading
|
196
|
+
|
197
|
+
require "ruby-next/language/rewriters/numbered_params"
|
198
|
+
rewriters << Rewriters::NumberedParams
|
199
|
+
|
200
|
+
require "ruby-next/language/rewriters/pattern_matching"
|
201
|
+
rewriters << Rewriters::PatternMatching
|
202
|
+
|
203
|
+
# Must be added after general pattern matching rewriter to become
|
204
|
+
# no-op in Ruby <2.7
|
205
|
+
require "ruby-next/language/rewriters/find_pattern"
|
206
|
+
rewriters << Rewriters::FindPattern
|
207
|
+
|
208
|
+
require "ruby-next/language/rewriters/in_pattern"
|
209
|
+
rewriters << Rewriters::InPattern
|
210
|
+
|
211
|
+
# Put endless range in the end, 'cause Parser fails to parse it in
|
212
|
+
# pattern matching
|
213
|
+
require "ruby-next/language/rewriters/endless_range"
|
214
|
+
rewriters << Rewriters::EndlessRange
|
215
|
+
|
216
|
+
require "ruby-next/language/rewriters/endless_method"
|
217
|
+
RubyNext::Language.rewriters << RubyNext::Language::Rewriters::EndlessMethod
|
218
|
+
|
219
|
+
if ENV["RUBY_NEXT_EDGE"] == "1"
|
220
|
+
require "ruby-next/language/edge"
|
221
|
+
end
|
222
|
+
|
223
|
+
if ENV["RUBY_NEXT_PROPOSED"] == "1"
|
224
|
+
require "ruby-next/language/proposed"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -57,7 +57,7 @@ module RubyNext
|
|
57
57
|
|
58
58
|
opts.on(
|
59
59
|
"--transpile-mode=MODE",
|
60
|
-
"Transpiler mode (ast or rewrite). Default:
|
60
|
+
"Transpiler mode (ast or rewrite). Default: rewrite"
|
61
61
|
) do |val|
|
62
62
|
Language.mode = val.to_sym
|
63
63
|
end
|
@@ -95,7 +95,7 @@ module RubyNext
|
|
95
95
|
exit 0
|
96
96
|
end
|
97
97
|
|
98
|
-
unless ((!
|
98
|
+
unless ((((__safe_lvar__ = lib_path) || true) && (!__safe_lvar__.nil? || nil)) && __safe_lvar__.then(&File.method(:exist?)))
|
99
99
|
$stdout.puts "Path not found: #{lib_path}"
|
100
100
|
$stdout.puts optparser.help
|
101
101
|
exit 2
|
@@ -8,7 +8,7 @@ module RubyNext
|
|
8
8
|
def eval(source, bind = nil, *args)
|
9
9
|
new_source = ::RubyNext::Language::Runtime.transform(
|
10
10
|
source,
|
11
|
-
using: ((!
|
11
|
+
using: ((((__safe_lvar__ = bind) || true) && (!__safe_lvar__.nil? || nil)) && __safe_lvar__.receiver) == TOPLEVEL_BINDING.receiver || ((((__safe_lvar__ = ((((__safe_lvar__ = bind) || true) && (!__safe_lvar__.nil? || nil)) && __safe_lvar__.receiver)) || true) && (!__safe_lvar__.nil? || nil)) && __safe_lvar__.is_a?(Module))
|
12
12
|
)
|
13
13
|
RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
|
14
14
|
super new_source, bind, *args
|
@@ -20,7 +20,7 @@ module RubyNext
|
|
20
20
|
module InstanceEval # :nodoc:
|
21
21
|
refine Object do
|
22
22
|
def instance_eval(*args, &block)
|
23
|
-
return super(*args, &block) if
|
23
|
+
return super(*args, &block) if block
|
24
24
|
|
25
25
|
source = args.shift
|
26
26
|
new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
|
@@ -33,7 +33,7 @@ module RubyNext
|
|
33
33
|
module ClassEval
|
34
34
|
refine Module do
|
35
35
|
def module_eval(*args, &block)
|
36
|
-
return super(*args, &block) if
|
36
|
+
return super(*args, &block) if block
|
37
37
|
|
38
38
|
source = args.shift
|
39
39
|
new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
|
@@ -42,7 +42,7 @@ module RubyNext
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def class_eval(*args, &block)
|
45
|
-
return super(*args, &block) if
|
45
|
+
return super(*args, &block) if block
|
46
46
|
|
47
47
|
source = args.shift
|
48
48
|
new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
|