ruby-next-core 0.2.0 → 0.3.0

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