oxc 0.0.1 → 0.2.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +21 -0
  3. data/README.md +602 -0
  4. data/ext/oxc/extconf.rb +123 -0
  5. data/ext/oxc/include/oxc.h +40 -0
  6. data/ext/oxc/oxc.c +136 -0
  7. data/lib/oxc/backend.rb +41 -0
  8. data/lib/oxc/diagnosed.rb +33 -0
  9. data/lib/oxc/diagnostic.rb +86 -0
  10. data/lib/oxc/errors.rb +26 -0
  11. data/lib/oxc/minifier.rb +31 -0
  12. data/lib/oxc/minify_result.rb +25 -0
  13. data/lib/oxc/mutation_visitor.rb +128 -0
  14. data/lib/oxc/node.rb +186 -0
  15. data/lib/oxc/options.rb +113 -0
  16. data/lib/oxc/parse_result.rb +116 -0
  17. data/lib/oxc/result.rb +51 -0
  18. data/lib/oxc/transform_result.rb +47 -0
  19. data/lib/oxc/transformer.rb +31 -0
  20. data/lib/oxc/version.rb +1 -1
  21. data/lib/oxc/visitor.rb +25 -0
  22. data/lib/oxc.rb +49 -0
  23. data/licenses/README.md +12 -0
  24. data/licenses/oxc-MIT.txt +22 -0
  25. data/licenses/oxc-THIRD-PARTY.txt +763 -0
  26. data/oxc.gemspec +14 -2
  27. data/rust/Cargo.lock +1436 -0
  28. data/rust/Cargo.toml +32 -0
  29. data/rust/build.rs +52 -0
  30. data/rust/cbindgen.toml +24 -0
  31. data/rust/rustfmt.toml +3 -0
  32. data/rust/src/diagnostic.rs +75 -0
  33. data/rust/src/lib.rs +288 -0
  34. data/rust/src/module_record.rs +262 -0
  35. data/rust/src/options.rs +744 -0
  36. data/rust/src/parse.rs +93 -0
  37. data/rust/src/result.rs +55 -0
  38. data/rust/src/source_type.rs +26 -0
  39. data/rust/src/symbols.rs +101 -0
  40. data/rust/src/transform.rs +116 -0
  41. data/sig/oxc/backend.rbs +29 -0
  42. data/sig/oxc/diagnosed.rbs +23 -0
  43. data/sig/oxc/diagnostic.rbs +57 -0
  44. data/sig/oxc/errors.rbs +31 -0
  45. data/sig/oxc/minifier.rbs +21 -0
  46. data/sig/oxc/minify_result.rbs +11 -0
  47. data/sig/oxc/mutation_visitor.rbs +80 -0
  48. data/sig/oxc/node.rbs +94 -0
  49. data/sig/oxc/options.rbs +42 -0
  50. data/sig/oxc/parse_result.rbs +66 -0
  51. data/sig/oxc/result.rbs +32 -0
  52. data/sig/oxc/transform_result.rbs +22 -0
  53. data/sig/oxc/transformer.rbs +21 -0
  54. data/sig/oxc/types.rbs +96 -0
  55. data/sig/oxc/version.rbs +5 -0
  56. data/sig/oxc/visitor.rbs +11 -0
  57. data/sig/oxc.rbs +13 -2
  58. metadata +56 -2
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "fileutils"
5
+
6
+ ext_dir = __dir__
7
+ root_dir = File.expand_path("../..", ext_dir)
8
+
9
+ rust_dir = File.join(root_dir, "rust")
10
+
11
+ unless File.exist?(File.join(rust_dir, "Cargo.toml"))
12
+ abort <<~MESSAGE
13
+
14
+ ERROR: Rust sources not found at #{rust_dir}.
15
+
16
+ MESSAGE
17
+ end
18
+
19
+ unless system("cargo --version > /dev/null 2>&1")
20
+ abort <<~MESSAGE
21
+
22
+ ERROR: Rust toolchain not found.
23
+
24
+ oxc requires the Rust toolchain to compile from source.
25
+
26
+ Install Rust: https://rustup.rs
27
+
28
+ MESSAGE
29
+ end
30
+
31
+ RUST_TARGETS = {
32
+ "aarch64-linux-gnu" => "aarch64-unknown-linux-gnu",
33
+ "aarch64-linux-musl" => "aarch64-unknown-linux-musl",
34
+ "arm-linux-gnu" => "armv7-unknown-linux-gnueabihf",
35
+ "arm-linux-musl" => "armv7-unknown-linux-musleabihf",
36
+ "arm64-darwin" => "aarch64-apple-darwin",
37
+ "x86_64-darwin" => "x86_64-apple-darwin",
38
+ "x86_64-linux-gnu" => "x86_64-unknown-linux-gnu",
39
+ "x86_64-linux-musl" => "x86_64-unknown-linux-musl",
40
+ "x86-linux-gnu" => "i686-unknown-linux-gnu",
41
+ "x86-linux-musl" => "i686-unknown-linux-musl",
42
+ }.freeze
43
+
44
+ cross_compiling = ENV.key?("RUBY_CC_VERSION")
45
+ target_platform = ENV.fetch("CARGO_BUILD_TARGET", nil)
46
+
47
+ if cross_compiling && target_platform.nil?
48
+ rcd_platform = ENV.fetch("RCD_PLATFORM", "")
49
+ target_platform = RUST_TARGETS[rcd_platform]
50
+
51
+ if target_platform.nil?
52
+ ruby_platform = RbConfig::CONFIG["arch"]
53
+ target_platform = RUST_TARGETS.values.find { |target| ruby_platform.include?(target.split("-").first) }
54
+ end
55
+ end
56
+
57
+ header_path = File.join(ext_dir, "include", "oxc.h")
58
+
59
+ FileUtils.mkdir_p(File.dirname(header_path))
60
+
61
+ target_dir = File.join(rust_dir, "target")
62
+
63
+ if target_platform
64
+ puts "oxc: Cross-compiling Rust for target: #{target_platform}"
65
+
66
+ system("rustup target add #{target_platform}") || warn("oxc: Failed to add Rust target #{target_platform}")
67
+
68
+ cargo_args = "--release --locked --target #{target_platform}"
69
+ lib_dir = File.join(target_dir, target_platform, "release")
70
+ else
71
+ puts "oxc: Compiling Rust library for native platform..."
72
+
73
+ cargo_args = "--release --locked"
74
+ lib_dir = File.join(target_dir, "release")
75
+ end
76
+
77
+ unless system("cd #{rust_dir} && cargo build #{cargo_args}")
78
+ abort "ERROR: Failed to compile oxc from Rust source."
79
+ end
80
+
81
+ unless File.exist?(header_path)
82
+ abort "ERROR: cbindgen did not generate #{header_path}. Try `cargo clean` in #{rust_dir} and reinstall."
83
+ end
84
+
85
+ static_lib = File.join(lib_dir, "liboxc_ffi.a")
86
+
87
+ developing = File.exist?(File.join(root_dir, ".git"))
88
+
89
+ if File.exist?(static_lib) && !developing
90
+ vendored = File.join(ext_dir, "liboxc_ffi.a")
91
+
92
+ FileUtils.cp(static_lib, vendored)
93
+ FileUtils.rm_rf(target_dir)
94
+
95
+ puts "oxc: Static library vendored at #{vendored}, Rust build directory removed"
96
+
97
+ $LDFLAGS << " #{vendored}"
98
+ elsif File.exist?(static_lib)
99
+ puts "oxc: Static library found at #{static_lib}"
100
+
101
+ $LDFLAGS << " #{static_lib}"
102
+ else
103
+ host_os = target_platform || RbConfig::CONFIG["host_os"]
104
+
105
+ lib_name = case host_os
106
+ when /darwin/ then "liboxc_ffi.dylib"
107
+ when /mingw|mswin|windows/ then "oxc_ffi.dll"
108
+ else "liboxc_ffi.so"
109
+ end
110
+
111
+ lib_path = File.join(lib_dir, lib_name)
112
+
113
+ abort "ERROR: Shared library not found at #{lib_path}" unless File.exist?(lib_path)
114
+
115
+ puts "oxc: Shared library found at #{lib_path} (dynamic)"
116
+
117
+ $LDFLAGS << " -L#{lib_dir} -loxc_ffi"
118
+ $LDFLAGS << " -Wl,-rpath,#{lib_dir}" if RbConfig::CONFIG["host_os"].match?(/darwin|linux/)
119
+ end
120
+
121
+ $CFLAGS << " -I#{ext_dir}"
122
+
123
+ create_makefile("oxc/oxc")
@@ -0,0 +1,40 @@
1
+ /* Generated by cbindgen — do not edit manually */
2
+
3
+ #include <stdbool.h>
4
+ #include <stdint.h>
5
+ #include <stddef.h>
6
+
7
+ #ifndef OXC_H
8
+ #define OXC_H
9
+
10
+ typedef enum OxcErrorCode {
11
+ OXC_ERROR_CODE_NONE = 0,
12
+ OXC_ERROR_CODE_OPTION,
13
+ OXC_ERROR_CODE_ENCODING,
14
+ OXC_ERROR_CODE_TRANSFORM,
15
+ OXC_ERROR_CODE_INTERNAL,
16
+ OXC_ERROR_CODE_PANIC,
17
+ } OxcErrorCode;
18
+
19
+ typedef struct OxcResult {
20
+ char *value;
21
+ uintptr_t value_len;
22
+ char *error;
23
+ enum OxcErrorCode code;
24
+ } OxcResult;
25
+
26
+ struct OxcResult oxc_parse(const char *source, const char *options_json);
27
+
28
+ struct OxcResult oxc_transform(const char *source, const char *options_json);
29
+
30
+ struct OxcResult oxc_minify(const char *source, const char *options_json);
31
+
32
+ char *oxc_version(void);
33
+
34
+ char *oxc_oxc_version(void);
35
+
36
+ void oxc_string_free(char *value);
37
+
38
+ void oxc_result_free(struct OxcResult result);
39
+
40
+ #endif /* OXC_H */
data/ext/oxc/oxc.c ADDED
@@ -0,0 +1,136 @@
1
+ #include <ruby.h>
2
+ #include <ruby/encoding.h>
3
+ #include <ruby/thread.h>
4
+ #include "include/oxc.h"
5
+
6
+ static VALUE rb_mOxc;
7
+ static VALUE rb_mBackend;
8
+ static VALUE rb_eError;
9
+ static VALUE rb_eOptionError;
10
+ static VALUE rb_eOxcEncodingError;
11
+ static VALUE rb_eTransformError;
12
+ static VALUE rb_eInternalError;
13
+ static VALUE rb_ePanicError;
14
+
15
+ typedef struct OxcResult (*oxc_function)(const char *, const char *);
16
+
17
+ struct call_arguments {
18
+ oxc_function function;
19
+ const char *source;
20
+ const char *options;
21
+ struct OxcResult result;
22
+ };
23
+
24
+ static VALUE make_utf8_string(const char *cstring) {
25
+ return rb_enc_str_new_cstr(cstring, rb_utf8_encoding());
26
+ }
27
+
28
+ static VALUE take_utf8_string(char *cstring) {
29
+ if (!cstring) return Qnil;
30
+
31
+ VALUE string = make_utf8_string(cstring);
32
+ oxc_string_free(cstring);
33
+
34
+ return string;
35
+ }
36
+
37
+ static VALUE error_class_for(enum OxcErrorCode code) {
38
+ switch (code) {
39
+ case OXC_ERROR_CODE_OPTION: return rb_eOptionError;
40
+ case OXC_ERROR_CODE_ENCODING: return rb_eOxcEncodingError;
41
+ case OXC_ERROR_CODE_TRANSFORM: return rb_eTransformError;
42
+ case OXC_ERROR_CODE_PANIC: return rb_ePanicError;
43
+ default: return rb_eInternalError;
44
+ }
45
+ }
46
+
47
+ static VALUE unwrap(struct OxcResult result) {
48
+ if (result.error) {
49
+ VALUE message = make_utf8_string(result.error);
50
+ VALUE error_class = error_class_for(result.code);
51
+
52
+ oxc_result_free(result);
53
+
54
+ rb_raise(error_class, "%s", StringValueCStr(message));
55
+ }
56
+
57
+ if (!result.value) {
58
+ oxc_result_free(result);
59
+
60
+ rb_raise(rb_eInternalError, "oxc returned no result");
61
+ }
62
+
63
+ VALUE value = rb_enc_str_new(result.value, (long) result.value_len, rb_utf8_encoding());
64
+
65
+ oxc_result_free(result);
66
+
67
+ return value;
68
+ }
69
+
70
+ static void *without_gvl(void *data) {
71
+ struct call_arguments *arguments = (struct call_arguments *) data;
72
+
73
+ arguments->result = arguments->function(arguments->source, arguments->options);
74
+
75
+ return NULL;
76
+ }
77
+
78
+ static VALUE call(oxc_function function, VALUE source, VALUE options) {
79
+ struct call_arguments arguments;
80
+
81
+ arguments.function = function;
82
+ arguments.source = StringValueCStr(source);
83
+ arguments.options = StringValueCStr(options);
84
+
85
+ rb_thread_call_without_gvl(without_gvl, &arguments, NULL, NULL);
86
+
87
+ return unwrap(arguments.result);
88
+ }
89
+
90
+ static VALUE rb_minify(VALUE self, VALUE source, VALUE options) {
91
+ (void) self;
92
+
93
+ return call(oxc_minify, source, options);
94
+ }
95
+
96
+ static VALUE rb_transform(VALUE self, VALUE source, VALUE options) {
97
+ (void) self;
98
+
99
+ return call(oxc_transform, source, options);
100
+ }
101
+
102
+ static VALUE rb_parse(VALUE self, VALUE source, VALUE options) {
103
+ (void) self;
104
+
105
+ return call(oxc_parse, source, options);
106
+ }
107
+
108
+ static VALUE rb_native_version(VALUE self) {
109
+ (void) self;
110
+
111
+ return take_utf8_string(oxc_version());
112
+ }
113
+
114
+ static VALUE rb_oxc_version(VALUE self) {
115
+ (void) self;
116
+
117
+ return take_utf8_string(oxc_oxc_version());
118
+ }
119
+
120
+ void Init_oxc(void) {
121
+ rb_mOxc = rb_define_module("Oxc");
122
+ rb_mBackend = rb_define_module_under(rb_mOxc, "Backend");
123
+
124
+ rb_eError = rb_define_class_under(rb_mOxc, "Error", rb_eStandardError);
125
+ rb_eOptionError = rb_define_class_under(rb_mOxc, "OptionError", rb_eError);
126
+ rb_eOxcEncodingError = rb_define_class_under(rb_mOxc, "EncodingError", rb_eError);
127
+ rb_eTransformError = rb_define_class_under(rb_mOxc, "TransformError", rb_eError);
128
+ rb_eInternalError = rb_define_class_under(rb_mOxc, "InternalError", rb_eError);
129
+ rb_ePanicError = rb_define_class_under(rb_mOxc, "PanicError", rb_eInternalError);
130
+
131
+ rb_define_singleton_method(rb_mBackend, "minify", rb_minify, 2);
132
+ rb_define_singleton_method(rb_mBackend, "transform", rb_transform, 2);
133
+ rb_define_singleton_method(rb_mBackend, "parse", rb_parse, 2);
134
+ rb_define_singleton_method(rb_mBackend, "version", rb_native_version, 0);
135
+ rb_define_singleton_method(rb_mBackend, "oxc_version", rb_oxc_version, 0);
136
+ }
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oxc
4
+ module Backend
5
+ module Unavailable
6
+ #: (String, String) -> String
7
+ def minify(_source, _options_json)
8
+ unavailable(__method__)
9
+ end
10
+
11
+ #: (String, String) -> String
12
+ def transform(_source, _options_json)
13
+ unavailable(__method__)
14
+ end
15
+
16
+ #: (String, String) -> String
17
+ def parse(_source, _options_json)
18
+ unavailable(__method__)
19
+ end
20
+
21
+ #: () -> String
22
+ def version
23
+ unavailable(__method__)
24
+ end
25
+
26
+ #: () -> String
27
+ def oxc_version
28
+ unavailable(__method__)
29
+ end
30
+
31
+ private
32
+
33
+ #: (Symbol?) -> bot
34
+ def unavailable(name)
35
+ raise NotImplementedError, "Oxc::Backend.#{name} is defined by the native extension, which did not load"
36
+ end
37
+ end
38
+
39
+ extend Unavailable
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oxc
4
+ # @rbs module-self _Diagnosed
5
+ module Diagnosed
6
+ # @rbs @panicked: bool
7
+
8
+ #: () -> Array[Oxc::Diagnostic]
9
+ def errors
10
+ diagnostics.select(&:error?)
11
+ end
12
+
13
+ #: () -> Array[Oxc::Diagnostic]
14
+ def warnings
15
+ diagnostics.select(&:warning?)
16
+ end
17
+
18
+ #: () -> bool
19
+ def errors?
20
+ !errors.empty?
21
+ end
22
+
23
+ #: () -> bool
24
+ def warnings?
25
+ !warnings.empty?
26
+ end
27
+
28
+ #: () -> bool
29
+ def panicked?
30
+ @panicked
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oxc
4
+ class Diagnostic
5
+ ERROR = "error" #: String
6
+ WARNING = "warning" #: String
7
+
8
+ attr_reader :severity #: String
9
+ attr_reader :message #: String
10
+ attr_reader :labels #: Array[Oxc::Label]
11
+ attr_reader :help #: String?
12
+ attr_reader :codeframe #: String?
13
+
14
+ #: (Hash[String, untyped]) -> Oxc::Diagnostic
15
+ def self.from_hash(parsed)
16
+ new(
17
+ severity: parsed.fetch("severity"),
18
+ message: parsed.fetch("message"),
19
+ labels: parsed.fetch("labels").map { |label| Label.from_hash(label) },
20
+ help: parsed["help"],
21
+ codeframe: parsed["codeframe"]
22
+ )
23
+ end
24
+
25
+ #: (severity: String, message: String, labels: Array[Oxc::Label], ?help: String?, ?codeframe: String?) -> void
26
+ def initialize(severity:, message:, labels:, help: nil, codeframe: nil)
27
+ @severity = severity
28
+ @message = message
29
+ @labels = labels.freeze
30
+ @help = help
31
+ @codeframe = codeframe
32
+
33
+ freeze
34
+ end
35
+
36
+ #: () -> bool
37
+ def error?
38
+ severity == ERROR
39
+ end
40
+
41
+ #: () -> bool
42
+ def warning?
43
+ severity == WARNING
44
+ end
45
+
46
+ #: () -> String
47
+ def to_s
48
+ message
49
+ end
50
+
51
+ #: () -> String
52
+ def inspect
53
+ "#<#{self.class.name} #{severity} #{message.inspect}>"
54
+ end
55
+ end
56
+
57
+ class Label
58
+ attr_reader :message #: String?
59
+ attr_reader :start #: Integer
60
+ attr_reader :finish #: Integer
61
+
62
+ #: (Hash[String, untyped]) -> Oxc::Label
63
+ def self.from_hash(parsed)
64
+ new(message: parsed["message"], start: parsed.fetch("start"), finish: parsed.fetch("end"))
65
+ end
66
+
67
+ #: (start: Integer, finish: Integer, ?message: String?) -> void
68
+ def initialize(start:, finish:, message: nil)
69
+ @start = start
70
+ @finish = finish
71
+ @message = message
72
+
73
+ freeze
74
+ end
75
+
76
+ #: (String) -> String?
77
+ def slice(source)
78
+ source.byteslice(start, finish - start)
79
+ end
80
+
81
+ #: () -> String
82
+ def inspect
83
+ "#<#{self.class.name} range=[#{start}, #{finish}]#{" #{message.inspect}" if message}>"
84
+ end
85
+ end
86
+ end
data/lib/oxc/errors.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oxc
4
+ class Error < StandardError; end
5
+ class OptionError < Error; end
6
+ class EncodingError < Error; end
7
+ class TransformError < Error; end
8
+ class InternalError < Error; end
9
+ class PanicError < InternalError; end
10
+
11
+ class SyntaxError < Error
12
+ attr_reader :result #: (Oxc::Result | Oxc::ParseResult)?
13
+
14
+ #: (String, ?(Oxc::Result | Oxc::ParseResult)?) -> void
15
+ def initialize(message, result = nil)
16
+ super(message)
17
+
18
+ @result = result
19
+ end
20
+
21
+ #: () -> Array[Oxc::Diagnostic]
22
+ def diagnostics
23
+ result ? result.diagnostics : []
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oxc
4
+ class Minifier
5
+ attr_reader :options #: Hash[Symbol, untyped]
6
+
7
+ #: (?filename: String?, ?lang: String?, ?source_type: String?, ?compress: compress?, ?mangle: mangle?, ?codegen: codegen?, ?sourcemap: bool, ?strict: bool) -> void
8
+ def initialize(**options)
9
+ @options = options.transform_keys(&:to_sym).freeze
10
+
11
+ freeze
12
+ end
13
+
14
+ #: (String, ?filename: String?, ?lang: String?, ?source_type: String?, ?compress: compress?, ?mangle: mangle?, ?codegen: codegen?, ?sourcemap: bool, ?strict: bool) -> Oxc::MinifyResult
15
+ def minify(source, **overrides)
16
+ Oxc.minify(source, **options, **overrides)
17
+ end
18
+
19
+ alias call minify
20
+
21
+ #: (?filename: String?, ?lang: String?, ?source_type: String?, ?compress: compress?, ?mangle: mangle?, ?codegen: codegen?, ?sourcemap: bool, ?strict: bool) -> Oxc::Minifier
22
+ def with(**overrides)
23
+ self.class.new(**options, **overrides)
24
+ end
25
+
26
+ #: () -> String
27
+ def inspect
28
+ "#<#{self.class.name} #{options.inspect}>"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oxc
4
+ class MinifyResult < Result
5
+ #: (String) -> Oxc::MinifyResult
6
+ def self.from_json(payload)
7
+ parsed = JSON.parse(payload)
8
+
9
+ new(
10
+ code: parsed.fetch("code"),
11
+ map: parsed["map"],
12
+ legal_comments: parsed.fetch("legal_comments"),
13
+ diagnostics: parsed.fetch("errors").map { |diagnostic| Diagnostic.from_hash(diagnostic) },
14
+ panicked: parsed.fetch("panicked")
15
+ )
16
+ end
17
+
18
+ #: (code: String, diagnostics: Array[Oxc::Diagnostic], ?map: String?, ?legal_comments: Array[String], ?panicked: bool) -> void
19
+ def initialize(...)
20
+ super
21
+
22
+ freeze
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oxc
4
+ # A visitor that rewrites the source it walked, by recording what to do to a node and splicing the
5
+ # original text at the end. Everything nothing touched survives byte for byte.
6
+ #
7
+ # class Renamer < Oxc::MutationVisitor
8
+ # def visit_identifier(node)
9
+ # replace(node, "renamed") if node["name"] == "count"
10
+ # end
11
+ # end
12
+ #
13
+ # Renamer.new.rewrite("let count = 1") #=> "let renamed = 1"
14
+ #
15
+ class MutationVisitor < Visitor
16
+ class Overlap < StandardError; end
17
+ class Invalid < Error; end
18
+
19
+ Edit = Data.define(
20
+ :start, #: Integer
21
+ :finish, #: Integer
22
+ :text, #: String
23
+ :order #: Integer
24
+ )
25
+
26
+ attr_reader :source #: String
27
+ attr_reader :parsed #: Oxc::ParseResult
28
+
29
+ #: (String, ?verify: bool, **untyped) -> String
30
+ def rewrite(source, verify: true, **options)
31
+ @source = source
32
+ @edits = [] #: Array[Edit]
33
+ @replaced = [] #: Array[[Integer, Integer]]
34
+ @parsed = Oxc.parse(source, **options).validate!
35
+
36
+ visit(parsed)
37
+
38
+ rewritten = apply
39
+
40
+ verify ? verified(rewritten, options) : rewritten
41
+ end
42
+
43
+ #: (Oxc::Node, String) -> void
44
+ def replace(node, text)
45
+ @replaced << [node.start, node.finish]
46
+
47
+ edit(node.start, node.finish, text)
48
+ end
49
+
50
+ #: (Oxc::Node) -> void
51
+ def remove(node)
52
+ replace(node, "")
53
+ end
54
+
55
+ #: (Oxc::Node, String) -> void
56
+ def insert_before(node, text)
57
+ edit(node.start, node.start, text)
58
+ end
59
+
60
+ #: (Oxc::Node, String) -> void
61
+ def insert_after(node, text)
62
+ edit(node.finish, node.finish, text)
63
+ end
64
+
65
+ #: (Oxc::Node, String, String) -> void
66
+ def wrap(node, before, after)
67
+ insert_before(node, before)
68
+ insert_after(node, after)
69
+ end
70
+
71
+ #: (Oxc::Node) -> void
72
+ def visit_children(node)
73
+ return nil if replaced?(node)
74
+
75
+ super
76
+ end
77
+
78
+ private
79
+
80
+ #: (String, Hash[Symbol, untyped]) -> String
81
+ def verified(rewritten, options)
82
+ answer = Oxc.parse(rewritten, **options, ast: false)
83
+
84
+ return rewritten unless answer.errors?
85
+
86
+ raise Invalid, "what was rewritten no longer reads as JavaScript: #{answer.errors.first&.message}"
87
+ end
88
+
89
+ #: (Oxc::Node) -> bool
90
+ def replaced?(node)
91
+ @replaced.any? { |start, finish| node.start >= start && node.finish <= finish }
92
+ end
93
+
94
+ #: (Integer, Integer, String) -> void
95
+ def edit(start, finish, text)
96
+ @edits.each do |existing|
97
+ next unless overlaps?(existing, start, finish)
98
+
99
+ raise Overlap, "an edit at #{start}..#{finish} overlaps one at #{existing.start}..#{existing.finish}"
100
+ end
101
+
102
+ @edits << Edit.new(start: start, finish: finish, text: text, order: @edits.length)
103
+
104
+ nil
105
+ end
106
+
107
+ #: (Edit, Integer, Integer) -> bool
108
+ def overlaps?(existing, start, finish)
109
+ return false if existing.start == existing.finish && start == finish
110
+
111
+ start < existing.finish && existing.start < finish
112
+ end
113
+
114
+ #: () -> String
115
+ def apply
116
+ taken = 0
117
+ result = +""
118
+
119
+ @edits.sort_by { |edit| [edit.start, edit.order] }.each do |edit|
120
+ result << source.byteslice(taken, edit.start - taken).to_s << edit.text
121
+
122
+ taken = edit.finish
123
+ end
124
+
125
+ result << source.byteslice(taken..).to_s
126
+ end
127
+ end
128
+ end