ruby-next-core 0.10.1 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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