ruby-next-core 0.10.1 → 0.11.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -1
  3. data/README.md +12 -14
  4. data/bin/reparse +19 -0
  5. data/bin/transform +22 -8
  6. data/lib/.rbnext/2.3/ruby-next/commands/nextify.rb +5 -2
  7. data/lib/.rbnext/2.3/ruby-next/language/eval.rb +4 -4
  8. data/lib/.rbnext/2.3/ruby-next/language/rewriters/base.rb +25 -10
  9. data/lib/.rbnext/2.3/ruby-next/language/rewriters/endless_range.rb +1 -1
  10. data/lib/.rbnext/2.3/ruby-next/language/rewriters/pattern_matching.rb +133 -103
  11. data/lib/.rbnext/2.3/ruby-next/utils.rb +1 -1
  12. data/lib/.rbnext/2.7/ruby-next/core.rb +203 -0
  13. data/lib/ruby-next.rb +5 -38
  14. data/lib/ruby-next/commands/nextify.rb +4 -1
  15. data/lib/ruby-next/config.rb +45 -0
  16. data/lib/ruby-next/core.rb +5 -4
  17. data/lib/ruby-next/core/array/deconstruct.rb +1 -1
  18. data/lib/ruby-next/core/constants/no_matching_pattern_error.rb +1 -1
  19. data/lib/ruby-next/core/hash/deconstruct_keys.rb +1 -1
  20. data/lib/ruby-next/core/struct/deconstruct_keys.rb +3 -3
  21. data/lib/ruby-next/core_ext.rb +2 -0
  22. data/lib/ruby-next/language.rb +16 -5
  23. data/lib/ruby-next/language/bootsnap.rb +1 -1
  24. data/lib/ruby-next/language/edge.rb +0 -6
  25. data/lib/ruby-next/language/eval.rb +3 -3
  26. data/lib/ruby-next/language/parser.rb +19 -0
  27. data/lib/ruby-next/language/rewriters/args_forward.rb +8 -4
  28. data/lib/ruby-next/language/rewriters/args_forward_leading.rb +75 -0
  29. data/lib/ruby-next/language/rewriters/base.rb +21 -6
  30. data/lib/ruby-next/language/rewriters/in_pattern.rb +56 -0
  31. data/lib/ruby-next/language/rewriters/numbered_params.rb +6 -2
  32. data/lib/ruby-next/language/rewriters/pattern_matching.rb +131 -101
  33. data/lib/ruby-next/language/rewriters/safe_navigation.rb +30 -26
  34. data/lib/ruby-next/language/setup.rb +6 -3
  35. data/lib/ruby-next/language/unparser.rb +10 -0
  36. data/lib/ruby-next/rubocop.rb +79 -12
  37. data/lib/ruby-next/setup_self.rb +2 -2
  38. data/lib/ruby-next/version.rb +1 -1
  39. metadata +15 -6
  40. data/lib/.rbnext/2.3/ruby-next/language/rewriters/right_hand_assignment.rb +0 -107
  41. data/lib/ruby-next/language/rewriters/right_hand_assignment.rb +0 -107
@@ -6,7 +6,7 @@ module RubyNext
6
6
 
7
7
  if $LOAD_PATH.respond_to?(:resolve_feature_path)
8
8
  def resolve_feature_path(feature)
9
- ((!$LOAD_PATH.resolve_feature_path(feature).nil?) || nil) && $LOAD_PATH.resolve_feature_path(feature).last
9
+ ((!$LOAD_PATH.resolve_feature_path(feature).nil? || nil) && $LOAD_PATH.resolve_feature_path(feature).last)
10
10
  rescue LoadError
11
11
  end
12
12
  else
@@ -0,0 +1,203 @@
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:, 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/array/deconstruct"
185
+ require "ruby-next/core/hash/deconstruct_keys"
186
+ require "ruby-next/core/struct/deconstruct"
187
+ require "ruby-next/core/struct/deconstruct_keys"
188
+
189
+ require "ruby-next/core/hash/except"
190
+
191
+ # Generate refinements
192
+ RubyNext.module_eval do
193
+ RubyNext::Core.patches.refined.each do |mod, patches|
194
+ # Only refine modules when supported
195
+ next unless mod.is_a?(Class) || RubyNext::Utils.refine_modules?
196
+
197
+ refine mod do
198
+ patches.each do |patch|
199
+ module_eval(patch.body, *patch.location)
200
+ end
201
+ end
202
+ end
203
+ end
@@ -1,41 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ruby-next/version"
4
-
5
- module RubyNext
6
- # Mininum Ruby version supported by RubyNext
7
- MIN_SUPPORTED_VERSION = Gem::Version.new("2.2.0")
8
-
9
- # Where to store transpiled files (relative from the project LOAD_PATH, usually `lib/`)
10
- RUBY_NEXT_DIR = ".rbnext"
11
-
12
- # Defines last minor version for every major version
13
- LAST_MINOR_VERSIONS = {
14
- 2 => 8, # 2.8 is required for backward compatibility: some gems already uses it
15
- 3 => 0
16
- }.freeze
17
-
18
- LATEST_VERSION = [3, 0].freeze
19
-
20
- class << self
21
- def next_version(version = RUBY_VERSION)
22
- major, minor = Gem::Version.new(version).segments.map(&:to_i)
23
-
24
- return if major >= LATEST_VERSION.first && minor >= LATEST_VERSION.last
25
-
26
- nxt =
27
- if LAST_MINOR_VERSIONS[major] == minor
28
- "#{major + 1}.0.0"
29
- else
30
- "#{major}.#{minor + 1}.0"
31
- end
32
-
33
- Gem::Version.new(nxt)
34
- end
35
- end
36
-
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"
41
- end
4
+ require "ruby-next/config"
5
+ require "ruby-next/setup_self"
6
+ require "ruby-next/core"
7
+ require "ruby-next/core_ext" if RubyNext::Core.core_ext?
8
+ require "ruby-next/logging"
@@ -57,7 +57,7 @@ module RubyNext
57
57
 
58
58
  opts.on(
59
59
  "--transpile-mode=MODE",
60
- "Transpiler mode (ast or rewrite). Default: ast"
60
+ "Transpiler mode (ast or rewrite). Default: rewrite"
61
61
  ) do |val|
62
62
  Language.mode = val.to_sym
63
63
  end
@@ -148,6 +148,9 @@ module RubyNext
148
148
 
149
149
  # Then, generate the source code for the next version
150
150
  transpile path, contents, version: version
151
+ rescue SyntaxError, StandardError => e
152
+ warn "Failed to transpile #{path}: #{e.class} — #{e.message}"
153
+ exit 1
151
154
  end
152
155
 
153
156
  def save(contents, path, version)
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ # Mininum Ruby version supported by RubyNext
5
+ MIN_SUPPORTED_VERSION = Gem::Version.new("2.2.0")
6
+
7
+ # Where to store transpiled files (relative from the project LOAD_PATH, usually `lib/`)
8
+ RUBY_NEXT_DIR = ".rbnext"
9
+
10
+ # Defines last minor version for every major version
11
+ LAST_MINOR_VERSIONS = {
12
+ 2 => 8, # 2.8 is required for backward compatibility: some gems already uses it
13
+ 3 => 0
14
+ }.freeze
15
+
16
+ LATEST_VERSION = [3, 0].freeze
17
+
18
+ class << self
19
+ # TruffleRuby claims it's 2.7.2 compatible but...
20
+ if defined?(TruffleRuby) && ::RUBY_VERSION =~ /^2\.7/
21
+ def current_ruby_version
22
+ "2.6.5"
23
+ end
24
+ else
25
+ def current_ruby_version
26
+ ::RUBY_VERSION
27
+ end
28
+ end
29
+
30
+ def next_ruby_version(version = current_ruby_version)
31
+ major, minor = Gem::Version.new(version).segments.map(&:to_i)
32
+
33
+ return if major >= LATEST_VERSION.first && minor >= LATEST_VERSION.last
34
+
35
+ nxt =
36
+ if LAST_MINOR_VERSIONS[major] == minor
37
+ "#{major + 1}.0.0"
38
+ else
39
+ "#{major}.#{minor + 1}.0"
40
+ end
41
+
42
+ Gem::Version.new(nxt)
43
+ end
44
+ end
45
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "set"
4
4
 
5
+ require "ruby-next/config"
5
6
  require "ruby-next/utils"
6
7
 
7
8
  module RubyNext
@@ -17,7 +18,7 @@ module RubyNext
17
18
  # `core_ext` defines the strategy for core extensions:
18
19
  # - :patch — extend class directly
19
20
  # - :prepend — extend class by prepending a module (e.g., when needs `super`)
20
- def initialize(mod = nil, method:, name: nil, version:, supported: nil, native: nil, location: nil, refineable: mod, core_ext: :patch, singleton: nil)
21
+ def initialize(mod = nil, method:, version:, name: nil, supported: nil, native: nil, location: nil, refineable: mod, core_ext: :patch, singleton: nil)
21
22
  @mod = mod
22
23
  @method_name = method
23
24
  @version = version
@@ -122,8 +123,8 @@ module RubyNext
122
123
  strategy == :backports
123
124
  end
124
125
 
125
- def patch(*args, **kwargs, &block)
126
- patches << Patch.new(*args, **kwargs, &block)
126
+ def patch(...)
127
+ patches << Patch.new(...)
127
128
  end
128
129
 
129
130
  # Inject `using RubyNext` at the top of the source code
@@ -175,7 +176,7 @@ require "ruby-next/core/time/ceil"
175
176
  # Core extensions required for pattern matching
176
177
  # Required for pattern matching with refinements
177
178
  unless defined?(NoMatchingPatternError)
178
- class NoMatchingPatternError < RuntimeError
179
+ class NoMatchingPatternError < StandardError
179
180
  end
180
181
  end
181
182
 
@@ -9,7 +9,7 @@ end
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
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.6")
12
+ if Gem::Version.new(::RubyNext.current_ruby_version) < Gem::Version.new("2.6")
13
13
  RubyNext::Core.patch refineable: Array, name: "ArrayRespondToDeconstruct", method: :deconstruct, version: "2.7" do
14
14
  <<-RUBY
15
15
  def respond_to?(mid, *)
@@ -11,7 +11,7 @@ RubyNext::Core.patch Object,
11
11
  supported: true,
12
12
  location: [__FILE__, __LINE__ + 2] do
13
13
  <<-RUBY
14
- class NoMatchingPatternError < RuntimeError
14
+ class NoMatchingPatternError < StandardError
15
15
  end
16
16
  RUBY
17
17
  end
@@ -9,7 +9,7 @@ end
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
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.6")
12
+ if Gem::Version.new(::RubyNext.current_ruby_version) < Gem::Version.new("2.6")
13
13
  RubyNext::Core.patch refineable: Hash, name: "HashRespondToDeconstructKeys", method: :deconstruct_keys, version: "2.7" do
14
14
  <<-RUBY
15
15
  def respond_to?(mid, *)
@@ -10,9 +10,9 @@ def deconstruct_keys(keys)
10
10
 
11
11
  keys.each_with_object({}) do |k, acc|
12
12
  # if k is Symbol and not a member of a Struct return {}
13
- next if (Symbol === k || String === k) && !members.include?(k.to_sym)
13
+ return {} if (Symbol === k || String === k) && !members.include?(k.to_sym)
14
14
  # if k is Integer check that index is not ouf of bounds
15
- next if Integer === k && k > size - 1
15
+ return {} if Integer === k && k > size - 1
16
16
  acc[k] = self[k]
17
17
  end
18
18
  end
@@ -20,7 +20,7 @@ end
20
20
  end
21
21
 
22
22
  # We need to hack `respond_to?` in Ruby 2.5, since it's not working with refinements
23
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.6")
23
+ if Gem::Version.new(::RubyNext.current_ruby_version) < Gem::Version.new("2.6")
24
24
  RubyNext::Core.patch refineable: Struct, name: "StructRespondToDeconstruct", method: :deconstruct_keys, version: "2.7" do
25
25
  <<-RUBY
26
26
  def respond_to?(mid, *)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ruby-next/config"
4
+ require "ruby-next/setup_self"
3
5
  require "ruby-next/core"
4
6
 
5
7
  # Monkey-patch core classes using the same patches as for refinements
@@ -39,7 +39,7 @@ module RubyNext
39
39
  # Called by rewriter when it performs transfomrations
40
40
  def track!(rewriter)
41
41
  @dirty = true
42
- versions << rewriter.class.min_supported_minor_version
42
+ versions << rewriter.class::MIN_SUPPORTED_VERSION
43
43
  end
44
44
 
45
45
  def use_ruby_next!
@@ -101,9 +101,9 @@ module RubyNext
101
101
  regenerate(*args, **kwargs)
102
102
  end
103
103
  rescue Unparser::UnknownNodeError
104
- if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7.0")
105
- RubyNext.warn "Ruby Next fallbacks to \"rewrite\" transpiling mode since Unparser doesn't support 2.7+ AST yet.\n" \
106
- "See https://github.com/mbj/unparser/pull/142"
104
+ if Gem::Version.new(::RubyNext.current_ruby_version) >= Gem::Version.new("3.0.0")
105
+ RubyNext.warn "Ruby Next fallbacks to \"rewrite\" transpiling mode since Unparser doesn't support 3.0+ AST yet.\n" \
106
+ "See https://github.com/mbj/unparser/issues/168"
107
107
  self.mode = :rewrite
108
108
  end
109
109
  rewrite(*args, **kwargs)
@@ -169,7 +169,7 @@ module RubyNext
169
169
 
170
170
  self.rewriters = []
171
171
  self.watch_dirs = %w[app lib spec test].map { |path| File.join(Dir.pwd, path) }
172
- self.mode = ENV.fetch("RUBY_NEXT_TRANSPILE_MODE", "ast").to_sym
172
+ self.mode = ENV.fetch("RUBY_NEXT_TRANSPILE_MODE", "rewrite").to_sym
173
173
 
174
174
  require "ruby-next/language/rewriters/base"
175
175
 
@@ -182,6 +182,11 @@ module RubyNext
182
182
  require "ruby-next/language/rewriters/args_forward"
183
183
  rewriters << Rewriters::ArgsForward
184
184
 
185
+ # Must be added after general args forward rewriter to become
186
+ # no-op in Ruby <2.7
187
+ require "ruby-next/language/rewriters/args_forward_leading"
188
+ rewriters << Rewriters::ArgsForwardLeading
189
+
185
190
  require "ruby-next/language/rewriters/numbered_params"
186
191
  rewriters << Rewriters::NumberedParams
187
192
 
@@ -193,11 +198,17 @@ module RubyNext
193
198
  require "ruby-next/language/rewriters/find_pattern"
194
199
  rewriters << Rewriters::FindPattern
195
200
 
201
+ require "ruby-next/language/rewriters/in_pattern"
202
+ rewriters << Rewriters::InPattern
203
+
196
204
  # Put endless range in the end, 'cause Parser fails to parse it in
197
205
  # pattern matching
198
206
  require "ruby-next/language/rewriters/endless_range"
199
207
  rewriters << Rewriters::EndlessRange
200
208
 
209
+ require "ruby-next/language/rewriters/endless_method"
210
+ RubyNext::Language.rewriters << RubyNext::Language::Rewriters::EndlessMethod
211
+
201
212
  if ENV["RUBY_NEXT_EDGE"] == "1"
202
213
  require "ruby-next/language/edge"
203
214
  end