ruby-next-core 0.8.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/README.md +85 -11
  4. data/bin/transform +9 -1
  5. data/lib/.rbnext/2.3/ruby-next/commands/core_ext.rb +167 -0
  6. data/lib/.rbnext/2.3/ruby-next/commands/nextify.rb +198 -0
  7. data/lib/.rbnext/2.3/ruby-next/language/eval.rb +66 -0
  8. data/lib/.rbnext/2.3/ruby-next/language/rewriters/base.rb +121 -0
  9. data/lib/.rbnext/2.3/ruby-next/language/rewriters/endless_range.rb +63 -0
  10. data/lib/.rbnext/2.3/ruby-next/language/rewriters/pattern_matching.rb +944 -0
  11. data/lib/.rbnext/2.3/ruby-next/utils.rb +65 -0
  12. data/lib/ruby-next.rb +8 -5
  13. data/lib/ruby-next/cli.rb +2 -2
  14. data/lib/ruby-next/commands/core_ext.rb +2 -2
  15. data/lib/ruby-next/commands/nextify.rb +64 -22
  16. data/lib/ruby-next/core.rb +39 -22
  17. data/lib/ruby-next/core/array/deconstruct.rb +9 -9
  18. data/lib/ruby-next/core/array/difference_union_intersection.rb +12 -12
  19. data/lib/ruby-next/core/constants/no_matching_pattern_error.rb +3 -3
  20. data/lib/ruby-next/core/enumerable/filter.rb +8 -8
  21. data/lib/ruby-next/core/enumerable/filter_map.rb +25 -25
  22. data/lib/ruby-next/core/enumerable/tally.rb +7 -7
  23. data/lib/ruby-next/core/enumerator/produce.rb +12 -12
  24. data/lib/ruby-next/core/hash/deconstruct_keys.rb +9 -9
  25. data/lib/ruby-next/core/hash/except.rb +11 -0
  26. data/lib/ruby-next/core/hash/merge.rb +8 -8
  27. data/lib/ruby-next/core/kernel/then.rb +2 -2
  28. data/lib/ruby-next/core/proc/compose.rb +11 -11
  29. data/lib/ruby-next/core/string/split.rb +6 -6
  30. data/lib/ruby-next/core/struct/deconstruct.rb +2 -2
  31. data/lib/ruby-next/core/struct/deconstruct_keys.rb +17 -17
  32. data/lib/ruby-next/core/symbol/end_with.rb +4 -4
  33. data/lib/ruby-next/core/symbol/start_with.rb +4 -4
  34. data/lib/ruby-next/core/time/ceil.rb +6 -5
  35. data/lib/ruby-next/core/time/floor.rb +4 -4
  36. data/lib/ruby-next/core/unboundmethod/bind_call.rb +4 -4
  37. data/lib/ruby-next/core_ext.rb +2 -2
  38. data/lib/ruby-next/language.rb +31 -5
  39. data/lib/ruby-next/language/eval.rb +10 -8
  40. data/lib/ruby-next/language/proposed.rb +3 -0
  41. data/lib/ruby-next/language/rewriters/args_forward.rb +24 -20
  42. data/lib/ruby-next/language/rewriters/base.rb +2 -2
  43. data/lib/ruby-next/language/rewriters/endless_method.rb +26 -3
  44. data/lib/ruby-next/language/rewriters/endless_range.rb +1 -0
  45. data/lib/ruby-next/language/rewriters/find_pattern.rb +44 -0
  46. data/lib/ruby-next/language/rewriters/method_reference.rb +2 -1
  47. data/lib/ruby-next/language/rewriters/numbered_params.rb +1 -0
  48. data/lib/ruby-next/language/rewriters/pattern_matching.rb +105 -13
  49. data/lib/ruby-next/language/rewriters/right_hand_assignment.rb +2 -1
  50. data/lib/ruby-next/language/rewriters/runtime.rb +6 -0
  51. data/lib/ruby-next/language/rewriters/runtime/dir.rb +32 -0
  52. data/lib/ruby-next/language/rewriters/safe_navigation.rb +87 -0
  53. data/lib/ruby-next/language/rewriters/shorthand_hash.rb +47 -0
  54. data/lib/ruby-next/language/rewriters/squiggly_heredoc.rb +36 -0
  55. data/lib/ruby-next/language/runtime.rb +3 -2
  56. data/lib/ruby-next/logging.rb +1 -1
  57. data/lib/ruby-next/rubocop.rb +15 -9
  58. data/lib/ruby-next/setup_self.rb +22 -0
  59. data/lib/ruby-next/utils.rb +30 -0
  60. data/lib/ruby-next/version.rb +1 -1
  61. data/lib/uby-next.rb +8 -4
  62. metadata +22 -7
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Utils
5
+ module_function
6
+
7
+ if $LOAD_PATH.respond_to?(:resolve_feature_path)
8
+ def resolve_feature_path(feature)
9
+ ((!$LOAD_PATH.resolve_feature_path(feature).nil?) || nil) && $LOAD_PATH.resolve_feature_path(feature).last
10
+ rescue LoadError
11
+ end
12
+ else
13
+ def resolve_feature_path(path)
14
+ if File.file?(relative = File.expand_path(path))
15
+ path = relative
16
+ end
17
+
18
+ path = "#{path}.rb" if File.extname(path).empty?
19
+
20
+ return path if Pathname.new(path).absolute?
21
+
22
+ $LOAD_PATH.find do |lp|
23
+ lpath = File.join(lp, path)
24
+ return File.realpath(lpath) if File.file?(lpath)
25
+ end
26
+ end
27
+ end
28
+
29
+ def source_with_lines(source, path)
30
+ source.lines.map.with_index do |line, i|
31
+ "#{(i + 1).to_s.rjust(4)}: #{line}"
32
+ end.tap do |lines|
33
+ lines.unshift " 0: # source: #{path}"
34
+ end
35
+ end
36
+
37
+ # Returns true if modules refinement is supported in current version
38
+ def refine_modules?
39
+ @refine_modules ||=
40
+ begin
41
+ # Make sure that including modules within refinements works
42
+ # See https://github.com/oracle/truffleruby/issues/2026
43
+ eval <<-RUBY, TOPLEVEL_BINDING, __FILE__, __LINE__ + 1
44
+ module RubyNext::Utils::A; end
45
+ class RubyNext::Utils::B
46
+ include RubyNext::Utils::A
47
+ end
48
+ using(Module.new do
49
+ refine RubyNext::Utils::A do
50
+ include(Module.new do
51
+ def i_am_refinement
52
+ "yes, you are!"
53
+ end
54
+ end)
55
+ end
56
+ end)
57
+ RubyNext::Utils::B.new.i_am_refinement
58
+ RUBY
59
+ true
60
+ rescue TypeError, NoMethodError
61
+ false
62
+ end
63
+ end
64
+ end
65
+ end
@@ -4,17 +4,18 @@ require "ruby-next/version"
4
4
 
5
5
  module RubyNext
6
6
  # Mininum Ruby version supported by RubyNext
7
- MIN_SUPPORTED_VERSION = Gem::Version.new("2.5.0")
7
+ MIN_SUPPORTED_VERSION = Gem::Version.new("2.2.0")
8
8
 
9
9
  # Where to store transpiled files (relative from the project LOAD_PATH, usually `lib/`)
10
10
  RUBY_NEXT_DIR = ".rbnext"
11
11
 
12
12
  # Defines last minor version for every major version
13
13
  LAST_MINOR_VERSIONS = {
14
- 2 => 8
14
+ 2 => 8, # 2.8 is required for backward compatibility: some gems already uses it
15
+ 3 => 0
15
16
  }.freeze
16
17
 
17
- LATEST_VERSION = [2, 8].freeze
18
+ LATEST_VERSION = [3, 0].freeze
18
19
 
19
20
  class << self
20
21
  def next_version(version = RUBY_VERSION)
@@ -33,6 +34,8 @@ module RubyNext
33
34
  end
34
35
  end
35
36
 
36
- require_relative "ruby-next/core"
37
- require_relative "ruby-next/logging"
37
+ require "ruby-next/setup_self"
38
+ require "ruby-next/core"
39
+ require "ruby-next/core_ext" if RubyNext::Core.core_ext?
40
+ require "ruby-next/logging"
38
41
  end
@@ -61,7 +61,7 @@ module RubyNext
61
61
  def maybe_print_help
62
62
  return unless @print_help
63
63
 
64
- STDOUT.puts optparser.help
64
+ $stdout.puts optparser.help
65
65
  exit 0
66
66
  end
67
67
 
@@ -85,7 +85,7 @@ module RubyNext
85
85
  opts.banner = "Usage: ruby-next COMMAND [options]"
86
86
 
87
87
  opts.on("-v", "--version", "Print version") do
88
- STDOUT.puts RubyNext::VERSION
88
+ $stdout.puts RubyNext::VERSION
89
89
  exit 0
90
90
  end
91
91
 
@@ -69,7 +69,7 @@ module RubyNext
69
69
  RubyNext::Core.patches.extensions
70
70
  .values
71
71
  .flatten
72
- .filter do |patch|
72
+ .select do |patch|
73
73
  next if min_version && Gem::Version.new(patch.version) <= min_version
74
74
  next if filter && !filter.match?(patch.name)
75
75
  true
@@ -160,7 +160,7 @@ module RubyNext
160
160
  # remove empty lines
161
161
  new_src.gsub!(/^\s+$/, "")
162
162
  # remove traling blank lines
163
- new_src.gsub!(/\n\z/, "")
163
+ new_src.delete_suffix!("\n")
164
164
  new_src
165
165
  end
166
166
  end
@@ -10,12 +10,16 @@ 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
13
+ attr_reader :lib_path, :paths, :out_path, :min_version, :single_version, :specified_rewriters
14
14
 
15
15
  def run
16
16
  log "RubyNext core strategy: #{RubyNext::Core.strategy}"
17
17
  log "RubyNext transpile mode: #{RubyNext::Language.mode}"
18
18
 
19
+ remove_rbnext!
20
+
21
+ @min_version ||= MIN_SUPPORTED_VERSION
22
+
19
23
  paths.each do |path|
20
24
  contents = File.read(path)
21
25
  transpile path, contents
@@ -24,7 +28,8 @@ module RubyNext
24
28
 
25
29
  def parse!(args)
26
30
  print_help = false
27
- @min_version = MIN_SUPPORTED_VERSION
31
+ print_rewriters = false
32
+ rewriter_names = []
28
33
  @single_version = false
29
34
 
30
35
  optparser = base_parser do |opts|
@@ -61,6 +66,14 @@ module RubyNext
61
66
  Core.strategy = :core_ext unless val
62
67
  end
63
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
+
64
77
  opts.on("-h", "--help", "Print help") do
65
78
  print_help = true
66
79
  end
@@ -75,12 +88,35 @@ module RubyNext
75
88
  exit 0
76
89
  end
77
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
+
78
98
  unless lib_path&.then(&File.method(:exist?))
79
99
  $stdout.puts "Path not found: #{lib_path}"
80
100
  $stdout.puts optparser.help
81
101
  exit 2
82
102
  end
83
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
+
84
120
  @paths =
85
121
  if File.directory?(lib_path)
86
122
  Dir[File.join(lib_path, "**/*.rb")]
@@ -94,16 +130,11 @@ module RubyNext
94
130
  private
95
131
 
96
132
  def transpile(path, contents, version: min_version)
97
- rewriters = Language.rewriters.select { |rw| rw.unsupported_version?(version) }
133
+ rewriters = specified_rewriters || Language.rewriters.select { |rw| rw.unsupported_version?(version) }
98
134
 
99
135
  context = Language::TransformContext.new
100
136
 
101
- new_contents =
102
- if Gem::Version.new(version) >= Gem::Version.new("2.7.0") && !defined?(Unparser::Emitter::CaseMatch)
103
- Language.rewrite contents, context: context, rewriters: rewriters
104
- else
105
- Language.transform contents, context: context, rewriters: rewriters
106
- end
137
+ new_contents = Language.transform contents, context: context, rewriters: rewriters
107
138
 
108
139
  return unless context.dirty?
109
140
 
@@ -120,25 +151,17 @@ module RubyNext
120
151
  end
121
152
 
122
153
  def save(contents, path, version)
123
- return $stdout.puts(contents) if out_path == "stdout"
154
+ return $stdout.puts(contents) if stdout?
124
155
 
125
156
  paths = [Pathname.new(path).relative_path_from(Pathname.new(lib_path))]
126
157
 
127
158
  paths.unshift(version.segments[0..1].join(".")) unless single_version?
128
159
 
129
160
  next_path =
130
- if out_path
131
- if out_path.end_with?(".rb")
132
- out_path
133
- else
134
- File.join(out_path, *paths)
135
- end
161
+ if next_dir_path.end_with?(".rb")
162
+ out_path
136
163
  else
137
- File.join(
138
- lib_path,
139
- RUBY_NEXT_DIR,
140
- *paths
141
- )
164
+ File.join(next_dir_path, *paths)
142
165
  end
143
166
 
144
167
  unless CLI.dry_run?
@@ -150,7 +173,26 @@ module RubyNext
150
173
  log "Generated: #{next_path}"
151
174
  end
152
175
 
153
- alias single_version? single_version
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
154
196
  end
155
197
  end
156
198
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "set"
4
4
 
5
+ require "ruby-next/utils"
6
+
5
7
  module RubyNext
6
8
  module Core
7
9
  # Patch contains the extension implementation
@@ -23,7 +25,7 @@ module RubyNext
23
25
  @supported = supported.nil? ? mod.method_defined?(method_name) : supported
24
26
  # define whether running Ruby has a native implementation for this method
25
27
  # for that, we check the source_location (which is nil for C defined methods)
26
- @native = native.nil? ? (supported? && mod.instance_method(method_name).source_location.nil?) : native
28
+ @native = native.nil? ? (supported? && native_location?(mod.instance_method(method_name).source_location)) : native
27
29
  end
28
30
  @singleton = singleton
29
31
  @refineables = Array(refineable)
@@ -73,6 +75,10 @@ module RubyNext
73
75
 
74
76
  [trace_location.absolute_path, trace_location.lineno + 2]
75
77
  end
78
+
79
+ def native_location?(location)
80
+ location.nil? || location.first.match?(/(<internal:|resource:\/truffleruby\/core)/)
81
+ end
76
82
  end
77
83
 
78
84
  # Registry for patches
@@ -95,7 +101,7 @@ module RubyNext
95
101
  end
96
102
 
97
103
  class << self
98
- STRATEGIES = %i[refine core_ext].freeze
104
+ STRATEGIES = %i[refine core_ext backports].freeze
99
105
 
100
106
  attr_reader :strategy
101
107
 
@@ -109,7 +115,11 @@ module RubyNext
109
115
  end
110
116
 
111
117
  def core_ext?
112
- strategy == :core_ext
118
+ strategy == :core_ext || strategy == :backports
119
+ end
120
+
121
+ def backports?
122
+ strategy == :backports
113
123
  end
114
124
 
115
125
  def patch(*args, **kwargs, &block)
@@ -136,29 +146,31 @@ module RubyNext
136
146
  end
137
147
  end
138
148
 
139
- require_relative "core/kernel/then"
149
+ require "backports/2.5" if RubyNext::Core.backports?
150
+
151
+ require "ruby-next/core/kernel/then"
140
152
 
141
- require_relative "core/proc/compose"
153
+ require "ruby-next/core/proc/compose"
142
154
 
143
- require_relative "core/enumerable/tally"
144
- require_relative "core/enumerable/filter"
145
- require_relative "core/enumerable/filter_map"
155
+ require "ruby-next/core/enumerable/tally"
156
+ require "ruby-next/core/enumerable/filter"
157
+ require "ruby-next/core/enumerable/filter_map"
146
158
 
147
- require_relative "core/enumerator/produce"
159
+ require "ruby-next/core/enumerator/produce"
148
160
 
149
- require_relative "core/array/difference_union_intersection"
161
+ require "ruby-next/core/array/difference_union_intersection"
150
162
 
151
- require_relative "core/hash/merge"
163
+ require "ruby-next/core/hash/merge"
152
164
 
153
- require_relative "core/string/split"
165
+ require "ruby-next/core/string/split"
154
166
 
155
- require_relative "core/symbol/start_with"
156
- require_relative "core/symbol/end_with"
167
+ require "ruby-next/core/symbol/start_with"
168
+ require "ruby-next/core/symbol/end_with"
157
169
 
158
- require_relative "core/unboundmethod/bind_call"
170
+ require "ruby-next/core/unboundmethod/bind_call"
159
171
 
160
- require_relative "core/time/floor"
161
- require_relative "core/time/ceil"
172
+ require "ruby-next/core/time/floor"
173
+ require "ruby-next/core/time/ceil"
162
174
 
163
175
  # Core extensions required for pattern matching
164
176
  # Required for pattern matching with refinements
@@ -167,15 +179,20 @@ unless defined?(NoMatchingPatternError)
167
179
  end
168
180
  end
169
181
 
170
- require_relative "core/constants/no_matching_pattern_error"
171
- require_relative "core/array/deconstruct"
172
- require_relative "core/hash/deconstruct_keys"
173
- require_relative "core/struct/deconstruct"
174
- require_relative "core/struct/deconstruct_keys"
182
+ require "ruby-next/core/constants/no_matching_pattern_error"
183
+ require "ruby-next/core/array/deconstruct"
184
+ require "ruby-next/core/hash/deconstruct_keys"
185
+ require "ruby-next/core/struct/deconstruct"
186
+ require "ruby-next/core/struct/deconstruct_keys"
187
+
188
+ require "ruby-next/core/hash/except"
175
189
 
176
190
  # Generate refinements
177
191
  RubyNext.module_eval do
178
192
  RubyNext::Core.patches.refined.each do |mod, patches|
193
+ # Only refine modules when supported
194
+ next unless mod.is_a?(Class) || RubyNext::Utils.refine_modules?
195
+
179
196
  refine mod do
180
197
  patches.each do |patch|
181
198
  module_eval(patch.body, *patch.location)
@@ -1,21 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RubyNext::Core.patch Array, method: :deconstruct, version: "2.7" do
4
- <<~RUBY
5
- def deconstruct
6
- self
7
- end
4
+ <<-RUBY
5
+ def deconstruct
6
+ self
7
+ end
8
8
  RUBY
9
9
  end
10
10
 
11
11
  # We need to hack `respond_to?` in Ruby 2.5, since it's not working with refinements
12
12
  if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.6")
13
13
  RubyNext::Core.patch refineable: Array, name: "ArrayRespondToDeconstruct", method: :deconstruct, version: "2.7" do
14
- <<~RUBY
15
- def respond_to?(mid, *)
16
- return true if mid == :deconstruct
17
- super
18
- end
14
+ <<-RUBY
15
+ def respond_to?(mid, *)
16
+ return true if mid == :deconstruct
17
+ super
18
+ end
19
19
  RUBY
20
20
  end
21
21
  end
@@ -1,25 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RubyNext::Core.patch Array, method: :union, version: "2.6" do
4
- <<~RUBY
5
- def union(*others)
6
- others.reduce(Array.new(self).uniq) { |acc, arr| acc | arr }
7
- end
4
+ <<-RUBY
5
+ def union(*others)
6
+ others.reduce(Array.new(self).uniq) { |acc, arr| acc | arr }
7
+ end
8
8
  RUBY
9
9
  end
10
10
 
11
11
  RubyNext::Core.patch Array, method: :difference, version: "2.6" do
12
- <<~RUBY
13
- def difference(*others)
14
- others.reduce(Array.new(self)) { |acc, arr| acc - arr }
15
- end
12
+ <<-RUBY
13
+ def difference(*others)
14
+ others.reduce(Array.new(self)) { |acc, arr| acc - arr }
15
+ end
16
16
  RUBY
17
17
  end
18
18
 
19
19
  RubyNext::Core.patch Array, method: :intersection, version: "2.7" do
20
- <<~RUBY
21
- def intersection(*others)
22
- others.reduce(Array.new(self)) { |acc, arr| acc & arr }
23
- end
20
+ <<-RUBY
21
+ def intersection(*others)
22
+ others.reduce(Array.new(self)) { |acc, arr| acc & arr }
23
+ end
24
24
  RUBY
25
25
  end