diff --git a/.gitattributes b/.gitattributes index 4a0480e2..ae050f23 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ crates/jsshaker/tests/fixtures/** linguist-vendored text eol=lf crates/jsshaker/tests/snapshots/** linguist-generated text eol=lf +crates/jsshaker/tests/module_fixtures/** linguist-vendored text eol=lf diff --git a/crates/jsshaker/src/analyzer/conditional.rs b/crates/jsshaker/src/analyzer/conditional.rs index a2fbc036..91f1d82b 100644 --- a/crates/jsshaker/src/analyzer/conditional.rs +++ b/crates/jsshaker/src/analyzer/conditional.rs @@ -18,6 +18,7 @@ struct ConditionalData<'a> { maybe_false: bool, impure_true: bool, impure_false: bool, + exiting_branches: Vec>, tests_to_include: Vec>, } @@ -53,6 +54,22 @@ impl<'a> ConditionalBranch<'a> { } } +#[derive(Debug)] +struct ConditionalExitLink { + id: DepAtom, +} + +impl<'a> CustomDepTrait<'a> for ConditionalExitLink { + fn include(&self, analyzer: &mut Analyzer<'a>) { + let Some(data) = analyzer.conditional_data.node_to_data.get_mut(&self.id) else { + return; + }; + for branch in mem::take(&mut data.exiting_branches) { + analyzer.include(branch); + } + } +} + impl<'a> CustomDepTrait<'a> for ConditionalBranch<'a> { fn include(&self, analyzer: &mut Analyzer<'a>) { let data = analyzer.conditional_data.node_to_data.get_mut(&self.id).unwrap(); @@ -182,6 +199,20 @@ impl<'a> Analyzer<'a> { Some(Dep(branch)) } + pub fn note_conditional_branch_exit(&mut self, id: impl Into, dep: Option>) { + let Some(dep) = dep else { return }; + if !self.cf_scope().must_exited() { + return; + } + if let Some(data) = self.conditional_data.node_to_data.get_mut(&id.into()) { + data.exiting_branches.push(dep); + } + } + + pub fn conditional_exit_link(&mut self, id: impl Into) -> Dep<'a> { + Dep(self.allocator.alloc(ConditionalExitLink { id: id.into() })) + } + pub fn post_analyze_handle_conditional(&mut self) -> bool { if !self.config.branch_folding { return false; diff --git a/crates/jsshaker/src/analyzer/mod.rs b/crates/jsshaker/src/analyzer/mod.rs index a28b8999..e0da469d 100644 --- a/crates/jsshaker/src/analyzer/mod.rs +++ b/crates/jsshaker/src/analyzer/mod.rs @@ -28,7 +28,7 @@ use crate::{ mangling::Mangler, module::{ModuleId, Modules}, scope::Scoping, - utils::ExtraData, + utils::{ExtraData, private_identifier_name::PrivateEnvs}, value::{FnStats, literal::symbol::SymbolRegistry}, vfs::Vfs, }; @@ -59,6 +59,7 @@ pub struct Analyzer<'a> { pub diagnostics: BTreeSet, pub fn_stats: Option>, pub symbol_registry: SymbolRegistry<'a>, + pub private_envs: PrivateEnvs<'a>, } impl<'a> Analyzer<'a> { @@ -92,6 +93,7 @@ impl<'a> Analyzer<'a> { pending_deps: Default::default(), diagnostics: Default::default(), fn_stats: config.enable_fn_stats.then(|| RefCell::new(FnStats::new())), + private_envs: Default::default(), } } diff --git a/crates/jsshaker/src/builtins/globals/object_constructor.rs b/crates/jsshaker/src/builtins/globals/object_constructor.rs index f2e14168..9f2dc162 100644 --- a/crates/jsshaker/src/builtins/globals/object_constructor.rs +++ b/crates/jsshaker/src/builtins/globals/object_constructor.rs @@ -1,6 +1,6 @@ use std::borrow::BorrowMut; -use oxc::allocator; +use oxc::{allocator, ast::ast::PropertyKind}; use crate::{ Analyzer, builtin_string, @@ -188,7 +188,8 @@ impl<'a> Builtins<'a> { } let enumerated = descriptor.enumerate_properties(analyzer, dep); let mut value = None; - let mut deps = vec![]; + let mut accessors = vec![]; + let mut attributes = vec![]; for (definite, key, value2) in enumerated.known.into_values() { if !definite { break 'trackable; @@ -201,46 +202,49 @@ impl<'a> Builtins<'a> { value = Some(self.factory.computed(value2, (key, value))); } "get" => { - // FIXME: This is not safe, but OK for now. - value = Some(self.factory.computed_unknown((value2, key, value))); + accessors.push((PropertyKind::Get, self.factory.computed(value2, key))); } - "set" | "enumerable" | "configurable" | "writable" => { + "set" => { + accessors.push((PropertyKind::Set, self.factory.computed(value2, key))); + } + "enumerable" | "configurable" | "writable" => { // TODO: actually handle these - deps.push(key); - deps.push(value2); + attributes.push(key); + attributes.push(value2); } _ => {} } } - if value.is_none() { - analyzer.push_non_det_cf_scope(); - } - object.set_property( - analyzer, - analyzer.factory.dep((enumerated.dep, descriptor.get_shallow_dep(analyzer.factory))), - key, - { - let value = value.unwrap_or(analyzer.factory.undefined); - if deps.is_empty() { - value - } else { - analyzer - .factory - .computed(value, allocator::Vec::from_iter_in(deps, analyzer.allocator)) - } - }, - ); - if value.is_none() { - analyzer.pop_cf_scope(); + let attributes = allocator::Vec::from_iter_in(attributes, analyzer.allocator); + let descriptor_dep = descriptor.get_shallow_dep(analyzer.factory); + let write_dep = self.factory.dep((enumerated.dep, descriptor_dep)); + + if accessors.is_empty() { + let has_value = value.is_some(); + if !has_value { + analyzer.push_non_det_cf_scope(); + } + let value = value.unwrap_or(analyzer.factory.undefined); + let value = + if attributes.is_empty() { value } else { self.factory.computed(value, attributes) }; + object.set_property(analyzer, write_dep, key, value); + if !has_value { + analyzer.pop_cf_scope(); + } + } else { + let Some(object_value) = object.value.as_object() else { + break 'trackable; + }; + let accessor_dep = self.factory.dep((write_dep, value, attributes)); + for (kind, accessor) in accessors { + let accessor = self.factory.computed(accessor, accessor_dep); + object_value.init_property(analyzer, kind, key, accessor, true); + } } - let deps = self.factory.dep(( - dep, - object.get_shallow_dep(analyzer.factory), - key, - descriptor.get_shallow_dep(analyzer.factory), - )); + let deps = + self.factory.dep((dep, object.get_shallow_dep(analyzer.factory), key, descriptor_dep)); analyzer.add_callsite_dep(deps); diff --git a/crates/jsshaker/src/entity.rs b/crates/jsshaker/src/entity.rs index f5f801f6..0eedaa84 100644 --- a/crates/jsshaker/src/entity.rs +++ b/crates/jsshaker/src/entity.rs @@ -184,7 +184,7 @@ impl<'a> Entity<'a> { /// Returns vec![(definite, key)] pub fn get_keys( &self, - analyzer: &Analyzer<'a>, + analyzer: &mut Analyzer<'a>, check_proto: bool, ) -> Option)>> { self.value.get_keys(analyzer, check_proto) diff --git a/crates/jsshaker/src/folding/mod.rs b/crates/jsshaker/src/folding/mod.rs index 283ecfe2..815a8cc2 100644 --- a/crates/jsshaker/src/folding/mod.rs +++ b/crates/jsshaker/src/folding/mod.rs @@ -82,10 +82,7 @@ impl<'a> Analyzer<'a> { if let FoldingData::NonFoldable = self.folder.bump.get(data) { value } else if let Some(literal) = self.get_foldable_literal(value) { - let mangle_atom = match literal { - LiteralValue::String(_, Some(atom)) => Some(atom), - _ => None, - }; + let mangle_atom = literal.mangle_atom(); value.override_dep(self.factory.dep(FoldableDep { data, literal, value, mangle_atom })) } else { self.factory.computed(value, NonFoldableDep { data }) diff --git a/crates/jsshaker/src/module.rs b/crates/jsshaker/src/module.rs index 3cbd5a21..0dc9c95c 100644 --- a/crates/jsshaker/src/module.rs +++ b/crates/jsshaker/src/module.rs @@ -131,6 +131,7 @@ impl<'a> Analyzer<'a> { self.add_diagnostic(format!("[{}] {}", path, error)); } let semantic = SemanticBuilder::new().build(program).semantic; + self.private_envs.collect(program); let module_id = ModuleId::from_usize(self.modules.modules.len()); let variable_scope = self.push_variable_scope(); self.variable_scope_mut().this = Some(self.factory.unknown); @@ -295,7 +296,7 @@ impl<'a> Analyzer<'a> { let reexport_all = module.reexport_all.iter().copied().collect::>(); self.include((call_id, default_export)); for named_export in named_exports { - let value = self.get_named_export_value(module_id, named_export); + let value = self.get_named_export_value(module_id, named_export, &mut FxHashSet::default()); self.include(value); } for reexport_module_id in reexport_all { @@ -307,12 +308,18 @@ impl<'a> Analyzer<'a> { &mut self, module_id: ModuleId, named_export: ExportedValue<'a>, + searched: &mut FxHashSet, ) -> Entity<'a> { match named_export { ExportedValue::Variable(scope, symbol, dep) => { let old_module = self.set_current_module(module_id); + if !self.is_readonly_symbol(symbol) { + self.include_on_scope(scope, symbol); + } let value = self.read_on_scope(scope, symbol).unwrap(); - // TODO: handle TDZ + if value.is_none() { + self.include_on_scope(scope, symbol); + } let value = value.unwrap_or(self.factory.unknown); self.set_current_module(old_module); self.factory.computed(value, dep) @@ -320,9 +327,8 @@ impl<'a> Analyzer<'a> { ExportedValue::Function(entity, dep) => self.factory.computed(entity, dep), ExportedValue::Namespace(entity, dep) => self.factory.computed(entity, dep), ExportedValue::ReExport(module, name, dep) => { - let value = self - .get_export_value_by_name(module, name, &mut FxHashSet::default()) - .unwrap_or(self.factory.unknown); + let value = + self.get_export_value_by_name(module, name, searched).unwrap_or(self.factory.unknown); self.factory.computed(value, dep) } ExportedValue::Unknown(dep) => self.factory.computed_unknown(dep), @@ -339,18 +345,22 @@ impl<'a> Analyzer<'a> { return None; } let module = &self.modules.modules[module_id]; - if name == "default" { - module.default_export.map(|e| e.unwrap_or(self.factory.unknown)) - } else if let Some(exported_value) = module.named_exports.get(&name) { - Some(self.get_named_export_value(module_id, *exported_value)) - } else { + if name == "default" + && let Some(default_export) = module.default_export + { + return Some(default_export.unwrap_or(self.factory.unknown)); + } + if let Some(&exported_value) = module.named_exports.get(&name) { + return Some(self.get_named_export_value(module_id, exported_value, searched)); + } + if name != "default" { for reexport_module_id in module.reexport_all.clone() { if let Some(entity) = self.get_export_value_by_name(reexport_module_id, name, searched) { return Some(entity); } } - None } + None } pub fn does_module_reexport_unknown( diff --git a/crates/jsshaker/src/nodes/expr/await_expression.rs b/crates/jsshaker/src/nodes/expr/await_expression.rs index 972be01a..e3c52d33 100644 --- a/crates/jsshaker/src/nodes/expr/await_expression.rs +++ b/crates/jsshaker/src/nodes/expr/await_expression.rs @@ -1,6 +1,11 @@ use oxc::ast::ast::{AwaitExpression, Expression}; -use crate::{analyzer::Analyzer, entity::Entity, transformer::Transformer, utils::ast::AstKind2}; +use crate::{ + analyzer::Analyzer, + entity::Entity, + transformer::Transformer, + utils::{CalleeNode, ast::AstKind2}, +}; impl<'a> Analyzer<'a> { pub fn exec_await_expression(&mut self, node: &'a AwaitExpression<'a>) -> Entity<'a> { @@ -11,8 +16,12 @@ impl<'a> Analyzer<'a> { self.global_effect(); + let dep = AstKind2::AwaitExpression(node); let value = self.exec_expression(&node.argument); - value.r#await(self, AstKind2::AwaitExpression(node)) + if !matches!(self.call_scope().callee.node, CalleeNode::Module | CalleeNode::Root) { + self.include((dep, value)); + } + value.r#await(self, dep) } } diff --git a/crates/jsshaker/src/nodes/expr/call_expression.rs b/crates/jsshaker/src/nodes/expr/call_expression.rs index 10fd8899..97ec4690 100644 --- a/crates/jsshaker/src/nodes/expr/call_expression.rs +++ b/crates/jsshaker/src/nodes/expr/call_expression.rs @@ -65,6 +65,10 @@ impl<'a> Analyzer<'a> { let ret_val = callee.call(self, callsite, this, args); self.scoping.current_callsite = AstKind2::ENVIRONMENT; + if matches!(&node.callee, Expression::Super(_)) { + self.init_pending_instance_fields(); + } + Ok((scope_count, ret_val, undefined)) } } diff --git a/crates/jsshaker/src/nodes/expr/chain_expression.rs b/crates/jsshaker/src/nodes/expr/chain_expression.rs index a9352780..29e09f2f 100644 --- a/crates/jsshaker/src/nodes/expr/chain_expression.rs +++ b/crates/jsshaker/src/nodes/expr/chain_expression.rs @@ -1,9 +1,12 @@ -use oxc::ast::{ - ast::{ChainElement, ChainExpression, Expression}, - match_member_expression, +use oxc::{ + ast::{ + ast::{ChainElement, ChainExpression, Expression}, + match_member_expression, + }, + span::GetSpan, }; -use crate::{analyzer::Analyzer, entity::Entity, transformer::Transformer}; +use crate::{analyzer::Analyzer, build_effect, entity::Entity, transformer::Transformer}; impl<'a> Analyzer<'a> { pub fn exec_chain_expression(&mut self, node: &'a ChainExpression<'a>) -> Entity<'a> { @@ -57,7 +60,14 @@ impl<'a> Transformer<'a> { ChainElement::CallExpression(node) => self.transform_call_expression_in_chain(node, need_val), node => self.transform_member_expression_read_in_chain(node.to_member_expression(), need_val), } - .unwrap_or_else(|v| v) + .unwrap_or_else(|effect| { + if need_val { + let span = node.span(); + Some(build_effect!(&self.ast, span, effect; self.build_undefined(span))) + } else { + effect + } + }) } pub fn transform_expression_in_chain( diff --git a/crates/jsshaker/src/nodes/expr/import_expression.rs b/crates/jsshaker/src/nodes/expr/import_expression.rs index dc2fa7c7..ea84fcda 100644 --- a/crates/jsshaker/src/nodes/expr/import_expression.rs +++ b/crates/jsshaker/src/nodes/expr/import_expression.rs @@ -10,6 +10,7 @@ impl<'a> Analyzer<'a> { let specifier = self.exec_expression(&node.source).coerce_string(self); let options = node.options.as_ref().map(|option| self.exec_expression(option)); let dep = self.dep((AstKind2::ImportExpression(node), specifier, options)); + self.include(dep); if let Some(LiteralValue::String(specifier, _m)) = specifier.get_literal(self) && let Some(module_id) = self.resolve_and_parse_module(specifier) diff --git a/crates/jsshaker/src/nodes/expr/member_expression.rs b/crates/jsshaker/src/nodes/expr/member_expression.rs index b209aefc..b8444ef8 100644 --- a/crates/jsshaker/src/nodes/expr/member_expression.rs +++ b/crates/jsshaker/src/nodes/expr/member_expression.rs @@ -1,13 +1,14 @@ use oxc::{ ast::ast::{ ComputedMemberExpression, Expression, MemberExpression, PrivateFieldExpression, - StaticMemberExpression, + PrivateIdentifier, StaticMemberExpression, }, span::GetSpan, }; use crate::{ analyzer::Analyzer, ast::AstKind2, build_effect, entity::Entity, transformer::Transformer, + value::LiteralValue, }; pub type MemberExpressionReadResult<'a> = @@ -72,6 +73,10 @@ impl<'a> Analyzer<'a> { let key = self.exec_key(node, will_write.then_some(object)); + if let MemberExpression::PrivateFieldExpression(private) = node { + self.check_private_brand(dep_id, object, key, &private.field); + } + let value = object.get_property(self, dep_id, key); Ok((scope_count, value, undefined, (object, key))) @@ -101,6 +106,30 @@ impl<'a> Analyzer<'a> { } } + fn check_private_brand( + &mut self, + dep_id: AstKind2<'a>, + object: Entity<'a>, + key: Entity<'a>, + field: &'a PrivateIdentifier<'a>, + ) { + let Some(LiteralValue::String(name, _)) = key.get_literal(self) else { return }; + let Some(keys) = object.get_keys(self, true) else { return }; + let absent = keys.into_iter().all(|(_, existing)| { + matches!(existing.get_literal(self), Some(LiteralValue::String(other, _)) if other != name) + }); + if !absent { + return; + } + self.throw_builtin_error( + "TypeError: Cannot access private member on an object that does not have it", + ); + if self.config.preserve_exceptions { + let key = self.link_private_declaration(field, key); + self.include((dep_id, object, key)); + } + } + fn exec_key(&mut self, node: &'a MemberExpression<'a>, dep: Option>) -> Entity<'a> { match node { MemberExpression::ComputedMemberExpression(node) => { diff --git a/crates/jsshaker/src/nodes/expr/private_in_expression.rs b/crates/jsshaker/src/nodes/expr/private_in_expression.rs index cdf43a81..2c42d056 100644 --- a/crates/jsshaker/src/nodes/expr/private_in_expression.rs +++ b/crates/jsshaker/src/nodes/expr/private_in_expression.rs @@ -4,8 +4,10 @@ use crate::{analyzer::Analyzer, entity::Entity, transformer::Transformer}; impl<'a> Analyzer<'a> { pub fn exec_private_in_expression(&mut self, node: &'a PrivateInExpression<'a>) -> Entity<'a> { + let left = self.exec_private_identifier(&node.left); + let left = self.link_private_declaration(&node.left, left); let right = self.exec_expression(&node.right); - self.factory.computed_unknown_boolean(right) + self.factory.computed_unknown_boolean((left, right)) } } @@ -20,7 +22,8 @@ impl<'a> Transformer<'a> { let right = self.transform_expression(right, need_val); if need_val { - Some(self.ast.expression_private_in(*span, left.clone(), right.unwrap())) + let left = self.transform_private_identifier(left, true).unwrap(); + Some(self.ast.expression_private_in(*span, left, right.unwrap())) } else { right } diff --git a/crates/jsshaker/src/nodes/expr/super_expression.rs b/crates/jsshaker/src/nodes/expr/super_expression.rs index 0870ebb0..3f4b6b4a 100644 --- a/crates/jsshaker/src/nodes/expr/super_expression.rs +++ b/crates/jsshaker/src/nodes/expr/super_expression.rs @@ -5,7 +5,12 @@ use crate::{analyzer::Analyzer, builtin_string, entity::Entity, transformer::Tra impl<'a> Analyzer<'a> { pub fn exec_super(&mut self, _node: &'a Super) -> Entity<'a> { // Should only be called in member expression - self.get_super().get_property(self, self.factory.no_dep, builtin_string!("prototype")) + let super_scope = self.get_super(); + match super_scope.super_class { + None => self.factory.unknown, + Some(class) if super_scope.is_static => class, + Some(class) => class.get_property(self, self.factory.no_dep, builtin_string!("prototype")), + } } } diff --git a/crates/jsshaker/src/nodes/misc/callee.rs b/crates/jsshaker/src/nodes/misc/callee.rs index 61ee3d0e..925ea8ae 100644 --- a/crates/jsshaker/src/nodes/misc/callee.rs +++ b/crates/jsshaker/src/nodes/misc/callee.rs @@ -34,25 +34,29 @@ impl<'a> Analyzer<'a> { node: &'a Expression<'a>, ) -> Result<(usize, Entity<'a>, Option>, Entity<'a>), Entity<'a>> { if matches!(node, Expression::Super(_)) { - return Ok((0, self.get_super(), None, self.get_this())); + let super_class = self.get_super().super_class.unwrap_or(self.factory.undefined); + return Ok((0, super_class, None, self.get_this())); } let dep = AstKind2::Callee(node); if let Some((member_expr, same_chain)) = unwrap_to_member_expression(node) { + let super_receiver = matches!(member_expr.object(), Expression::Super(_)); if same_chain { let (scope_count, callee, undefined, (object, _)) = self.exec_member_expression_read_in_chain(member_expr, false)?; - Ok((scope_count, callee, undefined, self.factory.computed(object, dep))) + let this = if super_receiver { self.get_this() } else { object }; + Ok((scope_count, callee, undefined, self.factory.computed(this, dep))) } else { let result = self.exec_member_expression_read_in_chain(member_expr, false); Ok(match result { Ok((scope_count, value, undefined, (object, _))) => { self.pop_multiple_cf_scopes(scope_count); + let this = if super_receiver { self.get_this() } else { object }; ( 0, self.factory.optional_union(value, undefined), None, - self.factory.computed(object, dep), + self.factory.computed(this, dep), ) } Err(value) => (0, value, None, self.factory.unknown), diff --git a/crates/jsshaker/src/nodes/misc/class.rs b/crates/jsshaker/src/nodes/misc/class.rs index 5f79cc00..48253967 100644 --- a/crates/jsshaker/src/nodes/misc/class.rs +++ b/crates/jsshaker/src/nodes/misc/class.rs @@ -6,7 +6,7 @@ use oxc::{ NONE, ast::{ Class, ClassBody, ClassElement, ClassType, MethodDefinitionKind, PropertyDefinitionType, - PropertyKind, StaticBlock, + PropertyKey, PropertyKind, StaticBlock, }, }, span::GetSpan, @@ -17,6 +17,7 @@ use crate::{ ast::{AstKind2, DeclarationKind}, dep::DepAtom, entity::Entity, + scope::SuperScope, transformer::Transformer, utils::{CalleeNode, ClassData}, value::{ObjectPrototype, cache::FnCacheTrackingData, call::FnCallInfo}, @@ -59,15 +60,21 @@ impl<'a> Analyzer<'a> { prototype.set_prototype(ObjectPrototype::ImplicitOrNull); }; + let super_class = data.super_class; + // Enter class statics scope - self.variable_scope_mut().super_class = - Some(data.super_class.unwrap_or(self.factory.undefined)); - self.variable_scope_mut().this = Some(class.into()); + self.push_variable_scope(); + let variable_scope = self.variable_scope_mut(); + variable_scope.super_class = Some(SuperScope { super_class, is_static: true }); + variable_scope.this = Some(class.into()); // 2. Execute keys and find constructor for element in &node.body.body { - let key = element.property_key().map(|key| self.exec_property_key(key, None)); - data.keys.push(key); + let property_key = element.property_key(); + if let Some(PropertyKey::PrivateIdentifier(name)) = property_key { + self.private_envs.declare(name); + } + data.keys.push(property_key.map(|key| self.exec_property_key(key, None))); if let ClassElement::MethodDefinition(method) = element && method.kind.is_constructor() @@ -90,10 +97,15 @@ impl<'a> Analyzer<'a> { MethodDefinitionKind::Get => PropertyKind::Get, MethodDefinitionKind::Set => PropertyKind::Set, }; - let value = self.exec_function(&node.value); if node.r#static { + let value = self.exec_function(&node.value); class.statics.init_property(self, kind, key.unwrap(), value, true); } else { + self.push_variable_scope(); + self.variable_scope_mut().super_class = + Some(SuperScope { super_class, is_static: false }); + let value = self.exec_function(&node.value); + self.pop_variable_scope(); prototype.init_property(self, kind, key.unwrap(), value, true); } } @@ -153,6 +165,9 @@ impl<'a> Analyzer<'a> { } } + // Leave class statics scope + self.pop_variable_scope(); + // 5. Execute class decorators (ES2025 Stage 3) let class = class.into(); let class = if !node.decorators.is_empty() { @@ -180,10 +195,25 @@ impl<'a> Analyzer<'a> { value } + pub fn init_pending_instance_fields(&mut self) { + let Some((node, data_cell, this)) = self.call_scope_mut().pending_instance_fields.take() else { + return; + }; + let Ok(data) = data_cell.try_borrow() else { return }; + for (key, element) in data.keys.iter().zip(node.body.body.iter()) { + if let ClassElement::PropertyDefinition(node) = element + && !node.r#static + { + let value = self.exec_property_definition(node); + this.set_property(self, self.factory.no_dep, key.unwrap(), value); + } + } + } + pub fn call_class_constructor( &mut self, node: &'a Class<'a>, - data_cell: &RefCell>, + data_cell: &'a RefCell>, info: FnCallInfo<'a>, ) -> (Entity<'a>, FnCacheTrackingData<'a>) { let Ok(data) = data_cell.try_borrow() else { @@ -199,12 +229,12 @@ impl<'a> Analyzer<'a> { let factory = self.factory; self.push_call_scope(info, false, false); - let super_class = data.super_class.unwrap_or(self.factory.undefined); + let super_class = data.super_class; let variable_scope = self.variable_scope_mut(); variable_scope.this = Some(info.this); variable_scope.arguments = Some((info.args, factory.vec(/* later filled by formal parameters */))); - variable_scope.super_class = Some(super_class); + variable_scope.super_class = Some(SuperScope { super_class, is_static: false }); if let Some(id) = &node.id { self.declare_binding_identifier(id, None, DeclarationKind::NamedFunctionInBody); @@ -216,13 +246,9 @@ impl<'a> Analyzer<'a> { } // 1. Init properties - for (key, element) in data.keys.iter().zip(node.body.body.iter()) { - if let ClassElement::PropertyDefinition(node) = element - && !node.r#static - { - let value = self.exec_property_definition(node); - info.this.set_property(self, self.factory.no_dep, key.unwrap(), value); - } + self.call_scope_mut().pending_instance_fields = Some((node, data_cell, info.this)); + if super_class.is_none() { + self.init_pending_instance_fields(); } // 2. Call constructor @@ -232,15 +258,17 @@ impl<'a> Analyzer<'a> { self.cf_scope_mut().push_dep(dep); self.exec_formal_parameters(&function.params, info.args, DeclarationKind::FunctionParameter); self.exec_function_body(function.body.as_ref().unwrap()); + self.init_pending_instance_fields(); if info.include { self.include_return_values(); } let (ret_val, _) = self.pop_call_scope(); let ret_val = self.factory.computed(ret_val, dep); (ret_val, FnCacheTrackingData::worst_case()) - } else if let Some(super_class) = &data.super_class { - self.pop_call_scope(); + } else if let Some(super_class) = super_class { let ret_val = super_class.call(self, self.factory.no_dep, info.this, info.args); + self.init_pending_instance_fields(); + self.pop_call_scope(); (ret_val, FnCacheTrackingData::worst_case()) } else { let (_, cache_tracking) = self.pop_call_scope(); diff --git a/crates/jsshaker/src/nodes/misc/function.rs b/crates/jsshaker/src/nodes/misc/function.rs index e0892559..d2005bf9 100644 --- a/crates/jsshaker/src/nodes/misc/function.rs +++ b/crates/jsshaker/src/nodes/misc/function.rs @@ -11,6 +11,7 @@ use crate::{ ast::{AstKind2, DeclarationKind}, dep::DepAtom, entity::Entity, + scope::SuperScope, transformer::Transformer, utils::CalleeNode, value::{cache::FnCacheTrackingData, call::FnCallInfo}, @@ -55,7 +56,7 @@ impl<'a> Analyzer<'a> { let factory = analyzer.factory; let variable_scope = analyzer.variable_scope_mut(); variable_scope.this = Some(info.this); - variable_scope.super_class = info.include.then_some(factory.unknown); + variable_scope.super_class = info.include.then_some(SuperScope::default()); variable_scope.arguments = Some((info.args, factory.vec(/* later filled by formal parameters */))); diff --git a/crates/jsshaker/src/nodes/misc/private_identifier.rs b/crates/jsshaker/src/nodes/misc/private_identifier.rs index eed20078..5c887101 100644 --- a/crates/jsshaker/src/nodes/misc/private_identifier.rs +++ b/crates/jsshaker/src/nodes/misc/private_identifier.rs @@ -4,10 +4,12 @@ use crate::{ analyzer::Analyzer, ast::AstKind2, entity::Entity, + mangling::MangleConstraint, transformer::Transformer, utils::private_identifier_name::{ escape_private_identifier_name, unescape_private_identifier_name, }, + value::LiteralValue, }; impl<'a> Analyzer<'a> { @@ -15,11 +17,29 @@ impl<'a> Analyzer<'a> { self.factory.computed( self.exec_mangable_static_string( AstKind2::PrivateIdentifier(node), - escape_private_identifier_name(node.name.as_str()), + escape_private_identifier_name(self.private_envs.get(node), node.name.as_str()), ), AstKind2::PrivateIdentifier(node), ) } + + pub fn link_private_declaration( + &mut self, + node: &'a PrivateIdentifier<'a>, + key: Entity<'a>, + ) -> Entity<'a> { + let Some(declaration) = self.private_envs.declaration(node) else { return key }; + let declared = self.exec_private_identifier(declaration); + let linked = self.factory.computed(key, declared); + match MangleConstraint::equality( + true, + key.get_literal(self).and_then(LiteralValue::mangle_atom), + declared.get_literal(self).and_then(LiteralValue::mangle_atom), + ) { + Some(constraint) => self.factory.mangable(linked, (key, declared), constraint), + None => linked, + } + } } impl<'a> Transformer<'a> { diff --git a/crates/jsshaker/src/nodes/stmt/if_statement.rs b/crates/jsshaker/src/nodes/stmt/if_statement.rs index 8d3cec33..2db79c1e 100644 --- a/crates/jsshaker/src/nodes/stmt/if_statement.rs +++ b/crates/jsshaker/src/nodes/stmt/if_statement.rs @@ -8,6 +8,7 @@ use crate::{analyzer::Analyzer, ast::AstKind2, scope::CfScopeKind, transformer:: impl<'a> Analyzer<'a> { pub fn exec_if_statement(&mut self, node: &'a IfStatement) { let factory = self.factory; + let id = AstKind2::IfStatement(node); let test = self.exec_expression_no_fold(&node.test).coerce_primitive(self); @@ -24,8 +25,8 @@ impl<'a> Analyzer<'a> { let mut acc_dep_2 = None; if maybe_consequent { - self.push_if_like_branch_cf_scope( - AstKind2::IfStatement(node), + let branch_dep = self.push_if_like_branch_cf_scope( + id, CfScopeKind::ExitBlocker(None), test, maybe_consequent, @@ -34,6 +35,7 @@ impl<'a> Analyzer<'a> { node.alternate.is_some(), ); self.exec_statement(&node.consequent); + self.note_conditional_branch_exit(id, branch_dep); let mut conditional_scope = self.pop_cf_scope(); if let CfScopeKind::ExitBlocker(Some(stopped_exit)) = &conditional_scope.kind { exit_target_inner = exit_target_inner.max(*stopped_exit); @@ -44,8 +46,8 @@ impl<'a> Analyzer<'a> { acc_dep_1 = conditional_scope.deps.collect(factory); } if maybe_alternate { - self.push_if_like_branch_cf_scope( - AstKind2::IfStatement(node), + let branch_dep = self.push_if_like_branch_cf_scope( + id, CfScopeKind::ExitBlocker(None), test, maybe_consequent, @@ -55,6 +57,7 @@ impl<'a> Analyzer<'a> { ); if let Some(alternate) = &node.alternate { self.exec_statement(alternate); + self.note_conditional_branch_exit(id, branch_dep); let mut conditional_scope = self.pop_cf_scope(); if let CfScopeKind::ExitBlocker(Some(stopped_exit)) = &conditional_scope.kind { exit_target_inner = exit_target_inner.max(*stopped_exit); @@ -79,6 +82,11 @@ impl<'a> Analyzer<'a> { } else { self.exit_to_impl(exit_target_outer, self.scoping.cf.stack_len(), false, acc_dep); } + + if self.config.branch_folding && !self.cf_scope().must_exited() { + let link = self.conditional_exit_link(id); + self.cf_scope_mut().push_dep(link); + } } } diff --git a/crates/jsshaker/src/nodes/stmt/module_declaration.rs b/crates/jsshaker/src/nodes/stmt/module_declaration.rs index a90c630e..388e2fab 100644 --- a/crates/jsshaker/src/nodes/stmt/module_declaration.rs +++ b/crates/jsshaker/src/nodes/stmt/module_declaration.rs @@ -1,15 +1,34 @@ -use oxc::ast::ast::{ - ExportDefaultDeclaration, ExportDefaultDeclarationKind, ExportNamedDeclaration, - ImportDeclaration, ImportDeclarationSpecifier, ImportDefaultSpecifier, ImportNamespaceSpecifier, - ImportOrExportKind, ImportSpecifier, ModuleDeclaration, ModuleExportName, Statement, +use oxc::{ + ast::ast::{ + BindingIdentifier, ExportDefaultDeclaration, ExportDefaultDeclarationKind, + ExportNamedDeclaration, ImportDeclaration, ImportDeclarationSpecifier, ImportDefaultSpecifier, + ImportNamespaceSpecifier, ImportOrExportKind, ImportSpecifier, ModuleDeclaration, + ModuleExportName, Statement, + }, + span::Atom, }; use crate::{ - Analyzer, ast::DeclarationKind, module::ExportedValue, transformer::Transformer, + Analyzer, ast::DeclarationKind, entity::Entity, module::ExportedValue, transformer::Transformer, utils::ast::AstKind2, }; impl<'a> Analyzer<'a> { + fn live_default_export( + &mut self, + id: &Option>, + value: Entity<'a>, + ) -> Entity<'a> { + match id { + Some(id) if !self.is_readonly_symbol(id.symbol_id()) => { + let scope = self.scoping.variable.top().unwrap(); + self.include_on_scope(scope, id.symbol_id()); + self.factory.computed_unknown(value) + } + _ => value, + } + } + pub fn declare_module_declaration(&mut self, node: &'a ModuleDeclaration<'a>) { let module = self.module_info_mut(); match node { @@ -150,15 +169,18 @@ impl<'a> Analyzer<'a> { } } } else if let Some(resolved) = resolved { - let module = &self.modules.modules[resolved]; - match specifier { + let name = match specifier { ImportDeclarationSpecifier::ImportDefaultSpecifier(_node) => { - module.default_export.flatten().unwrap_or(self.factory.unknown) + Some(Atom::from("default")) } - ImportDeclarationSpecifier::ImportNamespaceSpecifier(_node) => module.module_object, - ImportDeclarationSpecifier::ImportSpecifier(node) => self - .get_export_value_by_name(resolved, node.imported.name(), &mut Default::default()) + ImportDeclarationSpecifier::ImportNamespaceSpecifier(_node) => None, + ImportDeclarationSpecifier::ImportSpecifier(node) => Some(node.imported.name()), + }; + match name { + Some(name) => self + .get_export_value_by_name(resolved, name, &mut Default::default()) .unwrap_or(self.factory.unknown), + None => self.modules.modules[resolved].module_object, } } else { self.builtins.factory.unknown @@ -183,13 +205,17 @@ impl<'a> Analyzer<'a> { } ModuleDeclaration::ExportDefaultDeclaration(node) => { let value = match &node.declaration { - ExportDefaultDeclarationKind::FunctionDeclaration(node) => self.exec_function(node), + ExportDefaultDeclarationKind::FunctionDeclaration(node) => { + let value = self.exec_function(node); + self.live_default_export(&node.id, value) + } ExportDefaultDeclarationKind::ClassDeclaration(node) => { if node.id.is_none() { // Patch `export default class{}` self.exec_class(node) } else { - self.init_class(node) + let value = self.init_class(node); + self.live_default_export(&node.id, value) } } node => self.exec_expression(node.to_expression()), diff --git a/crates/jsshaker/src/scope/call_scope.rs b/crates/jsshaker/src/scope/call_scope.rs index adc095fa..8f0361e9 100644 --- a/crates/jsshaker/src/scope/call_scope.rs +++ b/crates/jsshaker/src/scope/call_scope.rs @@ -1,6 +1,6 @@ -use std::mem; +use std::{cell::RefCell, mem}; -use oxc::allocator; +use oxc::{allocator, ast::ast::Class}; use super::variable_scope::VariableScopeId; use crate::{ @@ -8,9 +8,11 @@ use crate::{ dep::DepTrait, entity::Entity, module::ModuleId, - utils::{CalleeInfo, ast::AstKind2}, + utils::{CalleeInfo, ClassData, ast::AstKind2}, }; +pub type PendingInstanceFields<'a> = (&'a Class<'a>, &'a RefCell>, Entity<'a>); + pub struct CallScope<'a> { pub callsite: AstKind2<'a>, pub callee: CalleeInfo<'a>, @@ -22,6 +24,7 @@ pub struct CallScope<'a> { pub is_async: bool, pub is_generator: bool, pub need_include_arguments: bool, + pub pending_instance_fields: Option>, #[cfg(feature = "flame")] pub scope_guard: flame::SpanGuard, @@ -49,6 +52,7 @@ impl<'a> CallScope<'a> { is_async, is_generator, need_include_arguments: false, + pending_instance_fields: None, #[cfg(feature = "flame")] scope_guard: flame::start_guard(callee.debug_name.to_string()), diff --git a/crates/jsshaker/src/scope/mod.rs b/crates/jsshaker/src/scope/mod.rs index f53844e6..61d2d940 100644 --- a/crates/jsshaker/src/scope/mod.rs +++ b/crates/jsshaker/src/scope/mod.rs @@ -11,7 +11,7 @@ use cf_scope::CfScope; pub use cf_scope::{CfScopeId, CfScopeKind, CfScopeVer}; use stacked_tree::StackedTree; use variable_scope::VariableScope; -pub use variable_scope::VariableScopeId; +pub use variable_scope::{SuperScope, VariableScopeId}; use crate::{ analyzer::{Analyzer, Factory}, diff --git a/crates/jsshaker/src/scope/variable_scope.rs b/crates/jsshaker/src/scope/variable_scope.rs index 876021f0..bdf484b6 100644 --- a/crates/jsshaker/src/scope/variable_scope.rs +++ b/crates/jsshaker/src/scope/variable_scope.rs @@ -37,11 +37,17 @@ pub struct Variable<'a> { pub decl_node: AstKind2<'a>, } +#[derive(Debug, Clone, Copy, Default)] +pub struct SuperScope<'a> { + pub super_class: Option>, + pub is_static: bool, +} + pub struct VariableScope<'a> { pub variables: allocator::HashMap<'a, SymbolId, &'a RefCell>>, pub this: Option>, pub arguments: Option<(ArgumentsValue<'a>, allocator::Vec<'a, SymbolId>)>, - pub super_class: Option>, + pub super_class: Option>, } impl fmt::Debug for VariableScope<'_> { @@ -388,9 +394,10 @@ impl<'a> Analyzer<'a> { if let Some(exporting) = exporting { let name = Atom::from_in(self.semantic().scoping().symbol_name(symbol), self.allocator); + let readonly = self.is_readonly_symbol(symbol); self.module_info_mut().named_exports.insert( name, - if let Some(fn_value) = fn_value { + if let Some(fn_value) = fn_value.filter(|_| readonly) { ExportedValue::Function(fn_value, exporting) } else { ExportedValue::Variable(variable_scope, symbol, exporting) @@ -463,12 +470,12 @@ impl<'a> Analyzer<'a> { unreachable!() } - pub fn get_super(&mut self) -> Entity<'a> { + pub fn get_super(&mut self) -> SuperScope<'a> { for scope in self.scoping.variable.iter_rev() { if let Some(super_class) = scope.super_class { return super_class; } } - self.factory.unknown + SuperScope::default() } } diff --git a/crates/jsshaker/src/utils/private_identifier_name.rs b/crates/jsshaker/src/utils/private_identifier_name.rs index ba3e8363..661b2acc 100644 --- a/crates/jsshaker/src/utils/private_identifier_name.rs +++ b/crates/jsshaker/src/utils/private_identifier_name.rs @@ -1,13 +1,22 @@ +use oxc::ast::ast::{Class, PrivateIdentifier, Program, PropertyKey}; +use oxc_ast_visit::{Visit, walk}; +use rustc_hash::FxHashMap; + use crate::value::PropertyKeyValue; const PRIVATE_IDENTIFIER_PREFIX: &str = "__#private__"; -pub fn escape_private_identifier_name(name: &str) -> String { - format!("{}{}", PRIVATE_IDENTIFIER_PREFIX, name) +pub type PrivateEnvId = u32; + +pub fn escape_private_identifier_name(env: PrivateEnvId, name: &str) -> String { + format!("{}{}#{}", PRIVATE_IDENTIFIER_PREFIX, env, name) } pub fn unescape_private_identifier_name(name: &str) -> &str { - if let Some(stripped) = name.strip_prefix(PRIVATE_IDENTIFIER_PREFIX) { stripped } else { name } + match name.strip_prefix(PRIVATE_IDENTIFIER_PREFIX) { + Some(rest) => rest.split_once('#').map_or(rest, |(_, name)| name), + None => name, + } } impl<'a> PropertyKeyValue<'a> { @@ -18,3 +27,60 @@ impl<'a> PropertyKeyValue<'a> { } } } + +#[derive(Default)] +pub struct PrivateEnvs<'a> { + nodes: FxHashMap<*const PrivateIdentifier<'a>, PrivateEnvId>, + declarations: FxHashMap<(PrivateEnvId, &'a str), &'a PrivateIdentifier<'a>>, + stack: Vec<(PrivateEnvId, Vec<&'a str>)>, + next_id: PrivateEnvId, +} + +impl<'a> PrivateEnvs<'a> { + pub fn collect(&mut self, program: &Program<'a>) { + self.visit_program(program); + } + + pub fn get(&self, node: &PrivateIdentifier<'a>) -> PrivateEnvId { + self.nodes.get(&std::ptr::from_ref(node)).copied().unwrap_or_default() + } + + pub fn declare(&mut self, node: &'a PrivateIdentifier<'a>) { + self.declarations.insert((self.get(node), node.name.as_str()), node); + } + + pub fn declaration(&self, node: &PrivateIdentifier<'a>) -> Option<&'a PrivateIdentifier<'a>> { + self.declarations.get(&(self.get(node), node.name.as_str())).copied() + } +} + +impl<'a> Visit<'a> for PrivateEnvs<'a> { + fn visit_class(&mut self, node: &Class<'a>) { + self.next_id += 1; + let id = self.next_id; + let declared = node + .body + .body + .iter() + .filter_map(|element| match element.property_key() { + Some(PropertyKey::PrivateIdentifier(key)) => Some(key.name.as_str()), + _ => None, + }) + .collect(); + + self.stack.push((id, declared)); + walk::walk_class(self, node); + self.stack.pop(); + } + + fn visit_private_identifier(&mut self, node: &PrivateIdentifier<'a>) { + let env = self + .stack + .iter() + .rev() + .find(|(_, declared)| declared.contains(&node.name.as_str())) + .or_else(|| self.stack.last()) + .map_or(0, |(id, _)| *id); + self.nodes.insert(std::ptr::from_ref(node), env); + } +} diff --git a/crates/jsshaker/src/value/escaped.rs b/crates/jsshaker/src/value/escaped.rs index 6686cd79..a596e6cc 100644 --- a/crates/jsshaker/src/value/escaped.rs +++ b/crates/jsshaker/src/value/escaped.rs @@ -31,6 +31,21 @@ pub fn set_property<'a>( analyzer.include((dep, key, value)); } +pub const PRIMITIVE_SET_PROPERTY_ERROR: &str = "Cannot create property on a primitive value"; + +pub fn set_property_on_primitive<'a>( + analyzer: &mut Analyzer<'a>, + message: &str, + dep: Dep<'a>, + key: Entity<'a>, + value: Entity<'a>, +) { + analyzer.throw_builtin_error(message); + if analyzer.config.preserve_exceptions { + set_property(analyzer, dep, key, value); + } +} + pub fn enumerate_properties<'a>( target: Value<'a>, analyzer: &mut Analyzer<'a>, diff --git a/crates/jsshaker/src/value/function/mod.rs b/crates/jsshaker/src/value/function/mod.rs index 969d3983..4003ff8a 100644 --- a/crates/jsshaker/src/value/function/mod.rs +++ b/crates/jsshaker/src/value/function/mod.rs @@ -171,7 +171,7 @@ impl<'a> ValueTrait<'a> for FunctionValue<'a> { fn get_keys( &'a self, - analyzer: &Analyzer<'a>, + analyzer: &mut Analyzer<'a>, check_proto: bool, ) -> Option)>> { self.statics.get_keys(analyzer, check_proto) diff --git a/crates/jsshaker/src/value/literal/mod.rs b/crates/jsshaker/src/value/literal/mod.rs index aeb22969..d130b789 100644 --- a/crates/jsshaker/src/value/literal/mod.rs +++ b/crates/jsshaker/src/value/literal/mod.rs @@ -88,14 +88,12 @@ impl<'a> ValueTrait<'a> for LiteralValue<'a> { key: Entity<'a>, value: Entity<'a>, ) { - if matches!(self, LiteralValue::Null | LiteralValue::Undefined) { - analyzer.throw_builtin_error("Cannot set property of null or undefined"); - if analyzer.config.preserve_exceptions { - escaped::set_property(analyzer, dep, key, value) - } + let message = if matches!(self, LiteralValue::Null | LiteralValue::Undefined) { + "Cannot set property of null or undefined" } else { - // No effect - } + escaped::PRIMITIVE_SET_PROPERTY_ERROR + }; + escaped::set_property_on_primitive(analyzer, message, dep, key, value); } fn enumerate_properties( @@ -186,10 +184,7 @@ impl<'a> ValueTrait<'a> for LiteralValue<'a> { analyzer .factory - .alloc(LiteralValue::String( - self.to_string(analyzer.allocator), - if let LiteralValue::String(_, Some(atom)) = self { Some(*atom) } else { None }, - )) + .alloc(LiteralValue::String(self.to_string(analyzer.allocator), self.mangle_atom())) .into() } @@ -251,7 +246,7 @@ impl<'a> ValueTrait<'a> for LiteralValue<'a> { fn get_keys( &self, - _analyzer: &Analyzer<'a>, + _analyzer: &mut Analyzer<'a>, _check_proto: bool, ) -> Option)>> { match self { @@ -322,6 +317,13 @@ impl<'a> ValueTrait<'a> for LiteralValue<'a> { } impl<'a> LiteralValue<'a> { + pub fn mangle_atom(self) -> Option { + match self { + LiteralValue::String(_, atom) => atom, + _ => None, + } + } + pub fn build_expr( &self, transformer: &Transformer<'a>, @@ -448,13 +450,7 @@ impl<'a> From> for PropertyKeyValue<'a> { impl<'a> From> for (PropertyKeyValue<'a>, Option) { fn from(val: LiteralValue<'a>) -> Self { - ( - val.into(), - match val { - LiteralValue::String(_, m) => m, - _ => None, - }, - ) + (val.into(), val.mangle_atom()) } } diff --git a/crates/jsshaker/src/value/literal/string.rs b/crates/jsshaker/src/value/literal/string.rs index ff2507f6..75a5801c 100644 --- a/crates/jsshaker/src/value/literal/string.rs +++ b/crates/jsshaker/src/value/literal/string.rs @@ -72,12 +72,18 @@ impl<'a> ValueTrait<'a> for Atom<'a> { fn set_property( &'a self, - _analyzer: &mut Analyzer<'a>, - _dep: Dep<'a>, - _key: Entity<'a>, - _value: Entity<'a>, + analyzer: &mut Analyzer<'a>, + dep: Dep<'a>, + key: Entity<'a>, + value: Entity<'a>, ) { - // No effect + escaped::set_property_on_primitive( + analyzer, + escaped::PRIMITIVE_SET_PROPERTY_ERROR, + dep, + key, + value, + ); } fn enumerate_properties( @@ -194,7 +200,7 @@ impl<'a> ValueTrait<'a> for Atom<'a> { fn get_keys( &self, - _analyzer: &Analyzer<'a>, + _analyzer: &mut Analyzer<'a>, _check_proto: bool, ) -> Option)>> { None diff --git a/crates/jsshaker/src/value/logical_result.rs b/crates/jsshaker/src/value/logical_result.rs index 0a49b67a..4a423ff2 100644 --- a/crates/jsshaker/src/value/logical_result.rs +++ b/crates/jsshaker/src/value/logical_result.rs @@ -126,7 +126,7 @@ impl<'a> ValueTrait<'a> for LogicalResultValue<'a> { fn get_keys( &'a self, - analyzer: &Analyzer<'a>, + analyzer: &mut Analyzer<'a>, check_proto: bool, ) -> Option)>> { self.value.get_keys(analyzer, check_proto) diff --git a/crates/jsshaker/src/value/mod.rs b/crates/jsshaker/src/value/mod.rs index 1b457696..b2a067f5 100644 --- a/crates/jsshaker/src/value/mod.rs +++ b/crates/jsshaker/src/value/mod.rs @@ -137,7 +137,7 @@ pub trait ValueTrait<'a>: Debug { /// Returns vec![(definite, key)] fn get_keys( &'a self, - _analyzer: &Analyzer<'a>, + _analyzer: &mut Analyzer<'a>, _check_proto: bool, ) -> Option)>> { None diff --git a/crates/jsshaker/src/value/module_object.rs b/crates/jsshaker/src/value/module_object.rs index fc8e2938..0639dc9c 100644 --- a/crates/jsshaker/src/value/module_object.rs +++ b/crates/jsshaker/src/value/module_object.rs @@ -82,6 +82,7 @@ impl<'a> ValueTrait<'a> for ModuleObjectValue { analyzer: &mut Analyzer<'a>, dep: Dep<'a>, ) -> EnumeratedProperties<'a> { + self.include(analyzer); // TODO: Optimize this escaped::enumerate_properties(self, analyzer, dep) } @@ -149,9 +150,10 @@ impl<'a> ValueTrait<'a> for ModuleObjectValue { fn get_keys( &'a self, - analyzer: &Analyzer<'a>, + analyzer: &mut Analyzer<'a>, _check_proto: bool, ) -> Option)>> { + self.include(analyzer); if analyzer.does_module_reexport_unknown(self.module, &mut Default::default()) { return None; } diff --git a/crates/jsshaker/src/value/never.rs b/crates/jsshaker/src/value/never.rs index 8b12a21b..8ec6254a 100644 --- a/crates/jsshaker/src/value/never.rs +++ b/crates/jsshaker/src/value/never.rs @@ -93,7 +93,7 @@ impl<'a> ValueTrait<'a> for NeverValue { } fn get_keys( &'a self, - _analyzer: &Analyzer<'a>, + _analyzer: &mut Analyzer<'a>, _check_proto: bool, ) -> Option)>> { Some(vec![]) diff --git a/crates/jsshaker/src/value/object/mod.rs b/crates/jsshaker/src/value/object/mod.rs index 0ff1c941..18aeb191 100644 --- a/crates/jsshaker/src/value/object/mod.rs +++ b/crates/jsshaker/src/value/object/mod.rs @@ -222,7 +222,7 @@ impl<'a> ValueTrait<'a> for ObjectValue<'a> { fn get_keys( &'a self, - analyzer: &Analyzer<'a>, + analyzer: &mut Analyzer<'a>, check_proto: bool, ) -> Option)>> { if self.included.get() { diff --git a/crates/jsshaker/src/value/primitive.rs b/crates/jsshaker/src/value/primitive.rs index b4308439..2156a082 100644 --- a/crates/jsshaker/src/value/primitive.rs +++ b/crates/jsshaker/src/value/primitive.rs @@ -46,12 +46,18 @@ impl<'a> ValueTrait<'a> for PrimitiveValue { fn set_property( &'a self, - _analyzer: &mut Analyzer<'a>, - _dep: Dep<'a>, - _key: Entity<'a>, - _value: Entity<'a>, + analyzer: &mut Analyzer<'a>, + dep: Dep<'a>, + key: Entity<'a>, + value: Entity<'a>, ) { - // No effect + escaped::set_property_on_primitive( + analyzer, + escaped::PRIMITIVE_SET_PROPERTY_ERROR, + dep, + key, + value, + ); } fn enumerate_properties( @@ -153,7 +159,7 @@ impl<'a> ValueTrait<'a> for PrimitiveValue { } fn get_keys( &'a self, - _analyzer: &Analyzer<'a>, + _analyzer: &mut Analyzer<'a>, _check_proto: bool, ) -> Option)>> { if self.maybe_string() { None } else { Some(vec![]) } diff --git a/crates/jsshaker/src/value/union.rs b/crates/jsshaker/src/value/union.rs index 8eb41979..6bf73215 100644 --- a/crates/jsshaker/src/value/union.rs +++ b/crates/jsshaker/src/value/union.rs @@ -292,7 +292,7 @@ impl<'a, V: UnionValues<'a> + Debug + 'a> ValueTrait<'a> for UnionValue<'a, V> { fn get_keys( &'a self, - analyzer: &Analyzer<'a>, + analyzer: &mut Analyzer<'a>, check_proto: bool, ) -> Option)>> { let mut result = Vec::new(); diff --git a/crates/jsshaker/tests/fixtures/chain_expression.js b/crates/jsshaker/tests/fixtures/chain_expression.js index e639e52e..a422d0df 100644 --- a/crates/jsshaker/tests/fixtures/chain_expression.js +++ b/crates/jsshaker/tests/fixtures/chain_expression.js @@ -4,3 +4,9 @@ export function main(unknown) { let a = { b: unknown }; effect(a?.b?.c, a.c?.d, a.c?.() + 1); } + +export async function short_circuit(effectful) { + const nullish = undefined; + await nullish?.a; + await (effectful(), nullish)?.b; +} diff --git a/crates/jsshaker/tests/fixtures/class/private_brand_unused.js b/crates/jsshaker/tests/fixtures/class/private_brand_unused.js new file mode 100644 index 00000000..8660e567 --- /dev/null +++ b/crates/jsshaker/tests/fixtures/class/private_brand_unused.js @@ -0,0 +1,12 @@ +const results = []; + +class Ghost { + #tag = 1; + static has(o) { + return #tag in o; + } +} + +results.push(Ghost.has({})); + +export default results; diff --git a/crates/jsshaker/tests/fixtures/class/private_inherit.js b/crates/jsshaker/tests/fixtures/class/private_inherit.js new file mode 100644 index 00000000..0404916c --- /dev/null +++ b/crates/jsshaker/tests/fixtures/class/private_inherit.js @@ -0,0 +1,33 @@ +const results = []; + +class Base { + #id; + constructor(id) { + this.#id = id; + } + getId() { + return this.#id; + } +} + +class Derived extends Base { + #id = "derived"; + constructor() { + super("base"); + } + both() { + return [super.getId(), this.#id]; + } +} + +const d = new Derived(); +results.push(d.getId()); +results.push(...d.both()); + +try { + Base.prototype.getId.call({}); +} catch (e) { + results.push(e.constructor.name); +} + +export default results; diff --git a/crates/jsshaker/tests/fixtures/class/private_shadow.js b/crates/jsshaker/tests/fixtures/class/private_shadow.js new file mode 100644 index 00000000..e1f74d84 --- /dev/null +++ b/crates/jsshaker/tests/fixtures/class/private_shadow.js @@ -0,0 +1,40 @@ +const results = []; + +class Outer { + #x = "outer"; + + readSelf() { + return this.#x; + } + + makeShadow() { + class Inner { + #x = "inner"; + read() { + return this.#x; + } + } + return new Inner().read(); + } + + makeReader() { + class Reader { + readFrom(o) { + return o.#x; + } + } + return new Reader().readFrom(this); + } +} + +class Other { + #x = "other"; + read() { + return this.#x; + } +} + +const o = new Outer(); +results.push(o.readSelf(), o.makeShadow(), o.makeReader(), new Other().read()); + +export default results; diff --git a/crates/jsshaker/tests/fixtures/class/super_static.js b/crates/jsshaker/tests/fixtures/class/super_static.js new file mode 100644 index 00000000..ae6ee92c --- /dev/null +++ b/crates/jsshaker/tests/fixtures/class/super_static.js @@ -0,0 +1,21 @@ +class Base { + static tag = "static-base"; + static who() { + return "static " + this.tag; + } + who() { + return "instance"; + } +} +class Child extends Base { + static tag = "static-child"; + static viaStaticMethod() { + return super.who(); + } + static viaStaticBlockResult = (() => Base.tag)(); + viaInstanceMethod() { + return super.who(); + } +} +console.log(Child.viaStaticMethod()); +console.log(new Child().viaInstanceMethod()); diff --git a/crates/jsshaker/tests/fixtures/define_property.js b/crates/jsshaker/tests/fixtures/define_property.js new file mode 100644 index 00000000..e6edab02 --- /dev/null +++ b/crates/jsshaker/tests/fixtures/define_property.js @@ -0,0 +1,25 @@ +const o = {}; +let backing = 0; +Object.defineProperty(o, "p", { + get() { + return backing * 2; + }, + set(v) { + backing = v + 1; + }, + enumerable: true, + configurable: true, +}); +o.p = 4; +console.log(o.p, backing, JSON.stringify(Object.keys(o))); + +const ro = { z: 1 }; +Object.defineProperty(ro, "hidden", { + get() { + return "H"; + }, +}); +console.log(ro.hidden, JSON.stringify(Object.keys(ro)), JSON.stringify(ro)); + +const d = Object.getOwnPropertyDescriptor(o, "p"); +console.log(typeof d.get, typeof d.set, d.enumerable, d.configurable); diff --git a/crates/jsshaker/tests/mod.rs b/crates/jsshaker/tests/mod.rs index eb267f3d..ad4bf8b6 100644 --- a/crates/jsshaker/tests/mod.rs +++ b/crates/jsshaker/tests/mod.rs @@ -1,29 +1,42 @@ -use std::fs; +use std::{collections::HashMap, fs, path::Path}; use insta::{assert_snapshot, glob}; -use jsshaker::{JsShakerOptions, TreeShakeConfig, tree_shake, vfs::SingleFileFs}; +use jsshaker::{ + JsShakerOptions, TreeShakeConfig, tree_shake, + vfs::{MultiModuleFs, SingleFileFs, normalize_path::normalize_str}, +}; use oxc::{ codegen::{CodegenOptions, CommentOptions}, minifier::MinifierOptions, }; +fn config_for(input: &str) -> TreeShakeConfig { + let mut config = TreeShakeConfig::recommended(); + if input.contains("@react-jsx") { + config.jsx = jsshaker::TreeShakeJsxPreset::React; + } + config.unknown_global_side_effects = true; + config.advanced = true; + config +} + +fn minify_options(input: &str) -> Option { + input.contains("@minify").then(|| MinifierOptions { mangle: None, ..Default::default() }) +} + +fn codegen_options() -> CodegenOptions { + CodegenOptions { comments: CommentOptions::default(), ..Default::default() } +} + fn do_tree_shake(input: String) -> String { - let do_minify = input.contains("@minify"); - let react_jsx = input.contains("@react-jsx"); + let config = config_for(&input); + let minify_options = minify_options(&input); let result = tree_shake( JsShakerOptions { vfs: SingleFileFs(input), - config: { - let mut config = TreeShakeConfig::recommended(); - if react_jsx { - config.jsx = jsshaker::TreeShakeJsxPreset::React; - } - config.unknown_global_side_effects = true; - config.advanced = true; - config - }, - minify_options: do_minify.then(|| MinifierOptions { mangle: None, ..Default::default() }), - codegen_options: CodegenOptions { comments: CommentOptions::default(), ..Default::default() }, + config, + minify_options, + codegen_options: codegen_options(), source_map: false, }, SingleFileFs::ENTRY_PATH.to_string(), @@ -42,3 +55,62 @@ fn test() { }) }); } + +fn collect_modules(dir: &Path, prefix: &str, files: &mut HashMap) { + let mut entries: Vec<_> = fs::read_dir(dir).unwrap().map(|entry| entry.unwrap()).collect(); + entries.sort_by_key(|entry| entry.file_name()); + for entry in entries { + let name = entry.file_name().to_string_lossy().into_owned(); + let path = entry.path(); + if path.is_dir() { + collect_modules(&path, &format!("{prefix}{name}/"), files); + } else if path.extension().is_some_and(|ext| ext == "js" || ext == "mjs" || ext == "cjs") { + let key = normalize_str(&format!("/{prefix}{name}")); + files.insert(key, fs::read_to_string(&path).unwrap()); + } + } +} + +fn do_tree_shake_modules(dir: &Path) -> String { + let mut files = HashMap::new(); + collect_modules(dir, "", &mut files); + + let entry = normalize_str("/entry.js"); + assert!(files.contains_key(&entry), "module fixture {} must contain an entry.js", dir.display()); + + let markers = files.values().map(String::as_str).collect::>().join("\n"); + + let result = tree_shake( + JsShakerOptions { + config: config_for(&markers), + minify_options: minify_options(&markers), + vfs: MultiModuleFs(files), + codegen_options: codegen_options(), + source_map: false, + }, + entry.clone(), + ); + + let mut modules: Vec<_> = result.codegen_return.iter().collect(); + modules.sort_by_key(|(path, _)| (**path != entry, *path)); + + let mut output = String::new(); + for (path, codegen) in modules { + output.push_str(&format!("//#region {}\n", path.replace('\\', "/"))); + output.push_str(codegen.code.trim_end()); + output.push_str("\n\n"); + } + output.trim_end().to_string() +} + +#[test] +fn module_test() { + glob!("module_fixtures/*/entry.js", |path| { + let dir = path.parent().unwrap(); + let mut settings = insta::Settings::clone_current(); + settings.set_prepend_module_to_snapshot(false); + settings.bind(|| { + assert_snapshot!(do_tree_shake_modules(dir)); + }) + }); +} diff --git a/crates/jsshaker/tests/module_fixtures/async_sequential/entry.js b/crates/jsshaker/tests/module_fixtures/async_sequential/entry.js new file mode 100644 index 00000000..414732dd --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/async_sequential/entry.js @@ -0,0 +1,6 @@ +import { order, mark, run } from "./m.js"; + +Promise.resolve().then(() => mark("X")); +Promise.resolve().then(() => mark("Y")); +await run(); +console.log(order); diff --git a/crates/jsshaker/tests/module_fixtures/async_sequential/m.js b/crates/jsshaker/tests/module_fixtures/async_sequential/m.js new file mode 100644 index 00000000..773b5464 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/async_sequential/m.js @@ -0,0 +1,11 @@ +export let order = ""; +export function mark(s) { + order += s; +} +export async function run() { + order += "a"; + await null; + order += "b"; + await null; + order += "c"; +} diff --git a/crates/jsshaker/tests/module_fixtures/async_try_catch/entry.js b/crates/jsshaker/tests/module_fixtures/async_try_catch/entry.js new file mode 100644 index 00000000..0583e2f2 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/async_try_catch/entry.js @@ -0,0 +1,3 @@ +import { v, run } from "./m.js"; +await run(); +console.log(v); diff --git a/crates/jsshaker/tests/module_fixtures/async_try_catch/m.js b/crates/jsshaker/tests/module_fixtures/async_try_catch/m.js new file mode 100644 index 00000000..3c9388a5 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/async_try_catch/m.js @@ -0,0 +1,10 @@ +export let v = ""; +export async function run() { + try { + await Promise.reject(new Error("x")); + } catch { + v = "caught"; + } finally { + v += "|done"; + } +} diff --git a/crates/jsshaker/tests/module_fixtures/cf_constructs/entry.js b/crates/jsshaker/tests/module_fixtures/cf_constructs/entry.js new file mode 100644 index 00000000..28d0db93 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/cf_constructs/entry.js @@ -0,0 +1,3 @@ +import { log, a, b, run } from "./m.js"; +run(); +console.log(log, a, b); diff --git a/crates/jsshaker/tests/module_fixtures/cf_constructs/m.js b/crates/jsshaker/tests/module_fixtures/cf_constructs/m.js new file mode 100644 index 00000000..6764a7b1 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/cf_constructs/m.js @@ -0,0 +1,62 @@ +export let log = ""; +export let a = 0, + b = 0; + +function bail(stop) { + log += "1"; + if (stop) return; + log += "2"; +} + +function pick(n) { + switch (n) { + case 1: + log += "a"; + case 2: + log += "b"; + break; + case 3: + log += "c"; + } +} + +export function run() { + bail(true); + bail(false); + log += ";"; + + log += (0 ? "yes" : "no") + ";"; + + pick(1); + log += ";"; + + for (const k in { a: 1, b: 2 }) log += k; + log += ";"; + + for (const x of [1, 2, 3, 4]) { + if (x === 3) break; + log += x; + } + log += ";"; + + outer: for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { + if (j === 1) continue outer; + log += "" + i + j; + } + } + log += ";"; + + let i = 0; + while (i < 3) { + let j = 0; + while (j < 2) { + log += "w"; + j++; + } + i++; + } + log += ";"; + + a = ((b = 2), 3); +} diff --git a/crates/jsshaker/tests/module_fixtures/cls_inheritance_super/entry.js b/crates/jsshaker/tests/module_fixtures/cls_inheritance_super/entry.js new file mode 100644 index 00000000..600ddf4c --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/cls_inheritance_super/entry.js @@ -0,0 +1,2 @@ +import { Child } from "./m.js"; +console.log(new Child().describe()); diff --git a/crates/jsshaker/tests/module_fixtures/cls_inheritance_super/m.js b/crates/jsshaker/tests/module_fixtures/cls_inheritance_super/m.js new file mode 100644 index 00000000..b89def12 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/cls_inheritance_super/m.js @@ -0,0 +1,17 @@ +class Base { + constructor() { + this.tag = "base"; + } + describe() { + return "I am " + this.tag; + } +} +export class Child extends Base { + constructor() { + super(); + this.tag = "child"; + } + describe() { + return super.describe() + "!"; + } +} diff --git a/crates/jsshaker/tests/module_fixtures/cls_static_block_order/entry.js b/crates/jsshaker/tests/module_fixtures/cls_static_block_order/entry.js new file mode 100644 index 00000000..42e796bf --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/cls_static_block_order/entry.js @@ -0,0 +1,3 @@ +import { log, K } from "./m.js"; +new K(); +console.log(log); diff --git a/crates/jsshaker/tests/module_fixtures/cls_static_block_order/m.js b/crates/jsshaker/tests/module_fixtures/cls_static_block_order/m.js new file mode 100644 index 00000000..03ab9180 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/cls_static_block_order/m.js @@ -0,0 +1,9 @@ +export let log = ""; +export class K { + static { + log += "static;"; + } + constructor() { + log += "ctor;"; + } +} diff --git a/crates/jsshaker/tests/module_fixtures/cls_super_static/entry.js b/crates/jsshaker/tests/module_fixtures/cls_super_static/entry.js new file mode 100644 index 00000000..5c55cc64 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/cls_super_static/entry.js @@ -0,0 +1,2 @@ +import { C } from "./m.js"; +console.log(C.make()); diff --git a/crates/jsshaker/tests/module_fixtures/cls_super_static/m.js b/crates/jsshaker/tests/module_fixtures/cls_super_static/m.js new file mode 100644 index 00000000..6f8ce7fc --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/cls_super_static/m.js @@ -0,0 +1,10 @@ +export class B { + static who() { + return "B"; + } +} +export class C extends B { + static make() { + return super.who(); + } +} diff --git a/crates/jsshaker/tests/module_fixtures/default_export_reassignment/entry.js b/crates/jsshaker/tests/module_fixtures/default_export_reassignment/entry.js new file mode 100644 index 00000000..40d23b0a --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/default_export_reassignment/entry.js @@ -0,0 +1,4 @@ +import impl, { reassign } from "./mod.js"; + +reassign(); +console.log(impl()); diff --git a/crates/jsshaker/tests/module_fixtures/default_export_reassignment/mod.js b/crates/jsshaker/tests/module_fixtures/default_export_reassignment/mod.js new file mode 100644 index 00000000..5e9b5aa4 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/default_export_reassignment/mod.js @@ -0,0 +1,6 @@ +export default function impl() { + return 1; +} +export function reassign() { + impl = () => 2; +} diff --git a/crates/jsshaker/tests/module_fixtures/destructure_targets/entry.js b/crates/jsshaker/tests/module_fixtures/destructure_targets/entry.js new file mode 100644 index 00000000..a125bd4b --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/destructure_targets/entry.js @@ -0,0 +1,3 @@ +import { a, b, c, head, rest, one, others, dyn, f } from "./m.js"; +f(); +console.log(a, b, c, head, JSON.stringify(rest), one, JSON.stringify(others), dyn); diff --git a/crates/jsshaker/tests/module_fixtures/destructure_targets/m.js b/crates/jsshaker/tests/module_fixtures/destructure_targets/m.js new file mode 100644 index 00000000..3f9ed0d0 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/destructure_targets/m.js @@ -0,0 +1,25 @@ +export let a = 1, + b = 2, + c = 0; +export let head = 0, + rest = []; +export let one = 0, + others = {}; +export let dyn = 0; + +const k = "dyn"; + +export function f() { + [a, b] = [b, a]; + [a = 5, c = 6] = [undefined]; + ({ + x: b, + y: { + z: c, + w: [head], + }, + } = { x: 1, y: { z: 2, w: [3] } }); + [head, ...rest] = [1, 2, 3]; + ({ one, ...others } = { one: 1, two: 2, three: 3 }); + ({ [k]: dyn } = { dyn: 42 }); +} diff --git a/crates/jsshaker/tests/module_fixtures/exc_flow/entry.js b/crates/jsshaker/tests/module_fixtures/exc_flow/entry.js new file mode 100644 index 00000000..f3b58ace --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/exc_flow/entry.js @@ -0,0 +1,8 @@ +import { v, count, rethrow, loop } from "./m.js"; +try { + rethrow(); +} catch (e) { + console.log("caught", e.message); +} +loop(); +console.log(v, count); diff --git a/crates/jsshaker/tests/module_fixtures/exc_flow/m.js b/crates/jsshaker/tests/module_fixtures/exc_flow/m.js new file mode 100644 index 00000000..2354e0d6 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/exc_flow/m.js @@ -0,0 +1,25 @@ +export let v = ""; +export let count = 0; + +export function rethrow() { + try { + v = "try"; + throw new Error("boom"); + } catch (e) { + v += "|catch"; + throw e; + } finally { + v += "|finally"; + } +} + +export function loop() { + for (let i = 0; i < 5; i++) { + try { + if (i % 2) throw new Error(); + count += 10; + } catch { + count += 1; + } + } +} diff --git a/crates/jsshaker/tests/module_fixtures/fn_getter_setter/entry.js b/crates/jsshaker/tests/module_fixtures/fn_getter_setter/entry.js new file mode 100644 index 00000000..2e659bc1 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/fn_getter_setter/entry.js @@ -0,0 +1,3 @@ +import { obj, backing } from "./m.js"; +obj.val = 5; +console.log(obj.val, backing); diff --git a/crates/jsshaker/tests/module_fixtures/fn_getter_setter/m.js b/crates/jsshaker/tests/module_fixtures/fn_getter_setter/m.js new file mode 100644 index 00000000..caf44963 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/fn_getter_setter/m.js @@ -0,0 +1,9 @@ +export let backing = 0; +export const obj = { + get val() { + return backing * 2; + }, + set val(v) { + backing = v; + }, +}; diff --git a/crates/jsshaker/tests/module_fixtures/fn_param_forms/entry.js b/crates/jsshaker/tests/module_fixtures/fn_param_forms/entry.js new file mode 100644 index 00000000..6692453a --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/fn_param_forms/entry.js @@ -0,0 +1,7 @@ +import { v, total, n, withDefault, sum, viaArguments, apply } from "./m.js"; +withDefault(); +withDefault(9); +sum(1, 2, 3); +viaArguments(1, 2, 3); +apply((x) => x * 3); +console.log(v, total, n); diff --git a/crates/jsshaker/tests/module_fixtures/fn_param_forms/m.js b/crates/jsshaker/tests/module_fixtures/fn_param_forms/m.js new file mode 100644 index 00000000..9295397b --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/fn_param_forms/m.js @@ -0,0 +1,16 @@ +export let v = ""; +export let total = 0; +export let n = 2; + +export function withDefault(x = 5) { + v += x; +} +export function sum(...xs) { + for (const x of xs) total += x; +} +export function viaArguments() { + total += arguments.length + arguments[2]; +} +export function apply(fn) { + n = fn(n); +} diff --git a/crates/jsshaker/tests/module_fixtures/fn_reassign_then_call/entry.js b/crates/jsshaker/tests/module_fixtures/fn_reassign_then_call/entry.js new file mode 100644 index 00000000..cecc6b1e --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/fn_reassign_then_call/entry.js @@ -0,0 +1,5 @@ +import { impl, swap, call } from "./m.js"; +console.log(call()); +swap(); +console.log(call()); +console.log(impl()); diff --git a/crates/jsshaker/tests/module_fixtures/fn_reassign_then_call/m.js b/crates/jsshaker/tests/module_fixtures/fn_reassign_then_call/m.js new file mode 100644 index 00000000..c21d03cc --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/fn_reassign_then_call/m.js @@ -0,0 +1,7 @@ +export let impl = () => "first"; +export function swap() { + impl = () => "second"; +} +export function call() { + return impl(); +} diff --git a/crates/jsshaker/tests/module_fixtures/fn_recursion/entry.js b/crates/jsshaker/tests/module_fixtures/fn_recursion/entry.js new file mode 100644 index 00000000..f6755f60 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/fn_recursion/entry.js @@ -0,0 +1,2 @@ +import { calls, fact } from "./m.js"; +console.log(fact(5), calls); diff --git a/crates/jsshaker/tests/module_fixtures/fn_recursion/m.js b/crates/jsshaker/tests/module_fixtures/fn_recursion/m.js new file mode 100644 index 00000000..b528687f --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/fn_recursion/m.js @@ -0,0 +1,5 @@ +export let calls = 0; +export function fact(n) { + calls++; + return n <= 1 ? 1 : n * fact(n - 1); +} diff --git a/crates/jsshaker/tests/module_fixtures/gen_early_return/entry.js b/crates/jsshaker/tests/module_fixtures/gen_early_return/entry.js new file mode 100644 index 00000000..5695b682 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/gen_early_return/entry.js @@ -0,0 +1,5 @@ +import { state, gen } from "./m.js"; +const it = gen(); +it.next(); +it.return(); +console.log(state); diff --git a/crates/jsshaker/tests/module_fixtures/gen_early_return/m.js b/crates/jsshaker/tests/module_fixtures/gen_early_return/m.js new file mode 100644 index 00000000..7a19b3d8 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/gen_early_return/m.js @@ -0,0 +1,10 @@ +export let state = ""; +export function* gen() { + try { + state += "start;"; + yield 1; + state += "never;"; + } finally { + state += "cleanup;"; + } +} diff --git a/crates/jsshaker/tests/module_fixtures/live_binding/entry.js b/crates/jsshaker/tests/module_fixtures/live_binding/entry.js new file mode 100644 index 00000000..8c43a11f --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/live_binding/entry.js @@ -0,0 +1,6 @@ +import { counter, increment, $window } from "./state.js"; + +increment(); +increment(); +console.log(counter); +console.log($window); diff --git a/crates/jsshaker/tests/module_fixtures/live_binding/state.js b/crates/jsshaker/tests/module_fixtures/live_binding/state.js new file mode 100644 index 00000000..d21a1de5 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/live_binding/state.js @@ -0,0 +1,12 @@ +export let counter = 0; +export function increment() { + counter++; +} +export function neverCalled() { + counter = 999; +} + +export let $window; +export function init() { + $window = window; +} diff --git a/crates/jsshaker/tests/module_fixtures/misc_binding_reads/entry.js b/crates/jsshaker/tests/module_fixtures/misc_binding_reads/entry.js new file mode 100644 index 00000000..97d3a303 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/misc_binding_reads/entry.js @@ -0,0 +1,4 @@ +import { v, count, data, obj, run } from "./m.js"; +console.log(typeof v, `hello ${v}`); +run(); +console.log(typeof v, count, JSON.stringify(data), obj, obj === null); diff --git a/crates/jsshaker/tests/module_fixtures/misc_binding_reads/m.js b/crates/jsshaker/tests/module_fixtures/misc_binding_reads/m.js new file mode 100644 index 00000000..e9762e2f --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/misc_binding_reads/m.js @@ -0,0 +1,15 @@ +export let v = "str"; +export let count = 0; +export let data = null; +export let obj = { a: 1 }; + +export function run() { + count = 1; + count = 2; + count = 3; + count = count + 1; + + data = JSON.parse('{"k":[1,2]}'); + obj = null; + v = undefined; +} diff --git a/crates/jsshaker/tests/module_fixtures/mod_default_class_reassign/entry.js b/crates/jsshaker/tests/module_fixtures/mod_default_class_reassign/entry.js new file mode 100644 index 00000000..12045930 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_default_class_reassign/entry.js @@ -0,0 +1,4 @@ +import D, { bump } from "./m.js"; +console.log(typeof D); +bump(); +console.log(typeof D, D); diff --git a/crates/jsshaker/tests/module_fixtures/mod_default_class_reassign/m.js b/crates/jsshaker/tests/module_fixtures/mod_default_class_reassign/m.js new file mode 100644 index 00000000..64c14511 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_default_class_reassign/m.js @@ -0,0 +1,6 @@ +export default class D { + static tag = "cls"; +} +export function bump() { + D = 42; +} diff --git a/crates/jsshaker/tests/module_fixtures/mod_diamond/entry.js b/crates/jsshaker/tests/module_fixtures/mod_diamond/entry.js new file mode 100644 index 00000000..7cf8001d --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_diamond/entry.js @@ -0,0 +1,3 @@ +import { bump, readL, readR } from "./l.js"; +bump(); +console.log(readL(), readR()); diff --git a/crates/jsshaker/tests/module_fixtures/mod_diamond/l.js b/crates/jsshaker/tests/module_fixtures/mod_diamond/l.js new file mode 100644 index 00000000..4fff0123 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_diamond/l.js @@ -0,0 +1,2 @@ +export { bump, readL } from "./left.js"; +export { readR } from "./right.js"; diff --git a/crates/jsshaker/tests/module_fixtures/mod_diamond/left.js b/crates/jsshaker/tests/module_fixtures/mod_diamond/left.js new file mode 100644 index 00000000..89ac76d7 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_diamond/left.js @@ -0,0 +1,5 @@ +import { n } from "./shared.js"; +export { bump } from "./shared.js"; +export function readL() { + return n; +} diff --git a/crates/jsshaker/tests/module_fixtures/mod_diamond/right.js b/crates/jsshaker/tests/module_fixtures/mod_diamond/right.js new file mode 100644 index 00000000..e7b56be0 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_diamond/right.js @@ -0,0 +1,4 @@ +import { n } from "./shared.js"; +export function readR() { + return n; +} diff --git a/crates/jsshaker/tests/module_fixtures/mod_diamond/shared.js b/crates/jsshaker/tests/module_fixtures/mod_diamond/shared.js new file mode 100644 index 00000000..0359058c --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_diamond/shared.js @@ -0,0 +1,4 @@ +export let n = 1; +export function bump() { + n = 99; +} diff --git a/crates/jsshaker/tests/module_fixtures/mod_dynamic_import/entry.js b/crates/jsshaker/tests/module_fixtures/mod_dynamic_import/entry.js new file mode 100644 index 00000000..3e75e3c6 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_dynamic_import/entry.js @@ -0,0 +1,2 @@ +import("./side.js"); +export const x = 1; diff --git a/crates/jsshaker/tests/module_fixtures/mod_dynamic_import/side.js b/crates/jsshaker/tests/module_fixtures/mod_dynamic_import/side.js new file mode 100644 index 00000000..f7b13881 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_dynamic_import/side.js @@ -0,0 +1,2 @@ +console.log("side"); +export const y = 2; diff --git a/crates/jsshaker/tests/module_fixtures/mod_import_order/b.js b/crates/jsshaker/tests/module_fixtures/mod_import_order/b.js new file mode 100644 index 00000000..ba21b064 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_import_order/b.js @@ -0,0 +1,2 @@ +import { add } from "./c.js"; +add("b;"); diff --git a/crates/jsshaker/tests/module_fixtures/mod_import_order/c.js b/crates/jsshaker/tests/module_fixtures/mod_import_order/c.js new file mode 100644 index 00000000..ba446de9 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_import_order/c.js @@ -0,0 +1,4 @@ +export let log = ""; +export function add(s) { + log += s; +} diff --git a/crates/jsshaker/tests/module_fixtures/mod_import_order/entry.js b/crates/jsshaker/tests/module_fixtures/mod_import_order/entry.js new file mode 100644 index 00000000..6dff65f8 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_import_order/entry.js @@ -0,0 +1,3 @@ +import { log } from "./c.js"; +import "./b.js"; +console.log(log); diff --git a/crates/jsshaker/tests/module_fixtures/mod_reexport_cycle/a.js b/crates/jsshaker/tests/module_fixtures/mod_reexport_cycle/a.js new file mode 100644 index 00000000..a98c49ea --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_reexport_cycle/a.js @@ -0,0 +1,2 @@ +export { x } from "./b.js"; +export const y = 1; diff --git a/crates/jsshaker/tests/module_fixtures/mod_reexport_cycle/b.js b/crates/jsshaker/tests/module_fixtures/mod_reexport_cycle/b.js new file mode 100644 index 00000000..e5c11ef0 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_reexport_cycle/b.js @@ -0,0 +1 @@ +export { x } from "./a.js"; diff --git a/crates/jsshaker/tests/module_fixtures/mod_reexport_cycle/entry.js b/crates/jsshaker/tests/module_fixtures/mod_reexport_cycle/entry.js new file mode 100644 index 00000000..5233a3ca --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_reexport_cycle/entry.js @@ -0,0 +1,2 @@ +import { x, y } from "./a.js"; +console.log(x, y); diff --git a/crates/jsshaker/tests/module_fixtures/mod_reexport_default_bare/entry.js b/crates/jsshaker/tests/module_fixtures/mod_reexport_default_bare/entry.js new file mode 100644 index 00000000..7acbc099 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_reexport_default_bare/entry.js @@ -0,0 +1,2 @@ +import d from "./mid.js"; +console.log(d()); diff --git a/crates/jsshaker/tests/module_fixtures/mod_reexport_default_bare/mid.js b/crates/jsshaker/tests/module_fixtures/mod_reexport_default_bare/mid.js new file mode 100644 index 00000000..1c1047e3 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_reexport_default_bare/mid.js @@ -0,0 +1 @@ +export { default } from "./src.js"; diff --git a/crates/jsshaker/tests/module_fixtures/mod_reexport_default_bare/src.js b/crates/jsshaker/tests/module_fixtures/mod_reexport_default_bare/src.js new file mode 100644 index 00000000..300a1a6d --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/mod_reexport_default_bare/src.js @@ -0,0 +1,3 @@ +export default function () { + return "passthrough"; +} diff --git a/crates/jsshaker/tests/module_fixtures/ns_object_keys/entry.js b/crates/jsshaker/tests/module_fixtures/ns_object_keys/entry.js new file mode 100644 index 00000000..7cea6fbb --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/ns_object_keys/entry.js @@ -0,0 +1,2 @@ +import * as ns from "./m.js"; +console.log(Object.keys(ns).sort().join(",")); diff --git a/crates/jsshaker/tests/module_fixtures/ns_object_keys/m.js b/crates/jsshaker/tests/module_fixtures/ns_object_keys/m.js new file mode 100644 index 00000000..54c6af45 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/ns_object_keys/m.js @@ -0,0 +1,3 @@ +export const a = 1; +export const b = 2; +export function c() {} diff --git a/crates/jsshaker/tests/module_fixtures/ns_spread/entry.js b/crates/jsshaker/tests/module_fixtures/ns_spread/entry.js new file mode 100644 index 00000000..77291531 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/ns_spread/entry.js @@ -0,0 +1,3 @@ +import * as ns from "./m.js"; +const copy = { ...ns }; +console.log(copy.a, copy.b); diff --git a/crates/jsshaker/tests/module_fixtures/ns_spread/m.js b/crates/jsshaker/tests/module_fixtures/ns_spread/m.js new file mode 100644 index 00000000..72ab60e1 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/ns_spread/m.js @@ -0,0 +1,2 @@ +export const a = 1; +export const b = 2; diff --git a/crates/jsshaker/tests/module_fixtures/obj_getter_side_effect/entry.js b/crates/jsshaker/tests/module_fixtures/obj_getter_side_effect/entry.js new file mode 100644 index 00000000..586d6b8b --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/obj_getter_side_effect/entry.js @@ -0,0 +1,4 @@ +import { hits, o } from "./m.js"; +o.val; +o.val; +console.log(hits); diff --git a/crates/jsshaker/tests/module_fixtures/obj_getter_side_effect/m.js b/crates/jsshaker/tests/module_fixtures/obj_getter_side_effect/m.js new file mode 100644 index 00000000..6fc2dae8 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/obj_getter_side_effect/m.js @@ -0,0 +1,7 @@ +export let hits = 0; +export const o = { + get val() { + hits++; + return 1; + }, +}; diff --git a/crates/jsshaker/tests/module_fixtures/op_compound_assign/entry.js b/crates/jsshaker/tests/module_fixtures/op_compound_assign/entry.js new file mode 100644 index 00000000..8e4bb69a --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/op_compound_assign/entry.js @@ -0,0 +1,3 @@ +import { n, m, a, b, c, f } from "./m.js"; +f(); +console.log(n, m, a, b, c); diff --git a/crates/jsshaker/tests/module_fixtures/op_compound_assign/m.js b/crates/jsshaker/tests/module_fixtures/op_compound_assign/m.js new file mode 100644 index 00000000..e10c0891 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/op_compound_assign/m.js @@ -0,0 +1,27 @@ +export let n = 10; +export let m = 20; +export let a = null, + b = 1, + c = 0; + +export function f() { + n += 3; + n -= 1; + + m *= 3; + m /= 2; + m %= 7; + + n **= 2; + n &= 0xff; + n |= 1; + n ^= 3; + + n <<= 1; + n >>= 2; + n >>>= 0; + + a ??= "set"; + b &&= 9; + c ||= 7; +} diff --git a/crates/jsshaker/tests/module_fixtures/reexported_binding/entry.js b/crates/jsshaker/tests/module_fixtures/reexported_binding/entry.js new file mode 100644 index 00000000..df65efb4 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/reexported_binding/entry.js @@ -0,0 +1,4 @@ +import { value, bump } from "./middle.js"; + +bump(); +console.log(value); diff --git a/crates/jsshaker/tests/module_fixtures/reexported_binding/middle.js b/crates/jsshaker/tests/module_fixtures/reexported_binding/middle.js new file mode 100644 index 00000000..affdf399 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/reexported_binding/middle.js @@ -0,0 +1 @@ +export { value, bump } from "./source.js"; diff --git a/crates/jsshaker/tests/module_fixtures/reexported_binding/source.js b/crates/jsshaker/tests/module_fixtures/reexported_binding/source.js new file mode 100644 index 00000000..720b88b6 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/reexported_binding/source.js @@ -0,0 +1,4 @@ +export let value = 1; +export function bump() { + value += 1; +} diff --git a/crates/jsshaker/tests/module_fixtures/tdz_circular_throw/a.js b/crates/jsshaker/tests/module_fixtures/tdz_circular_throw/a.js new file mode 100644 index 00000000..bb1ce261 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/tdz_circular_throw/a.js @@ -0,0 +1,3 @@ +import { readEarly } from "./b.js"; +export let x = 1; +console.log(readEarly()); diff --git a/crates/jsshaker/tests/module_fixtures/tdz_circular_throw/b.js b/crates/jsshaker/tests/module_fixtures/tdz_circular_throw/b.js new file mode 100644 index 00000000..0c361fc9 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/tdz_circular_throw/b.js @@ -0,0 +1,4 @@ +import { x } from "./a.js"; +export function readEarly() { + return x; +} diff --git a/crates/jsshaker/tests/module_fixtures/tdz_circular_throw/entry.js b/crates/jsshaker/tests/module_fixtures/tdz_circular_throw/entry.js new file mode 100644 index 00000000..1c125510 --- /dev/null +++ b/crates/jsshaker/tests/module_fixtures/tdz_circular_throw/entry.js @@ -0,0 +1 @@ +import "./a.js"; diff --git a/crates/jsshaker/tests/snapshots/module_test@async_sequential__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@async_sequential__entry.js.snap new file mode 100644 index 00000000..da9ed58e --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@async_sequential__entry.js.snap @@ -0,0 +1,24 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/async_sequential/entry.js +--- +//#region /entry.js +import { order, mark, run } from "./m.js"; +Promise.resolve().then(() => (mark("X"), void 0)); +Promise.resolve().then(() => (mark("Y"), void 0)); +await run(); +console.log(order); + +//#region /m.js +export let order = ""; +export function mark(s) { + order += s; +} +export async function run() { + order += "a"; + await null; + order += "b"; + await null; + order += "c"; +} diff --git a/crates/jsshaker/tests/snapshots/module_test@async_try_catch__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@async_try_catch__entry.js.snap new file mode 100644 index 00000000..e26a05f4 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@async_try_catch__entry.js.snap @@ -0,0 +1,21 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/async_try_catch/entry.js +--- +//#region /entry.js +import { v, run } from "./m.js"; +await run(); +console.log(v); + +//#region /m.js +export let v = ""; +export async function run() { + try { + await Promise.reject(new Error("x")); + } catch { + v = "caught"; + } finally { + v += "|done"; + } +} diff --git a/crates/jsshaker/tests/snapshots/module_test@cf_constructs__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@cf_constructs__entry.js.snap new file mode 100644 index 00000000..83f0bdf5 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@cf_constructs__entry.js.snap @@ -0,0 +1,67 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/cf_constructs/entry.js +--- +//#region /entry.js +import { log, a, b, run } from "./m.js"; +run(); +console.log(log, a, b); + +//#region /m.js +export let log = ""; +export let a = 0, b = 0; +function bail(stop) { + log += "1"; + if (stop) return; + log += "2"; +} +function pick() { + switch (1) { + case 1: + log += "a"; + log += "b"; + break; + } +} +export function run() { + bail(true); + bail(false); + log += ";"; + log += "no;"; + pick(); + log += ";"; + for (const k in { + a: 1, + b: 2 + }) log += k; + log += ";"; + for (const x of [ + 1, + 2, + 3, + , + ]) { + if (x === 3) break; + log += x; + } + log += ";"; + outer: for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { + if (j === 1) continue outer; + log += "" + i + j; + } + } + log += ";"; + let i = 0; + while (i < 3) { + let j = 0; + while (j < 2) { + log += "w"; + j++; + } + i++; + } + log += ";"; + a = (b = 2, 3); +} diff --git a/crates/jsshaker/tests/snapshots/module_test@cls_inheritance_super__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@cls_inheritance_super__entry.js.snap new file mode 100644 index 00000000..cc047069 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@cls_inheritance_super__entry.js.snap @@ -0,0 +1,10 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/cls_inheritance_super/entry.js +--- +//#region /entry.js +import {} from "./m.js"; +console.log("I am child!"); + +//#region /m.js diff --git a/crates/jsshaker/tests/snapshots/module_test@cls_static_block_order__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@cls_static_block_order__entry.js.snap new file mode 100644 index 00000000..7b9ff5c2 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@cls_static_block_order__entry.js.snap @@ -0,0 +1,20 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/cls_static_block_order/entry.js +--- +//#region /entry.js +import { log, K } from "./m.js"; +new K(); +console.log(log); + +//#region /m.js +export let log = ""; +export class K { + static { + log += "static;"; + } + constructor() { + log += "ctor;"; + } +} diff --git a/crates/jsshaker/tests/snapshots/module_test@cls_super_static__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@cls_super_static__entry.js.snap new file mode 100644 index 00000000..fc91ea3e --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@cls_super_static__entry.js.snap @@ -0,0 +1,10 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/cls_super_static/entry.js +--- +//#region /entry.js +import {} from "./m.js"; +console.log("B"); + +//#region /m.js diff --git a/crates/jsshaker/tests/snapshots/module_test@default_export_reassignment__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@default_export_reassignment__entry.js.snap new file mode 100644 index 00000000..871f21de --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@default_export_reassignment__entry.js.snap @@ -0,0 +1,17 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/default_export_reassignment/entry.js +--- +//#region /entry.js +import impl, { reassign } from "./mod.js"; +reassign(); +console.log(impl()); + +//#region /mod.js +export default function impl() { + return 1; +} +export function reassign() { + impl = () => 2; +} diff --git a/crates/jsshaker/tests/snapshots/module_test@destructure_targets__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@destructure_targets__entry.js.snap new file mode 100644 index 00000000..1eee7f19 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@destructure_targets__entry.js.snap @@ -0,0 +1,38 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/destructure_targets/entry.js +--- +//#region /entry.js +import { a, b, c, head, rest, one, others, dyn, f } from "./m.js"; +f(); +console.log(a, b, c, head, JSON.stringify(rest), one, JSON.stringify(others), dyn); + +//#region /m.js +export let a = 1, b = 2, c = 0; +export let head = 0, rest = []; +export let one = 0, others = {}; +export let dyn = 0; +export function f() { + [a, b] = [b, a]; + [a = 5, c = 6] = [void 0]; + ({a: b, b: {a: c, b: [head]}} = { + a: 1, + b: { + a: 2, + b: [3] + } + }); + [head, ...rest] = [ + 1, + 2, + 3 + ]; + ({one: one, ...others} = { + one: 1, + two: 2, + three: 3 + }); + ({["a"]: dyn} = { a: 42 }); +} +var __unused__; diff --git a/crates/jsshaker/tests/snapshots/module_test@exc_flow__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@exc_flow__entry.js.snap new file mode 100644 index 00000000..d2c9bf41 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@exc_flow__entry.js.snap @@ -0,0 +1,39 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/exc_flow/entry.js +--- +//#region /entry.js +import { v, count, rethrow, loop } from "./m.js"; +try { + rethrow(); +} catch (e) { + console.log("caught", e.message); +} +loop(); +console.log(v, count); + +//#region /m.js +export let v = ""; +export let count = 0; +export function rethrow() { + try { + v = "try"; + throw new Error("boom"); + } catch (e) { + v += "|catch"; + throw e; + } finally { + v += "|finally"; + } +} +export function loop() { + for (let i = 0; i < 5; i++) { + try { + if (i % 2) throw new Error(); + count += 10; + } catch { + count += 1; + } + } +} diff --git a/crates/jsshaker/tests/snapshots/module_test@fn_getter_setter__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@fn_getter_setter__entry.js.snap new file mode 100644 index 00000000..ab30809b --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@fn_getter_setter__entry.js.snap @@ -0,0 +1,20 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/fn_getter_setter/entry.js +--- +//#region /entry.js +import { obj, backing } from "./m.js"; +obj.val = 5; +console.log(obj.val, backing); + +//#region /m.js +export let backing = 0; +export const obj = { + get val() { + return backing * 2; + }, + set val(__unused_447C) { + backing = 5; + } +}; diff --git a/crates/jsshaker/tests/snapshots/module_test@fn_param_forms__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@fn_param_forms__entry.js.snap new file mode 100644 index 00000000..4b53c00c --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@fn_param_forms__entry.js.snap @@ -0,0 +1,30 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/fn_param_forms/entry.js +--- +//#region /entry.js +import { v, total, n, withDefault, sum, viaArguments, apply } from "./m.js"; +withDefault(); +withDefault(9); +sum(1, 2, 3); +viaArguments(1, 2, 3); +apply((x) => x * 3); +console.log(v, total, n); + +//#region /m.js +export let v = ""; +export let total = 0; +export let n = 2; +export function withDefault(x = 5) { + v += x; +} +export function sum(...xs) { + for (const x of xs) total += x; +} +export function viaArguments() { + total += arguments.length + arguments[2]; +} +export function apply(fn) { + n = fn(n); +} diff --git a/crates/jsshaker/tests/snapshots/module_test@fn_reassign_then_call__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@fn_reassign_then_call__entry.js.snap new file mode 100644 index 00000000..18d6df22 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@fn_reassign_then_call__entry.js.snap @@ -0,0 +1,20 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/fn_reassign_then_call/entry.js +--- +//#region /entry.js +import { impl, swap, call } from "./m.js"; +console.log(call()); +swap(); +console.log(call()); +console.log(impl()); + +//#region /m.js +export let impl = () => "first"; +export function swap() { + impl = () => "second"; +} +export function call() { + return impl(); +} diff --git a/crates/jsshaker/tests/snapshots/module_test@fn_recursion__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@fn_recursion__entry.js.snap new file mode 100644 index 00000000..9f889fee --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@fn_recursion__entry.js.snap @@ -0,0 +1,15 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/fn_recursion/entry.js +--- +//#region /entry.js +import { calls, fact } from "./m.js"; +console.log(fact(5), calls); + +//#region /m.js +export let calls = 0; +export function fact(n) { + calls++; + return n <= 1 ? 1 : n * fact(n - 1); +} diff --git a/crates/jsshaker/tests/snapshots/module_test@gen_early_return__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@gen_early_return__entry.js.snap new file mode 100644 index 00000000..6a9a0dac --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@gen_early_return__entry.js.snap @@ -0,0 +1,23 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/gen_early_return/entry.js +--- +//#region /entry.js +import { state, gen } from "./m.js"; +const it = gen(); +it.next(); +it.return(); +console.log(state); + +//#region /m.js +export let state = ""; +export function* gen() { + try { + state += "start;"; + yield 1; + state += "never;"; + } finally { + state += "cleanup;"; + } +} diff --git a/crates/jsshaker/tests/snapshots/module_test@live_binding__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@live_binding__entry.js.snap new file mode 100644 index 00000000..beabe107 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@live_binding__entry.js.snap @@ -0,0 +1,18 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/live_binding/entry.js +--- +//#region /entry.js +import { counter, increment, $window } from "./state.js"; +increment(); +increment(); +console.log(counter); +console.log($window); + +//#region /state.js +export let counter = 0; +export function increment() { + counter++; +} +export let $window; diff --git a/crates/jsshaker/tests/snapshots/module_test@misc_binding_reads__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@misc_binding_reads__entry.js.snap new file mode 100644 index 00000000..4904c71b --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@misc_binding_reads__entry.js.snap @@ -0,0 +1,25 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/misc_binding_reads/entry.js +--- +//#region /entry.js +import { v, count, data, obj, run } from "./m.js"; +console.log(typeof v, `hello ${v}`); +run(); +console.log(typeof v, count, JSON.stringify(data), obj, obj === null); + +//#region /m.js +export let v = "str"; +export let count = 0; +export let data = null; +export let obj = { a: 1 }; +export function run() { + count = 1; + count = 2; + count = 3; + count = count + 1; + data = JSON.parse("{\"k\":[1,2]}"); + obj = null; + v = void 0; +} diff --git a/crates/jsshaker/tests/snapshots/module_test@mod_default_class_reassign__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@mod_default_class_reassign__entry.js.snap new file mode 100644 index 00000000..0ddaf73b --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@mod_default_class_reassign__entry.js.snap @@ -0,0 +1,18 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/mod_default_class_reassign/entry.js +--- +//#region /entry.js +import D, { bump } from "./m.js"; +console.log(typeof D); +bump(); +console.log(typeof D, D); + +//#region /m.js +export default class D { + static tag = "cls"; +} +export function bump() { + D = 42; +} diff --git a/crates/jsshaker/tests/snapshots/module_test@mod_diamond__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@mod_diamond__entry.js.snap new file mode 100644 index 00000000..274dfb5b --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@mod_diamond__entry.js.snap @@ -0,0 +1,32 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/mod_diamond/entry.js +--- +//#region /entry.js +import { bump, readL, readR } from "./l.js"; +bump(); +console.log(readL(), readR()); + +//#region /l.js +export { bump, readL } from "./left.js"; +export { readR } from "./right.js"; + +//#region /left.js +import { n } from "./shared.js"; +export { bump } from "./shared.js"; +export function readL() { + return n; +} + +//#region /right.js +import { n } from "./shared.js"; +export function readR() { + return n; +} + +//#region /shared.js +export let n = 1; +export function bump() { + n = 99; +} diff --git a/crates/jsshaker/tests/snapshots/module_test@mod_dynamic_import__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@mod_dynamic_import__entry.js.snap new file mode 100644 index 00000000..3b52dcea --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@mod_dynamic_import__entry.js.snap @@ -0,0 +1,11 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/mod_dynamic_import/entry.js +--- +//#region /entry.js +import("./side.js"); +export const x = 1; + +//#region /side.js +console.log("side"); diff --git a/crates/jsshaker/tests/snapshots/module_test@mod_import_order__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@mod_import_order__entry.js.snap new file mode 100644 index 00000000..2d12fcac --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@mod_import_order__entry.js.snap @@ -0,0 +1,19 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/mod_import_order/entry.js +--- +//#region /entry.js +import { log } from "./c.js"; +import "./b.js"; +console.log(log); + +//#region /b.js +import { add } from "./c.js"; +add(); + +//#region /c.js +export let log = ""; +export function add() { + log += "b;"; +} diff --git a/crates/jsshaker/tests/snapshots/module_test@mod_reexport_cycle__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@mod_reexport_cycle__entry.js.snap new file mode 100644 index 00000000..7fc588b7 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@mod_reexport_cycle__entry.js.snap @@ -0,0 +1,14 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/mod_reexport_cycle/entry.js +--- +//#region /entry.js +import { x } from "./a.js"; +console.log(x, 1); + +//#region /a.js +export { x } from "./b.js"; + +//#region /b.js +export { x } from "./a.js"; diff --git a/crates/jsshaker/tests/snapshots/module_test@mod_reexport_default_bare__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@mod_reexport_default_bare__entry.js.snap new file mode 100644 index 00000000..7b8e7d42 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@mod_reexport_default_bare__entry.js.snap @@ -0,0 +1,14 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/mod_reexport_default_bare/entry.js +--- +//#region /entry.js +import {} from "./mid.js"; +console.log("passthrough"); + +//#region /mid.js +import "./src.js"; + +//#region /src.js +export default function() {} diff --git a/crates/jsshaker/tests/snapshots/module_test@ns_object_keys__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@ns_object_keys__entry.js.snap new file mode 100644 index 00000000..12d1f70c --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@ns_object_keys__entry.js.snap @@ -0,0 +1,13 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/ns_object_keys/entry.js +--- +//#region /entry.js +import * as ns from "./m.js"; +console.log(Object.keys(ns).sort().join(",")); + +//#region /m.js +export const a = 1; +export const b = 2; +export function c() {} diff --git a/crates/jsshaker/tests/snapshots/module_test@ns_spread__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@ns_spread__entry.js.snap new file mode 100644 index 00000000..e00821f8 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@ns_spread__entry.js.snap @@ -0,0 +1,13 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/ns_spread/entry.js +--- +//#region /entry.js +import * as ns from "./m.js"; +const copy = { ...ns }; +console.log(copy.a, copy.b); + +//#region /m.js +export const a = 1; +export const b = 2; diff --git a/crates/jsshaker/tests/snapshots/module_test@obj_getter_side_effect__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@obj_getter_side_effect__entry.js.snap new file mode 100644 index 00000000..564e56cc --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@obj_getter_side_effect__entry.js.snap @@ -0,0 +1,17 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/obj_getter_side_effect/entry.js +--- +//#region /entry.js +import { hits, o } from "./m.js"; +o.a; +o.a; +console.log(hits); + +//#region /m.js +export let hits = 0; +export const o = { get a() { + hits++; + return; +} }; diff --git a/crates/jsshaker/tests/snapshots/module_test@op_compound_assign__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@op_compound_assign__entry.js.snap new file mode 100644 index 00000000..3538b181 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@op_compound_assign__entry.js.snap @@ -0,0 +1,31 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/op_compound_assign/entry.js +--- +//#region /entry.js +import { n, m, a, b, c, f } from "./m.js"; +f(); +console.log(n, m, a, b, c); + +//#region /m.js +export let n = 10; +export let m = 20; +export let a = null, b = 1, c = 0; +export function f() { + n += 3; + n -= 1; + m *= 3; + m /= 2; + m %= 7; + n **= 2; + n &= 255; + n |= 1; + n ^= 3; + n <<= 1; + n >>= 2; + n >>>= 0; + a ??= "set"; + b &&= 9; + c ||= 7; +} diff --git a/crates/jsshaker/tests/snapshots/module_test@reexported_binding__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@reexported_binding__entry.js.snap new file mode 100644 index 00000000..57ba0ee9 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@reexported_binding__entry.js.snap @@ -0,0 +1,18 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/reexported_binding/entry.js +--- +//#region /entry.js +import { value, bump } from "./middle.js"; +bump(); +console.log(value); + +//#region /middle.js +export { value, bump } from "./source.js"; + +//#region /source.js +export let value = 1; +export function bump() { + value += 1; +} diff --git a/crates/jsshaker/tests/snapshots/module_test@tdz_circular_throw__entry.js.snap b/crates/jsshaker/tests/snapshots/module_test@tdz_circular_throw__entry.js.snap new file mode 100644 index 00000000..9d1bb976 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/module_test@tdz_circular_throw__entry.js.snap @@ -0,0 +1,18 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake_modules(dir) +input_file: crates/jsshaker/tests/module_fixtures/tdz_circular_throw/entry.js +--- +//#region /entry.js +import "./a.js"; + +//#region /a.js +import { readEarly } from "./b.js"; +export let x = 1; +console.log(readEarly()); + +//#region /b.js +import { x } from "./a.js"; +export function readEarly() { + return x; +} diff --git a/crates/jsshaker/tests/snapshots/test@assignment_expression.js.snap b/crates/jsshaker/tests/snapshots/test@assignment_expression.js.snap index 3646e46b..d457c5d6 100644 --- a/crates/jsshaker/tests/snapshots/test@assignment_expression.js.snap +++ b/crates/jsshaker/tests/snapshots/test@assignment_expression.js.snap @@ -23,8 +23,6 @@ export function f5(a, b) { export function f6(a, b) { a.p = b; global.p = 1; - effect; - return a; } export function f7(a) { ({x: __unused__, a: __unused__, b: {c: __unused__}, ...__unused__} = a); diff --git a/crates/jsshaker/tests/snapshots/test@chain_expression.js.snap b/crates/jsshaker/tests/snapshots/test@chain_expression.js.snap index fa4a352a..079ec165 100644 --- a/crates/jsshaker/tests/snapshots/test@chain_expression.js.snap +++ b/crates/jsshaker/tests/snapshots/test@chain_expression.js.snap @@ -1,10 +1,14 @@ --- -source: src/tests/mod.rs -expression: tree_shake(input) -input_file: src/tests/fixtures/chain_expression.js +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake(input) +input_file: crates/jsshaker/tests/fixtures/chain_expression.js --- export function main(unknown) { g?.h.i?.j?.(o?.p)?.q.r; let a = { a: unknown }; effect(a.a?.c, void 0, NaN); } +export async function short_circuit(effectful) { + await void 0; + await (effectful(), void 0); +} diff --git a/crates/jsshaker/tests/snapshots/test@class__extends.js.snap b/crates/jsshaker/tests/snapshots/test@class__extends.js.snap index c1c581b6..863e29ca 100644 --- a/crates/jsshaker/tests/snapshots/test@class__extends.js.snap +++ b/crates/jsshaker/tests/snapshots/test@class__extends.js.snap @@ -11,5 +11,5 @@ class Derived extends Base { } const a = new Derived(); console.log("Base1Derived"); -console.log(1, 2, 4); +console.log(1, 3, 4); a.a(); diff --git a/crates/jsshaker/tests/snapshots/test@class__private_brand_unused.js.snap b/crates/jsshaker/tests/snapshots/test@class__private_brand_unused.js.snap new file mode 100644 index 00000000..b0fd0ea2 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/test@class__private_brand_unused.js.snap @@ -0,0 +1,14 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake(input) +input_file: crates/jsshaker/tests/fixtures/class/private_brand_unused.js +--- +const results = []; +class Ghost { + #tag; + static a(o) { + return #tag in o; + } +} +results.push(Ghost.a({})); +export default results; diff --git a/crates/jsshaker/tests/snapshots/test@class__private_inherit.js.snap b/crates/jsshaker/tests/snapshots/test@class__private_inherit.js.snap new file mode 100644 index 00000000..068958f2 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/test@class__private_inherit.js.snap @@ -0,0 +1,25 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake(input) +input_file: crates/jsshaker/tests/fixtures/class/private_inherit.js +--- +const results = []; +class Base { + a() { + return; + } +} +class Derived extends Base { + a() { + return ["base", "derived"]; + } +} +const d = new Derived(); +results.push("base"); +results.push(...d.a()); +try { + Base.prototype.a.call(); +} catch (e) { + results.push(e.constructor.name); +} +export default results; diff --git a/crates/jsshaker/tests/snapshots/test@class__private_shadow.js.snap b/crates/jsshaker/tests/snapshots/test@class__private_shadow.js.snap new file mode 100644 index 00000000..90428218 --- /dev/null +++ b/crates/jsshaker/tests/snapshots/test@class__private_shadow.js.snap @@ -0,0 +1,8 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake(input) +input_file: crates/jsshaker/tests/fixtures/class/private_shadow.js +--- +const results = []; +results.push("outer", "inner", "outer", "other"); +export default results; diff --git a/crates/jsshaker/tests/snapshots/test@class__super_static.js.snap b/crates/jsshaker/tests/snapshots/test@class__super_static.js.snap new file mode 100644 index 00000000..45c8517c --- /dev/null +++ b/crates/jsshaker/tests/snapshots/test@class__super_static.js.snap @@ -0,0 +1,7 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake(input) +input_file: crates/jsshaker/tests/fixtures/class/super_static.js +--- +console.log("static static-child"); +console.log("instance"); diff --git a/crates/jsshaker/tests/snapshots/test@class__traditional.js.snap b/crates/jsshaker/tests/snapshots/test@class__traditional.js.snap index 7134c395..6bbd5b28 100644 --- a/crates/jsshaker/tests/snapshots/test@class__traditional.js.snap +++ b/crates/jsshaker/tests/snapshots/test@class__traditional.js.snap @@ -10,6 +10,6 @@ A.a = function() { A.prototype.a = function() { return unknown(); }; -console.log(2, 10, 10, 21); +console.log(2, 10, 12, 21); const b = A.a(); b.a(); diff --git a/crates/jsshaker/tests/snapshots/test@define_property.js.snap b/crates/jsshaker/tests/snapshots/test@define_property.js.snap new file mode 100644 index 00000000..279d1e7c --- /dev/null +++ b/crates/jsshaker/tests/snapshots/test@define_property.js.snap @@ -0,0 +1,26 @@ +--- +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake(input) +input_file: crates/jsshaker/tests/fixtures/define_property.js +--- +const o = {}; +let backing; +Object.defineProperty(o, "p", { + get() { + return backing * 2; + }, + set(v) { + backing = v + 1; + }, + enumerable: true, + configurable: true +}); +o.p = 4; +console.log(10, 5, JSON.stringify(Object.keys(o))); +const ro = { z: 1 }; +Object.defineProperty(ro, "hidden", { get() { + return "H"; +} }); +console.log("H", JSON.stringify(Object.keys(ro)), JSON.stringify(ro)); +const d = Object.getOwnPropertyDescriptor(o, "p"); +console.log(typeof d.get, typeof d.set, d.enumerable, d.configurable); diff --git a/crates/jsshaker/tests/snapshots/test@for_of_statement.js.snap b/crates/jsshaker/tests/snapshots/test@for_of_statement.js.snap index 360b161b..826671e0 100644 --- a/crates/jsshaker/tests/snapshots/test@for_of_statement.js.snap +++ b/crates/jsshaker/tests/snapshots/test@for_of_statement.js.snap @@ -1,6 +1,5 @@ --- source: crates/jsshaker/tests/mod.rs -assertion_line: 41 expression: do_tree_shake(input) input_file: crates/jsshaker/tests/fixtures/for_of_statement.js --- diff --git a/crates/jsshaker/tests/snapshots/test@import_expression.js.snap b/crates/jsshaker/tests/snapshots/test@import_expression.js.snap index ef4f56e8..efbc7c56 100644 --- a/crates/jsshaker/tests/snapshots/test@import_expression.js.snap +++ b/crates/jsshaker/tests/snapshots/test@import_expression.js.snap @@ -1,9 +1,10 @@ --- -source: src/tests/mod.rs -expression: tree_shake(input) -input_file: src/tests/fixtures/import_expression.js +source: crates/jsshaker/tests/mod.rs +expression: do_tree_shake(input) +input_file: crates/jsshaker/tests/fixtures/import_expression.js --- export function main() { - effect(1), effect(2); + import("a"); + import(effect(1) + "b", { c: effect(2) }); effect(import("c", effect(3))); } diff --git a/crates/jsshaker/tests/snapshots/test@throw_catch.js.snap b/crates/jsshaker/tests/snapshots/test@throw_catch.js.snap index d948ce78..98f6e219 100644 --- a/crates/jsshaker/tests/snapshots/test@throw_catch.js.snap +++ b/crates/jsshaker/tests/snapshots/test@throw_catch.js.snap @@ -1,6 +1,5 @@ --- source: crates/jsshaker/tests/mod.rs -assertion_line: 41 expression: do_tree_shake(input) input_file: crates/jsshaker/tests/fixtures/throw_catch.js --- diff --git a/packages/napi/src/lib.rs b/packages/napi/src/lib.rs index b6253c00..9803730d 100644 --- a/packages/napi/src/lib.rs +++ b/packages/napi/src/lib.rs @@ -14,7 +14,7 @@ pub struct Options { #[napi(ts_type = "'safest' | 'recommended' | 'smallest' | 'disabled'")] pub preset: Option, pub minify: Option, - #[napi(ts_type = "'react'")] + #[napi(ts_type = "'none' | 'react'")] pub jsx: Option, pub source_map: Option, @@ -24,6 +24,7 @@ pub struct Options { #[napi(ts_type = "'enabled' | 'disabled' | 'only'")] pub property_mangling: Option, pub branch_folding: Option, + pub max_folding_string_length: Option, pub max_recursion_depth: Option, pub remember_exhausted_variables: Option, @@ -31,6 +32,17 @@ pub struct Options { pub enable_fn_cache: Option, pub enable_fn_stats: Option, pub enable_mangling_stats: Option, + + pub unknown_global_side_effects: Option, + pub preserve_function_name: Option, + pub preserve_function_length: Option, + pub iterate_side_effects: Option, + pub unknown_property_read_side_effects: Option, + pub unmatched_prototype_property_as_undefined: Option, + pub preserve_exceptions: Option, + pub preserve_property_attributes: Option, + pub impure_json_stringify: Option, + pub precise_dynamic_prototype: Option, } #[napi(object)] @@ -80,6 +92,16 @@ impl From for Chunk { } } +macro_rules! apply_options { + ($config:ident, $options:ident, $($field:ident $(as $ty:ty)?),+ $(,)?) => { + $( + if let Some(value) = $options.$field { + $config.$field = value $(as $ty)?; + } + )+ + }; +} + fn resolve_options(vfs: F, options: Options) -> JsShakerOptions { let preset = options.preset.as_deref().unwrap_or("recommended"); @@ -90,8 +112,12 @@ fn resolve_options(vfs: F, options: Options) -> JsShakerOptions { "disabled" => TreeShakeConfig::disabled(), _ => panic!("Invalid tree shake option {:?}", preset), }; - if options.jsx.as_deref() == Some("react") { - config.jsx = TreeShakeJsxPreset::React; + if let Some(jsx) = options.jsx.as_deref() { + config.jsx = match jsx { + "none" => TreeShakeJsxPreset::None, + "react" => TreeShakeJsxPreset::React, + _ => panic!("Invalid jsx option {:?}", jsx), + }; } config.advanced = options.advanced.unwrap_or(false); @@ -111,28 +137,29 @@ fn resolve_options(vfs: F, options: Options) -> JsShakerOptions { _ => panic!("Invalid property_mangling option {:?}", property_mangling), }; } - if let Some(branch_folding) = options.branch_folding { - config.branch_folding = branch_folding; - } - if let Some(depth) = options.max_recursion_depth { - config.max_recursion_depth = depth as usize; - } - if let Some(remember) = options.remember_exhausted_variables { - config.remember_exhausted_variables = remember; - } - if let Some(eager) = options.eager_exhaustive_callbacks { - config.eager_exhaustive_callbacks = eager; - } - if let Some(enable) = options.enable_fn_cache { - config.enable_fn_cache = enable; - } - if let Some(enable) = options.enable_fn_stats { - config.enable_fn_stats = enable; - } - if let Some(enable) = options.enable_mangling_stats { - config.enable_mangling_stats = enable; - } + apply_options!( + config, + options, + max_folding_string_length as usize, + max_recursion_depth as usize, + branch_folding, + remember_exhausted_variables, + eager_exhaustive_callbacks, + enable_fn_cache, + enable_fn_stats, + enable_mangling_stats, + unknown_global_side_effects, + preserve_function_name, + preserve_function_length, + iterate_side_effects, + unknown_property_read_side_effects, + unmatched_prototype_property_as_undefined, + preserve_exceptions, + preserve_property_attributes, + impure_json_stringify, + precise_dynamic_prototype, + ); let minify = options.minify.unwrap_or(false); let minify_options =