Skip to content

Commit

Permalink
feat(transformer): add ESTarget (#7091)
Browse files Browse the repository at this point in the history
closes #6982
  • Loading branch information
Boshen committed Nov 3, 2024
1 parent 64e2499 commit 21b8e49
Show file tree
Hide file tree
Showing 13 changed files with 251 additions and 41 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/oxc_transformer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ serde_json = { workspace = true }
sha1 = { workspace = true }

[dev-dependencies]
insta = { workspace = true }
oxc_codegen = { workspace = true }
oxc_parser = { workspace = true }
pico-args = { workspace = true }
Expand Down
17 changes: 10 additions & 7 deletions crates/oxc_transformer/examples/transformer.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
#![allow(clippy::print_stdout)]
use std::{env, path::Path};
use std::{path::Path, str::FromStr};

use oxc_allocator::Allocator;
use oxc_codegen::CodeGenerator;
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType;
use oxc_transformer::{TransformOptions, Transformer};
use oxc_transformer::{ESTarget, TransformOptions, Transformer};
use pico_args::Arguments;

// Instruction:
// create a `test.tsx`,
// run `cargo run -p oxc_transformer --example transformer`
// or `just watch "run -p oxc_transformer --example transformer"`
// create a `test.js`,
// run `just example transformer` or `just watch-example transformer`

fn main() {
let mut args = Arguments::from_env();
let name = env::args().nth(1).unwrap_or_else(|| "test.js".to_string());
let targets: Option<String> = args.opt_value_from_str("--targets").unwrap_or(None);
let target: Option<String> = args.opt_value_from_str("--target").unwrap_or(None);
let name = args.free_from_str().unwrap_or_else(|_| "test.js".to_string());

let path = Path::new(&name);
let source_text = std::fs::read_to_string(path).expect("{name} not found");
let source_text =
std::fs::read_to_string(path).unwrap_or_else(|err| panic!("{name} not found.\n{err}"));
let allocator = Allocator::default();
let source_type = SourceType::from_path(path).unwrap();

Expand Down Expand Up @@ -62,6 +63,8 @@ fn main() {
// ..BabelEnvOptions::default()
// })
// .unwrap()
} else if let Some(target) = &target {
TransformOptions::from(ESTarget::from_str(target).unwrap())
} else {
TransformOptions::enable_all()
};
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_transformer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pub use crate::{
jsx::{JsxOptions, JsxRuntime, ReactRefreshOptions},
options::{
babel::{BabelEnvOptions, BabelOptions, Targets},
EnvOptions, TransformOptions,
ESTarget, EnvOptions, TransformOptions,
},
plugins::*,
typescript::{RewriteExtensionsMode, TypeScriptOptions},
Expand Down
84 changes: 80 additions & 4 deletions crates/oxc_transformer/src/options/env.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use std::str::FromStr;

use cow_utils::CowUtils;
use serde::Deserialize;

use crate::{
Expand All @@ -14,6 +17,45 @@ use crate::{

use super::babel::BabelEnvOptions;

#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
pub enum ESTarget {
ES5,
ES2015,
ES2016,
ES2017,
ES2018,
ES2019,
ES2020,
ES2021,
ES2022,
ES2023,
ES2024,
#[default]
ESNext,
}

impl FromStr for ESTarget {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.cow_to_lowercase().as_ref() {
"es5" => Ok(Self::ES5),
"es2015" => Ok(Self::ES2015),
"es2016" => Ok(Self::ES2016),
"es2017" => Ok(Self::ES2017),
"es2018" => Ok(Self::ES2018),
"es2019" => Ok(Self::ES2019),
"es2020" => Ok(Self::ES2020),
"es2021" => Ok(Self::ES2021),
"es2022" => Ok(Self::ES2022),
"es2023" => Ok(Self::ES2023),
"es2024" => Ok(Self::ES2024),
"esnext" => Ok(Self::ESNext),
_ => Err(format!("Invalid target \"{s}\".")),
}
}
}

#[derive(Debug, Default, Clone, Deserialize)]
#[serde(try_from = "BabelEnvOptions")]
pub struct EnvOptions {
Expand Down Expand Up @@ -46,10 +88,10 @@ impl EnvOptions {
regexp: RegExpOptions {
sticky_flag: true,
unicode_flag: true,
unicode_property_escapes: true,
dot_all_flag: true,
look_behind_assertions: true,
named_capture_groups: true,
unicode_property_escapes: true,
look_behind_assertions: true,
match_indices: true,
set_notation: true,
},
Expand Down Expand Up @@ -91,6 +133,40 @@ impl EnvOptions {
}
}

impl From<ESTarget> for EnvOptions {
fn from(target: ESTarget) -> Self {
Self {
regexp: RegExpOptions {
sticky_flag: target < ESTarget::ES2015,
unicode_flag: target < ESTarget::ES2015,
unicode_property_escapes: target < ESTarget::ES2018,
dot_all_flag: target < ESTarget::ES2015,
named_capture_groups: target < ESTarget::ES2018,
look_behind_assertions: target < ESTarget::ES2018,
match_indices: target < ESTarget::ES2022,
set_notation: target < ESTarget::ES2024,
},
es2015: ES2015Options {
arrow_function: (target < ESTarget::ES2015).then(ArrowFunctionsOptions::default),
},
es2016: ES2016Options { exponentiation_operator: target < ESTarget::ES2016 },
es2017: ES2017Options { async_to_generator: target < ESTarget::ES2017 },
es2018: ES2018Options {
object_rest_spread: (target < ESTarget::ES2018)
.then(ObjectRestSpreadOptions::default),
async_generator_functions: target < ESTarget::ES2018,
},
es2019: ES2019Options { optional_catch_binding: target < ESTarget::ES2019 },
es2020: ES2020Options { nullish_coalescing_operator: target < ESTarget::ES2020 },
es2021: ES2021Options { logical_assignment_operators: target < ESTarget::ES2021 },
es2022: ES2022Options {
class_static_block: target < ESTarget::ES2022,
class_properties: (target < ESTarget::ES2022).then(ClassPropertiesOptions::default),
},
}
}
}

impl TryFrom<BabelEnvOptions> for EnvOptions {
type Error = String;

Expand All @@ -100,10 +176,10 @@ impl TryFrom<BabelEnvOptions> for EnvOptions {
regexp: RegExpOptions {
sticky_flag: o.can_enable_plugin("transform-sticky-regex"),
unicode_flag: o.can_enable_plugin("transform-unicode-regex"),
unicode_property_escapes: o.can_enable_plugin("transform-unicode-property-regex"),
dot_all_flag: o.can_enable_plugin("transform-dotall-regex"),
look_behind_assertions: o.can_enable_plugin("esbuild-regexp-lookbehind-assertions"),
named_capture_groups: o.can_enable_plugin("transform-named-capturing-groups-regex"),
unicode_property_escapes: o.can_enable_plugin("transform-unicode-property-regex"),
look_behind_assertions: o.can_enable_plugin("esbuild-regexp-lookbehind-assertions"),
match_indices: o.can_enable_plugin("esbuild-regexp-match-indices"),
set_notation: o.can_enable_plugin("transform-unicode-sets-regex"),
},
Expand Down
10 changes: 8 additions & 2 deletions crates/oxc_transformer/src/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ use std::path::PathBuf;

use oxc_diagnostics::Error;

pub use env::EnvOptions;

use crate::{
common::helper_loader::{HelperLoaderMode, HelperLoaderOptions},
compiler_assumptions::CompilerAssumptions,
Expand All @@ -24,6 +22,8 @@ use crate::{
ReactRefreshOptions,
};

pub use env::{ESTarget, EnvOptions};

use babel::BabelOptions;

/// <https://babel.dev/docs/options>
Expand Down Expand Up @@ -79,6 +79,12 @@ impl TransformOptions {
}
}

impl From<ESTarget> for TransformOptions {
fn from(target: ESTarget) -> Self {
Self { env: EnvOptions::from(target), ..Self::default() }
}
}

impl TryFrom<&BabelOptions> for TransformOptions {
type Error = Vec<Error>;

Expand Down
30 changes: 19 additions & 11 deletions crates/oxc_transformer/src/regexp/options.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
#[derive(Default, Debug, Clone, Copy)]
pub struct RegExpOptions {
/// Enables plugin to transform the RegExp literal has `y` flag
/// Enables plugin to transform the RegExp literal with a `y` flag
/// ES2015 <https://babel.dev/docs/babel-plugin-transform-sticky-regex>
pub sticky_flag: bool,

/// Enables plugin to transform the RegExp literal has `u` flag
/// Enables plugin to transform the RegExp literal with a `u` flag
/// ES2015 <https://babel.dev/docs/babel-plugin-transform-unicode-regex>
pub unicode_flag: bool,

/// Enables plugin to transform the RegExp literal has `s` flag
pub dot_all_flag: bool,
/// Enables plugin to transform the RegExp literal that has `\p{}` and `\P{}` unicode property escapes
/// ES2018 <https://babel.dev/docs/babel-plugin-transform-unicode-property-regex>
pub unicode_property_escapes: bool,

/// Enables plugin to transform the RegExp literal has `(?<=)` or `(?<!)` lookbehind assertions
pub look_behind_assertions: bool,
/// Enables plugin to transform the RegExp literal with a `s` flag
/// ES2018 <https://babel.dev/docs/babel-plugin-transform-dotall-regex>
pub dot_all_flag: bool,

/// Enables plugin to transform the RegExp literal has `(?<name>x)` named capture groups
/// Enables plugin to transform the RegExp literal that has `(?<name>x)` named capture groups
/// ES2018 <https://babel.dev/docs/babel-plugin-transform-named-capturing-groups-regex>
pub named_capture_groups: bool,

/// Enables plugin to transform the RegExp literal has `\p{}` and `\P{}` unicode property escapes
pub unicode_property_escapes: bool,
/// Enables plugin to transform the RegExp literal that has `(?<=)` or `(?<!)` lookbehind assertions
/// ES2018 <https://github.com/tc39/proposal-regexp-lookbehind>
pub look_behind_assertions: bool,

/// Enables plugin to transform `d` flag
/// Enables plugin to transform the `d` flag
/// ES2022 <https://github.com/tc39/proposal-regexp-match-indices>
pub match_indices: bool,

/// Enables plugin to transform the RegExp literal has `v` flag
/// Enables plugin to transform the RegExp literal that has `v` flag
/// ES2024 <https://babel.dev/docs/babel-plugin-transform-unicode-sets-regex>
pub set_notation: bool,
}
60 changes: 60 additions & 0 deletions crates/oxc_transformer/tests/es_target/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use std::{path::Path, str::FromStr};

use oxc_allocator::Allocator;
use oxc_codegen::{CodeGenerator, CodegenOptions};
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType;
use oxc_transformer::{ESTarget, TransformOptions, Transformer};

use crate::run;

pub(crate) fn test(source_text: &str, target: &str) -> String {
let source_type = SourceType::default();
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, source_type).parse();
let mut program = ret.program;
let (symbols, scopes) =
SemanticBuilder::new().build(&program).semantic.into_symbol_table_and_scope_tree();
let options = TransformOptions::from(ESTarget::from_str(target).unwrap());
Transformer::new(&allocator, Path::new(""), options).build_with_symbols_and_scopes(
symbols,
scopes,
&mut program,
);
CodeGenerator::new()
.with_options(CodegenOptions { single_quote: true, ..CodegenOptions::default() })
.build(&program)
.code
}

#[test]
fn es2015() {
use std::fmt::Write;

let cases = [
("es5", "() => {}"),
("es2015", "a ** b"),
("es2016", "async function foo() {}"),
("es2017", "({ ...x })"),
("es2018", "try {} catch {}"),
("es2019", "a ?? b"),
("es2020", "a ||= b"),
("es2021", "class foo { static {} }"),
];

// Test no transformation for esnext.
for (_, case) in cases {
assert_eq!(run(case, SourceType::mjs()), test(case, "esnext"));
}

let snapshot = cases.iter().enumerate().fold(String::new(), |mut w, (i, (target, case))| {
let result = test(case, target);
write!(w, "########## {i} {target}\n{case}\n----------\n{result}\n").unwrap();
w
});

insta::with_settings!({ prepend_module_to_snapshot => false, snapshot_suffix => "", omit_expression => true }, {
insta::assert_snapshot!("es_target", snapshot);
});
}
54 changes: 54 additions & 0 deletions crates/oxc_transformer/tests/es_target/snapshots/es_target.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
source: crates/oxc_transformer/tests/es_target/mod.rs
snapshot_kind: text
---
########## 0 es5
() => {}
----------
(function() {});

########## 1 es2015
a ** b
----------
Math.pow(a, b);

########## 2 es2016
async function foo() {}
----------
import _asyncToGenerator from '@babel/runtime/helpers/asyncToGenerator';
function foo() {
return _foo.apply(this, arguments);
}
function _foo() {
_foo = _asyncToGenerator(function* () {});
return _foo.apply(this, arguments);
}

########## 3 es2017
({ ...x })
----------
import _objectSpread from '@babel/runtime/helpers/objectSpread2';
_objectSpread({}, x);

########## 4 es2018
try {} catch {}
----------
try {} catch (_unused) {}

########## 5 es2019
a ?? b
----------
var _a;
(_a = a) !== null && _a !== void 0 ? _a : b;

########## 6 es2020
a ||= b
----------
a || (a = b);

########## 7 es2021
class foo { static {} }
----------
class foo {
static #_ = (() => {})();
}
15 changes: 15 additions & 0 deletions crates/oxc_transformer/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
mod es_target;
mod plugins;

use oxc_allocator::Allocator;
use oxc_codegen::{CodeGenerator, CodegenOptions};
use oxc_parser::Parser;
use oxc_span::SourceType;

pub fn run(source_text: &str, source_type: SourceType) -> String {
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, source_type).parse();
CodeGenerator::new()
.with_options(CodegenOptions { single_quote: true, ..CodegenOptions::default() })
.build(&ret.program)
.code
}
Loading

0 comments on commit 21b8e49

Please sign in to comment.