ruby-next-core 0.14.0 → 1.0.1

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 (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?