ruby-next-core 0.14.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -0
  3. data/README.md +163 -56
  4. data/bin/mspec +11 -0
  5. data/lib/.rbnext/2.1/ruby-next/commands/nextify.rb +295 -0
  6. data/lib/.rbnext/2.1/ruby-next/core.rb +12 -4
  7. data/lib/.rbnext/2.1/ruby-next/language.rb +62 -12
  8. data/lib/.rbnext/2.3/ruby-next/commands/nextify.rb +97 -3
  9. data/lib/.rbnext/2.3/ruby-next/config.rb +79 -0
  10. data/lib/.rbnext/2.3/ruby-next/core/data.rb +163 -0
  11. data/lib/.rbnext/2.3/ruby-next/language/eval.rb +4 -4
  12. data/lib/.rbnext/2.3/ruby-next/language/rewriters/2.7/args_forward.rb +134 -0
  13. data/lib/.rbnext/2.3/ruby-next/language/rewriters/2.7/pattern_matching.rb +122 -47
  14. data/lib/.rbnext/2.3/ruby-next/language/rewriters/base.rb +6 -32
  15. data/lib/.rbnext/2.3/ruby-next/utils.rb +3 -22
  16. data/lib/.rbnext/2.6/ruby-next/core/data.rb +163 -0
  17. data/lib/.rbnext/2.7/ruby-next/core/data.rb +163 -0
  18. data/lib/.rbnext/2.7/ruby-next/core.rb +12 -4
  19. data/lib/.rbnext/2.7/ruby-next/language/paco_parsers/string_literals.rb +109 -0
  20. data/lib/.rbnext/2.7/ruby-next/language/rewriters/2.7/pattern_matching.rb +1095 -0
  21. data/lib/.rbnext/2.7/ruby-next/language/rewriters/text.rb +132 -0
  22. data/lib/.rbnext/3.2/ruby-next/commands/base.rb +55 -0
  23. data/lib/.rbnext/3.2/ruby-next/language/rewriters/2.7/pattern_matching.rb +1095 -0
  24. data/lib/.rbnext/3.2/ruby-next/rubocop.rb +210 -0
  25. data/lib/ruby-next/cli.rb +10 -15
  26. data/lib/ruby-next/commands/nextify.rb +99 -3
  27. data/lib/ruby-next/config.rb +31 -4
  28. data/lib/ruby-next/core/data.rb +163 -0
  29. data/lib/ruby-next/core/matchdata/deconstruct.rb +9 -0
  30. data/lib/ruby-next/core/matchdata/deconstruct_keys.rb +20 -0
  31. data/lib/ruby-next/core/matchdata/named_captures.rb +11 -0
  32. data/lib/ruby-next/core/proc/compose.rb +0 -1
  33. data/lib/ruby-next/core/refinement/import.rb +44 -36
  34. data/lib/ruby-next/core/time/deconstruct_keys.rb +30 -0
  35. data/lib/ruby-next/core.rb +11 -3
  36. data/lib/ruby-next/irb.rb +24 -0
  37. data/lib/ruby-next/language/bootsnap.rb +2 -25
  38. data/lib/ruby-next/language/eval.rb +4 -4
  39. data/lib/ruby-next/language/paco_parser.rb +7 -0
  40. data/lib/ruby-next/language/paco_parsers/base.rb +47 -0
  41. data/lib/ruby-next/language/paco_parsers/comments.rb +26 -0
  42. data/lib/ruby-next/language/paco_parsers/string_literals.rb +109 -0
  43. data/lib/ruby-next/language/parser.rb +31 -6
  44. data/lib/ruby-next/language/rewriters/2.5/rescue_within_block.rb +41 -0
  45. data/lib/ruby-next/language/rewriters/2.7/args_forward.rb +57 -0
  46. data/lib/ruby-next/language/rewriters/2.7/pattern_matching.rb +120 -45
  47. data/lib/ruby-next/language/rewriters/3.0/args_forward_leading.rb +2 -2
  48. data/lib/ruby-next/language/rewriters/3.1/oneline_pattern_parensless.rb +1 -1
  49. data/lib/ruby-next/language/rewriters/3.1/shorthand_hash.rb +2 -1
  50. data/lib/ruby-next/language/rewriters/3.2/anonymous_restargs.rb +104 -0
  51. data/lib/ruby-next/language/rewriters/abstract.rb +57 -0
  52. data/lib/ruby-next/language/rewriters/base.rb +6 -32
  53. data/lib/ruby-next/language/rewriters/edge/it_param.rb +58 -0
  54. data/lib/ruby-next/language/rewriters/edge.rb +12 -0
  55. data/lib/ruby-next/language/rewriters/proposed/bind_vars_pattern.rb +3 -0
  56. data/lib/ruby-next/language/rewriters/proposed/method_reference.rb +9 -20
  57. data/lib/ruby-next/language/rewriters/text.rb +132 -0
  58. data/lib/ruby-next/language/runtime.rb +9 -86
  59. data/lib/ruby-next/language/setup.rb +5 -2
  60. data/lib/ruby-next/language/unparser.rb +5 -0
  61. data/lib/ruby-next/language.rb +62 -12
  62. data/lib/ruby-next/pry.rb +90 -0
  63. data/lib/ruby-next/rubocop.rb +2 -0
  64. data/lib/ruby-next/utils.rb +3 -22
  65. data/lib/ruby-next/version.rb +1 -1
  66. data/lib/uby-next/irb.rb +3 -0
  67. data/lib/uby-next/pry.rb +3 -0
  68. data/lib/uby-next.rb +2 -2
  69. metadata +70 -10
@@ -0,0 +1,295 @@
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
+ class Stats
14
+ def initialize
15
+ @started_at = ::Process.clock_gettime(Process::CLOCK_MONOTONIC)
16
+ @files = 0
17
+ @scans = 0
18
+ @transpiled_files = 0
19
+ end
20
+
21
+ def file!
22
+ @files += 1
23
+ end
24
+
25
+ def scan!
26
+ @scans += 1
27
+ end
28
+
29
+ def transpiled!
30
+ @transpiled_files += 1
31
+ end
32
+
33
+ def report
34
+ <<-TXT
35
+ Files processed: #{@files}
36
+ Total scans: #{@scans}
37
+ Files transpiled: #{@transpiled_files}
38
+ Completed in #{::Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at}s
39
+ TXT
40
+ end
41
+ end
42
+
43
+ attr_reader :lib_path, :paths, :out_path, :min_version, :single_version, :specified_rewriters, :overwrite
44
+ alias_method :overwrite?, :overwrite
45
+ attr_reader :stats
46
+
47
+ def run
48
+ @stats = Stats.new
49
+
50
+ log "RubyNext core strategy: #{RubyNext::Core.strategy}"
51
+ log "RubyNext transpile mode: #{RubyNext::Language.mode}"
52
+
53
+ remove_rbnext!
54
+
55
+ @min_version ||= MIN_SUPPORTED_VERSION
56
+
57
+ paths.each do |path|
58
+ stats.file!
59
+
60
+ contents = File.read(path)
61
+ transpile path, contents
62
+ end
63
+
64
+ ensure_rbnext!
65
+
66
+ log stats.report
67
+ end
68
+
69
+ def parse!(args)
70
+ print_help = false
71
+ print_rewriters = false
72
+ rewriter_names = []
73
+ custom_rewriters = []
74
+ @single_version = false
75
+ @overwrite = false
76
+
77
+ optparser = base_parser do |opts|
78
+ opts.banner = "Usage: ruby-next nextify DIRECTORY_OR_FILE [options]"
79
+
80
+ opts.on("-o", "--output=OUTPUT", "Specify output directory or file or stdout") do |val|
81
+ @out_path = val
82
+ end
83
+
84
+ opts.on("--min-version=VERSION", "Specify the minimum Ruby version to support") do |val|
85
+ @min_version = Gem::Version.new(val)
86
+ end
87
+
88
+ opts.on("--single-version", "Only create one version of a file (for the earliest Ruby version)") do
89
+ @single_version = true
90
+ end
91
+
92
+ opts.on("--overwrite", "Overwrite original file") do
93
+ @overwrite = true
94
+ end
95
+
96
+ opts.on("--edge", "Enable edge (master) Ruby features") do |val|
97
+ ENV["RUBY_NEXT_EDGE"] = val.to_s
98
+ require "ruby-next/language/rewriters/edge"
99
+ end
100
+
101
+ opts.on("--proposed", "Enable proposed/experimental Ruby features") do |val|
102
+ ENV["RUBY_NEXT_PROPOSED"] = val.to_s
103
+ require "ruby-next/language/rewriters/proposed"
104
+ end
105
+
106
+ opts.on(
107
+ "--transpile-mode=MODE",
108
+ "Transpiler mode (ast or rewrite). Default: rewrite"
109
+ ) do |val|
110
+ Language.mode = val.to_sym
111
+ end
112
+
113
+ opts.on("--[no-]refine", "Do not inject `using RubyNext`") do |val|
114
+ Core.strategy = :core_ext unless val
115
+ end
116
+
117
+ opts.on("--list-rewriters", "List available rewriters") do |val|
118
+ print_rewriters = true
119
+ end
120
+
121
+ opts.on("--rewrite=REWRITERS...", "Specify particular Ruby features to rewrite") do |val|
122
+ rewriter_names << val
123
+ end
124
+
125
+ opts.on("--import-rewriter=REWRITERS...", "Specify paths to load custom rewritiers") do |val|
126
+ custom_rewriters << val
127
+ end
128
+
129
+ opts.on("-h", "--help", "Print help") do
130
+ print_help = true
131
+ end
132
+ end
133
+
134
+ optparser.parse!(args)
135
+
136
+ @lib_path = args[0]
137
+
138
+ if print_help
139
+ $stdout.puts optparser.help
140
+ exit 0
141
+ end
142
+
143
+ # Load custom rewriters
144
+ custom_rewriters.each do |path|
145
+ Kernel.load path
146
+ end
147
+
148
+ if print_rewriters
149
+ Language.rewriters.each do |rewriter|
150
+ $stdout.puts "#{rewriter::NAME} (\"#{rewriter::SYNTAX_PROBE}\")#{rewriter.unsupported_syntax? ? " (unsupported)" : ""}"
151
+ end
152
+ exit 0
153
+ end
154
+
155
+ unless ((((__safe_lvar__ = lib_path) || true) && (!__safe_lvar__.nil? || nil)) && __safe_lvar__.then(&File.method(:exist?)))
156
+ $stdout.puts "Path not found: #{lib_path}"
157
+ $stdout.puts optparser.help
158
+ exit 2
159
+ end
160
+
161
+ if rewriter_names.any? && min_version
162
+ $stdout.puts "--rewrite cannot be used with --min-version simultaneously"
163
+ exit 2
164
+ end
165
+
166
+ @specified_rewriters =
167
+ if rewriter_names.any?
168
+ begin
169
+ Language.select_rewriters(*rewriter_names)
170
+ rescue Language::RewriterNotFoundError => error
171
+ $stdout.puts error.message
172
+ $stdout.puts "Try --list-rewriters to see list of available rewriters"
173
+ exit 2
174
+ end
175
+ end
176
+
177
+ if overwrite? && !single_version?
178
+ $stdout.puts "--overwrite only works with --single-version or explcit rewritires specified (via --rewrite)"
179
+ exit 2
180
+ end
181
+
182
+ @paths =
183
+ if File.directory?(lib_path)
184
+ Dir[File.join(lib_path, "**/*.rb")]
185
+ elsif File.file?(lib_path)
186
+ [lib_path].tap do |_|
187
+ @lib_path = File.dirname(lib_path)
188
+ end
189
+ end
190
+ end
191
+
192
+ private
193
+
194
+ def transpile(path, contents, version: min_version)
195
+ stats.scan!
196
+
197
+ rewriters = specified_rewriters || Language.rewriters.select { |rw| rw.unsupported_version?(version) }
198
+
199
+ context = Language::TransformContext.new
200
+
201
+ new_contents = Language.transform contents, context: context, rewriters: rewriters
202
+
203
+ return unless context.dirty?
204
+
205
+ versions = context.sorted_versions
206
+ version = versions.shift
207
+
208
+ # First, store already transpiled contents in the minimum required version dir
209
+ save new_contents, path, version
210
+
211
+ return if versions.empty? || single_version?
212
+
213
+ # Then, generate the source code for the next version
214
+ transpile path, contents, version: version
215
+ rescue SyntaxError, StandardError => e
216
+ warn "Failed to transpile #{path}: #{e.class} — #{e.message}"
217
+ warn e.backtrace.take(10).join("\n") if ENV["RUBY_NEXT_DEBUG"] == "1"
218
+ exit 1
219
+ end
220
+
221
+ def save(contents, path, version)
222
+ stats.transpiled!
223
+
224
+ return $stdout.puts(contents) if stdout?
225
+
226
+ paths = [Pathname.new(path).relative_path_from(Pathname.new(lib_path))]
227
+
228
+ paths.unshift(version.segments[0..1].join(".")) unless single_version?
229
+
230
+ if overwrite?
231
+ overwrite_file_content!(path: path, contents: contents)
232
+
233
+ return
234
+ end
235
+
236
+ next_path =
237
+ if next_dir_path.end_with?(".rb")
238
+ out_path
239
+ else
240
+ File.join(next_dir_path, *paths)
241
+ end
242
+
243
+ unless CLI.dry_run?
244
+ FileUtils.mkdir_p File.dirname(next_path)
245
+
246
+ File.write(next_path, contents)
247
+ end
248
+
249
+ log "Generated: #{next_path}"
250
+ end
251
+
252
+ def overwrite_file_content!(path: ::Kernel.raise(::ArgumentError, "missing keyword: path"), contents: ::Kernel.raise(::ArgumentError, "missing keyword: contents"))
253
+ unless CLI.dry_run?
254
+ File.write(path, contents)
255
+ end
256
+
257
+ log "Overwritten: #{path}"
258
+ end
259
+
260
+ def remove_rbnext!
261
+ return if CLI.dry_run? || stdout?
262
+
263
+ return unless File.directory?(next_dir_path)
264
+
265
+ log "Remove old files: #{next_dir_path}"
266
+ FileUtils.rm_r(next_dir_path)
267
+ end
268
+
269
+ def ensure_rbnext!
270
+ return if CLI.dry_run? || stdout?
271
+
272
+ return if File.directory?(next_dir_path)
273
+
274
+ return if next_dir_path.end_with?(".rb")
275
+
276
+ return if overwrite?
277
+
278
+ FileUtils.mkdir_p next_dir_path
279
+ File.write(File.join(next_dir_path, ".keep"), "")
280
+ end
281
+
282
+ def next_dir_path
283
+ @next_dir_path ||= out_path || File.join(lib_path, RUBY_NEXT_DIR)
284
+ end
285
+
286
+ def stdout?
287
+ out_path == "stdout"
288
+ end
289
+
290
+ def single_version?
291
+ single_version || specified_rewriters
292
+ end
293
+ end
294
+ end
295
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
3
+ require "set" # rubocop:disable Lint/RedundantRequireStatement
4
4
 
5
5
  require "ruby-next/config"
6
6
  require "ruby-next/utils"
@@ -62,7 +62,7 @@ module RubyNext
62
62
  mod_name = singleton? ? singleton.name : mod.name
63
63
  camelized_method_name = method_name.to_s.split("_").map(&:capitalize).join
64
64
 
65
- "#{mod_name}#{camelized_method_name}".gsub(/\W/, "") # rubocop:disable Performance/StringReplacement
65
+ "#{mod_name}#{camelized_method_name}".gsub(/\W/, "")
66
66
  end
67
67
 
68
68
  def build_location(trace_locations)
@@ -78,7 +78,7 @@ module RubyNext
78
78
  end
79
79
 
80
80
  def native_location?(location)
81
- location.nil? || location.first.match?(/(<internal:|resource:\/truffleruby\/core)/)
81
+ location.nil? || location.first.match?(/(<internal:|resource:\/truffleruby\/core|uri:classloader:\/jruby)/)
82
82
  end
83
83
  end
84
84
 
@@ -125,7 +125,7 @@ module RubyNext
125
125
 
126
126
  def patch(*__rest__, &__block__)
127
127
  patches << Patch.new(*__rest__, &__block__)
128
- end
128
+ end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :patch)
129
129
 
130
130
  # Inject `using RubyNext` at the top of the source code
131
131
  def inject!(contents)
@@ -197,6 +197,11 @@ require "ruby-next/core/matchdata/match"
197
197
  require "ruby-next/core/enumerable/compact"
198
198
  require "ruby-next/core/integer/try_convert"
199
199
 
200
+ require "ruby-next/core/matchdata/deconstruct"
201
+ require "ruby-next/core/matchdata/deconstruct_keys"
202
+ require "ruby-next/core/matchdata/named_captures"
203
+ require "ruby-next/core/time/deconstruct_keys"
204
+
200
205
  # Generate refinements
201
206
  RubyNext.module_eval do
202
207
  RubyNext::Core.patches.refined.each do |mod, patches|
@@ -210,3 +215,6 @@ RubyNext.module_eval do
210
215
  end
211
216
  end
212
217
  end
218
+
219
+ # Load backports
220
+ require "ruby-next/core/data" unless ENV["RUBY_NEXT_DISABLE_DATA"] == "true"
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- gem "ruby-next-parser", ">= 2.8.0.3"
4
- gem "unparser", ">= 0.4.7"
3
+ # Checking gem specs doesn't work in ruby.wasm
4
+ unless RUBY_PLATFORM.match?(/wasm/)
5
+ gem "ruby-next-parser", ">= 2.8.0.3"
6
+ gem "unparser", ">= 0.4.7"
7
+ end
5
8
 
6
- require "set"
9
+ require "set" # rubocop:disable Lint/RedundantRequireStatement
7
10
 
8
11
  require "ruby-next"
9
12
 
@@ -62,8 +65,15 @@ module RubyNext
62
65
  end
63
66
 
64
67
  class << self
68
+ attr_reader :include_patterns
69
+ attr_reader :exclude_patterns
70
+
71
+ def watch_dirs
72
+ warn "[DEPRECATED] Use `RubyNext::Language.include_patterns` instead of `RubyNext::Language.watch_dirs`"
73
+ @watch_dirs
74
+ end
75
+
65
76
  attr_accessor :rewriters
66
- attr_reader :watch_dirs
67
77
 
68
78
  attr_accessor :strategy
69
79
 
@@ -95,14 +105,17 @@ module RubyNext
95
105
  end
96
106
 
97
107
  def transform(source, rewriters: self.rewriters, using: RubyNext::Core.refine?, context: TransformContext.new)
108
+ text_rewriters, ast_rewriters = rewriters.partition(&:text?)
109
+
98
110
  retried = 0
99
- new_source = nil
111
+ new_source = text_rewrite(source, rewriters: text_rewriters, using: using, context: context)
112
+
100
113
  begin
101
114
  new_source =
102
115
  if mode == :rewrite
103
- rewrite(source, rewriters: rewriters, using: using, context: context)
116
+ rewrite(new_source, rewriters: ast_rewriters, using: using, context: context)
104
117
  else
105
- regenerate(source, rewriters: rewriters, using: using, context: context)
118
+ regenerate(new_source, rewriters: ast_rewriters, using: using, context: context)
106
119
  end
107
120
  rescue Unparser::UnknownNodeError => err
108
121
  RubyNext.warn "Ruby Next fallbacks to \"rewrite\" transpiling mode since the version of Unparser you use doesn't support some syntax yet: #{err.message}.\n" \
@@ -119,8 +132,17 @@ module RubyNext
119
132
  Core.inject! new_source.dup
120
133
  end
121
134
 
135
+ def target_dir?(dirname)
136
+ # fnmatch? requires a file name, not a folder
137
+ fname = File.join(dirname, "x.rb")
138
+
139
+ include_patterns.any? { |pattern| File.fnmatch?(pattern, fname) } &&
140
+ exclude_patterns.none? { |pattern| File.fnmatch?(pattern, fname) }
141
+ end
142
+
122
143
  def transformable?(path)
123
- watch_dirs.any? { |dir| path.start_with?(dir) }
144
+ include_patterns.any? { |pattern| File.fnmatch?(pattern, path) } &&
145
+ exclude_patterns.none? { |pattern| File.fnmatch?(pattern, path) }
124
146
  end
125
147
 
126
148
  # Rewriters required for the current version
@@ -165,14 +187,36 @@ module RubyNext
165
187
  end
166
188
  end
167
189
 
190
+ def text_rewrite(source, rewriters: ::Kernel.raise(::ArgumentError, "missing keyword: rewriters"), using: ::Kernel.raise(::ArgumentError, "missing keyword: using"), context: ::Kernel.raise(::ArgumentError, "missing keyword: context"))
191
+ rewriters.inject(source) do |src, rewriter|
192
+ rewriter.new(context).rewrite(src)
193
+ end.then do |new_source|
194
+ next source unless context.dirty?
195
+
196
+ new_source
197
+ end
198
+ end
199
+
168
200
  attr_writer :watch_dirs
201
+ attr_writer :include_patterns, :exclude_patterns
169
202
  end
170
203
 
171
204
  self.rewriters = []
172
- self.watch_dirs = %w[app lib spec test].map { |path| File.join(Dir.pwd, path) }
205
+ self.watch_dirs = [].tap do |dirs|
206
+ # For backward compatibility
207
+ dirs.define_singleton_method(:<<) do |dir|
208
+ super(dir)
209
+ RubyNext::Language.include_patterns << File.join(dir, "*.rb")
210
+ end
211
+ end
212
+
213
+ self.include_patterns = %w[app lib spec test].map { |path| File.join(Dir.pwd, path, "*.rb") }
214
+ self.exclude_patterns = %w[vendor/bundle].map { |path| File.join(Dir.pwd, path, "*") }
173
215
  self.mode = ENV.fetch("RUBY_NEXT_TRANSPILE_MODE", "rewrite").to_sym
174
216
 
217
+ require "ruby-next/language/rewriters/abstract"
175
218
  require "ruby-next/language/rewriters/base"
219
+ require "ruby-next/language/rewriters/text"
176
220
 
177
221
  require "ruby-next/language/rewriters/2.1/numeric_literals"
178
222
  rewriters << Rewriters::NumericLiterals
@@ -186,6 +230,9 @@ module RubyNext
186
230
  require "ruby-next/language/rewriters/2.3/safe_navigation"
187
231
  rewriters << Rewriters::SafeNavigation
188
232
 
233
+ require "ruby-next/language/rewriters/2.5/rescue_within_block"
234
+ rewriters << Rewriters::RescueWithinBlock
235
+
189
236
  require "ruby-next/language/rewriters/2.7/args_forward"
190
237
  rewriters << Rewriters::ArgsForward
191
238
 
@@ -209,7 +256,7 @@ module RubyNext
209
256
  rewriters << Rewriters::InPattern
210
257
 
211
258
  require "ruby-next/language/rewriters/3.0/endless_method"
212
- RubyNext::Language.rewriters << RubyNext::Language::Rewriters::EndlessMethod
259
+ rewriters << RubyNext::Language::Rewriters::EndlessMethod
213
260
 
214
261
  require "ruby-next/language/rewriters/3.1/oneline_pattern_parensless"
215
262
  rewriters << Rewriters::OnelinePatternParensless
@@ -229,16 +276,19 @@ module RubyNext
229
276
  require "ruby-next/language/rewriters/3.1/shorthand_hash"
230
277
  rewriters << RubyNext::Language::Rewriters::ShorthandHash
231
278
 
279
+ require "ruby-next/language/rewriters/3.2/anonymous_restargs"
280
+ rewriters << RubyNext::Language::Rewriters::AnonymousRestArgs
281
+
232
282
  # Put endless range in the end, 'cause Parser fails to parse it in
233
283
  # pattern matching
234
284
  require "ruby-next/language/rewriters/2.6/endless_range"
235
285
  rewriters << Rewriters::EndlessRange
236
286
 
237
- if ENV["RUBY_NEXT_EDGE"] == "1"
287
+ if RubyNext.edge_syntax?
238
288
  require "ruby-next/language/rewriters/edge"
239
289
  end
240
290
 
241
- if ENV["RUBY_NEXT_PROPOSED"] == "1"
291
+ if RubyNext.proposed_syntax?
242
292
  require "ruby-next/language/rewriters/proposed"
243
293
  end
244
294
  end
@@ -10,9 +10,43 @@ module RubyNext
10
10
  class Nextify < Base
11
11
  using RubyNext
12
12
 
13
- attr_reader :lib_path, :paths, :out_path, :min_version, :single_version, :specified_rewriters
13
+ class Stats
14
+ def initialize
15
+ @started_at = ::Process.clock_gettime(Process::CLOCK_MONOTONIC)
16
+ @files = 0
17
+ @scans = 0
18
+ @transpiled_files = 0
19
+ end
20
+
21
+ def file!
22
+ @files += 1
23
+ end
24
+
25
+ def scan!
26
+ @scans += 1
27
+ end
28
+
29
+ def transpiled!
30
+ @transpiled_files += 1
31
+ end
32
+
33
+ def report
34
+ <<-TXT
35
+ Files processed: #{@files}
36
+ Total scans: #{@scans}
37
+ Files transpiled: #{@transpiled_files}
38
+ Completed in #{::Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at}s
39
+ TXT
40
+ end
41
+ end
42
+
43
+ attr_reader :lib_path, :paths, :out_path, :min_version, :single_version, :specified_rewriters, :overwrite
44
+ alias_method :overwrite?, :overwrite
45
+ attr_reader :stats
14
46
 
15
47
  def run
48
+ @stats = Stats.new
49
+
16
50
  log "RubyNext core strategy: #{RubyNext::Core.strategy}"
17
51
  log "RubyNext transpile mode: #{RubyNext::Language.mode}"
18
52
 
@@ -21,16 +55,24 @@ module RubyNext
21
55
  @min_version ||= MIN_SUPPORTED_VERSION
22
56
 
23
57
  paths.each do |path|
58
+ stats.file!
59
+
24
60
  contents = File.read(path)
25
61
  transpile path, contents
26
62
  end
63
+
64
+ ensure_rbnext!
65
+
66
+ log stats.report
27
67
  end
28
68
 
29
69
  def parse!(args)
30
70
  print_help = false
31
71
  print_rewriters = false
32
72
  rewriter_names = []
73
+ custom_rewriters = []
33
74
  @single_version = false
75
+ @overwrite = false
34
76
 
35
77
  optparser = base_parser do |opts|
36
78
  opts.banner = "Usage: ruby-next nextify DIRECTORY_OR_FILE [options]"
@@ -47,11 +89,17 @@ module RubyNext
47
89
  @single_version = true
48
90
  end
49
91
 
92
+ opts.on("--overwrite", "Overwrite original file") do
93
+ @overwrite = true
94
+ end
95
+
50
96
  opts.on("--edge", "Enable edge (master) Ruby features") do |val|
97
+ ENV["RUBY_NEXT_EDGE"] = val.to_s
51
98
  require "ruby-next/language/rewriters/edge"
52
99
  end
53
100
 
54
101
  opts.on("--proposed", "Enable proposed/experimental Ruby features") do |val|
102
+ ENV["RUBY_NEXT_PROPOSED"] = val.to_s
55
103
  require "ruby-next/language/rewriters/proposed"
56
104
  end
57
105
 
@@ -74,6 +122,10 @@ module RubyNext
74
122
  rewriter_names << val
75
123
  end
76
124
 
125
+ opts.on("--import-rewriter=REWRITERS...", "Specify paths to load custom rewritiers") do |val|
126
+ custom_rewriters << val
127
+ end
128
+
77
129
  opts.on("-h", "--help", "Print help") do
78
130
  print_help = true
79
131
  end
@@ -88,9 +140,14 @@ module RubyNext
88
140
  exit 0
89
141
  end
90
142
 
143
+ # Load custom rewriters
144
+ custom_rewriters.each do |path|
145
+ Kernel.load path
146
+ end
147
+
91
148
  if print_rewriters
92
149
  Language.rewriters.each do |rewriter|
93
- $stdout.puts "#{rewriter::NAME} (\"#{rewriter::SYNTAX_PROBE}\")"
150
+ $stdout.puts "#{rewriter::NAME} (\"#{rewriter::SYNTAX_PROBE}\")#{rewriter.unsupported_syntax? ? " (unsupported)" : ""}"
94
151
  end
95
152
  exit 0
96
153
  end
@@ -117,6 +174,11 @@ module RubyNext
117
174
  end
118
175
  end
119
176
 
177
+ if overwrite? && !single_version?
178
+ $stdout.puts "--overwrite only works with --single-version or explcit rewritires specified (via --rewrite)"
179
+ exit 2
180
+ end
181
+
120
182
  @paths =
121
183
  if File.directory?(lib_path)
122
184
  Dir[File.join(lib_path, "**/*.rb")]
@@ -130,6 +192,8 @@ module RubyNext
130
192
  private
131
193
 
132
194
  def transpile(path, contents, version: min_version)
195
+ stats.scan!
196
+
133
197
  rewriters = specified_rewriters || Language.rewriters.select { |rw| rw.unsupported_version?(version) }
134
198
 
135
199
  context = Language::TransformContext.new
@@ -150,16 +214,25 @@ module RubyNext
150
214
  transpile path, contents, version: version
151
215
  rescue SyntaxError, StandardError => e
152
216
  warn "Failed to transpile #{path}: #{e.class} — #{e.message}"
217
+ warn e.backtrace.take(10).join("\n") if ENV["RUBY_NEXT_DEBUG"] == "1"
153
218
  exit 1
154
219
  end
155
220
 
156
221
  def save(contents, path, version)
222
+ stats.transpiled!
223
+
157
224
  return $stdout.puts(contents) if stdout?
158
225
 
159
226
  paths = [Pathname.new(path).relative_path_from(Pathname.new(lib_path))]
160
227
 
161
228
  paths.unshift(version.segments[0..1].join(".")) unless single_version?
162
229
 
230
+ if overwrite?
231
+ overwrite_file_content!(path: path, contents: contents)
232
+
233
+ return
234
+ end
235
+
163
236
  next_path =
164
237
  if next_dir_path.end_with?(".rb")
165
238
  out_path
@@ -176,6 +249,14 @@ module RubyNext
176
249
  log "Generated: #{next_path}"
177
250
  end
178
251
 
252
+ def overwrite_file_content!(path:, contents:)
253
+ unless CLI.dry_run?
254
+ File.write(path, contents)
255
+ end
256
+
257
+ log "Overwritten: #{path}"
258
+ end
259
+
179
260
  def remove_rbnext!
180
261
  return if CLI.dry_run? || stdout?
181
262
 
@@ -185,8 +266,21 @@ module RubyNext
185
266
  FileUtils.rm_r(next_dir_path)
186
267
  end
187
268
 
269
+ def ensure_rbnext!
270
+ return if CLI.dry_run? || stdout?
271
+
272
+ return if File.directory?(next_dir_path)
273
+
274
+ return if next_dir_path.end_with?(".rb")
275
+
276
+ return if overwrite?
277
+
278
+ FileUtils.mkdir_p next_dir_path
279
+ File.write(File.join(next_dir_path, ".keep"), "")
280
+ end
281
+
188
282
  def next_dir_path
189
- @next_dir_path ||= (out_path || File.join(lib_path, RUBY_NEXT_DIR))
283
+ @next_dir_path ||= out_path || File.join(lib_path, RUBY_NEXT_DIR)
190
284
  end
191
285
 
192
286
  def stdout?