Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions crates/jsshaker/src/analyzer/conditional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct ConditionalData<'a> {
maybe_false: bool,
impure_true: bool,
impure_false: bool,
exiting_branches: Vec<Dep<'a>>,
tests_to_include: Vec<Entity<'a>>,
}

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -182,6 +199,20 @@ impl<'a> Analyzer<'a> {
Some(Dep(branch))
}

pub fn note_conditional_branch_exit(&mut self, id: impl Into<DepAtom>, dep: Option<Dep<'a>>) {
let Some(dep) = dep.filter(|_| self.cf_scope().must_exited()) else {
return;
};
let id = id.into();
if let Some(data) = self.conditional_data.node_to_data.get_mut(&id) {
data.exiting_branches.push(dep);
}
}

pub fn conditional_exit_link(&mut self, id: impl Into<DepAtom>) -> 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;
Expand Down
74 changes: 47 additions & 27 deletions crates/jsshaker/src/builtins/globals/object_constructor.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::borrow::BorrowMut;

use oxc::allocator;
use oxc::{allocator, ast::ast::PropertyKind};

use crate::{
Analyzer, builtin_string,
Expand Down Expand Up @@ -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;
Expand All @@ -201,38 +202,57 @@ 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();
if !accessors.is_empty() {
let Some(object_value) = object.value.as_object() else {
break 'trackable;
};
let dep = self.factory.dep((
enumerated.dep,
descriptor.get_shallow_dep(analyzer.factory),
value,
allocator::Vec::from_iter_in(attributes, analyzer.allocator),
));
for (kind, accessor) in accessors {
let accessor = self.factory.computed(accessor, dep);
object_value.init_property(analyzer, kind, key, accessor, true);
}
} else {
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 attributes.is_empty() {
value
} else {
analyzer
.factory
.computed(value, allocator::Vec::from_iter_in(attributes, analyzer.allocator))
}
},
);
if value.is_none() {
analyzer.pop_cf_scope();
}
}

let deps = self.factory.dep((
Expand Down
2 changes: 1 addition & 1 deletion crates/jsshaker/src/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<(bool, Entity<'a>)>> {
self.value.get_keys(analyzer, check_proto)
Expand Down
31 changes: 20 additions & 11 deletions crates/jsshaker/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ impl<'a> Analyzer<'a> {
let reexport_all = module.reexport_all.iter().copied().collect::<Vec<_>>();
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 {
Expand All @@ -307,22 +307,27 @@ impl<'a> Analyzer<'a> {
&mut self,
module_id: ModuleId,
named_export: ExportedValue<'a>,
searched: &mut FxHashSet<ModuleId>,
) -> 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)
}
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),
Expand All @@ -339,18 +344,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(
Expand Down
10 changes: 9 additions & 1 deletion crates/jsshaker/src/nodes/expr/await_expression.rs
Original file line number Diff line number Diff line change
@@ -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> {
Expand All @@ -12,6 +17,9 @@ impl<'a> Analyzer<'a> {
self.global_effect();

let value = self.exec_expression(&node.argument);
if !matches!(self.call_scope().callee.node, CalleeNode::Module | CalleeNode::Root) {
self.include(AstKind2::AwaitExpression(node));
}
value.r#await(self, AstKind2::AwaitExpression(node))
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/jsshaker/src/nodes/expr/call_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/jsshaker/src/nodes/expr/import_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion crates/jsshaker/src/nodes/expr/super_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_class, is_static) = self.get_super();
if is_static {
super_class
} else {
super_class.get_property(self, self.factory.no_dep, builtin_string!("prototype"))
}
}
}

Expand Down
9 changes: 6 additions & 3 deletions crates/jsshaker/src/nodes/misc/callee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,28 @@ impl<'a> Analyzer<'a> {
node: &'a Expression<'a>,
) -> Result<(usize, Entity<'a>, Option<Entity<'a>>, Entity<'a>), Entity<'a>> {
if matches!(node, Expression::Super(_)) {
return Ok((0, self.get_super(), None, self.get_this()));
return Ok((0, self.get_super().0, 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),
Expand Down
Loading
Loading