ruby-next-core 0.8.0 → 0.10.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 (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,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+
6
+ require "ruby-next/language"
7
+
8
+ module RubyNext
9
+ module Commands
10
+ class Nextify < Base
11
+ using RubyNext
12
+
13
+ attr_reader :lib_path, :paths, :out_path, :min_version, :single_version, :specified_rewriters
14
+
15
+ def run
16
+ log "RubyNext core strategy: #{RubyNext::Core.strategy}"
17
+ log "RubyNext transpile mode: #{RubyNext::Language.mode}"
18
+
19
+ remove_rbnext!
20
+
21
+ @min_version ||= MIN_SUPPORTED_VERSION
22
+
23
+ paths.each do |path|
24
+ contents = File.read(path)
25
+ transpile path, contents
26
+ end
27
+ end
28
+
29
+ def parse!(args)
30
+ print_help = false
31
+ print_rewriters = false
32
+ rewriter_names = []
33
+ @single_version = false
34
+
35
+ optparser = base_parser do |opts|
36
+ opts.banner = "Usage: ruby-next nextify DIRECTORY_OR_FILE [options]"
37
+
38
+ opts.on("-o", "--output=OUTPUT", "Specify output directory or file or stdout") do |val|
39
+ @out_path = val
40
+ end
41
+
42
+ opts.on("--min-version=VERSION", "Specify the minimum Ruby version to support") do |val|
43
+ @min_version = Gem::Version.new(val)
44
+ end
45
+
46
+ opts.on("--single-version", "Only create one version of a file (for the earliest Ruby version)") do
47
+ @single_version = true
48
+ end
49
+
50
+ opts.on("--edge", "Enable edge (master) Ruby features") do |val|
51
+ require "ruby-next/language/edge"
52
+ end
53
+
54
+ opts.on("--proposed", "Enable proposed/experimental Ruby features") do |val|
55
+ require "ruby-next/language/proposed"
56
+ end
57
+
58
+ opts.on(
59
+ "--transpile-mode=MODE",
60
+ "Transpiler mode (ast or rewrite). Default: ast"
61
+ ) do |val|
62
+ Language.mode = val.to_sym
63
+ end
64
+
65
+ opts.on("--[no-]refine", "Do not inject `using RubyNext`") do |val|
66
+ Core.strategy = :core_ext unless val
67
+ end
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
+
77
+ opts.on("-h", "--help", "Print help") do
78
+ print_help = true
79
+ end
80
+ end
81
+
82
+ optparser.parse!(args)
83
+
84
+ @lib_path = args[0]
85
+
86
+ if print_help
87
+ $stdout.puts optparser.help
88
+ exit 0
89
+ end
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
+
98
+ unless ((!lib_path.nil?) || nil) && lib_path.then(&File.method(:exist?))
99
+ $stdout.puts "Path not found: #{lib_path}"
100
+ $stdout.puts optparser.help
101
+ exit 2
102
+ end
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
+
120
+ @paths =
121
+ if File.directory?(lib_path)
122
+ Dir[File.join(lib_path, "**/*.rb")]
123
+ elsif File.file?(lib_path)
124
+ [lib_path].tap do |_|
125
+ @lib_path = File.dirname(lib_path)
126
+ end
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ def transpile(path, contents, version: min_version)
133
+ rewriters = specified_rewriters || Language.rewriters.select { |rw| rw.unsupported_version?(version) }
134
+
135
+ context = Language::TransformContext.new
136
+
137
+ new_contents = Language.transform contents, context: context, rewriters: rewriters
138
+
139
+ return unless context.dirty?
140
+
141
+ versions = context.sorted_versions
142
+ version = versions.shift
143
+
144
+ # First, store already transpiled contents in the minimum required version dir
145
+ save new_contents, path, version
146
+
147
+ return if versions.empty? || single_version?
148
+
149
+ # Then, generate the source code for the next version
150
+ transpile path, contents, version: version
151
+ end
152
+
153
+ def save(contents, path, version)
154
+ return $stdout.puts(contents) if stdout?
155
+
156
+ paths = [Pathname.new(path).relative_path_from(Pathname.new(lib_path))]
157
+
158
+ paths.unshift(version.segments[0..1].join(".")) unless single_version?
159
+
160
+ next_path =
161
+ if next_dir_path.end_with?(".rb")
162
+ out_path
163
+ else
164
+ File.join(next_dir_path, *paths)
165
+ end
166
+
167
+ unless CLI.dry_run?
168
+ FileUtils.mkdir_p File.dirname(next_path)
169
+
170
+ File.write(next_path, contents)
171
+ end
172
+
173
+ log "Generated: #{next_path}"
174
+ end
175
+
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
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Language
5
+ module KernelEval
6
+ if Utils.refine_modules?
7
+ refine Kernel do
8
+ def eval(source, bind = nil, *args)
9
+ new_source = ::RubyNext::Language::Runtime.transform(
10
+ source,
11
+ using: ((!bind.nil?) || nil) && bind.receiver == TOPLEVEL_BINDING.receiver || ((!(((!bind.nil?) || nil) && bind.receiver).nil?) || nil) && (((!bind.nil?) || nil) && bind.receiver).is_a?(Module)
12
+ )
13
+ RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
14
+ super new_source, bind, *args
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ module InstanceEval # :nodoc:
21
+ refine Object do
22
+ def instance_eval(*args, &block)
23
+ return super(*args, &block) if block_given?
24
+
25
+ source = args.shift
26
+ new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
27
+ RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
28
+ super new_source, *args
29
+ end
30
+ end
31
+ end
32
+
33
+ module ClassEval
34
+ refine Module do
35
+ def module_eval(*args, &block)
36
+ return super(*args, &block) if block_given?
37
+
38
+ source = args.shift
39
+ new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
40
+ RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
41
+ super new_source, *args
42
+ end
43
+
44
+ def class_eval(*args, &block)
45
+ return super(*args, &block) if block_given?
46
+
47
+ source = args.shift
48
+ new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
49
+ RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
50
+ super new_source, *args
51
+ end
52
+ end
53
+ end
54
+
55
+ # Refinements for `eval`-like methods.
56
+ # Transpiling eval is only possible if we do not use local from the binding,
57
+ # because we cannot access the binding of caller (without non-production ready hacks).
58
+ #
59
+ # This module is meant mainly for testing purposes.
60
+ module Eval
61
+ include InstanceEval
62
+ include ClassEval
63
+ include KernelEval
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Language
5
+ module Rewriters
6
+ using RubyNext
7
+
8
+ CUSTOM_PARSER_REQUIRED = <<-MSG
9
+ The %s feature is not a part of the latest stable Ruby release
10
+ and is not supported by your Parser gem version.
11
+ Use RubyNext's parser to use it: https://github.com/ruby-next/parser
12
+
13
+ MSG
14
+
15
+ class Base < ::Parser::TreeRewriter
16
+ class LocalsTracker
17
+ attr_reader :stacks
18
+
19
+ def initialize
20
+ @stacks = []
21
+ end
22
+
23
+ def with(**locals)
24
+ stacks << locals
25
+ yield.tap { stacks.pop }
26
+ end
27
+
28
+ def [](name, suffix = nil)
29
+ fetch(name).then do |name|
30
+ next name unless suffix
31
+ :"#{name}#{suffix}__"
32
+ end
33
+ end
34
+
35
+ def key?(name)
36
+ !!fetch(name) { false } # rubocop:disable Style/RedundantFetchBlock
37
+ end
38
+
39
+ def fetch(name)
40
+ ind = -1
41
+
42
+ loop do
43
+ break stacks[ind][name] if stacks[ind].key?(name)
44
+ ind -= 1
45
+ break if stacks[ind].nil?
46
+ end.then do |name|
47
+ next name unless name.nil?
48
+
49
+ return yield if block_given?
50
+ raise ArgumentError, "Local var not found in scope: #{name}"
51
+ end
52
+ end
53
+ end
54
+
55
+ class << self
56
+ # Returns true if the syntax is supported
57
+ # by the current Ruby (performs syntax check, not version check)
58
+ def unsupported_syntax?
59
+ save_verbose, $VERBOSE = $VERBOSE, nil
60
+ eval_mid = Kernel.respond_to?(:eval_without_ruby_next) ? :eval_without_ruby_next : :eval
61
+ Kernel.send eval_mid, self::SYNTAX_PROBE, nil, __FILE__, __LINE__
62
+ false
63
+ rescue SyntaxError, NameError
64
+ true
65
+ ensure
66
+ $VERBOSE = save_verbose
67
+ end
68
+
69
+ # Returns true if the syntax is supported
70
+ # by the specified version
71
+ def unsupported_version?(version)
72
+ self::MIN_SUPPORTED_VERSION > version
73
+ end
74
+
75
+ private
76
+
77
+ def transform(source)
78
+ Language.transform(source, rewriters: [self], using: false)
79
+ end
80
+ end
81
+
82
+ attr_reader :locals
83
+
84
+ def initialize(context)
85
+ @context = context
86
+ @locals = LocalsTracker.new
87
+ super()
88
+ end
89
+
90
+ def s(type, *children)
91
+ ::Parser::AST::Node.new(type, children)
92
+ end
93
+
94
+ private
95
+
96
+ def replace(range, ast)
97
+ ((!@source_rewriter.nil?) || nil) && @source_rewriter.replace(range, unparse(ast))
98
+ end
99
+
100
+ def remove(range)
101
+ ((!@source_rewriter.nil?) || nil) && @source_rewriter.remove(range)
102
+ end
103
+
104
+ def insert_after(range, ast)
105
+ ((!@source_rewriter.nil?) || nil) && @source_rewriter.insert_after(range, unparse(ast))
106
+ end
107
+
108
+ def insert_before(range, ast)
109
+ ((!@source_rewriter.nil?) || nil) && @source_rewriter.insert_before(range, unparse(ast))
110
+ end
111
+
112
+ def unparse(ast)
113
+ return ast if ast.is_a?(String)
114
+ Unparser.unparse(ast)
115
+ end
116
+
117
+ attr_reader :context
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Language
5
+ module Rewriters
6
+ class EndlessRange < Base
7
+ NAME = "endless-range"
8
+ SYNTAX_PROBE = "[0, 1][1..]"
9
+ MIN_SUPPORTED_VERSION = Gem::Version.new("2.6.0")
10
+
11
+ def on_index(node)
12
+ @current_index = node
13
+ new_index = process(node.children.last)
14
+ return unless new_index != node.children.last
15
+
16
+ node.updated(
17
+ nil,
18
+ [
19
+ node.children.first,
20
+ new_index
21
+ ]
22
+ )
23
+ end
24
+
25
+ def on_erange(node)
26
+ return unless node.children.last.nil?
27
+
28
+ context.track! self
29
+
30
+ new_end =
31
+ if index_arg?(node)
32
+ s(:int, -1)
33
+ else
34
+ s(:const,
35
+ s(:const,
36
+ s(:cbase), :Float),
37
+ :INFINITY)
38
+ end
39
+
40
+ replace(node.loc.expression, "#{node.children.first.loc.expression.source}..#{unparse(new_end)}")
41
+
42
+ node.updated(
43
+ :irange,
44
+ [
45
+ node.children.first,
46
+ new_end
47
+ ]
48
+ )
49
+ end
50
+
51
+ alias_method :on_irange, :on_erange
52
+
53
+ private
54
+
55
+ attr_reader :current_index
56
+
57
+ def index_arg?(node)
58
+ ((!(((!current_index.nil?) || nil) && current_index.children).nil?) || nil) && (((!current_index.nil?) || nil) && current_index.children).include?(node)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end