diff --git a/Cargo.lock b/Cargo.lock index 1ed778e6fb7..9e498f989b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1218,6 +1218,7 @@ dependencies = [ "crossbeam-channel", "dashmap", "getrandom", + "indexmap 1.9.3", "jemallocator", "libc", "mimalloc", diff --git a/crates/node-bindings/Cargo.toml b/crates/node-bindings/Cargo.toml index 118f72869be..dffb3350a8f 100644 --- a/crates/node-bindings/Cargo.toml +++ b/crates/node-bindings/Cargo.toml @@ -22,6 +22,7 @@ mozjpeg-sys = "1.0.0" libc = "0.2" rayon = "1.7.0" crossbeam-channel = "0.5.6" +indexmap = "1.9.2" [target.'cfg(target_arch = "wasm32")'.dependencies] napi = {version = "2.12.6", features = ["serde-json"]} diff --git a/crates/node-bindings/src/transformer.rs b/crates/node-bindings/src/transformer.rs index 68cda51b03a..19fbcb605b1 100644 --- a/crates/node-bindings/src/transformer.rs +++ b/crates/node-bindings/src/transformer.rs @@ -13,6 +13,7 @@ pub fn transform(opts: JsObject, env: Env) -> napi::Result { mod native_only { use super::*; use crossbeam_channel::{Receiver, Sender}; + use indexmap::IndexMap; use napi::{ threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunctionCallMode}, JsBoolean, JsFunction, JsNumber, JsString, ValueType, @@ -177,12 +178,12 @@ mod native_only { let names = obj.get_property_names()?; let len = names.get_array_length()?; - let mut props = Vec::with_capacity(len as usize); + let mut props = IndexMap::with_capacity(len as usize); for i in 0..len { let prop = names.get_element::(i)?; let name = prop.into_utf8()?.into_owned()?; let value = napi_to_js_value(obj.get_property(prop)?, env)?; - props.push((name, value)); + props.insert(name, value); } Ok(JsValue::Object(props)) } diff --git a/packages/core/integration-tests/test/macros.js b/packages/core/integration-tests/test/macros.js index 7ae65444431..487a34584c5 100644 --- a/packages/core/integration-tests/test/macros.js +++ b/packages/core/integration-tests/test/macros.js @@ -163,7 +163,7 @@ describe('macros', function () { await fsFixture(overlayFS, dir)` index.js: import { test } from "./macro.js" with { type: "macro" }; - output = test(1 + 2, 'foo ' + 'bar', !true, [1, ...[2, 3]], true ? 1 : 0, typeof false, null ?? 2); + output = test(1 + 2, 'foo ' + 'bar', 3 + 'em', 'test'.length, 'test'['length'], 'test'[1], !true, [1, ...[2, 3]], {x: 2, ...{y: 3}}, true ? 1 : 0, typeof false, null ?? 2); macro.js: export function test(...args) { @@ -177,7 +177,20 @@ describe('macros', function () { }); let res = await run(b); - assert.deepEqual(res, [3, 'foo bar', false, [1, 2, 3], 1, 'boolean', 2]); + assert.deepEqual(res, [ + 3, + 'foo bar', + '3em', + 4, + 4, + 'e', + false, + [1, 2, 3], + {x: 2, y: 3}, + 1, + 'boolean', + 2, + ]); }); it('should dead code eliminate falsy branches', async function () { @@ -547,4 +560,245 @@ describe('macros', function () { assert(match2); assert.notEqual(match[1], match2[1]); }); + + it('should support evaluating constants', async function () { + await fsFixture(overlayFS, dir)` + index.js: + import { hashString } from "@parcel/rust" with { type: "macro" }; + import { test } from './macro' with { type: "macro" }; + const hi = "hi"; + const ref = hi; + const arr = [hi]; + const obj = {a: {b: hi}}; + const [a, [b], ...c] = [hi, [hi], 2, 3, hi]; + const [x, y = hi] = [1]; + const {hi: d, e, ...f} = {hi, e: hi, x: 2, y: hi}; + const res = test(); + output1 = hashString(hi); + output2 = hashString(ref); + output3 = hashString(arr[0]); + output4 = hashString(obj.a.b); + output5 = hashString(a); + output6 = hashString(b); + output7 = hashString(c[2]); + output8 = hashString(y); + output9 = hashString(d); + output10 = hashString(e); + output11 = hashString(f.y); + output12 = hashString(f?.y); + output13 = hashString(res); + + macro.js: + export function test() { + return "hi"; + } + `; + + let b = await bundle(path.join(dir, '/index.js'), { + inputFS: overlayFS, + mode: 'production', + }); + + let res = await overlayFS.readFile(b.getBundles()[0].filePath, 'utf8'); + for (let i = 1; i <= 13; i++) { + assert(res.includes(`output${i}="2a2300bbd7ea6e9a"`)); + } + }); + + it('should throw a diagnostic when a constant is mutated', async function () { + await fsFixture(overlayFS, dir)` + index.js: + import { hashString } from "@parcel/rust" with { type: "macro" }; + const object = {foo: 'bar'}; + object.foo = 'test'; + output = hashString(object.foo); + + const arr = ['foo']; + arr[0] = 'bar'; + output = hashString(arr[0]); + `; + + try { + await bundle(path.join(dir, '/index.js'), { + inputFS: overlayFS, + mode: 'production', + }); + } catch (err) { + assert.deepEqual(err.diagnostics, [ + { + message: 'Could not statically evaluate macro argument', + origin: '@parcel/transformer-js', + codeFrames: [ + { + filePath: path.join(dir, 'index.js'), + codeHighlights: [ + { + message: undefined, + start: { + line: 3, + column: 1, + }, + end: { + line: 3, + column: 19, + }, + }, + ], + }, + ], + hints: null, + }, + { + message: 'Could not statically evaluate macro argument', + origin: '@parcel/transformer-js', + codeFrames: [ + { + filePath: path.join(dir, 'index.js'), + codeHighlights: [ + { + message: undefined, + start: { + line: 7, + column: 1, + }, + end: { + line: 7, + column: 14, + }, + }, + ], + }, + ], + hints: null, + }, + ]); + } + }); + + it('should throw a diagnostic when a constant object is passed to a function', async function () { + await fsFixture(overlayFS, dir)` + index.js: + import { hashString } from "@parcel/rust" with { type: "macro" }; + const bar = 'bar'; + const object = {foo: bar}; + doSomething(bar); // ok (string) + doSomething(object.foo); // ok (evaluates to a string) + doSomething(object); // error (object could be mutated) + output = hashString(object.foo); + + const object2 = {foo: bar, obj: {}}; + doSomething(object2.obj); // error (object could be mutated) + output2 = hashString(object2); + + const arr = ['foo']; + doSomething(arr); + output3 = hashString(arr[0]); + + const object3 = {foo: bar}; + doSomething(object3[unknown]); + output4 = hashString(object3); + `; + + try { + await bundle(path.join(dir, '/index.js'), { + inputFS: overlayFS, + mode: 'production', + }); + } catch (err) { + assert.deepEqual(err.diagnostics, [ + { + message: 'Could not statically evaluate macro argument', + origin: '@parcel/transformer-js', + codeFrames: [ + { + filePath: path.join(dir, 'index.js'), + codeHighlights: [ + { + message: undefined, + start: { + line: 6, + column: 13, + }, + end: { + line: 6, + column: 18, + }, + }, + ], + }, + ], + hints: null, + }, + { + message: 'Could not statically evaluate macro argument', + origin: '@parcel/transformer-js', + codeFrames: [ + { + filePath: path.join(dir, 'index.js'), + codeHighlights: [ + { + message: undefined, + start: { + line: 10, + column: 13, + }, + end: { + line: 10, + column: 19, + }, + }, + ], + }, + ], + hints: null, + }, + { + message: 'Could not statically evaluate macro argument', + origin: '@parcel/transformer-js', + codeFrames: [ + { + filePath: path.join(dir, 'index.js'), + codeHighlights: [ + { + message: undefined, + start: { + line: 14, + column: 13, + }, + end: { + line: 14, + column: 15, + }, + }, + ], + }, + ], + hints: null, + }, + { + message: 'Could not statically evaluate macro argument', + origin: '@parcel/transformer-js', + codeFrames: [ + { + filePath: path.join(dir, 'index.js'), + codeHighlights: [ + { + message: undefined, + start: { + line: 18, + column: 13, + }, + end: { + line: 18, + column: 19, + }, + }, + ], + }, + ], + hints: null, + }, + ]); + } + }); }); diff --git a/packages/transformers/js/core/src/macros.rs b/packages/transformers/js/core/src/macros.rs index c606c754a0b..87e3fa5617f 100644 --- a/packages/transformers/js/core/src/macros.rs +++ b/packages/transformers/js/core/src/macros.rs @@ -1,4 +1,5 @@ -use std::collections::HashMap; +use indexmap::IndexMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use swc_core::common::errors::Handler; @@ -22,9 +23,12 @@ pub type MacroCallback = Arc< pub struct Macros<'a> { /// Mapping of imported identifiers to import metadata. macros: HashMap, + constants: HashMap>, callback: MacroCallback, source_map: &'a SourceMap, diagnostics: &'a mut Vec, + assignment_span: Option, + in_call: bool, } struct MacroImport { @@ -42,9 +46,12 @@ impl<'a> Macros<'a> { ) -> Self { Macros { macros: HashMap::new(), + constants: HashMap::new(), callback, source_map, diagnostics, + assignment_span: None, + in_call: false, } } @@ -86,11 +93,11 @@ impl<'a> Macros<'a> { } } - fn call_macro(&self, src: &JsWord, export: &JsWord, call: &CallExpr) -> Result { + fn call_macro(&self, src: String, export: String, call: CallExpr) -> Result { // Try to statically evaluate all of the function arguments. let mut args = Vec::with_capacity(call.args.len()); for arg in &call.args { - match eval(&*arg.expr) { + match self.eval(&*arg.expr) { Ok(val) => { if arg.spread.is_none() { args.push(val); @@ -108,7 +115,7 @@ impl<'a> Macros<'a> { // If that was successful, call the function callback (on the JS thread). let loc = SourceLocation::from(self.source_map, call.span); - match (self.callback)(src.to_string(), export.to_string(), args, loc.clone()) { + match (self.callback)(src, export, args, loc.clone()) { Ok(val) => Ok(self.value_to_expr(val)?), Err(err) => Err(Diagnostic { message: format!("Error evaluating macro: {}", err), @@ -160,17 +167,18 @@ impl<'a> Fold for Macros<'a> { node } - fn fold_expr(&mut self, mut node: Expr) -> Expr { - node = node.fold_children_with(self); - - if let Expr::Call(call) = &node { + fn fold_expr(&mut self, node: Expr) -> Expr { + if let Expr::Call(call) = node { if let Callee::Expr(expr) = &call.callee { match &**expr { Expr::Ident(ident) => { if let Some(specifier) = self.macros.get(&ident.to_id()) { if let Some(imported) = &specifier.imported { + let specifier = specifier.src.to_string(); + let imported = imported.to_string(); + let call = call.fold_with(self); return handle_error( - self.call_macro(&specifier.src, imported, call), + self.call_macro(specifier, imported, call), &mut self.diagnostics, ); } @@ -185,8 +193,11 @@ impl<'a> Fold for Macros<'a> { ) { // Check that this is a namespace import. if specifier.imported.is_none() { + let specifier = specifier.src.to_string(); + let imported = prop.0.to_string(); + let call = call.fold_with(self); return handle_error( - self.call_macro(&specifier.src, &prop.0, call), + self.call_macro(specifier, imported, call), &mut self.diagnostics, ); } @@ -196,6 +207,81 @@ impl<'a> Fold for Macros<'a> { _ => {} } } + + // Not a macro. Track if we're in a call so we can error if constant + // objects are referenced that might be mutated. + self.in_call = true; + let call = call.fold_with(self); + self.in_call = false; + return Expr::Call(call); + } + + node.fold_children_with(self) + } + + fn fold_var_decl(&mut self, mut node: VarDecl) -> VarDecl { + node = node.fold_children_with(self); + + if node.kind == VarDeclKind::Const { + for decl in &node.decls { + if let Some(expr) = &decl.init { + let val = self.eval(&*expr); + self.eval_pat(val, &decl.name); + } + } + } + + node + } + + fn fold_assign_expr(&mut self, mut node: AssignExpr) -> AssignExpr { + self.assignment_span = Some(node.span.clone()); + node.left = node.left.fold_with(self); + self.assignment_span = None; + + node.right = node.right.fold_with(self); + node + } + + fn fold_member_expr(&mut self, node: MemberExpr) -> MemberExpr { + if let Some(assignment_span) = self.assignment_span { + // Error when re-assigning a property of a constant that's used in a macro. + let node = node.fold_children_with(self); + if let Expr::Ident(id) = &*node.obj { + if let Some(constant) = self.constants.get_mut(&id.to_id()) { + if constant.is_ok() { + *constant = Err(assignment_span.clone()); + } + } + } + + return node; + } else if self.in_call { + // We need to error when passing a constant object into a non-macro call, since it might be mutated. + // If the member expression evaluates to an object, continue traversing so we error in fold_ident. + // Otherwise, return early to allow other properties to be accessed without error. + let value = self + .eval(&*node.obj) + .and_then(|obj| self.eval_member_prop(obj, &node)); + if !matches!( + value, + Err(..) | Ok(JsValue::Object(..) | JsValue::Array(..)) + ) { + return node; + } + } + + node.fold_children_with(self) + } + + fn fold_ident(&mut self, node: Ident) -> Ident { + if self.in_call { + if let Some(constant) = self.constants.get_mut(&node.to_id()) { + if matches!(constant, Ok(JsValue::Object(..) | JsValue::Array(..))) { + // Mark access to constant object inside a call as an error since it could potentially be mutated. + *constant = Err(node.span.clone()); + } + } } node @@ -227,13 +313,16 @@ fn handle_error(result: Result, diagnostics: &mut Vec expr, Err(err) => { - diagnostics.push(err); + if !diagnostics.iter().any(|d| *d == err) { + diagnostics.push(err); + } Expr::Lit(Lit::Null(Null::dummy())) } } } // A type that represents a basic JS value. +#[derive(Clone, Debug)] pub enum JsValue { Undefined, Null, @@ -242,89 +331,85 @@ pub enum JsValue { String(String), Regex { source: String, flags: String }, Array(Vec), - Object(Vec<(String, JsValue)>), + Object(IndexMap), Function(String), } -/// Statically evaluate a JS expression to a value, if possible. -fn eval(expr: &Expr) -> Result { - match expr.unwrap_parens() { - Expr::Lit(lit) => match lit { - Lit::Null(_) => Ok(JsValue::Null), - Lit::Bool(v) => Ok(JsValue::Bool(v.value)), - Lit::Num(v) => Ok(JsValue::Number(v.value)), - Lit::Str(v) => Ok(JsValue::String(v.value.to_string())), - Lit::JSXText(v) => Ok(JsValue::String(v.value.to_string())), - Lit::Regex(v) => Ok(JsValue::Regex { - source: v.exp.to_string(), - flags: v.flags.to_string(), - }), - Lit::BigInt(v) => Err(v.span), - }, - Expr::Tpl(tpl) => { - let exprs: Vec<_> = tpl - .exprs - .iter() - .filter_map(|expr| eval(&*expr).ok()) - .collect(); - if exprs.len() == tpl.exprs.len() { - let mut res = String::new(); - let mut expr_iter = exprs.iter(); - for quasi in &tpl.quasis { - res.push_str(&quasi.raw); - match expr_iter.next() { - None => {} - Some(JsValue::String(s)) => res.push_str(s), - Some(JsValue::Number(n)) => res.push_str(&n.to_string()), - Some(JsValue::Bool(b)) => res.push_str(&b.to_string()), - _ => return Err(tpl.span), +impl<'a> Macros<'a> { + /// Statically evaluate a JS expression to a value, if possible. + fn eval(&self, expr: &Expr) -> Result { + match expr.unwrap_parens() { + Expr::Lit(lit) => match lit { + Lit::Null(_) => Ok(JsValue::Null), + Lit::Bool(v) => Ok(JsValue::Bool(v.value)), + Lit::Num(v) => Ok(JsValue::Number(v.value)), + Lit::Str(v) => Ok(JsValue::String(v.value.to_string())), + Lit::JSXText(v) => Ok(JsValue::String(v.value.to_string())), + Lit::Regex(v) => Ok(JsValue::Regex { + source: v.exp.to_string(), + flags: v.flags.to_string(), + }), + Lit::BigInt(v) => Err(v.span), + }, + Expr::Tpl(tpl) => { + let exprs: Vec<_> = tpl + .exprs + .iter() + .filter_map(|expr| self.eval(&*expr).ok()) + .collect(); + if exprs.len() == tpl.exprs.len() { + let mut res = String::new(); + let mut expr_iter = exprs.iter(); + for quasi in &tpl.quasis { + res.push_str(&quasi.raw); + match expr_iter.next() { + None => {} + Some(JsValue::String(s)) => res.push_str(s), + Some(JsValue::Number(n)) => res.push_str(&n.to_string()), + Some(JsValue::Bool(b)) => res.push_str(&b.to_string()), + _ => return Err(tpl.span), + } } - } - Ok(JsValue::String(res)) - } else { - Err(tpl.span) + Ok(JsValue::String(res)) + } else { + Err(tpl.span) + } } - } - Expr::Array(arr) => { - let mut res = Vec::with_capacity(arr.elems.len()); - for elem in &arr.elems { - if let Some(elem) = elem { - match eval(&*elem.expr) { - Err(e) => return Err(e), - Ok(val) => { - if elem.spread.is_some() { - match val { - JsValue::Array(arr) => { - res.extend(arr); - } - _ => return Err(arr.span), + Expr::Array(arr) => { + let mut res = Vec::with_capacity(arr.elems.len()); + for elem in &arr.elems { + if let Some(elem) = elem { + let val = self.eval(&*elem.expr)?; + if elem.spread.is_some() { + match val { + JsValue::Array(arr) => { + res.extend(arr); } - } else { - res.push(val); + _ => return Err(arr.span), } + } else { + res.push(val); } + } else { + res.push(JsValue::Undefined); } - } else { - res.push(JsValue::Undefined); } + Ok(JsValue::Array(res)) } - Ok(JsValue::Array(res)) - } - Expr::Object(obj) => { - let mut res = Vec::with_capacity(obj.props.len()); - for prop in &obj.props { - match prop { - PropOrSpread::Prop(prop) => match &**prop { - Prop::KeyValue(kv) => match eval(&*kv.value) { - Err(e) => return Err(e), - Ok(v) => { + Expr::Object(obj) => { + let mut res = IndexMap::with_capacity(obj.props.len()); + for prop in &obj.props { + match prop { + PropOrSpread::Prop(prop) => match &**prop { + Prop::KeyValue(kv) => { + let v = self.eval(&*kv.value)?; let k = match &kv.key { PropName::Ident(Ident { sym, .. }) | PropName::Str(Str { value: sym, .. }) => { sym.to_string() } PropName::Num(n) => n.value.to_string(), - PropName::Computed(c) => match eval(&*c.expr) { + PropName::Computed(c) => match self.eval(&*c.expr) { Err(e) => return Err(e), Ok(JsValue::String(s)) => s, Ok(JsValue::Number(n)) => n.to_string(), @@ -334,167 +419,228 @@ fn eval(expr: &Expr) -> Result { PropName::BigInt(v) => return Err(v.span), }; - res.push((k.to_string(), v)) + res.insert(k.to_string(), v); + } + Prop::Shorthand(s) => { + if let Some(val) = self.constants.get(&s.to_id()) { + res.insert(s.sym.to_string(), val.clone()?); + } else { + return Err(s.span); + } } - }, - _ => return Err(obj.span), - }, - PropOrSpread::Spread(spread) => match eval(&*spread.expr) { - Err(e) => return Err(e), - Ok(v) => match v { - JsValue::Object(o) => res.extend(o), _ => return Err(obj.span), }, - }, + PropOrSpread::Spread(spread) => { + let v = self.eval(&*spread.expr)?; + match v { + JsValue::Object(o) => res.extend(o), + _ => return Err(obj.span), + } + } + } } + Ok(JsValue::Object(res)) } - Ok(JsValue::Object(res)) - } - Expr::Bin(bin) => match (bin.op, eval(&*bin.left), eval(&*bin.right)) { - (BinaryOp::Add, Ok(JsValue::String(a)), Ok(JsValue::String(b))) => { - Ok(JsValue::String(format!("{}{}", a, b))) - } - (BinaryOp::BitAnd, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { - Ok(JsValue::Number(((a as i32) & (b as i32)) as f64)) - } - (BinaryOp::BitOr, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { - Ok(JsValue::Number(((a as i32) | (b as i32)) as f64)) - } - (BinaryOp::BitXor, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { - Ok(JsValue::Number(((a as i32) ^ (b as i32)) as f64)) - } - (BinaryOp::LShift, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { - Ok(JsValue::Number(((a as i32) << (b as i32)) as f64)) - } - (BinaryOp::RShift, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { - Ok(JsValue::Number(((a as i32) >> (b as i32)) as f64)) - } - (BinaryOp::ZeroFillRShift, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { - Ok(JsValue::Number(((a as i32) >> (b as u32)) as f64)) - } - (BinaryOp::Add, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => Ok(JsValue::Number(a + b)), - (BinaryOp::Sub, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => Ok(JsValue::Number(a - b)), - (BinaryOp::Div, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => Ok(JsValue::Number(a / b)), - (BinaryOp::Mul, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => Ok(JsValue::Number(a * b)), - (BinaryOp::Mod, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => Ok(JsValue::Number(a % b)), - (BinaryOp::Exp, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { - Ok(JsValue::Number(a.powf(b))) - } - (BinaryOp::EqEq, Ok(JsValue::Bool(a)), Ok(JsValue::Bool(b))) => Ok(JsValue::Bool(a == b)), - (BinaryOp::EqEqEq, Ok(JsValue::Bool(a)), Ok(JsValue::Bool(b))) => Ok(JsValue::Bool(a == b)), - (BinaryOp::NotEq, Ok(JsValue::Bool(a)), Ok(JsValue::Bool(b))) => Ok(JsValue::Bool(a != b)), - (BinaryOp::NotEqEq, Ok(JsValue::Bool(a)), Ok(JsValue::Bool(b))) => Ok(JsValue::Bool(a != b)), - (BinaryOp::EqEq, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => Ok(JsValue::Bool(a == b)), - (BinaryOp::EqEqEq, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { - Ok(JsValue::Bool(a == b)) - } - (BinaryOp::NotEq, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { - Ok(JsValue::Bool(a != b)) - } - (BinaryOp::NotEqEq, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { - Ok(JsValue::Bool(a != b)) - } - (BinaryOp::EqEq, Ok(JsValue::String(a)), Ok(JsValue::String(b))) => Ok(JsValue::Bool(a == b)), - (BinaryOp::EqEqEq, Ok(JsValue::String(a)), Ok(JsValue::String(b))) => { - Ok(JsValue::Bool(a == b)) - } - (BinaryOp::NotEq, Ok(JsValue::String(a)), Ok(JsValue::String(b))) => { - Ok(JsValue::Bool(a != b)) - } - (BinaryOp::NotEqEq, Ok(JsValue::String(a)), Ok(JsValue::String(b))) => { - Ok(JsValue::Bool(a != b)) - } - (BinaryOp::Gt, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => Ok(JsValue::Bool(a > b)), - (BinaryOp::GtEq, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => Ok(JsValue::Bool(a >= b)), - (BinaryOp::Lt, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => Ok(JsValue::Bool(a < b)), - (BinaryOp::LtEq, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => Ok(JsValue::Bool(a <= b)), - (BinaryOp::LogicalAnd, Ok(JsValue::Bool(a)), Ok(JsValue::Bool(b))) => { - Ok(JsValue::Bool(a && b)) - } - (BinaryOp::LogicalOr, Ok(JsValue::Bool(a)), Ok(JsValue::Bool(b))) => { - Ok(JsValue::Bool(a || b)) - } - (BinaryOp::NullishCoalescing, Ok(JsValue::Null | JsValue::Undefined), Ok(b)) => Ok(b), - (BinaryOp::NullishCoalescing, Ok(a), Ok(_)) => Ok(a), - _ => Err(bin.span), - }, - Expr::Unary(unary) => match (unary.op, eval(&*unary.arg)) { - (UnaryOp::Bang, Ok(JsValue::Bool(v))) => Ok(JsValue::Bool(!v)), - (UnaryOp::Minus, Ok(JsValue::Number(v))) => Ok(JsValue::Number(-v)), - (UnaryOp::Plus, Ok(JsValue::Number(v))) => Ok(JsValue::Number(v)), - (UnaryOp::Plus, Ok(JsValue::String(v))) => { - if let Ok(v) = v.parse() { - Ok(JsValue::Number(v)) - } else { - Err(unary.span) + Expr::Bin(bin) => match (bin.op, self.eval(&*bin.left), self.eval(&*bin.right)) { + (BinaryOp::Add, Ok(JsValue::String(a)), Ok(JsValue::String(b))) => { + Ok(JsValue::String(format!("{}{}", a, b))) } - } - (UnaryOp::Tilde, Ok(JsValue::Number(v))) => Ok(JsValue::Number((!(v as i32)) as f64)), - (UnaryOp::Void, Ok(_)) => Ok(JsValue::Undefined), - (UnaryOp::TypeOf, Ok(JsValue::Bool(_))) => Ok(JsValue::String("boolean".to_string())), - (UnaryOp::TypeOf, Ok(JsValue::Number(_))) => Ok(JsValue::String("number".to_string())), - (UnaryOp::TypeOf, Ok(JsValue::String(_))) => Ok(JsValue::String("string".to_string())), - (UnaryOp::TypeOf, Ok(JsValue::Object(_))) => Ok(JsValue::String("object".to_string())), - (UnaryOp::TypeOf, Ok(JsValue::Array(_))) => Ok(JsValue::String("object".to_string())), - (UnaryOp::TypeOf, Ok(JsValue::Regex { .. })) => Ok(JsValue::String("object".to_string())), - (UnaryOp::TypeOf, Ok(JsValue::Null)) => Ok(JsValue::String("object".to_string())), - (UnaryOp::TypeOf, Ok(JsValue::Undefined)) => Ok(JsValue::String("undefined".to_string())), - _ => Err(unary.span), - }, - Expr::Ident(id) if &id.sym == "undefined" => Ok(JsValue::Undefined), - Expr::Cond(cond) => match eval(&*&cond.test) { - Ok(JsValue::Bool(v)) => { - if v { - eval(&*&cond.cons) - } else { - eval(&*cond.alt) + (BinaryOp::Add, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Number(a + b)) } - } - Ok(JsValue::Null) | Ok(JsValue::Undefined) => eval(&*cond.alt), - Ok(JsValue::Object(_)) - | Ok(JsValue::Array(_)) - | Ok(JsValue::Function(_)) - | Ok(JsValue::Regex { .. }) => eval(&*cond.cons), - Ok(JsValue::String(s)) => { - if s.is_empty() { - eval(&*cond.alt) + (BinaryOp::Add, Ok(JsValue::String(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::String(format!("{}{}", a, b))) + } + (BinaryOp::Add, Ok(JsValue::Number(a)), Ok(JsValue::String(b))) => { + Ok(JsValue::String(format!("{}{}", a, b))) + } + (BinaryOp::BitAnd, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Number(((a as i32) & (b as i32)) as f64)) + } + (BinaryOp::BitOr, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Number(((a as i32) | (b as i32)) as f64)) + } + (BinaryOp::BitXor, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Number(((a as i32) ^ (b as i32)) as f64)) + } + (BinaryOp::LShift, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Number(((a as i32) << (b as i32)) as f64)) + } + (BinaryOp::RShift, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Number(((a as i32) >> (b as i32)) as f64)) + } + (BinaryOp::ZeroFillRShift, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Number(((a as i32) >> (b as u32)) as f64)) + } + (BinaryOp::Sub, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Number(a - b)) + } + (BinaryOp::Div, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Number(a / b)) + } + (BinaryOp::Mul, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Number(a * b)) + } + (BinaryOp::Mod, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Number(a % b)) + } + (BinaryOp::Exp, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Number(a.powf(b))) + } + (BinaryOp::EqEq, Ok(JsValue::Bool(a)), Ok(JsValue::Bool(b))) => Ok(JsValue::Bool(a == b)), + (BinaryOp::EqEqEq, Ok(JsValue::Bool(a)), Ok(JsValue::Bool(b))) => Ok(JsValue::Bool(a == b)), + (BinaryOp::NotEq, Ok(JsValue::Bool(a)), Ok(JsValue::Bool(b))) => Ok(JsValue::Bool(a != b)), + (BinaryOp::NotEqEq, Ok(JsValue::Bool(a)), Ok(JsValue::Bool(b))) => { + Ok(JsValue::Bool(a != b)) + } + (BinaryOp::EqEq, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Bool(a == b)) + } + (BinaryOp::EqEqEq, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Bool(a == b)) + } + (BinaryOp::NotEq, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Bool(a != b)) + } + (BinaryOp::NotEqEq, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Bool(a != b)) + } + (BinaryOp::EqEq, Ok(JsValue::String(a)), Ok(JsValue::String(b))) => { + Ok(JsValue::Bool(a == b)) + } + (BinaryOp::EqEqEq, Ok(JsValue::String(a)), Ok(JsValue::String(b))) => { + Ok(JsValue::Bool(a == b)) + } + (BinaryOp::NotEq, Ok(JsValue::String(a)), Ok(JsValue::String(b))) => { + Ok(JsValue::Bool(a != b)) + } + (BinaryOp::NotEqEq, Ok(JsValue::String(a)), Ok(JsValue::String(b))) => { + Ok(JsValue::Bool(a != b)) + } + (BinaryOp::Gt, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => Ok(JsValue::Bool(a > b)), + (BinaryOp::GtEq, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Bool(a >= b)) + } + (BinaryOp::Lt, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => Ok(JsValue::Bool(a < b)), + (BinaryOp::LtEq, Ok(JsValue::Number(a)), Ok(JsValue::Number(b))) => { + Ok(JsValue::Bool(a <= b)) + } + (BinaryOp::LogicalAnd, Ok(JsValue::Bool(a)), Ok(JsValue::Bool(b))) => { + Ok(JsValue::Bool(a && b)) + } + (BinaryOp::LogicalOr, Ok(JsValue::Bool(a)), Ok(JsValue::Bool(b))) => { + Ok(JsValue::Bool(a || b)) + } + (BinaryOp::NullishCoalescing, Ok(JsValue::Null | JsValue::Undefined), Ok(b)) => Ok(b), + (BinaryOp::NullishCoalescing, Ok(a), Ok(_)) => Ok(a), + _ => Err(bin.span), + }, + Expr::Unary(unary) => match (unary.op, self.eval(&*unary.arg)) { + (UnaryOp::Bang, Ok(JsValue::Bool(v))) => Ok(JsValue::Bool(!v)), + (UnaryOp::Minus, Ok(JsValue::Number(v))) => Ok(JsValue::Number(-v)), + (UnaryOp::Plus, Ok(JsValue::Number(v))) => Ok(JsValue::Number(v)), + (UnaryOp::Plus, Ok(JsValue::String(v))) => { + if let Ok(v) = v.parse() { + Ok(JsValue::Number(v)) + } else { + Err(unary.span) + } + } + (UnaryOp::Tilde, Ok(JsValue::Number(v))) => Ok(JsValue::Number((!(v as i32)) as f64)), + (UnaryOp::Void, Ok(_)) => Ok(JsValue::Undefined), + (UnaryOp::TypeOf, Ok(JsValue::Bool(_))) => Ok(JsValue::String("boolean".to_string())), + (UnaryOp::TypeOf, Ok(JsValue::Number(_))) => Ok(JsValue::String("number".to_string())), + (UnaryOp::TypeOf, Ok(JsValue::String(_))) => Ok(JsValue::String("string".to_string())), + (UnaryOp::TypeOf, Ok(JsValue::Object(_))) => Ok(JsValue::String("object".to_string())), + (UnaryOp::TypeOf, Ok(JsValue::Array(_))) => Ok(JsValue::String("object".to_string())), + (UnaryOp::TypeOf, Ok(JsValue::Regex { .. })) => Ok(JsValue::String("object".to_string())), + (UnaryOp::TypeOf, Ok(JsValue::Null)) => Ok(JsValue::String("object".to_string())), + (UnaryOp::TypeOf, Ok(JsValue::Undefined)) => Ok(JsValue::String("undefined".to_string())), + _ => Err(unary.span), + }, + Expr::Cond(cond) => match self.eval(&*&cond.test) { + Ok(JsValue::Bool(v)) => { + if v { + self.eval(&*&cond.cons) + } else { + self.eval(&*cond.alt) + } + } + Ok(JsValue::Null) | Ok(JsValue::Undefined) => self.eval(&*cond.alt), + Ok(JsValue::Object(_)) + | Ok(JsValue::Array(_)) + | Ok(JsValue::Function(_)) + | Ok(JsValue::Regex { .. }) => self.eval(&*cond.cons), + Ok(JsValue::String(s)) => { + if s.is_empty() { + self.eval(&*cond.alt) + } else { + self.eval(&*cond.cons) + } + } + Ok(JsValue::Number(n)) => { + if n == 0.0 { + self.eval(&*cond.alt) + } else { + self.eval(&*cond.cons) + } + } + Err(e) => Err(e), + }, + Expr::Ident(id) if &id.sym == "undefined" => Ok(JsValue::Undefined), + Expr::Ident(id) => { + if let Some(val) = self.constants.get(&id.to_id()) { + val.clone() } else { - eval(&*cond.cons) + Err(id.span) } } - Ok(JsValue::Number(n)) => { - if n == 0.0 { - eval(&*cond.alt) + Expr::Member(member) => { + let obj = self.eval(&*member.obj)?; + self.eval_member_prop(obj, &member) + } + Expr::OptChain(opt) => { + if let OptChainBase::Member(member) = &*opt.base { + let obj = self.eval(&*member.obj)?; + match obj { + JsValue::Undefined | JsValue::Null => Ok(JsValue::Undefined), + _ => self.eval_member_prop(obj, &member), + } } else { - eval(&*cond.cons) + Err(opt.span) } } - Err(e) => Err(e), - }, - Expr::Fn(FnExpr { function, .. }) => Err(function.span), - Expr::Class(ClassExpr { class, .. }) => Err(class.span), - Expr::JSXElement(el) => Err(el.span), - Expr::This(ThisExpr { span, .. }) - | Expr::Update(UpdateExpr { span, .. }) - | Expr::Assign(AssignExpr { span, .. }) - | Expr::Member(MemberExpr { span, .. }) - | Expr::Call(CallExpr { span, .. }) - | Expr::New(NewExpr { span, .. }) - | Expr::Seq(SeqExpr { span, .. }) - | Expr::TaggedTpl(TaggedTpl { span, .. }) - | Expr::Arrow(ArrowExpr { span, .. }) - | Expr::Yield(YieldExpr { span, .. }) - | Expr::Await(AwaitExpr { span, .. }) - | Expr::JSXFragment(JSXFragment { span, .. }) - | Expr::PrivateName(PrivateName { span, .. }) - | Expr::OptChain(OptChainExpr { span, .. }) - | Expr::Ident(Ident { span, .. }) => Err(*span), - _ => Err(DUMMY_SP), + Expr::Fn(FnExpr { function, .. }) => Err(function.span), + Expr::Class(ClassExpr { class, .. }) => Err(class.span), + Expr::JSXElement(el) => Err(el.span), + Expr::This(ThisExpr { span, .. }) + | Expr::Update(UpdateExpr { span, .. }) + | Expr::Assign(AssignExpr { span, .. }) + | Expr::Call(CallExpr { span, .. }) + | Expr::New(NewExpr { span, .. }) + | Expr::Seq(SeqExpr { span, .. }) + | Expr::TaggedTpl(TaggedTpl { span, .. }) + | Expr::Arrow(ArrowExpr { span, .. }) + | Expr::Yield(YieldExpr { span, .. }) + | Expr::Await(AwaitExpr { span, .. }) + | Expr::JSXFragment(JSXFragment { span, .. }) + | Expr::PrivateName(PrivateName { span, .. }) => Err(*span), + _ => Err(DUMMY_SP), + } } -} -// Convert JS value to AST. -impl<'a> Macros<'a> { + fn eval_member_prop(&self, obj: JsValue, member: &MemberExpr) -> Result { + match &member.prop { + MemberProp::Ident(id) => obj.get_id(id.as_ref()).ok_or(member.span), + MemberProp::Computed(prop) => { + let k = self.eval(&*prop.expr)?; + obj.get(&k).ok_or(prop.span) + } + _ => Err(member.span), + } + } + + /// Convert JS value to AST. fn value_to_expr(&self, value: JsValue) -> Result { Ok(match value { JsValue::Null => Expr::Lit(Lit::Null(Null::dummy())), @@ -575,4 +721,177 @@ impl<'a> Macros<'a> { } }) } + + fn eval_pat(&mut self, value: Result, pat: &Pat) { + match pat { + Pat::Ident(name) => { + self.constants.insert(name.to_id(), value); + } + Pat::Array(arr) => { + for (index, elem) in arr.elems.iter().enumerate() { + if let Some(elem) = elem { + match elem { + Pat::Array(ArrayPat { span, .. }) + | Pat::Object(ObjectPat { span, .. }) + | Pat::Ident(BindingIdent { + id: Ident { span, .. }, + .. + }) => self.eval_pat( + value + .as_ref() + .and_then(|v| v.get_index(index).ok_or(span)) + .map_err(|s| *s), + elem, + ), + Pat::Rest(rest) => self.eval_pat( + value + .as_ref() + .and_then(|v| v.rest(index).ok_or(&rest.span)) + .map_err(|s| *s), + &*rest.arg, + ), + Pat::Assign(assign) => self.eval_pat( + value.as_ref().map_err(|e| *e).and_then(|v| { + v.get_index(index) + .ok_or(assign.span) + .or_else(|_| self.eval(&*assign.right)) + }), + &*assign.left, + ), + _ => {} + } + } + } + } + Pat::Object(obj) => { + let mut consumed = HashSet::new(); + for prop in &obj.props { + match prop { + ObjectPatProp::KeyValue(kv) => { + let val = value + .as_ref() + .map_err(|e| *e) + .and_then(|value| match &kv.key { + PropName::Ident(id) => { + consumed.insert(id.sym.clone()); + value.get_id(id.sym.as_str()).ok_or(id.span) + } + PropName::Str(s) => { + consumed.insert(s.value.clone()); + value.get_id(s.value.as_str()).ok_or(s.span) + } + PropName::Num(n) => { + consumed.insert(n.value.to_string().into()); + value.get_index(n.value as usize).ok_or(n.span) + } + PropName::Computed(c) => { + let k = &self.eval(&*c.expr)?; + match k { + JsValue::String(s) => { + consumed.insert(s.clone().into()); + } + JsValue::Number(n) => { + consumed.insert(n.to_string().into()); + } + _ => {} + } + value.get(&k).ok_or(c.span) + } + PropName::BigInt(v) => Err(v.span), + }); + self.eval_pat(val, &*kv.value) + } + ObjectPatProp::Assign(assign) => { + let val = value.as_ref().map_err(|e| *e).and_then(|value| { + value + .get_id(assign.key.sym.as_str()) + .ok_or(assign.span) + .or_else(|_| { + assign + .value + .as_ref() + .map_or(Err(assign.span), |v| self.eval(&*v)) + }) + }); + self.constants.insert(assign.key.to_id(), val); + consumed.insert(assign.key.sym.clone()); + } + ObjectPatProp::Rest(rest) => { + let val = value.as_ref().map_err(|e| *e).and_then(|value| { + if let JsValue::Object(obj) = value { + let filtered = obj + .iter() + .filter(|(k, _)| !consumed.contains(&k.as_str().into())) + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + Ok(JsValue::Object(filtered)) + } else { + Err(rest.span) + } + }); + self.eval_pat(val, &*rest.arg); + } + } + } + } + _ => {} + } + } +} + +impl JsValue { + fn get(&self, prop: &JsValue) -> Option { + match self { + JsValue::Array(arr) => { + if let JsValue::Number(n) = prop { + arr.get(*n as usize).cloned() + } else { + None + } + } + JsValue::Object(_) => match prop { + JsValue::Number(n) => { + let index = n.to_string(); + self.get_id(&index) + } + JsValue::String(s) => self.get_id(s), + _ => None, + }, + JsValue::String(s) => match prop { + JsValue::String(prop) => self.get_id(prop), + JsValue::Number(n) => s + .get(*n as usize..=*n as usize) + .map(|c| JsValue::String(c.to_owned())), + _ => None, + }, + _ => None, + } + } + + fn get_index(&self, index: usize) -> Option { + if let JsValue::Array(arr) = self { + arr.get(index).cloned() + } else { + None + } + } + + fn get_id(&self, prop: &str) -> Option { + match self { + JsValue::Object(obj) => obj.get(prop).cloned(), + JsValue::String(s) => match prop { + "length" => Some(JsValue::Number(s.len() as f64)), + _ => None, + }, + _ => None, + } + } + + fn rest(&self, index: usize) -> Option { + if let JsValue::Array(arr) = self { + arr.get(index..).map(|s| JsValue::Array(s.to_vec())) + } else { + None + } + } } diff --git a/packages/transformers/js/core/src/utils.rs b/packages/transformers/js/core/src/utils.rs index d784a76f254..b3d6b8b41c1 100644 --- a/packages/transformers/js/core/src/utils.rs +++ b/packages/transformers/js/core/src/utils.rs @@ -239,13 +239,13 @@ impl PartialOrd for SourceLocation { } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct CodeHighlight { pub message: Option, pub loc: SourceLocation, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct Diagnostic { pub message: String, pub code_highlights: Option>,