ruby-next-core 0.2.0 → 0.3.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +69 -7
  4. data/bin/ruby-next +1 -1
  5. data/lib/ruby-next/cli.rb +45 -6
  6. data/lib/ruby-next/commands/core_ext.rb +166 -0
  7. data/lib/ruby-next/commands/nextify.rb +18 -3
  8. data/lib/ruby-next/core.rb +149 -1
  9. data/lib/ruby-next/core/array/deconstruct.rb +21 -0
  10. data/lib/ruby-next/core/array/difference_union_intersection.rb +15 -21
  11. data/lib/ruby-next/core/constants/no_matching_pattern_error.rb +17 -0
  12. data/lib/ruby-next/core/enumerable/filter.rb +20 -18
  13. data/lib/ruby-next/core/enumerable/filter_map.rb +28 -40
  14. data/lib/ruby-next/core/enumerable/tally.rb +9 -23
  15. data/lib/ruby-next/core/enumerator/produce.rb +12 -14
  16. data/lib/ruby-next/core/hash/deconstruct_keys.rb +21 -0
  17. data/lib/ruby-next/core/hash/merge.rb +8 -10
  18. data/lib/ruby-next/core/kernel/then.rb +7 -9
  19. data/lib/ruby-next/core/proc/compose.rb +12 -14
  20. data/lib/ruby-next/core/string/split.rb +11 -0
  21. data/lib/ruby-next/core/struct/deconstruct.rb +7 -0
  22. data/lib/ruby-next/core/struct/deconstruct_keys.rb +34 -0
  23. data/lib/ruby-next/core/time/ceil.rb +10 -0
  24. data/lib/ruby-next/core/time/floor.rb +9 -0
  25. data/lib/ruby-next/core/unboundmethod/bind_call.rb +9 -0
  26. data/lib/ruby-next/core_ext.rb +18 -0
  27. data/lib/ruby-next/language.rb +4 -2
  28. data/lib/ruby-next/language/parser.rb +5 -1
  29. data/lib/ruby-next/language/rewriters/base.rb +2 -2
  30. data/lib/ruby-next/language/rewriters/method_reference.rb +3 -1
  31. data/lib/ruby-next/language/rewriters/numbered_params.rb +2 -2
  32. data/lib/ruby-next/language/rewriters/pattern_matching.rb +36 -17
  33. data/lib/ruby-next/language/runtime.rb +2 -3
  34. data/lib/ruby-next/version.rb +1 -1
  35. metadata +13 -3
  36. data/lib/ruby-next/core/pattern_matching.rb +0 -37
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyNext::Core.patch Hash, method: :deconstruct_keys, version: "2.7" do
4
+ <<~RUBY
5
+ def deconstruct_keys(_)
6
+ self
7
+ end
8
+ RUBY
9
+ end
10
+
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")
13
+ RubyNext::Core.patch refineable: Hash, name: "HashRespondToDeconstructKeys", method: :deconstruct_keys, version: "2.7" do
14
+ <<~RUBY
15
+ def respond_to?(mid, *)
16
+ return true if mid == :deconstruct_keys
17
+ super
18
+ end
19
+ RUBY
20
+ end
21
+ end
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- unless {}.method(:merge).arity == -1
4
- RubyNext.module_eval do
5
- refine Hash do
6
- def merge(*others)
7
- return super if others.size == 1
8
- return dup if others.size == 0
3
+ RubyNext::Core.patch Hash, method: :merge, version: "2.6", supported: {}.method(:merge).arity < 0, core_ext: :prepend do
4
+ <<~RUBY
5
+ def merge(*others)
6
+ return super if others.size == 1
7
+ return dup if others.size == 0
9
8
 
10
- merge(others.shift).tap do |new_h|
11
- others.each { |h| new_h.merge!(h) }
12
- end
9
+ merge(others.shift).tap do |new_h|
10
+ others.each { |h| new_h.merge!(h) }
13
11
  end
14
12
  end
15
- end
13
+ RUBY
16
14
  end
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- unless Object.new.respond_to?(:then)
4
- RubyNext.module_eval do
5
- # Refine object, 'cause refining modules (Kernel) is vulnerable to prepend:
6
- # - https://bugs.ruby-lang.org/issues/13446
7
- # - Rails added `Kernel.prepend` in 6.1: https://github.com/rails/rails/commit/3124007bd674dcdc9c3b5c6b2964dfb7a1a0733c
8
- refine Object do
9
- alias then yield_self
10
- end
11
- end
3
+ # Refine object, 'cause refining modules (Kernel) is vulnerable to prepend:
4
+ # - https://bugs.ruby-lang.org/issues/13446
5
+ # - Rails added `Kernel.prepend` in 6.1: https://github.com/rails/rails/commit/3124007bd674dcdc9c3b5c6b2964dfb7a1a0733c
6
+ RubyNext::Core.patch Kernel, method: :then, version: "2.6", refineable: Object do
7
+ <<~RUBY
8
+ alias then yield_self
9
+ RUBY
12
10
  end
@@ -1,21 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # rubocop:disable Style/LambdaCall
4
- unless proc {}.respond_to?(:<<)
5
- RubyNext.module_eval do
6
- refine Proc do
7
- def <<(other)
8
- raise TypeError, "callable object is expected" unless other.respond_to?(:call)
9
- this = self
10
- proc { |*args, &block| this.(other.(*args, &block)) }
11
- end
4
+ RubyNext::Core.patch Proc, name: "ProcCompose", method: :<<, version: "2.6" do
5
+ <<~RUBY
6
+ def <<(other)
7
+ raise TypeError, "callable object is expected" unless other.respond_to?(:call)
8
+ this = self
9
+ proc { |*args, &block| this.(other.(*args, &block)) }
10
+ end
12
11
 
13
- def >>(other)
14
- raise TypeError, "callable object is expected" unless other.respond_to?(:call)
15
- this = self
16
- proc { |*args, &block| other.(this.(*args, &block)) }
17
- end
12
+ def >>(other)
13
+ raise TypeError, "callable object is expected" unless other.respond_to?(:call)
14
+ this = self
15
+ proc { |*args, &block| other.(this.(*args, &block)) }
18
16
  end
19
- end
17
+ RUBY
20
18
  end
21
19
  # rubocop:enable Style/LambdaCall
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyNext::Core.patch String, method: :split, version: "2.6", supported: ("a b".split(" ", &proc {}) == "a b"), core_ext: :prepend do
4
+ <<~RUBY
5
+ def split(*args, &block)
6
+ return super unless block_given?
7
+ super.each { |el| yield el }
8
+ self
9
+ end
10
+ RUBY
11
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyNext::Core.patch Struct, method: :deconstruct, version: "2.7" do
4
+ <<~'RUBY'
5
+ alias deconstruct to_a
6
+ RUBY
7
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Source: https://github.com/ruby/ruby/blob/b76a21aa45fff75909a66f8b20fc5856705f7862/struct.c#L953-L980
4
+ RubyNext::Core.patch Struct, method: :deconstruct_keys, version: "2.7" do
5
+ <<~'RUBY'
6
+ def deconstruct_keys(keys)
7
+ raise TypeError, "wrong argument type #{keys.class} (expected Array or nil)" if keys && !keys.is_a?(Array)
8
+
9
+ return to_h unless keys
10
+
11
+ return {} if size < keys.size
12
+
13
+ keys.each_with_object({}) do |k, acc|
14
+ # if k is Symbol and not a member of a Struct return {}
15
+ next if (Symbol === k || String === k) && !members.include?(k.to_sym)
16
+ # if k is Integer check that index is not ouf of bounds
17
+ next if Integer === k && k > size - 1
18
+ acc[k] = self[k]
19
+ end
20
+ end
21
+ RUBY
22
+ end
23
+
24
+ # We need to hack `respond_to?` in Ruby 2.5, since it's not working with refinements
25
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.6")
26
+ RubyNext::Core.patch refineable: Struct, name: "StructRespondToDeconstruct", method: :deconstruct_keys, version: "2.7" do
27
+ <<~RUBY
28
+ def respond_to?(mid, *)
29
+ return true if mid == :deconstruct_keys || mid == :deconstruct
30
+ super
31
+ end
32
+ RUBY
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyNext::Core.patch Time, method: :ceil, version: "2.7" do
4
+ <<~'RUBY'
5
+ def ceil(den = 0)
6
+ change = subsec.ceil(den) - subsec
7
+ self + change
8
+ end
9
+ RUBY
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyNext::Core.patch Time, method: :floor, version: "2.7" do
4
+ <<~'RUBY'
5
+ def floor(den = 0)
6
+ self - (subsec % (10**-den))
7
+ end
8
+ RUBY
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyNext::Core.patch UnboundMethod, method: :bind_call, version: "2.7" do
4
+ <<~'RUBY'
5
+ def bind_call(receiver, *args, &block)
6
+ bind(receiver).call(*args, &block)
7
+ end
8
+ RUBY
9
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "core"
4
+
5
+ # Monkey-patch core classes using the same patches as for refinements
6
+ RubyNext::Core.patches.extensions.each do |mod, patches|
7
+ patches.each do |patch|
8
+ next if patch.supported?
9
+
10
+ if patch.prepend?
11
+ mod.prepend(patch.to_module)
12
+ else
13
+ mod.module_eval(patch.body, *patch.location)
14
+ end
15
+ end
16
+ end
17
+
18
+ RubyNext::Core.strategy = :core_ext
@@ -6,7 +6,6 @@ gem "unparser", ">= 0.4.7"
6
6
  require "set"
7
7
 
8
8
  require "ruby-next"
9
- using RubyNext
10
9
 
11
10
  module RubyNext
12
11
  # Language module contains tools to transpile newer Ruby syntax
@@ -19,6 +18,8 @@ module RubyNext
19
18
  # - Each processor may modify the AST
20
19
  # - Generates a transpiled source code from the transformed AST (via the `unparser` gem)
21
20
  module Language
21
+ using RubyNext
22
+
22
23
  require "ruby-next/language/parser"
23
24
  require "ruby-next/language/unparser"
24
25
 
@@ -62,7 +63,7 @@ module RubyNext
62
63
  attr_accessor :rewriters
63
64
  attr_reader :watch_dirs
64
65
 
65
- def transform(source, rewriters: self.rewriters, using: true, context: TransformContext.new)
66
+ def transform(source, rewriters: self.rewriters, using: RubyNext::Core.refine?, context: TransformContext.new)
66
67
  parse(source).then do |ast|
67
68
  rewriters.inject(ast) do |tree, rewriter|
68
69
  rewriter.new(context).process(tree)
@@ -71,6 +72,7 @@ module RubyNext
71
72
 
72
73
  Unparser.unparse(new_ast)
73
74
  end.then do |source|
75
+ next source unless RubyNext::Core.refine?
74
76
  next source unless using && context.use_ruby_next?
75
77
 
76
78
  Core.inject! source.dup
@@ -10,7 +10,11 @@ module RubyNext
10
10
 
11
11
  class << self
12
12
  def parser
13
- ::Parser::Ruby27.new(Builder.new)
13
+ ::Parser::Ruby27.new(Builder.new).tap do |prs|
14
+ prs.diagnostics.tap do |diagnostics|
15
+ diagnostics.all_errors_are_fatal = true
16
+ end
17
+ end
14
18
  end
15
19
 
16
20
  def parse(source, file = "(string)")
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- using RubyNext
4
-
5
3
  module RubyNext
6
4
  module Language
7
5
  module Rewriters
6
+ using RubyNext
7
+
8
8
  CUSTOM_PARSER_REQUIRED = <<~MSG
9
9
  The %s feature is not a part of the latest stable Ruby release
10
10
  and is not supported by your Parser gem version.
@@ -22,7 +22,9 @@ module RubyNext
22
22
  )
23
23
  end
24
24
 
25
- unless transform(SYNTAX_PROBE) == "Language.method(:transform)"
25
+ begin
26
+ transform(SYNTAX_PROBE)
27
+ rescue ::Parser::SyntaxError
26
28
  warn_custom_parser_required_for("method reference")
27
29
  end
28
30
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- using RubyNext
4
-
5
3
  module RubyNext
6
4
  module Language
7
5
  module Rewriters
8
6
  class NumberedParams < Base
7
+ using RubyNext
8
+
9
9
  SYNTAX_PROBE = "proc { _1 }.call(1)"
10
10
  MIN_SUPPORTED_VERSION = Gem::Version.new("2.7.0")
11
11
 
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- using RubyNext
4
-
5
3
  module RubyNext
6
4
  module Language
7
5
  module Rewriters
6
+ using RubyNext
7
+
8
8
  using(Module.new do
9
9
  refine ::Parser::AST::Node do
10
10
  def to_ast_node
@@ -66,8 +66,6 @@ module RubyNext
66
66
  def on_in_match(node)
67
67
  context.track! self
68
68
 
69
- @deconstructed = []
70
-
71
69
  matchee =
72
70
  s(:lvasgn, MATCHEE, node.children[0])
73
71
 
@@ -146,10 +144,10 @@ module RubyNext
146
144
  s(:or, *children)
147
145
  end
148
146
 
149
- def match_as_clause(node)
147
+ def match_as_clause(node, right = s(:lvar, locals[:matchee]))
150
148
  s(:and,
151
149
  case_eq_clause(node.children[0]),
152
- match_var_clause(node.children[1], s(:lvar, locals[:matchee])))
150
+ match_var_clause(node.children[1], right))
153
151
  end
154
152
 
155
153
  def match_var_clause(node, left = s(:lvar, locals[:matchee]))
@@ -195,6 +193,10 @@ module RubyNext
195
193
  s(:and,
196
194
  dnode,
197
195
  right)
196
+ end.then do |right|
197
+ s(:and,
198
+ respond_to_check(matchee, :deconstruct),
199
+ right)
198
200
  end
199
201
  end
200
202
 
@@ -202,20 +204,21 @@ module RubyNext
202
204
 
203
205
  def deconstruct_node(matchee)
204
206
  # only deconstruct once per case
205
- return if deconstructed.include?(locals[:arr])
207
+ return if deconstructed&.include?(locals[:arr])
206
208
 
207
209
  context.use_ruby_next!
208
210
 
209
211
  right = s(:send, matchee, :deconstruct)
210
212
 
211
- deconstructed << locals[:arr]
213
+ deconstructed << locals[:arr] if deconstructed
214
+
212
215
  s(:and,
213
216
  s(:or,
214
217
  s(:lvasgn, locals[:arr], right),
215
218
  s(:true)), # rubocop:disable Lint/BooleanSymbol
216
219
  s(:or,
217
220
  case_eq_clause(s(:const, nil, :Array), s(:lvar, locals[:arr])),
218
- raise_error(:TypeError)))
221
+ raise_error(:TypeError, "#deconstruct must return Array")))
219
222
  end
220
223
 
221
224
  def array_element(index, head, *tail)
@@ -282,6 +285,10 @@ module RubyNext
282
285
  match_var_clause(node, arr_item_at(index))
283
286
  end
284
287
 
288
+ def match_as_array_element(node, index)
289
+ match_as_clause(node, arr_item_at(index))
290
+ end
291
+
285
292
  def pin_array_element(node, index)
286
293
  case_eq_array_element node.children[0], index
287
294
  end
@@ -321,11 +328,15 @@ module RubyNext
321
328
  hash_element(*node.children)
322
329
  end
323
330
 
324
- return dnode if right.nil?
331
+ next dnode if right.nil?
325
332
 
326
333
  s(:and,
327
334
  dnode,
328
335
  right)
336
+ end.then do |right|
337
+ s(:and,
338
+ respond_to_check(matchee, :deconstruct_keys),
339
+ right)
329
340
  end
330
341
  end
331
342
 
@@ -359,11 +370,12 @@ module RubyNext
359
370
  end
360
371
 
361
372
  # Create a copy of the original hash if already deconstructed
362
- return hash_dup if deconstructed.include?(locals[:hash])
373
+ # FIXME: need better algorithm to handle different key sets
374
+ # return hash_dup if deconstructed&.include?(locals[:hash])
363
375
 
364
376
  context.use_ruby_next!
365
377
 
366
- deconstructed << locals[:hash]
378
+ deconstructed << locals[:hash] if deconstructed
367
379
 
368
380
  right = s(:send,
369
381
  matchee, :deconstruct_keys, keys)
@@ -375,7 +387,7 @@ module RubyNext
375
387
  s(:and,
376
388
  s(:or,
377
389
  case_eq_clause(s(:const, nil, :Hash), s(:lvar, locals[:hash, :src])),
378
- raise_error(:TypeError)),
390
+ raise_error(:TypeError, "#deconstruct_keys must return Hash")),
379
391
  hash_dup))
380
392
  end
381
393
 
@@ -491,14 +503,21 @@ module RubyNext
491
503
  end
492
504
 
493
505
  def no_matching_pattern
494
- raise_error :NoMatchingPatternError
506
+ raise_error(
507
+ :NoMatchingPatternError,
508
+ s(:send,
509
+ s(:lvar, locals[:matchee]), :inspect)
510
+ )
495
511
  end
496
512
 
497
- def raise_error(type)
513
+ def raise_error(type, msg = "")
498
514
  s(:send, s(:const, nil, :Kernel), :raise,
499
515
  s(:const, nil, type),
500
- s(:send,
501
- s(:lvar, locals[:matchee]), :inspect))
516
+ msg.to_ast_node)
517
+ end
518
+
519
+ def respond_to_check(node, mid)
520
+ s(:send, node, :respond_to?, mid.to_ast_node)
502
521
  end
503
522
 
504
523
  def respond_to_missing?(mid, *)
@@ -7,13 +7,12 @@ require "ruby-next/utils"
7
7
  require "ruby-next/language"
8
8
  require "ruby-next/language/eval"
9
9
 
10
- using RubyNext
11
-
12
10
  module RubyNext
13
11
  module Language
14
12
  # Module responsible for runtime transformations
15
-
16
13
  module Runtime
14
+ using RubyNext
15
+
17
16
  class << self
18
17
  include Utils
19
18