diff --git a/crates/oxc_transformer/src/es2015/duplicate_keys.rs b/crates/oxc_transformer/src/es2015/duplicate_keys.rs new file mode 100644 index 000000000..dbc2091d8 --- /dev/null +++ b/crates/oxc_transformer/src/es2015/duplicate_keys.rs @@ -0,0 +1,74 @@ +#![allow(clippy::similar_names)] + +use std::{collections::HashSet, rc::Rc}; + +use oxc_ast::{ast::*, AstBuilder}; +use oxc_span::{Atom, SPAN}; + +use crate::options::{TransformOptions, TransformTarget}; + +/// ES2015: Duplicate Keys +/// +/// References: +/// * +/// * +pub struct DuplicateKeys<'a> { + ast: Rc>, +} + +impl<'a> DuplicateKeys<'a> { + pub fn new(ast: Rc>, options: &TransformOptions) -> Option { + (options.target < TransformTarget::ES2015 || options.duplicate_keys).then(|| Self { ast }) + } + + pub fn transform_object_expression<'b>(&mut self, obj_expr: &'b mut ObjectExpression<'a>) { + let mut visited_data: HashSet = HashSet::new(); + let mut visited_getters: HashSet = HashSet::new(); + let mut visited_setters: HashSet = HashSet::new(); + + for property in obj_expr.properties.iter_mut() { + let ObjectPropertyKind::ObjectProperty(obj_property) = property else { + continue; + }; + + if obj_property.computed { + continue; + } + + if let Some(name) = &obj_property.key.static_name() { + let mut is_duplicate = false; + + match obj_property.kind { + PropertyKind::Get => { + if visited_data.contains(name) || visited_getters.contains(name) { + is_duplicate = true; + } + visited_getters.insert(name.clone()); + } + PropertyKind::Set => { + if visited_data.contains(name) || visited_setters.contains(name) { + is_duplicate = true; + } + visited_setters.insert(name.clone()); + } + PropertyKind::Init => { + if visited_data.contains(name) + || visited_setters.contains(name) + || visited_getters.contains(name) + { + is_duplicate = true; + } + visited_data.insert(name.clone()); + } + } + + if is_duplicate { + obj_property.computed = true; + let string_literal = StringLiteral::new(SPAN, name.as_str().into()); + let expr = self.ast.literal_string_expression(string_literal); + obj_property.key = PropertyKey::Expression(expr); + } + } + } + } +} diff --git a/crates/oxc_transformer/src/es2015/mod.rs b/crates/oxc_transformer/src/es2015/mod.rs index bfc4f904e..1afd234a7 100644 --- a/crates/oxc_transformer/src/es2015/mod.rs +++ b/crates/oxc_transformer/src/es2015/mod.rs @@ -1,7 +1,9 @@ +mod duplicate_keys; mod function_name; mod shorthand_properties; mod template_literals; +pub use duplicate_keys::DuplicateKeys; pub use function_name::FunctionName; pub use shorthand_properties::ShorthandProperties; pub use template_literals::TemplateLiterals; diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 7fbb9a754..fbe44b7c5 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -65,6 +65,7 @@ pub struct Transformer<'a> { es2015_function_name: Option>, es2015_shorthand_properties: Option>, es2015_template_literals: Option>, + es2015_duplicate_keys: Option>, es3_property_literal: Option>, } @@ -101,6 +102,7 @@ impl<'a> Transformer<'a> { es2015_function_name: FunctionName::new(Rc::clone(&ast), ctx.clone(), &options), es2015_shorthand_properties: ShorthandProperties::new(Rc::clone(&ast), &options), es2015_template_literals: TemplateLiterals::new(Rc::clone(&ast), &options), + es2015_duplicate_keys: DuplicateKeys::new(Rc::clone(&ast), &options), // other es3_property_literal: PropertyLiteral::new(Rc::clone(&ast), &options), react_jsx: ReactJsx::new(Rc::clone(&ast), ctx.clone(), options) @@ -189,6 +191,7 @@ impl<'a> VisitMut<'a> for Transformer<'a> { fn visit_object_expression(&mut self, expr: &mut ObjectExpression<'a>) { self.es2015_function_name.as_mut().map(|t| t.transform_object_expression(expr)); + self.es2015_duplicate_keys.as_mut().map(|t| t.transform_object_expression(expr)); for property in expr.properties.iter_mut() { self.visit_object_property_kind(property); diff --git a/crates/oxc_transformer/src/options.rs b/crates/oxc_transformer/src/options.rs index cfd8f1d77..7279851c3 100644 --- a/crates/oxc_transformer/src/options.rs +++ b/crates/oxc_transformer/src/options.rs @@ -20,6 +20,7 @@ pub struct TransformOptions { // es2016 pub exponentiation_operator: bool, // es2015 + pub duplicate_keys: bool, pub function_name: bool, pub shorthand_properties: bool, pub sticky_regex: bool, diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index c460935c7..e21167a27 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -1,4 +1,4 @@ -Passed: 270/1103 +Passed: 277/1111 # All Passed: * babel-plugin-transform-numeric-separator @@ -772,6 +772,9 @@ Passed: 270/1103 * loose/ignoreToPrimitiveHint/input.js * loose/mutableTemplateObject/input.js +# babel-plugin-transform-duplicate-keys (7/8) +* combination/dupes/input.js + # babel-plugin-transform-typescript (66/158) * class/abstract-class-decorated/input.ts * class/abstract-class-decorated-method/input.ts diff --git a/tasks/transform_conformance/src/lib.rs b/tasks/transform_conformance/src/lib.rs index db25a9701..7522ca47f 100644 --- a/tasks/transform_conformance/src/lib.rs +++ b/tasks/transform_conformance/src/lib.rs @@ -78,6 +78,7 @@ const CASES: &[&str] = &[ "babel-plugin-transform-sticky-regex", "babel-plugin-transform-unicode-regex", "babel-plugin-transform-template-literals", + "babel-plugin-transform-duplicate-keys", // ES3 "babel-plugin-transform-property-literals", // TypeScript diff --git a/tasks/transform_conformance/src/test_case.rs b/tasks/transform_conformance/src/test_case.rs index 7ba1c7b6a..14a766a83 100644 --- a/tasks/transform_conformance/src/test_case.rs +++ b/tasks/transform_conformance/src/test_case.rs @@ -111,6 +111,7 @@ pub trait TestCase { sticky_regex: options.get_plugin("transform-sticky-regex").is_some(), template_literals: options.get_plugin("transform-template-literals").is_some(), property_literals: options.get_plugin("transform-property-literals").is_some(), + duplicate_keys: options.get_plugin("transform-duplicate-keys").is_some(), } }