ruby-next-core 0.8.0 → 0.10.0

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