use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
use oxc_ast::ast::{
Argument, ArrayExpression, ArrayExpressionElement, ArrowFunctionExpression, AssignmentExpression,
AssignmentOperator, AssignmentPattern, AssignmentTarget, AssignmentTargetMaybeDefault, AssignmentTargetPattern,
AssignmentTargetProperty, AssignmentTargetPropertyIdentifier, BindingIdentifier, BindingPattern, CallExpression,
ChainExpression, Class, ComputedMemberExpression, Declaration, DoWhileStatement, ExportDeclaration,
ExportDefaultDeclaration, ExportDefaultDeclarationKind, Expression, ExpressionStatement, ForInStatement,
ForOfStatement, ForStatement, ForStatementLeft, FormalParameter, FormalParameters, Function, IdentifierReference,
ImportDeclaration, ImportDeclarationSpecifier, LogicalOperator, MemberExpression, MethodDefinition,
MethodDefinitionKind, NewExpression, ObjectExpression, ObjectProperty, ObjectPropertyKind, PrivateFieldExpression,
Program, PropertyDefinition, PropertyKind, ReturnStatement, SimpleAssignmentTarget, StaticBlock,
StaticMemberExpression, Super, TaggedTemplateExpression, ThisExpression, UnaryExpression, UnaryOperator,
VariableDeclaration, VariableDeclarationKind, VariableDeclarator, WhileStatement, WithStatement,
};
use oxc_ast_visit::{Visit, walk};
use oxc_semantic::{ReferenceId, ScopeFlags, ScopeId, Scoping, SemanticBuilder, SymbolId};
use oxc_span::{GetSpan, Span};
use skif_core::{HashMap, HashSet};
use crate::config::defined;
use crate::finder::static_string;
type AliasPart = (SymbolId, Vec<String>);
pub(crate) type ObjectPart = (String, Vec<String>, bool);
const MAX_KEYS: usize = 8;
const MAX_PLACES: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Change {
Contents(Vec<Write>),
Any,
}
impl Change {
fn with(self, other: Self) -> Self {
match (self, other) {
(Self::Contents(mut writes), Self::Contents(more)) => {
for write in more {
add_write(&mut writes, write);
}
Self::Contents(writes)
}
_ => Self::Any,
}
}
}
fn add_write(writes: &mut Vec<Write>, write: Write) {
if writes.iter().any(|known| known.covers(&write)) {
return;
}
writes.retain(|known| !write.covers(known));
writes.push(write);
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct Write {
pub(crate) keys: Vec<String>,
pub(crate) key: Option<String>,
}
impl Write {
fn contents(keys: Vec<String>) -> Self {
Self { keys, key: None }
}
fn path(&self) -> Vec<String> {
self.keys.iter().chain(&self.key).cloned().collect()
}
fn covers(&self, other: &Self) -> bool {
match self.key {
None => other.keys.starts_with(&self.keys),
Some(_) => other.path().starts_with(&self.path()),
}
}
}
pub(crate) trait Imports {
fn follows(&self, specifier: &str) -> bool;
fn changes_modules(&self, specifier: &str) -> bool;
fn is_installed(&self, _specifier: &str) -> bool {
false
}
fn result_places(&self, _specifier: &str, _export: &str) -> Option<Vec<Write>> {
None
}
}
#[derive(Default)]
pub(crate) struct Unstable {
reads: HashMap<ReferenceId, Change>,
top: HashMap<String, Change>,
followed: HashSet<SymbolId>,
pure: HashSet<Span>,
effects: HashMap<Span, Effect>,
objects: HashMap<Span, Vec<ObjectPart>>,
calls_back: HashSet<Span>,
called_values: HashSet<Span>,
foreign_calls: HashSet<Span>,
namespaces: HashSet<String>,
runs_modules: bool,
writes_modules: bool,
exposed: Vec<String>,
exposes_unknown: bool,
results: HashMap<String, Vec<Write>>,
sole: HashSet<ReferenceId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Effect {
Keeps,
Followed,
}
impl Unstable {
pub(crate) fn of(program: &Program<'_>, imports: &dyn Imports) -> Self {
let semantic = SemanticBuilder::new().build(program).semantic;
let scoping = semantic.scoping();
let mut sharing = Sharing {
scoping,
imports,
known: known(program, scoping, imports),
pure: HashSet::default(),
effects: HashMap::default(),
objects: HashMap::default(),
calls_back: HashSet::default(),
called_values: HashSet::default(),
foreign_calls: HashSet::default(),
exported: HashSet::default(),
dynamic: false,
loops: 0,
repeated: HashSet::default(),
functions: vec![scoping.root_scope_id()],
links: Vec::new(),
classes: RefCell::new(None),
mutated: Vec::new(),
changes: BTreeMap::new(),
aliases: Vec::new(),
primitives: HashSet::default(),
primitive_lists: HashSet::default(),
element_links: Vec::new(),
context: Context::default(),
callables: callables(program, scoping),
alias_parts: HashMap::default(),
ends: HashMap::default(),
late: Vec::new(),
runs_modules: false,
callee_refs: HashSet::default(),
function_writes: Vec::new(),
suspending: HashSet::default(),
this_sites: vec![ThisSite::Module],
method_sites: HashMap::default(),
this_writes: Vec::new(),
this_bound: HashMap::default(),
makers: HashMap::default(),
named_inits: HashMap::default(),
default_exports: HashSet::default(),
iifes: HashSet::default(),
rebound: Vec::new(),
rebinds: false,
};
sharing.visit_program(program);
let (followed, writes_modules) = sharing.share();
let results = sharing.result_places();
let (exposed, exposes_unknown) = sharing.exposed();
let sole = if sharing.dynamic {
HashSet::default()
} else {
sole_references(scoping, &sharing.exported, &followed, &sharing.repeated)
};
let mut unstable = Self {
followed,
pure: std::mem::take(&mut sharing.pure),
effects: std::mem::take(&mut sharing.effects),
objects: std::mem::take(&mut sharing.objects),
calls_back: std::mem::take(&mut sharing.calls_back),
called_values: std::mem::take(&mut sharing.called_values),
foreign_calls: std::mem::take(&mut sharing.foreign_calls),
namespaces: sharing
.known
.namespaces
.iter()
.map(|&symbol| scoping.symbol_name(symbol).to_owned())
.collect(),
sole,
runs_modules: sharing.runs_modules,
writes_modules,
exposed,
exposes_unknown,
results,
..Self::default()
};
for (symbol, change) in std::mem::take(&mut sharing.changes) {
let change = sorted(change);
if scoping.symbol_scope_id(symbol) == scoping.root_scope_id() {
unstable.top.insert(scoping.symbol_name(symbol).to_owned(), change);
} else {
for &reference in scoping.get_resolved_reference_ids(symbol) {
unstable.reads.insert(reference, change.clone());
}
}
}
for (symbol, from, change) in std::mem::take(&mut sharing.late) {
for &reference in scoping.get_resolved_reference_ids(symbol) {
if sharing.read_before(reference, from) {
continue;
}
let merged = match unstable.reads.remove(&reference) {
Some(existing) => sorted(existing.with(change.clone())),
None => sorted(change.clone()),
};
unstable.reads.insert(reference, merged);
}
}
unstable
}
pub(crate) fn of_reference(&self, id: &IdentifierReference<'_>) -> Option<&Change> {
self.reads.get(&id.reference_id.get()?)
}
pub(crate) fn of_top(&self, name: &str) -> Option<&Change> {
self.top.get(name)
}
pub(crate) fn follows(&self, id: &BindingIdentifier<'_>) -> bool {
id.symbol_id.get().is_some_and(|symbol| self.followed.contains(&symbol))
}
pub(crate) fn is_pure(&self, call: &CallExpression<'_>) -> bool {
self.pure.contains(&call.span)
}
pub(crate) fn changes_arguments(&self, span: Span, statement: bool) -> bool {
match self.effects.get(&span) {
Some(Effect::Keeps) => false,
Some(Effect::Followed) => statement,
None => true,
}
}
pub(crate) fn parts_of(&self, object: &Expression<'_>) -> &[ObjectPart] {
self.objects.get(&object.span()).map_or(&[], Vec::as_slice)
}
pub(crate) fn calls_back(&self, call: &CallExpression<'_>) -> bool {
self.calls_back.contains(&call.span)
}
pub(crate) fn calls_value(&self, call: &CallExpression<'_>) -> bool {
self.called_values.contains(&call.span)
}
pub(crate) fn calls_foreign(&self, call: &CallExpression<'_>) -> bool {
self.foreign_calls.contains(&call.span)
}
pub(crate) fn is_namespace(&self, object: &Expression<'_>) -> bool {
matches!(object.get_inner_expression(), Expression::Identifier(id) if self.namespaces.contains(id.name.as_str()))
}
pub(crate) fn changes_modules(&self) -> bool {
self.runs_modules || self.writes_modules || self.exposes_unknown
}
pub(crate) fn result_places(&self) -> &HashMap<String, Vec<Write>> {
&self.results
}
pub(crate) fn exposed_modules(&self) -> &[String] {
&self.exposed
}
pub(crate) fn seen_after(&self, id: &IdentifierReference<'_>) -> bool {
!id.reference_id
.get()
.is_some_and(|reference| self.sole.contains(&reference))
}
}
fn sorted(mut change: Change) -> Change {
if let Change::Contents(writes) = &mut change {
writes.sort();
}
change
}
fn sole_references(
scoping: &Scoping,
exported: &HashSet<SymbolId>,
followed: &HashSet<SymbolId>,
repeated: &HashSet<ReferenceId>,
) -> HashSet<ReferenceId> {
scoping
.symbol_ids()
.filter(|symbol| !exported.contains(symbol) && !followed.contains(symbol))
.filter_map(|symbol| match scoping.get_resolved_reference_ids(symbol) {
[only] if !repeated.contains(only) => Some(*only),
_ => None,
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Place {
symbol: SymbolId,
keys: Vec<String>,
}
impl Place {
fn of(symbol: SymbolId) -> Self {
Self {
symbol,
keys: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Join {
Same,
Copy,
Within,
}
impl Join {
fn of(exact: bool) -> Self {
if exact { Self::Same } else { Self::Within }
}
}
struct Link {
a: Place,
b: Place,
join: Join,
loading: bool,
}
#[derive(Clone)]
struct Held {
symbol: SymbolId,
keys: Vec<String>,
at: Vec<String>,
join: Join,
result: bool,
}
#[derive(Debug, Default, Clone, Copy)]
struct Context {
in_const: bool,
defining: bool,
}
struct Sharing<'s> {
scoping: &'s Scoping,
imports: &'s dyn Imports,
known: Known,
pure: HashSet<Span>,
effects: HashMap<Span, Effect>,
objects: HashMap<Span, Vec<ObjectPart>>,
calls_back: HashSet<Span>,
called_values: HashSet<Span>,
foreign_calls: HashSet<Span>,
exported: HashSet<SymbolId>,
dynamic: bool,
loops: usize,
repeated: HashSet<ReferenceId>,
functions: Vec<ScopeId>,
links: Vec<Link>,
classes: RefCell<Option<(usize, Classes)>>,
mutated: Vec<(SymbolId, Write)>,
changes: BTreeMap<SymbolId, Change>,
aliases: Vec<(SymbolId, SymbolId)>,
primitives: HashSet<SymbolId>,
primitive_lists: HashSet<SymbolId>,
element_links: Vec<Link>,
context: Context,
callables: Callables,
alias_parts: HashMap<SymbolId, (Vec<String>, u32)>,
ends: HashMap<ReferenceId, u32>,
late: Vec<(SymbolId, u32, Change)>,
runs_modules: bool,
callee_refs: HashSet<ReferenceId>,
function_writes: Vec<(SymbolId, ScopeId, Change, bool)>,
suspending: HashSet<ScopeId>,
this_sites: Vec<ThisSite>,
method_sites: HashMap<ScopeId, ThisSite>,
this_writes: Vec<(ThisSite, bool)>,
this_bound: HashMap<SymbolId, ThisSite>,
makers: HashMap<ScopeId, Maker>,
named_inits: HashMap<ScopeId, SymbolId>,
default_exports: HashSet<ScopeId>,
iifes: HashSet<ScopeId>,
rebound: Vec<SymbolId>,
rebinds: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ThisSite {
Module,
Instance,
Load,
Class,
Plain(Option<SymbolId>, ScopeId),
Made(ScopeId),
Super,
}
struct Maker {
binding: Option<SymbolId>,
enclosing: ScopeId,
default_export: bool,
}
struct AppliedAlias {
alias: SymbolId,
root: SymbolId,
keys: Vec<String>,
from: u32,
}
impl AppliedAlias {
fn applied(&self, write: &Write) -> Write {
let mut keys = self.keys.clone();
keys.extend_from_slice(&write.keys);
Write {
keys,
key: write.key.clone(),
}
}
}
impl Sharing<'_> {
fn symbol(&self, id: &IdentifierReference<'_>) -> Option<SymbolId> {
resolved(self.scoping, id)
}
fn names(&self) -> Names<'_> {
Names {
scoping: self.scoping,
known: &self.known,
}
}
fn place(&self, expr: &Expression<'_>) -> Option<(Place, bool)> {
let (id, keys, exact) = reach(expr)?;
Some((
Place {
symbol: self.symbol(id)?,
keys,
},
exact,
))
}
fn member_place(&self, member: &MemberExpression<'_>) -> Option<(Place, bool)> {
let (mut place, exact) = self.place(member.object())?;
match member.static_property_name() {
Some(key) if exact => {
place.keys.push(crate::js_string(key));
Some((place, true))
}
_ => Some((place, false)),
}
}
fn parts(&mut self, expr: &Expression<'_>, keyed: bool) -> Vec<(Place, bool, bool)> {
self.note_this(expr, keyed);
if reach(expr).is_some() {
return self
.place(expr)
.into_iter()
.map(|(place, exact)| (place, exact, false))
.collect();
}
let parts: Vec<(Place, bool, bool)> = self
.refs(Part::Value, |refs| refs.visit_expression(expr))
.into_iter()
.map(|held| {
let exact = held.join == Join::Same && held.at.is_empty();
let place = Place {
symbol: held.symbol,
keys: held.keys,
};
(place, exact, held.result)
})
.collect();
let named = parts
.iter()
.map(|(place, exact, _)| {
let name = self.scoping.symbol_name(place.symbol).to_owned();
(name, place.keys.clone(), *exact)
})
.collect();
self.objects.insert(expr.span(), named);
parts
}
fn set(&mut self, member: &MemberExpression<'_>) {
let key = member.static_property_name().map(crate::js_string);
for (place, exact, result) in self.parts(member.object(), true) {
let write = match &key {
Some(key) if exact => Write {
keys: place.keys,
key: Some(key.clone()),
},
_ => Write::contents(place.keys),
};
self.changed_as(place.symbol, write, result);
}
}
fn member_places(&mut self, member: &MemberExpression<'_>) -> Vec<(Place, bool)> {
if reach(member.object()).is_some() {
return self.member_place(member).into_iter().collect();
}
self.parts(member.object(), true)
.into_iter()
.map(|(place, _, _)| (place, false))
.collect()
}
fn function_of(&self, symbol: SymbolId) -> ScopeId {
let scope = self.scoping.symbol_scope_id(symbol);
self.scoping
.scope_ancestors(scope)
.find(|&s| {
let flags = self.scoping.scope_flags(s);
flags.is_function() || flags.is_top()
})
.unwrap_or(scope)
}
fn change(&mut self, symbol: SymbolId, change: Change) {
let merged = match self.changes.remove(&symbol) {
Some(existing) => existing.with(change),
None => change,
};
self.changes.insert(symbol, merged);
}
fn written(&mut self, symbol: SymbolId, change: Change, result: bool) {
let root = self.scoping.root_scope_id();
let function = self.functions.last().copied().unwrap_or(root);
if function != root {
self.function_writes.push((symbol, function, change.clone(), result));
}
if function != self.function_of(symbol) {
self.change(symbol, change);
}
}
fn loading(&self) -> bool {
!self.context.defining && self.functions.last() == Some(&self.scoping.root_scope_id())
}
fn in_definition(&mut self, defining: bool, visit: impl FnOnce(&mut Self)) {
let saved = std::mem::replace(&mut self.context.defining, defining);
visit(self);
self.context.defining = saved;
}
fn mutate(&mut self, expr: &Expression<'_>) {
for (place, _, result) in self.parts(expr, false) {
self.changed_as(place.symbol, Write::contents(place.keys), result);
}
}
fn change_target(&mut self, call: &CallExpression<'_>, target: &Expression<'_>) {
if is_namespace(self.names(), target) {
return;
}
match (property_defined(call), self.place(target)) {
(Some(key), Some((place, true))) => self.changed(
place.symbol,
Write {
keys: place.keys,
key: Some(key),
},
),
_ => self.mutate(target),
}
}
fn in_loop(&mut self, visit: impl FnOnce(&mut Self)) {
self.loops += 1;
visit(self);
self.loops -= 1;
}
fn note_this(&mut self, expr: &Expression<'_>, keyed: bool) {
if self.functions.last() == Some(&self.scoping.root_scope_id()) {
return;
}
let mut finder = MentionsThis::default();
finder.visit_expression(expr);
if finder.this {
let keyed = keyed && matches!(expr.get_inner_expression(), Expression::ThisExpression(_));
self.wrote_this(self.this_site(), keyed);
}
if finder.sup {
self.wrote_this(ThisSite::Super, false);
}
}
fn this_site(&self) -> ThisSite {
self.this_sites.last().copied().unwrap_or(ThisSite::Module)
}
fn wrote_this(&mut self, site: ThisSite, keyed: bool) {
if !self.this_writes.contains(&(site, keyed)) {
self.this_writes.push((site, keyed));
}
}
fn bind_this(&mut self, symbols: &[SymbolId], value: &Expression<'_>) {
if self.functions.last() == Some(&self.scoping.root_scope_id()) {
return;
}
let mut finder = MentionsThis::default();
finder.visit_expression(value);
let site = match (finder.sup, finder.this) {
(true, _) => ThisSite::Super,
(false, true) => self.this_site(),
(false, false) => return,
};
for &symbol in symbols {
self.this_bound.insert(symbol, site);
}
}
fn enter_function(&mut self, scope: ScopeId, declared: Option<SymbolId>) -> ThisSite {
let binding = self.named_inits.get(&scope).copied().or(declared);
self.add_maker(scope, binding);
self.method_sites
.remove(&scope)
.unwrap_or(ThisSite::Plain(binding, scope))
}
fn add_maker(&mut self, scope: ScopeId, binding: Option<SymbolId>) {
let root = self.scoping.root_scope_id();
let maker = Maker {
binding,
enclosing: self.functions.last().copied().unwrap_or(root),
default_export: self.default_exports.contains(&scope),
};
self.makers.insert(scope, maker);
}
fn note_rebinding(&mut self, call: &CallExpression<'_>) {
let Expression::StaticMemberExpression(member) = call.callee.get_inner_expression() else {
return;
};
let method = member.property.name.as_str();
let global = |name: &str| {
matches!(member.object.get_inner_expression(), Expression::Identifier(id)
if id.name.as_str() == name && resolved(self.scoping, id).is_none())
};
if method == "getPrototypeOf" && (global("Object") || global("Reflect")) {
self.rebinds = true;
return;
}
let (target, this) = if method == "apply" && global("Reflect") {
(
call.arguments.first().and_then(Argument::as_expression),
call.arguments.get(1),
)
} else if matches!(method, "call" | "apply" | "bind") {
(Some(&member.object), call.arguments.first())
} else {
return;
};
let Some(this) = this.and_then(given) else {
return;
};
match this.get_inner_expression() {
Expression::NullLiteral(_) => return,
Expression::Identifier(id) if id.name.as_str() == "undefined" && resolved(self.scoping, id).is_none() => {
return;
}
_ => {}
}
let this_itself = matches!(this.get_inner_expression(), Expression::ThisExpression(_));
match target.map(Expression::get_inner_expression) {
Some(Expression::Identifier(id)) => self.rebound.extend(local_symbol(self.scoping, id)),
Some(other) if reach(other).is_some_and(|(id, _, _)| resolved(self.scoping, id).is_none()) => {}
_ => self.rebinds |= !this_itself,
}
}
fn this_changes_modules(&self) -> bool {
let rebinds = self.rebinds
|| self
.rebound
.iter()
.any(|&symbol| !self.makers.values().any(|maker| maker.binding == Some(symbol)));
let loaded = self.loaded_makers();
self.this_writes.iter().any(|&(site, keyed)| match site {
ThisSite::Module | ThisSite::Class | ThisSite::Super => true,
ThisSite::Load => false,
ThisSite::Instance => !keyed,
ThisSite::Plain(binding, scope) => rebinds || self.handed_out(binding, scope),
ThisSite::Made(maker) => rebinds || loaded.contains(&maker),
})
}
fn handed_out(&self, binding: Option<SymbolId>, scope: ScopeId) -> bool {
let Some(symbol) = binding else {
return !self.iifes.contains(&scope);
};
self.exported.contains(&symbol)
|| self.default_exports.contains(&scope)
|| self.rebound.contains(&symbol)
|| self.scoping.get_resolved_reference_ids(symbol).iter().any(|reference| {
self.scoping.get_reference(*reference).is_value() && !self.callee_refs.contains(reference)
})
}
fn loaded_makers(&self) -> HashSet<ScopeId> {
let root = self.scoping.root_scope_id();
let mut loaded: HashSet<ScopeId> = HashSet::default();
loaded.insert(root);
loop {
let mut grew = false;
for (&scope, maker) in &self.makers {
if loaded.contains(&scope) {
continue;
}
let made = match maker.binding {
Some(symbol) => self
.scoping
.get_resolved_reference_ids(symbol)
.iter()
.any(|&reference| {
loaded.contains(&self.maker_of(self.scoping.get_reference(reference).scope_id()))
}),
None => !maker.default_export && loaded.contains(&maker.enclosing),
};
if made {
loaded.insert(scope);
grew = true;
}
}
if !grew {
return loaded;
}
}
}
fn maker_of(&self, scope: ScopeId) -> ScopeId {
let root = self.scoping.root_scope_id();
self.scoping
.scope_ancestors(scope)
.find(|ancestor| *ancestor == root || self.makers.contains_key(ancestor))
.unwrap_or(root)
}
fn mutate_arguments(&mut self, arguments: &[Argument<'_>]) {
for argument in arguments.iter().filter_map(given) {
self.note_this(argument, false);
for (id, keys) in given_parts(argument) {
if let Some(symbol) = self.symbol(id) {
self.changed(symbol, Write::contents(keys));
}
}
}
}
fn callbacks(&mut self, callee: &Expression<'_>, arguments: &[Argument<'_>]) {
let functions: Vec<Parameters> = arguments
.iter()
.filter_map(Argument::as_expression)
.filter_map(|argument| match argument.get_inner_expression() {
Expression::ArrowFunctionExpression(f) => Some(Parameters::of(&f.params)),
Expression::FunctionExpression(f) => Some(Parameters::of(&f.params)),
_ => None,
})
.collect();
if functions.iter().all(|parameters| parameters.names().next().is_none()) {
return;
}
let receiver = callee.as_member_expression().map(MemberExpression::object);
let list = receiver.and_then(|receiver| self.primitive_source(receiver));
let of_receiver = receiver.map_or_else(Vec::new, |receiver| self.receiver_refs(receiver));
let mut held = if list.is_some() {
Vec::new()
} else {
of_receiver.clone()
};
for argument in arguments {
held.extend(self.refs(Part::Value, |refs| refs.visit_argument(argument)));
}
for parameters in functions {
let positions = parameters
.items
.iter()
.enumerate()
.flat_map(|(position, (names, _))| names.iter().map(move |&name| (name, position == 0)))
.chain(parameters.rest.iter().map(|&name| (name, false)));
let names: Vec<(SymbolId, bool)> = positions.collect();
if let Some(receiver) = receiver {
let symbols: Vec<SymbolId> = names.iter().map(|&(name, _)| name).collect();
self.bind_this(&symbols, receiver);
}
for (name, first) in names {
if let Some(list) = list {
if first {
let loading = self.loading();
self.element_links.push(Link {
a: Place::of(name),
b: Place::of(list),
join: Join::Within,
loading,
});
} else {
self.hold(&Place::of(name), false, of_receiver.clone());
}
}
self.hold(&Place::of(name), false, held.clone());
}
}
}
fn receiver_refs(&self, receiver: &Expression<'_>) -> Vec<Held> {
if is_inert_package(self.scoping, &self.known, receiver) {
return Vec::new();
}
self.refs(Part::Value, |refs| refs.visit_expression(receiver))
}
fn primitive_source(&self, expr: &Expression<'_>) -> Option<SymbolId> {
let source = match expr.get_inner_expression() {
Expression::CallExpression(call) if lists_entries(self.names(), call) => {
call.arguments.first()?.as_expression()?
}
other => other,
};
match source.get_inner_expression() {
Expression::Identifier(id) => self.symbol(id).filter(|symbol| self.primitive_lists.contains(symbol)),
_ => None,
}
}
fn target_parts<'b, 'a>(
&self,
target: &'b AssignmentTarget<'a>,
places: &mut Vec<Place>,
defaults: &mut Vec<&'b Expression<'a>>,
) {
if let Some(simple) = target.as_simple_assignment_target() {
let place = match simple {
SimpleAssignmentTarget::AssignmentTargetIdentifier(id) => self.symbol(id).map(Place::of),
other => other
.as_member_expression()
.and_then(|member| self.member_place(member))
.map(|(place, _)| place),
};
places.extend(place);
return;
}
match target.as_assignment_target_pattern() {
Some(AssignmentTargetPattern::ArrayAssignmentTarget(array)) => {
for element in array.elements.iter().flatten() {
self.default_target_parts(element, places, defaults);
}
if let Some(rest) = &array.rest {
self.target_parts(&rest.target, places, defaults);
}
}
Some(AssignmentTargetPattern::ObjectAssignmentTarget(object)) => {
for property in &object.properties {
match property {
AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(p) => {
places.extend(self.symbol(&p.binding).map(Place::of));
defaults.extend(&p.init);
}
AssignmentTargetProperty::AssignmentTargetPropertyProperty(p) => {
self.default_target_parts(&p.binding, places, defaults);
}
}
}
if let Some(rest) = &object.rest {
self.target_parts(&rest.target, places, defaults);
}
}
None => {}
}
}
fn default_target_parts<'b, 'a>(
&self,
target: &'b AssignmentTargetMaybeDefault<'a>,
places: &mut Vec<Place>,
defaults: &mut Vec<&'b Expression<'a>>,
) {
match target {
AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(with) => {
defaults.push(&with.init);
self.target_parts(&with.binding, places, defaults);
}
other => {
if let Some(target) = other.as_assignment_target() {
self.target_parts(target, places, defaults);
}
}
}
}
fn destructure(&mut self, target: &AssignmentTarget<'_>, mut held: Vec<Held>) {
let mut places = Vec::new();
let mut defaults = Vec::new();
self.target_parts(target, &mut places, &mut defaults);
if places.is_empty() {
return self.link_among(&held);
}
for default in defaults {
held.extend(self.refs(Part::Value, |refs| refs.visit_expression(default)));
}
for place in places {
self.hold(&place, false, held.clone());
}
}
fn effect(&self, call: &CallExpression<'_>) -> Option<Effect> {
if calls_callable(self.scoping, &self.callables, call) {
return Some(Effect::Followed);
}
let names = self.names();
let keeps = pure_function(names, call).is_some()
|| self.keeps_apart(call)
|| assigns(names, call)
|| (!self.calls_back_unknown(call)
&& matches!(
call.callee.get_inner_expression(),
Expression::StaticMemberExpression(m)
if (PURE_METHODS.contains(&m.property.name.as_str())
|| LIST_ADDERS.contains(&m.property.name.as_str())
|| OWN_RESULT_METHODS.contains(&m.property.name.as_str()))
&& builtin(names, &m.object)
));
keeps.then_some(Effect::Keeps)
}
fn calls_back_unknown(&self, call: &CallExpression<'_>) -> bool {
let Expression::StaticMemberExpression(m) = call.callee.get_inner_expression() else {
return false;
};
let method = m.property.name.as_str();
if !CALLBACK_METHODS.contains(&method) {
return false;
}
let followed = call.arguments.first().is_none_or(|argument| {
is_inline_function(argument)
|| matches!(
argument,
Argument::Identifier(id)
if local_symbol(self.scoping, id).is_some_and(|symbol| self.callables.contains(symbol))
)
});
let with_this = !matches!(method, "reduce" | "reduceRight" | "toSorted") && call.arguments.len() > 1;
!followed || with_this
}
fn keeps_apart(&self, call: &CallExpression<'_>) -> bool {
let names = self.names();
(defined(call).is_some() && foreign(names, &call.callee)) || copies(names, call)
}
fn of_changing_module(&self, expr: &Expression<'_>) -> bool {
reach(expr)
.and_then(|(id, _, _)| self.symbol(id))
.is_some_and(|symbol| self.changing_binding(symbol))
}
fn changing_binding(&self, symbol: SymbolId) -> bool {
let Some((module, _)) = self.known.modules.get(&symbol) else {
return false;
};
if self.known.followed.contains(&symbol) {
self.imports.changes_modules(module)
} else {
is_package(module) && !self.imports.is_installed(module)
}
}
fn note_callee(&mut self, callee: &Expression<'_>) {
if let Some(reference) = reach(callee).and_then(|(id, _, _)| id.reference_id.get()) {
self.callee_refs.insert(reference);
}
}
fn exposed(&self) -> (Vec<String>, bool) {
let mut specifiers: Vec<String> = Vec::new();
let mut unknown = false;
for (&symbol, (module, _)) in &self.known.modules {
let handed_out = self.scoping.get_resolved_reference_ids(symbol).iter().any(|reference| {
self.scoping.get_reference(*reference).is_value() && !self.callee_refs.contains(reference)
});
if !handed_out {
continue;
}
if self.known.followed.contains(&symbol) {
if !specifiers.contains(module) {
specifiers.push(module.clone());
}
} else {
unknown |= is_package(module) && !self.imports.is_installed(module);
}
}
specifiers.sort();
(specifiers, unknown)
}
fn runs_followed(&self, call: &CallExpression<'_>) -> bool {
let given_function = || {
self.effect(call).is_none()
&& call.arguments.iter().filter_map(given).any(|argument| {
matches!(argument.get_inner_expression(), Expression::Identifier(_))
&& self.of_changing_module(argument)
})
};
self.of_changing_module(&call.callee) || given_function()
}
fn changed(&mut self, symbol: SymbolId, write: Write) {
self.changed_as(symbol, write, false);
}
fn changed_as(&mut self, symbol: SymbolId, write: Write, result: bool) {
self.written(symbol, Change::Contents(vec![write.clone()]), result);
self.mutated.push((symbol, write));
}
fn holds_function(&self, item: &Held) -> bool {
!item.result && self.callables.contains(item.symbol)
}
fn hold(&mut self, hub: &Place, exact: bool, held: Vec<Held>) {
let loading = self.loading() && !self.callables.contains(hub.symbol);
self.link_held(hub, exact, held, loading);
}
fn link_held(&mut self, hub: &Place, exact: bool, held: Vec<Held>, loading: bool) {
for item in held {
let mut keys = hub.keys.clone();
if exact {
keys.extend(item.at);
}
self.links.push(Link {
a: Place {
symbol: hub.symbol,
keys,
},
b: Place {
symbol: item.symbol,
keys: item.keys,
},
join: if exact { item.join } else { Join::Within },
loading,
});
}
}
fn link_among(&mut self, held: &[Held]) {
let loading = self.loading() && !held.iter().any(|item| self.holds_function(item));
for pair in held.windows(2) {
self.links.push(Link {
a: Place {
symbol: pair[0].symbol,
keys: pair[0].keys.clone(),
},
b: Place {
symbol: pair[1].symbol,
keys: pair[1].keys.clone(),
},
join: Join::Within,
loading,
});
}
}
fn hold_or_link(&mut self, hub: Option<(Place, bool)>, held: Vec<Held>, callee: bool) {
match hub {
Some((place, exact)) if callee => {
let loading = self.loading();
self.link_held(&place, exact, held, loading);
}
Some((place, exact)) => self.hold(&place, exact, held),
None => self.link_among(&held),
}
}
fn refs(&self, part: Part, visit: impl FnOnce(&mut Refs<'_>)) -> Vec<Held> {
let mut refs = Refs {
scoping: self.scoping,
known: &self.known,
callables: &self.callables,
part,
at: Vec::new(),
exact: true,
frozen: false,
out: Vec::new(),
};
visit(&mut refs);
refs.out
}
fn function_part(&self, symbol: Option<SymbolId>) -> Part {
Part::Function {
parameters: !symbol.is_some_and(|symbol| self.callables.apart.contains_key(&symbol)),
}
}
fn pass(&mut self, parameters: &Parameters, call: &CallExpression<'_>) {
for (index, argument) in call.arguments.iter().enumerate() {
let held = self.refs(Part::Value, |refs| refs.visit_argument(argument));
let (names, exact) = parameters.taking(index);
for &name in names {
self.hold(&Place::of(name), exact, held.clone());
}
}
}
fn default_of(&mut self, pattern: &BindingPattern<'_>, value: &Expression<'_>) {
let held = self.refs(Part::Value, |refs| refs.visit_expression(value));
let whole = matches!(pattern, BindingPattern::BindingIdentifier(_));
for name in bound_symbols(pattern) {
self.hold(&Place::of(name), whole, held.clone());
}
}
fn alias(&self, pattern: &BindingPattern<'_>, init: &Expression<'_>) -> Option<(Vec<AliasPart>, SymbolId)> {
if !self.context.in_const {
return None;
}
let (source, keys) = alias_source(init)?;
let root = self.symbol(source)?;
let function = *self.functions.last()?;
if function == self.scoping.root_scope_id() || self.function_of(root) != function {
return None;
}
let names = alias_names(pattern)?
.into_iter()
.map(|(id, key)| {
let mut part = keys.clone();
part.extend(key);
id.symbol_id.get().map(|symbol| (symbol, part))
})
.collect::<Option<Vec<_>>>()?;
Some((names, root))
}
fn read_before(&self, reference: ReferenceId, from: u32) -> bool {
!self.dynamic
&& !self.repeated.contains(&reference)
&& self.ends.get(&reference).is_some_and(|&end| end <= from)
}
fn pairs(&self) -> Vec<(SymbolId, SymbolId)> {
self.links.iter().map(|link| (link.a.symbol, link.b.symbol)).collect()
}
fn follows(&self, index: usize, parent: &mut HashMap<SymbolId, SymbolId>) -> bool {
let (alias, root) = self.aliases[index];
self.aliases.iter().filter(|(_, other)| *other == root).count() == 1
&& !self.scoping.symbol_is_mutated(root)
&& !self.changes.contains_key(&alias)
&& !self.changes.contains_key(&root)
&& !self.mutated.iter().any(|(symbol, _)| *symbol == root)
&& !self.shares_with_changed(parent, alias, &[alias])
&& !self.shares_with_changed(parent, root, &[alias, root])
}
fn shares_with_changed(
&self,
parent: &mut HashMap<SymbolId, SymbolId>,
symbol: SymbolId,
except: &[SymbolId],
) -> bool {
let class = find(parent, symbol);
self.mutated
.iter()
.any(|&(other, _)| !except.contains(&other) && find(parent, other) == class)
}
fn link_aliases(&mut self) -> HashSet<SymbolId> {
let mut linked = vec![false; self.aliases.len()];
loop {
let mut parent = forest(&self.pairs());
let unfollowed: Vec<usize> = (0..self.aliases.len())
.filter(|&index| !linked[index] && !self.follows(index, &mut parent))
.collect();
if unfollowed.is_empty() {
break;
}
for index in unfollowed {
linked[index] = true;
let (alias, root) = self.aliases[index];
self.links.push(Link {
a: Place::of(alias),
b: Place::of(root),
join: Join::Within,
loading: false,
});
}
}
self.aliases
.iter()
.zip(linked)
.filter(|(_, linked)| !linked)
.map(|(&(alias, _), _)| alias)
.collect()
}
fn link_elements(&mut self) {
let mut pending = std::mem::take(&mut self.element_links);
if self.dynamic {
return self.links.append(&mut pending);
}
while !pending.is_empty() {
let base = self.links.len();
self.links.extend(self.aliases.iter().map(|&(alias, root)| Link {
a: Place::of(alias),
b: Place::of(root),
join: Join::Within,
loading: false,
}));
let links_of = self.links_of();
let reached: HashSet<SymbolId> = self
.mutated
.iter()
.flat_map(|(symbol, write)| self.reached(&links_of, *symbol, write))
.map(|(symbol, _)| symbol)
.collect();
self.links.truncate(base);
self.classes.replace(None);
let (linked, rest): (Vec<Link>, Vec<Link>) =
pending.into_iter().partition(|link| reached.contains(&link.b.symbol));
pending = rest;
if linked.is_empty() {
break;
}
self.links.extend(linked);
}
}
fn drop_primitives(&mut self) {
let primitives = &self.primitives;
self.links
.retain(|link| !primitives.contains(&link.a.symbol) && !primitives.contains(&link.b.symbol));
self.mutated.retain(|(symbol, _)| !primitives.contains(symbol));
self.changes.retain(|symbol, _| !primitives.contains(symbol));
self.function_writes
.retain(|(symbol, _, _, _)| !primitives.contains(symbol));
}
fn links_of(&self) -> HashMap<SymbolId, Vec<usize>> {
self.links_of_where(|_| true)
}
fn links_of_where(&self, keep: impl Fn(&Link) -> bool) -> HashMap<SymbolId, Vec<usize>> {
let mut out: HashMap<SymbolId, Vec<usize>> = HashMap::default();
for (index, link) in self.links.iter().enumerate().filter(|(_, link)| keep(link)) {
out.entry(link.a.symbol).or_default().push(index);
if link.b.symbol != link.a.symbol {
out.entry(link.b.symbol).or_default().push(index);
}
}
out
}
fn reached(
&self,
links_of: &HashMap<SymbolId, Vec<usize>>,
symbol: SymbolId,
origin: &Write,
) -> Vec<(SymbolId, Write)> {
let mut seen = vec![(symbol, origin.clone())];
let mut by_symbol: HashMap<SymbolId, Vec<usize>> = HashMap::default();
by_symbol.entry(symbol).or_default().push(0);
let mut queue = seen.clone();
while let Some((at, write)) = queue.pop() {
for &index in links_of.get(&at).map_or(&[][..], Vec::as_slice) {
let link = &self.links[index];
for (from, to) in [(&link.a, &link.b), (&link.b, &link.a)] {
if from.symbol != at {
continue;
}
let Some(mut next) = through(from, to, link.join, &write) else {
continue;
};
if next.keys.len() > MAX_KEYS {
next = Write::contents(next.keys[..MAX_KEYS].to_vec());
}
let known = by_symbol.entry(to.symbol).or_default();
if known.iter().any(|&at| seen[at].1.covers(&next)) {
continue;
}
if seen.len() >= MAX_PLACES {
return self.linked_in_full(symbol);
}
known.push(seen.len());
seen.push((to.symbol, next.clone()));
queue.push((to.symbol, next));
}
}
}
seen
}
fn linked_in_full(&self, symbol: SymbolId) -> Vec<(SymbolId, Write)> {
let mut cached = self.classes.borrow_mut();
if cached.as_ref().is_none_or(|(count, _)| *count != self.links.len()) {
*cached = Some((self.links.len(), classes(&self.pairs())));
}
let class = cached.as_ref().and_then(|(_, classes)| classes.get(&symbol));
class.map_or_else(
|| vec![(symbol, Write::default())],
|class| class.iter().map(|&other| (other, Write::default())).collect(),
)
}
fn share(&mut self) -> (HashSet<SymbolId>, bool) {
self.link_imports();
self.drop_primitives();
self.link_elements();
let followed = self.link_aliases();
let applied = self.applied_aliases(&followed);
for alias in &applied {
self.links.push(Link {
a: Place::of(alias.alias),
b: Place {
symbol: alias.root,
keys: alias.keys.clone(),
},
join: Join::Same,
loading: false,
});
}
let mut origins: Vec<(SymbolId, Write)> = Vec::new();
for origin in &self.mutated {
if !origins.contains(origin) {
origins.push(origin.clone());
}
}
let links_of = self.links_of();
for (symbol, origin) in origins {
let through_alias = applied.iter().find(|alias| alias.alias == symbol);
for (other, write) in self.reached(&links_of, symbol, &origin) {
if let Some(&site) = self.this_bound.get(&other) {
self.wrote_this(site, false);
}
if other == symbol && origin.covers(&write) {
continue;
}
let change = Change::Contents(vec![write.clone()]);
match through_alias {
Some(alias) if other == alias.root && alias.applied(&origin).covers(&write) => {}
Some(alias) if self.function_of(other) == self.function_of(symbol) => {
self.late.push((other, alias.from, change));
}
None | Some(_) => self.change(other, change),
}
}
}
let writes_modules = self.writes_modules(&links_of);
if self.runs_modules {
self.affect_imports(&links_of);
}
(followed, writes_modules)
}
fn writes_modules(&self, links_of: &HashMap<SymbolId, Vec<usize>>) -> bool {
if self.this_changes_modules() {
return true;
}
if self.function_writes.is_empty() {
return false;
}
if self.dynamic {
return true;
}
let fresh_links_of = self.links_of_where(|link| !link.loading);
let root = self.scoping.root_scope_id();
self.function_writes.iter().any(|(symbol, function, change, result)| {
let home = self.function_of(*symbol);
let fresh = (home == *function && !self.suspending.contains(function))
|| (home == root && *result && self.callables.contains(*symbol));
match change {
Change::Any => home != *function,
Change::Contents(writes) => writes.iter().any(|write| {
if fresh {
self.fresh_reach_modules(links_of, &fresh_links_of, *symbol, *function, write)
} else {
self.closure_reaches_modules(links_of, *symbol, write)
}
}),
}
})
}
fn fresh_reach_modules(
&self,
links_of: &HashMap<SymbolId, Vec<usize>>,
fresh_links_of: &HashMap<SymbolId, Vec<usize>>,
symbol: SymbolId,
function: ScopeId,
write: &Write,
) -> bool {
self.reached(fresh_links_of, symbol, write)
.iter()
.any(|(other, change)| {
self.is_module_value(*other)
|| (self.encloses(*other, function) && self.closure_reaches_modules(links_of, *other, change))
})
}
fn closure_reaches_modules(
&self,
links_of: &HashMap<SymbolId, Vec<usize>>,
symbol: SymbolId,
write: &Write,
) -> bool {
self.reached(links_of, symbol, write)
.iter()
.any(|(other, _)| self.is_module_value(*other))
}
fn result_places(&self) -> HashMap<String, Vec<Write>> {
if self.dynamic {
return HashMap::default();
}
let root = self.scoping.root_scope_id();
let mut places: HashMap<SymbolId, Vec<Write>> = self
.callables
.joined
.iter()
.chain(self.callables.apart.keys())
.filter(|&&symbol| self.scoping.symbol_scope_id(symbol) == root)
.map(|&symbol| (symbol, Vec::new()))
.collect();
let fresh_links_of = self.links_of_where(|link| !link.loading);
let mut values: Vec<SymbolId> = fresh_links_of
.keys()
.copied()
.filter(|&symbol| self.is_module_value(symbol))
.collect();
values.sort_unstable();
for value in values {
for (other, write) in self.reached(&fresh_links_of, value, &Write::default()) {
if let Some(writes) = places.get_mut(&other) {
add_write(writes, write);
}
}
}
places
.into_iter()
.map(|(symbol, mut writes)| {
writes.sort();
(self.scoping.symbol_name(symbol).to_owned(), writes)
})
.collect()
}
fn is_module_value(&self, symbol: SymbolId) -> bool {
self.function_of(symbol) == self.scoping.root_scope_id() && !self.callables.contains(symbol)
}
fn encloses(&self, symbol: SymbolId, function: ScopeId) -> bool {
let home = self.function_of(symbol);
home != function
&& home != self.scoping.root_scope_id()
&& self.scoping.scope_ancestors(function).any(|scope| scope == home)
}
fn link_imports(&mut self) {
let mut imports: Vec<SymbolId> = self.known.followed.iter().copied().collect();
imports.sort_unstable();
for pair in imports.windows(2) {
self.links.push(Link {
a: Place::of(pair[0]),
b: Place::of(pair[1]),
join: Join::Within,
loading: false,
});
}
for (index, &a) in imports.iter().enumerate() {
for &b in &imports[index + 1..] {
if let Some((a, b)) = self.export_part(a, b).or_else(|| self.export_part(b, a)) {
self.links.push(Link {
a,
b,
join: Join::Same,
loading: false,
});
}
}
}
}
fn export_part(&self, whole: SymbolId, part: SymbolId) -> Option<(Place, Place)> {
let (whole_module, _) = self.known.modules.get(&whole)?;
let (part_module, _) = self.known.modules.get(&part)?;
let whole_path = self.known.import_paths.get(&whole)?;
let below = self
.known
.import_paths
.get(&part)?
.strip_prefix(whole_path.as_slice())?;
(whole_module == part_module).then(|| {
(
Place {
symbol: whole,
keys: below.to_vec(),
},
Place::of(part),
)
})
}
fn affect_imports(&mut self, links_of: &HashMap<SymbolId, Vec<usize>>) {
let mut imports: Vec<SymbolId> = self.known.followed.iter().copied().collect();
imports.sort_unstable();
for import in imports {
for (other, write) in self.reached(links_of, import, &Write::default()) {
self.change(other, Change::Contents(vec![write]));
}
}
}
fn applied_aliases(&self, followed: &HashSet<SymbolId>) -> Vec<AppliedAlias> {
self.aliases
.iter()
.filter(|(alias, _)| followed.contains(alias))
.filter_map(|&(alias, root)| {
let (keys, from) = self.alias_parts.get(&alias)?;
Some(AppliedAlias {
alias,
root,
keys: keys.clone(),
from: *from,
})
})
.collect()
}
}
fn through(from: &Place, to: &Place, join: Join, write: &Write) -> Option<Write> {
if write.keys.starts_with(&from.keys) {
let below = &write.keys[from.keys.len()..];
return match join {
Join::Within => Some(Write::contents(to.keys.clone())),
Join::Copy if below.is_empty() && write.key.is_some() => None,
Join::Same | Join::Copy => {
let mut keys = to.keys.clone();
keys.extend_from_slice(below);
Some(Write {
keys,
key: write.key.clone(),
})
}
};
}
(from.keys.starts_with(&write.keys) && write.key.is_none()).then(|| Write::contents(to.keys.clone()))
}
impl<'a> Visit<'a> for Sharing<'_> {
fn visit_function(&mut self, it: &Function<'a>, flags: ScopeFlags) {
if let Some(symbol) = it.id.as_ref().and_then(|id| id.symbol_id.get()) {
let part = self.function_part(Some(symbol));
let held = self.refs(part, |refs| refs.visit_function(it, flags));
self.in_definition(true, |sharing| sharing.hold(&Place::of(symbol), true, held));
}
let scope = it.scope_id.get().unwrap_or_else(|| self.scoping.root_scope_id());
if it.generator || it.r#async {
self.suspending.insert(scope);
}
let site = self.enter_function(scope, it.id.as_ref().and_then(|id| id.symbol_id.get()));
self.this_sites.push(site);
self.functions.push(scope);
walk::walk_function(self, it, flags);
self.functions.pop();
self.this_sites.pop();
}
fn visit_arrow_function_expression(&mut self, it: &ArrowFunctionExpression<'a>) {
let scope = it.scope_id.get().unwrap_or_else(|| self.scoping.root_scope_id());
if it.r#async {
self.suspending.insert(scope);
}
self.add_maker(scope, self.named_inits.get(&scope).copied());
self.functions.push(scope);
walk::walk_arrow_function_expression(self, it);
self.functions.pop();
}
fn visit_class(&mut self, it: &Class<'a>) {
let Some(scope) = it.scope_id.get() else {
return walk::walk_class(self, it);
};
let binding = self
.named_inits
.get(&scope)
.copied()
.or_else(|| it.id.as_ref().and_then(|id| id.symbol_id.get()));
self.add_maker(scope, binding);
self.functions.push(scope);
walk::walk_class(self, it);
self.functions.pop();
}
fn visit_method_definition(&mut self, it: &MethodDefinition<'a>) {
if let Some(scope) = it.value.scope_id.get() {
let site = if matches!(it.kind, MethodDefinitionKind::Constructor) {
ThisSite::Instance
} else if it.r#static {
ThisSite::Class
} else {
ThisSite::Made(
self.functions
.last()
.copied()
.unwrap_or_else(|| self.scoping.root_scope_id()),
)
};
self.method_sites.insert(scope, site);
}
walk::walk_method_definition(self, it);
}
fn visit_property_definition(&mut self, it: &PropertyDefinition<'a>) {
self.this_sites.push(if it.r#static {
ThisSite::Load
} else {
ThisSite::Instance
});
walk::walk_property_definition(self, it);
self.this_sites.pop();
}
fn visit_static_block(&mut self, it: &StaticBlock<'a>) {
self.this_sites.push(ThisSite::Load);
walk::walk_static_block(self, it);
self.this_sites.pop();
}
fn visit_object_property(&mut self, it: &ObjectProperty<'a>) {
if let Expression::FunctionExpression(function) = it.value.get_inner_expression()
&& let Some(scope) = function.scope_id.get()
{
let maker = self
.functions
.last()
.copied()
.unwrap_or_else(|| self.scoping.root_scope_id());
self.method_sites.insert(scope, ThisSite::Made(maker));
}
walk::walk_object_property(self, it);
}
fn visit_export_default_declaration(&mut self, it: &ExportDefaultDeclaration<'a>) {
let scope = match &it.declaration {
ExportDefaultDeclarationKind::FunctionDeclaration(function) => function.scope_id.get(),
ExportDefaultDeclarationKind::ClassDeclaration(class) => class.scope_id.get(),
other => match other.as_expression().map(Expression::get_inner_expression) {
Some(Expression::FunctionExpression(function)) => function.scope_id.get(),
Some(Expression::ArrowFunctionExpression(function)) => function.scope_id.get(),
Some(Expression::ClassExpression(class)) => class.scope_id.get(),
_ => None,
},
};
self.default_exports.extend(scope);
walk::walk_export_default_declaration(self, it);
}
fn visit_static_member_expression(&mut self, it: &StaticMemberExpression<'a>) {
let name = it.property.name.as_str();
if matches!(name, "prototype" | "__proto__")
&& reach(&it.object).is_some_and(|(id, _, _)| resolved(self.scoping, id).is_some())
{
self.rebinds = true;
}
walk::walk_static_member_expression(self, it);
}
fn visit_variable_declaration(&mut self, it: &VariableDeclaration<'a>) {
let saved = std::mem::replace(&mut self.context.in_const, it.kind == VariableDeclarationKind::Const);
walk::walk_variable_declaration(self, it);
self.context.in_const = saved;
}
fn visit_variable_declarator(&mut self, it: &VariableDeclarator<'a>) {
if let (BindingPattern::BindingIdentifier(id), Some(init)) = (&it.id, &it.init) {
let scope = match init.get_inner_expression() {
Expression::ArrowFunctionExpression(function) => function.scope_id.get(),
Expression::FunctionExpression(function) => function.scope_id.get(),
Expression::ClassExpression(class) => class.scope_id.get(),
_ => None,
};
if let (Some(scope), Some(symbol)) = (scope, id.symbol_id.get()) {
self.named_inits.insert(scope, symbol);
}
}
if let Some(init) = &it.init {
self.bind_this(&bound_symbols(&it.id), init);
}
if let Some(init) = &it.init
&& let Some((names, root)) = self.alias(&it.id, init)
{
for (name, keys) in names {
self.aliases.push((name, root));
self.alias_parts.insert(name, (keys, it.span.end));
}
} else if let Some(init) = &it.init {
let function = matches!(
init.get_inner_expression(),
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)
);
let part = if function {
self.function_part(it.id.get_binding_identifier().and_then(|id| id.symbol_id.get()))
} else {
Part::Value
};
let held = self.refs(part, |refs| refs.visit_expression(init));
let whole = matches!(it.id, BindingPattern::BindingIdentifier(_));
let names: Vec<SymbolId> = it
.id
.get_binding_identifiers()
.iter()
.filter_map(|id| id.symbol_id.get())
.collect();
if whole && self.context.in_const && is_primitive(self.names(), init) {
self.primitives.extend(names.iter().copied());
}
if whole
&& self.context.in_const
&& (is_primitive_list(self.names(), init) || is_primitive_record(self.names(), init))
{
self.primitive_lists.extend(names.iter().copied());
}
self.in_definition(function, |sharing| {
if names.is_empty() {
sharing.link_among(&held);
}
for name in names {
sharing.hold(&Place::of(name), whole, held.clone());
}
});
}
walk::walk_variable_declarator(self, it);
}
fn visit_assignment_expression(&mut self, it: &AssignmentExpression<'a>) {
let mut places = Vec::new();
let mut defaults = Vec::new();
self.target_parts(&it.left, &mut places, &mut defaults);
let targets: Vec<SymbolId> = places.iter().map(|place| place.symbol).collect();
self.bind_this(&targets, &it.right);
let part = if matches!(
it.right.get_inner_expression(),
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)
) {
Part::Function { parameters: true }
} else {
Part::Value
};
let held = self.refs(part, |refs| refs.visit_expression(&it.right));
match it.left.as_simple_assignment_target() {
Some(target) => {
let hubs: Vec<(Place, bool)> = match target {
SimpleAssignmentTarget::AssignmentTargetIdentifier(id) => self
.symbol(id)
.map(|symbol| (Place::of(symbol), true))
.into_iter()
.collect(),
other => other
.as_member_expression()
.map_or_default(|member| self.member_places(member)),
};
let function = matches!(part, Part::Function { .. });
self.in_definition(function, |sharing| {
for (place, exact) in hubs {
sharing.hold(&place, exact, held.clone());
}
});
}
None => self.destructure(&it.left, held),
}
walk::walk_assignment_expression(self, it);
}
fn visit_for_of_statement(&mut self, it: &ForOfStatement<'a>) {
let held = self.refs(Part::Value, |refs| refs.visit_expression(&it.right));
let source = self.primitive_source(&it.right);
match &it.left {
ForStatementLeft::VariableDeclaration(declaration) => {
for declarator in &declaration.declarations {
self.bind_this(&bound_symbols(&declarator.id), &it.right);
for name in bound_symbols(&declarator.id) {
match source {
Some(list) => {
let loading = self.loading();
self.element_links.push(Link {
a: Place::of(name),
b: Place::of(list),
join: Join::Within,
loading,
});
}
None => self.hold(&Place::of(name), false, held.clone()),
}
}
}
}
left => {
if let Some(target) = left.as_assignment_target() {
self.destructure(target, held);
}
}
}
self.in_loop(|sharing| walk::walk_for_of_statement(sharing, it));
}
fn visit_formal_parameter(&mut self, it: &FormalParameter<'a>) {
if let Some(initializer) = &it.initializer {
self.bind_this(&bound_symbols(&it.pattern), initializer);
self.default_of(&it.pattern, initializer);
}
walk::walk_formal_parameter(self, it);
}
fn visit_assignment_pattern(&mut self, it: &AssignmentPattern<'a>) {
self.default_of(&it.left, &it.right);
walk::walk_assignment_pattern(self, it);
}
fn visit_simple_assignment_target(&mut self, it: &SimpleAssignmentTarget<'a>) {
match it {
SimpleAssignmentTarget::AssignmentTargetIdentifier(id) => {
if let Some(symbol) = self.symbol(id) {
self.written(symbol, Change::Any, false);
}
}
other => {
if let Some(member) = other.as_member_expression() {
self.set(member);
}
}
}
walk::walk_simple_assignment_target(self, it);
}
fn visit_assignment_target_property_identifier(&mut self, it: &AssignmentTargetPropertyIdentifier<'a>) {
if let Some(symbol) = self.symbol(&it.binding) {
self.written(symbol, Change::Any, false);
}
walk::walk_assignment_target_property_identifier(self, it);
}
fn visit_unary_expression(&mut self, it: &UnaryExpression<'a>) {
if it.operator == UnaryOperator::Delete
&& let Some(member) = it.argument.get_inner_expression().as_member_expression()
{
self.mutate(member.object());
}
walk::walk_unary_expression(self, it);
}
fn visit_call_expression(&mut self, it: &CallExpression<'a>) {
self.note_callee(&it.callee);
self.note_rebinding(it);
match it.callee.get_inner_expression() {
Expression::FunctionExpression(function) => self.iifes.extend(function.scope_id.get()),
Expression::ArrowFunctionExpression(function) => self.iifes.extend(function.scope_id.get()),
_ => {}
}
if foreign(self.names(), &it.callee) {
self.foreign_calls.insert(it.span);
}
self.runs_modules |= self.runs_followed(it);
let calls_back = self.calls_back_unknown(it);
if calls_back {
self.calls_back.insert(it.span);
}
match self.effect(it) {
Some(effect) => {
if effect == Effect::Followed {
for argument in it.arguments.iter().filter_map(given) {
self.note_this(argument, false);
}
}
self.effects.insert(it.span, effect);
}
None => self.mutate_arguments(&it.arguments),
}
self.callbacks(it.callee.get_inner_expression(), &it.arguments);
let receiver = it
.callee
.get_inner_expression()
.as_member_expression()
.map(MemberExpression::object);
if pure_function(self.names(), it).is_some() {
self.pure.insert(it.span);
return walk::walk_call_expression(self, it);
}
if self.keeps_apart(it) || assigns_fresh(self.names(), it) {
return walk::walk_call_expression(self, it);
}
let callee_hub = matches!(it.callee.get_inner_expression(), Expression::Identifier(_));
let hub = match it.callee.get_inner_expression() {
Expression::Identifier(id) => {
let symbol = local_symbol(self.scoping, id);
if let Some(parameters) = symbol.and_then(|symbol| self.callables.apart.get(&symbol)).cloned() {
self.pass(¶meters, it);
return walk::walk_call_expression(self, it);
}
let exact = calls_callable(self.scoping, &self.callables, it);
if !exact && let Some(symbol) = symbol.filter(|symbol| !self.known.modules.contains_key(symbol)) {
self.called_values.insert(it.span);
self.changed(symbol, Write::default());
}
symbol.map(|symbol| (Place::of(symbol), exact))
}
callee => {
if let Some(target) = changed_by_call(it, false, calls_back) {
self.change_target(it, target);
}
callee
.as_member_expression()
.filter(|member| !is_inert_package(self.scoping, &self.known, member.object()))
.and_then(|member| self.place(member.object()))
.map(|(place, _)| (place, false))
}
};
let keeps_callbacks = pure_method(self.names(), it);
let mut held: Vec<Held> = it
.arguments
.iter()
.filter(|argument| !(keeps_callbacks && is_inline_function(argument)))
.flat_map(|argument| self.refs(Part::Value, |refs| refs.visit_argument(argument)))
.collect();
if hub.is_none()
&& let Some(receiver) = receiver
{
held.extend(self.receiver_refs(receiver));
}
self.hold_or_link(hub, held, callee_hub);
walk::walk_call_expression(self, it);
}
fn visit_new_expression(&mut self, it: &NewExpression<'a>) {
self.note_callee(&it.callee);
self.runs_modules |= self.of_changing_module(&it.callee);
if constructs_builtin(self.scoping, it) {
self.effects.insert(it.span, Effect::Keeps);
} else {
self.mutate_arguments(&it.arguments);
let held: Vec<Held> = it
.arguments
.iter()
.flat_map(|argument| self.refs(Part::Value, |refs| refs.visit_argument(argument)))
.collect();
self.link_among(&held);
}
walk::walk_new_expression(self, it);
}
fn visit_expression_statement(&mut self, it: &ExpressionStatement<'a>) {
if let Some(call) = statement_call(&it.expression)
&& self.effect(call) == Some(Effect::Followed)
{
self.mutate_arguments(&call.arguments);
}
if let Some(call) = statement_call(&it.expression)
&& let Expression::Identifier(callee) = call.callee.get_inner_expression()
&& callee.name.as_str() == "require"
&& resolved(self.scoping, callee).is_none()
&& call
.arguments
.first()
.and_then(Argument::as_expression)
.and_then(static_string)
.is_some_and(|specifier| self.imports.follows(&specifier))
{
self.runs_modules = true;
}
walk::walk_expression_statement(self, it);
}
fn visit_tagged_template_expression(&mut self, it: &TaggedTemplateExpression<'a>) {
self.note_callee(&it.tag);
self.runs_modules |= self.of_changing_module(&it.tag);
walk::walk_tagged_template_expression(self, it);
}
fn visit_import_declaration(&mut self, it: &ImportDeclaration<'a>) {
let effects_only = it.specifiers.as_ref().is_none_or(|specifiers| specifiers.is_empty());
if effects_only && !it.import_kind.is_type() && self.imports.follows(&crate::js_string(it.source.value)) {
self.runs_modules = true;
}
walk::walk_import_declaration(self, it);
}
fn visit_export_declaration(&mut self, it: &ExportDeclaration<'a>) {
match &it.declaration {
Declaration::VariableDeclaration(declaration) => {
for declarator in &declaration.declarations {
self.exported.extend(bound_symbols(&declarator.id));
}
}
Declaration::FunctionDeclaration(function) => {
self.exported
.extend(function.id.as_ref().and_then(|id| id.symbol_id.get()));
}
Declaration::ClassDeclaration(class) => {
self.exported
.extend(class.id.as_ref().and_then(|id| id.symbol_id.get()));
}
_ => {}
}
walk::walk_export_declaration(self, it);
}
fn visit_identifier_reference(&mut self, it: &IdentifierReference<'a>) {
self.dynamic |= matches!(it.name.as_str(), "arguments" | "eval");
if let Some(reference) = it.reference_id.get() {
self.ends.insert(reference, it.span.end);
}
let elsewhere = self
.symbol(it)
.is_some_and(|symbol| self.functions.last() != Some(&self.function_of(symbol)));
if (self.loops > 0 || elsewhere)
&& let Some(reference) = it.reference_id.get()
{
self.repeated.insert(reference);
}
}
fn visit_for_statement(&mut self, it: &ForStatement<'a>) {
self.in_loop(|sharing| walk::walk_for_statement(sharing, it));
}
fn visit_with_statement(&mut self, it: &WithStatement<'a>) {
self.dynamic = true;
self.mutate(&it.object);
walk::walk_with_statement(self, it);
}
fn visit_for_in_statement(&mut self, it: &ForInStatement<'a>) {
self.in_loop(|sharing| walk::walk_for_in_statement(sharing, it));
}
fn visit_while_statement(&mut self, it: &WhileStatement<'a>) {
self.in_loop(|sharing| walk::walk_while_statement(sharing, it));
}
fn visit_do_while_statement(&mut self, it: &DoWhileStatement<'a>) {
self.in_loop(|sharing| walk::walk_do_while_statement(sharing, it));
}
}
struct Refs<'s> {
scoping: &'s Scoping,
known: &'s Known,
callables: &'s Callables,
part: Part,
at: Vec<String>,
exact: bool,
frozen: bool,
out: Vec<Held>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Part {
Function { parameters: bool },
Body,
Value,
}
impl Refs<'_> {
fn parameters(&mut self, params: &FormalParameters<'_>) {
self.out
.extend(Parameters::of(params).names().map(|(symbol, exact)| Held {
symbol,
keys: Vec::new(),
at: Vec::new(),
join: Join::of(exact),
result: false,
}));
}
fn hold(&mut self, id: &IdentifierReference<'_>, keys: Vec<String>, exact: bool) {
self.hold_as(id, keys, Join::of(self.exact && exact));
}
fn hold_as(&mut self, id: &IdentifierReference<'_>, keys: Vec<String>, join: Join) {
self.push(id, keys, join, false);
}
fn hold_result(&mut self, id: &IdentifierReference<'_>) {
self.push(id, Vec::new(), Join::of(self.exact), true);
}
fn push(&mut self, id: &IdentifierReference<'_>, keys: Vec<String>, join: Join, result: bool) {
if self.part == Part::Body {
return;
}
if let Some(symbol) = resolved(self.scoping, id) {
self.out.push(Held {
symbol,
keys,
at: self.at.clone(),
join,
result,
});
}
}
fn function(&mut self, params: &FormalParameters<'_>, body: Part, visit: impl FnOnce(&mut Self)) {
match self.part {
Part::Body => {}
Part::Function { parameters } => {
if parameters {
self.parameters(params);
}
self.part = body;
visit(self);
self.part = Part::Value;
}
Part::Value => self.opaque(|refs| {
refs.part = body;
visit(refs);
refs.part = Part::Value;
}),
}
}
fn opaque(&mut self, visit: impl FnOnce(&mut Self)) {
let saved = (self.exact, self.frozen);
self.exact = false;
self.frozen = true;
visit(self);
(self.exact, self.frozen) = saved;
}
fn callee_roots<'a>(&mut self, callee: &Expression<'a>)
where
Self: Visit<'a>,
{
match callee.get_inner_expression() {
Expression::SequenceExpression(sequence) => {
if let Some(last) = sequence.expressions.last() {
self.callee_roots(last);
}
}
Expression::ConditionalExpression(c) => {
self.callee_roots(&c.consequent);
self.callee_roots(&c.alternate);
}
Expression::LogicalExpression(l) => {
self.callee_roots(&l.left);
self.callee_roots(&l.right);
}
other => match reach(other) {
Some((id, _, _)) => self.hold(id, Vec::new(), false),
None => self.visit_expression(other),
},
}
}
fn at_key(&mut self, key: String, visit: impl FnOnce(&mut Self)) {
if self.frozen {
return visit(self);
}
self.at.push(key);
visit(self);
self.at.pop();
}
fn spread<'a>(&mut self, value: &Expression<'a>)
where
Self: Visit<'a>,
{
match value.get_inner_expression() {
Expression::ObjectExpression(object) => self.visit_object_expression(object),
Expression::ConditionalExpression(c) => {
self.spread(&c.consequent);
self.spread(&c.alternate);
}
Expression::LogicalExpression(l) if l.operator == LogicalOperator::And => self.spread(&l.right),
other => match reach(other) {
Some((id, keys, true)) if self.exact && !self.frozen => self.hold_as(id, keys, Join::Copy),
_ => self.opaque(|refs| refs.visit_expression(other)),
},
}
}
fn member(&mut self, expr: &Expression<'_>) {
match reach(expr) {
Some((id, keys, exact)) => self.hold(id, keys, exact),
None => self.opaque(|refs| walk::walk_expression(refs, expr)),
}
}
}
impl<'a> Visit<'a> for Refs<'_> {
fn visit_expression(&mut self, it: &Expression<'a>) {
match it {
Expression::StaticMemberExpression(_)
| Expression::ComputedMemberExpression(_)
| Expression::PrivateFieldExpression(_) => self.member(it),
Expression::TemplateLiteral(_) | Expression::BinaryExpression(_) | Expression::UnaryExpression(_) => {}
Expression::ConditionalExpression(c) => {
self.visit_expression(&c.consequent);
self.visit_expression(&c.alternate);
}
_ => walk::walk_expression(self, it),
}
}
fn visit_identifier_reference(&mut self, it: &IdentifierReference<'a>) {
self.hold(it, Vec::new(), true);
}
fn visit_object_expression(&mut self, it: &ObjectExpression<'a>) {
for property in &it.properties {
match property {
ObjectPropertyKind::ObjectProperty(p) => {
let key = p
.key
.static_name()
.filter(|_| !p.method && p.kind == PropertyKind::Init)
.map(|key| crate::js_string(key.as_js_str()))
.filter(|key| key != "__proto__");
match key {
Some(key) => self.at_key(key, |refs| refs.visit_expression(&p.value)),
None => self.opaque(|refs| {
refs.visit_property_key(&p.key);
refs.visit_expression(&p.value);
}),
}
}
ObjectPropertyKind::SpreadProperty(s) => self.spread(&s.argument),
}
}
}
fn visit_array_expression(&mut self, it: &ArrayExpression<'a>) {
self.opaque(|refs| walk::walk_array_expression(refs, it));
}
fn visit_call_expression(&mut self, it: &CallExpression<'a>) {
let names = Names {
scoping: self.scoping,
known: self.known,
};
match pure_function(names, it) {
Some(Returns::Shared) => {
return self.opaque(|refs| {
for argument in &it.arguments {
refs.visit_argument(argument);
}
});
}
Some(_) => return,
None => {}
}
if assigns_fresh(names, it) {
for (index, argument) in it.arguments.iter().enumerate() {
if let Some(value) = argument.as_expression() {
if index == 0 {
self.visit_expression(value);
} else {
self.spread(value);
}
}
}
return;
}
match it.callee.get_inner_expression() {
Expression::Identifier(id) if calls_callable(self.scoping, self.callables, it) => {
return self.hold_result(id);
}
Expression::Identifier(id) => {
let known = self.known;
let places = resolved(self.scoping, id).and_then(|symbol| known.results.get(&symbol));
if let Some(places) = places {
for place in places {
let depth = self.at.len();
if !self.frozen {
self.at.extend(place.keys.iter().cloned());
}
self.opaque(|refs| refs.hold(id, Vec::new(), true));
self.at.truncate(depth);
}
} else if is_followed_import(self.scoping, self.known, id) {
self.opaque(|refs| refs.hold(id, Vec::new(), true));
} else if local_symbol(self.scoping, id).is_some() {
self.hold(id, Vec::new(), true);
}
}
Expression::StaticMemberExpression(member)
if OWN_RESULT_METHODS.contains(&member.property.name.as_str()) && builtin(names, &member.object) => {}
callee if callee.is_member_expression() => {
if let Some(member) = callee.as_member_expression()
&& !is_inert_package(self.scoping, self.known, member.object())
{
let object = member.object();
self.opaque(|refs| refs.visit_expression(object));
}
}
callee => self.opaque(|refs| refs.callee_roots(callee)),
}
let defined = defined(it).is_some() && foreign(names, &it.callee);
for (index, argument) in it.arguments.iter().enumerate() {
if defined && index == 0 {
self.visit_argument(argument);
} else {
self.opaque(|refs| refs.visit_argument(argument));
}
}
}
fn visit_new_expression(&mut self, it: &NewExpression<'a>) {
self.opaque(|refs| {
match it.callee.get_inner_expression() {
Expression::Identifier(id) => {
if local_symbol(refs.scoping, id).is_some() || is_followed_import(refs.scoping, refs.known, id) {
refs.hold(id, Vec::new(), true);
}
}
callee => refs.visit_expression(callee),
}
for argument in &it.arguments {
refs.visit_argument(argument);
}
});
}
fn visit_tagged_template_expression(&mut self, it: &TaggedTemplateExpression<'a>) {
self.opaque(|refs| walk::walk_tagged_template_expression(refs, it));
}
fn visit_chain_expression(&mut self, it: &ChainExpression<'a>) {
self.opaque(|refs| walk::walk_chain_expression(refs, it));
}
fn visit_static_member_expression(&mut self, it: &StaticMemberExpression<'a>) {
self.opaque(|refs| walk::walk_static_member_expression(refs, it));
}
fn visit_computed_member_expression(&mut self, it: &ComputedMemberExpression<'a>) {
self.opaque(|refs| walk::walk_computed_member_expression(refs, it));
}
fn visit_private_field_expression(&mut self, it: &PrivateFieldExpression<'a>) {
self.opaque(|refs| walk::walk_private_field_expression(refs, it));
}
fn visit_assignment_expression(&mut self, it: &AssignmentExpression<'a>) {
if it.operator == AssignmentOperator::Assign {
self.visit_expression(&it.right);
} else {
self.opaque(|refs| walk::walk_assignment_expression(refs, it));
}
}
fn visit_return_statement(&mut self, it: &ReturnStatement<'a>) {
let saved = std::mem::replace(&mut self.part, Part::Value);
walk::walk_return_statement(self, it);
self.part = saved;
}
fn visit_function(&mut self, it: &Function<'a>, flags: ScopeFlags) {
self.function(&it.params, Part::Body, |refs| walk::walk_function(refs, it, flags));
}
fn visit_arrow_function_expression(&mut self, it: &ArrowFunctionExpression<'a>) {
let body = if it.get_expression().is_some() {
Part::Value
} else {
Part::Body
};
self.function(&it.params, body, |refs| walk::walk_arrow_function_expression(refs, it));
}
}
#[derive(Default)]
struct Callables {
joined: HashSet<SymbolId>,
apart: HashMap<SymbolId, Parameters>,
}
impl Callables {
fn contains(&self, symbol: SymbolId) -> bool {
self.joined.contains(&symbol) || self.apart.contains_key(&symbol)
}
}
#[derive(Debug, Clone, Default)]
struct Parameters {
items: Vec<(Vec<SymbolId>, bool)>,
rest: Vec<SymbolId>,
}
impl Parameters {
fn of(params: &FormalParameters<'_>) -> Self {
let items = params
.items
.iter()
.map(|param| {
let exact = param.pattern.get_binding_identifier().is_some();
(bound_symbols(¶m.pattern), exact)
})
.collect();
let rest = params
.rest
.as_ref()
.map_or_else(Vec::new, |rest| bound_symbols(&rest.rest.argument));
Self { items, rest }
}
fn taking(&self, index: usize) -> (&[SymbolId], bool) {
self.items
.get(index)
.map_or((self.rest.as_slice(), false), |(names, exact)| {
(names.as_slice(), *exact)
})
}
fn names(&self) -> impl Iterator<Item = (SymbolId, bool)> + '_ {
self.items
.iter()
.flat_map(|(names, exact)| names.iter().map(move |&name| (name, *exact)))
.chain(self.rest.iter().map(|&name| (name, false)))
}
}
fn bound_symbols(pattern: &BindingPattern<'_>) -> Vec<SymbolId> {
pattern
.get_binding_identifiers()
.iter()
.filter_map(|id| id.symbol_id.get())
.collect()
}
fn callables(program: &Program<'_>, scoping: &Scoping) -> Callables {
let mut finder = FindCallables {
scoping,
found: Vec::new(),
callees: HashSet::default(),
};
finder.visit_program(program);
let FindCallables { found, callees, .. } = finder;
let mut out = Callables::default();
for (symbol, scope, parameters) in found {
let apart = scoping.get_resolved_reference_ids(symbol).iter().all(|&reference| {
callees.contains(&reference)
&& !scoping
.scope_ancestors(scoping.get_reference(reference).scope_id())
.any(|ancestor| ancestor == scope)
});
if apart {
out.apart.insert(symbol, parameters);
} else {
out.joined.insert(symbol);
}
}
out
}
struct FindCallables<'s> {
scoping: &'s Scoping,
found: Vec<(SymbolId, ScopeId, Parameters)>,
callees: HashSet<ReferenceId>,
}
impl FindCallables<'_> {
fn add(
&mut self,
symbol: Option<SymbolId>,
scope: Option<ScopeId>,
params: &FormalParameters<'_>,
reads_arguments: bool,
) {
let (Some(symbol), Some(scope)) = (symbol, scope) else {
return;
};
if !reads_arguments && !self.scoping.symbol_is_mutated(symbol) {
self.found.push((symbol, scope, Parameters::of(params)));
}
}
}
impl<'a> Visit<'a> for FindCallables<'_> {
fn visit_function(&mut self, it: &Function<'a>, flags: ScopeFlags) {
if let Some(id) = &it.id {
let mut reads = ReadsArguments(false);
reads.visit_function(it, flags);
self.add(id.symbol_id.get(), it.scope_id.get(), &it.params, reads.0);
}
walk::walk_function(self, it, flags);
}
fn visit_variable_declarator(&mut self, it: &VariableDeclarator<'a>) {
if let (BindingPattern::BindingIdentifier(id), Some(init)) = (&it.id, &it.init) {
let function = match init.get_inner_expression() {
Expression::ArrowFunctionExpression(f) => Some((f.scope_id.get(), &f.params)),
Expression::FunctionExpression(f) => Some((f.scope_id.get(), &f.params)),
_ => None,
};
if let Some((scope, params)) = function {
let mut reads = ReadsArguments(false);
reads.visit_expression(init);
self.add(id.symbol_id.get(), scope, params, reads.0);
}
}
walk::walk_variable_declarator(self, it);
}
fn visit_call_expression(&mut self, it: &CallExpression<'a>) {
if let Expression::Identifier(id) = it.callee.get_inner_expression()
&& !has_spread(it)
&& let Some(reference) = id.reference_id.get()
{
self.callees.insert(reference);
}
walk::walk_call_expression(self, it);
}
}
#[derive(Default)]
struct MentionsThis {
this: bool,
sup: bool,
}
impl<'a> Visit<'a> for MentionsThis {
fn visit_this_expression(&mut self, _: &ThisExpression) {
self.this = true;
}
fn visit_super(&mut self, _: &Super) {
self.sup = true;
}
fn visit_function(&mut self, _: &Function<'a>, _: ScopeFlags) {}
fn visit_class(&mut self, _: &Class<'a>) {}
}
struct ReadsArguments(bool);
impl<'a> Visit<'a> for ReadsArguments {
fn visit_identifier_reference(&mut self, it: &IdentifierReference<'a>) {
self.0 |= it.name.as_str() == "arguments";
}
}
pub(crate) fn given<'b, 'a>(argument: &'b Argument<'a>) -> Option<&'b Expression<'a>> {
match argument {
Argument::SpreadElement(spread) => Some(&spread.argument),
other => other.as_expression(),
}
}
fn has_spread(call: &CallExpression<'_>) -> bool {
call.arguments
.iter()
.any(|argument| matches!(argument, Argument::SpreadElement(_)))
}
fn calls_callable(scoping: &Scoping, callables: &Callables, call: &CallExpression<'_>) -> bool {
let Expression::Identifier(id) = call.callee.get_inner_expression() else {
return false;
};
local_symbol(scoping, id).is_some_and(|symbol| callables.contains(symbol)) && !has_spread(call)
}
const BUILTIN_CLASSES: &[&str] = &[
"Array",
"Date",
"Error",
"Map",
"Promise",
"RegExp",
"Set",
"URL",
"URLSearchParams",
"WeakMap",
"WeakSet",
];
fn constructs_builtin(scoping: &Scoping, new: &NewExpression<'_>) -> bool {
matches!(
new.callee.get_inner_expression(),
Expression::Identifier(id) if resolved(scoping, id).is_none() && BUILTIN_CLASSES.contains(&id.name.as_str())
)
}
fn is_primitive_list(names: Names<'_>, expr: &Expression<'_>) -> bool {
let Expression::ArrayExpression(array) = expr.get_inner_expression() else {
return false;
};
array.elements.iter().all(|element| match element {
ArrayExpressionElement::SpreadElement(_) => false,
ArrayExpressionElement::Elision(_) => true,
other => other.as_expression().is_some_and(|value| is_primitive(names, value)),
})
}
fn is_primitive_record(names: Names<'_>, expr: &Expression<'_>) -> bool {
let Expression::ObjectExpression(object) = expr.get_inner_expression() else {
return false;
};
object.properties.iter().all(|property| match property {
ObjectPropertyKind::ObjectProperty(p) => {
!p.method
&& p.kind == PropertyKind::Init
&& p.key
.static_name()
.is_some_and(|key| crate::js_string(key.as_js_str()) != "__proto__")
&& is_primitive(names, &p.value)
}
ObjectPropertyKind::SpreadProperty(_) => false,
})
}
fn lists_entries(names: Names<'_>, call: &CallExpression<'_>) -> bool {
matches!(
call.callee.get_inner_expression(),
Expression::StaticMemberExpression(m)
if matches!(m.property.name.as_str(), "entries" | "values" | "keys")
&& matches!(
m.object.get_inner_expression(),
Expression::Identifier(object)
if object.name.as_str() == "Object" && resolved(names.scoping, object).is_none()
)
)
}
fn is_inline_function(argument: &Argument<'_>) -> bool {
argument.as_expression().is_some_and(|argument| {
matches!(
argument.get_inner_expression(),
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)
)
})
}
fn pure_method(names: Names<'_>, call: &CallExpression<'_>) -> bool {
matches!(
call.callee.get_inner_expression(),
Expression::StaticMemberExpression(m)
if PURE_METHODS.contains(&m.property.name.as_str()) && builtin(names, &m.object)
)
}
const COPYING_EXPORTS: &[(&str, &[&str])] = &[
("deepmerge", &[""]),
("deepmerge-ts", &["deepmerge"]),
("defu", &["", "defu"]),
("ts-deepmerge", &["", "merge"]),
("webpack-merge", &["", "merge"]),
];
const MERGE_CONFIG_MODULES: &[&str] = &["vite", "vitest", "vitest/config"];
const COPYING_GLOBALS: &[&str] = &["defu", "mergeConfig"];
fn copies(names: Names<'_>, call: &CallExpression<'_>) -> bool {
let Expression::Identifier(id) = call.callee.get_inner_expression() else {
return false;
};
match resolved(names.scoping, id) {
None => COPYING_GLOBALS.contains(&id.name.as_str()),
Some(symbol) if !names.known.foreign.contains(&symbol) => false,
Some(symbol) => names.known.modules.get(&symbol).is_some_and(|(module, imported)| {
let export = imported.as_deref().unwrap_or_default();
let copying = COPYING_EXPORTS
.iter()
.any(|(package, exports)| *package == module.as_str() && exports.contains(&export));
let options_free = module != "deepmerge"
|| call.arguments.iter().skip(2).all(|argument| {
argument
.as_expression()
.is_some_and(|options| function_free(names, options))
});
(copying && options_free) || (export == "mergeConfig" && MERGE_CONFIG_MODULES.contains(&module.as_str()))
}),
}
}
fn function_free(names: Names<'_>, expr: &Expression<'_>) -> bool {
match expr.get_inner_expression() {
Expression::ObjectExpression(object) => object.properties.iter().all(|property| match property {
ObjectPropertyKind::ObjectProperty(p) => {
!p.method && p.kind == PropertyKind::Init && function_free(names, &p.value)
}
ObjectPropertyKind::SpreadProperty(_) => false,
}),
Expression::ArrayExpression(array) => array.elements.iter().all(|element| match element {
ArrayExpressionElement::SpreadElement(_) => false,
ArrayExpressionElement::Elision(_) => true,
other => other.as_expression().is_some_and(|value| function_free(names, value)),
}),
other => is_primitive(names, other),
}
}
const FIRST_ARGUMENT_SETTERS: &[(&str, &str)] = &[
("Object", "assign"),
("Object", "defineProperties"),
("Object", "defineProperty"),
("Reflect", "defineProperty"),
];
fn first_argument_setter<'b, 'a>(callee: &'b Expression<'a>) -> Option<&'b IdentifierReference<'a>> {
let Expression::StaticMemberExpression(m) = callee.get_inner_expression() else {
return None;
};
let Expression::Identifier(object) = m.object.get_inner_expression() else {
return None;
};
FIRST_ARGUMENT_SETTERS
.contains(&(object.name.as_str(), m.property.name.as_str()))
.then_some(&**object)
}
fn assigns(names: Names<'_>, call: &CallExpression<'_>) -> bool {
first_argument_setter(&call.callee).is_some_and(|object| resolved(names.scoping, object).is_none())
&& call
.arguments
.first()
.is_some_and(|first| first.as_expression().is_some())
}
fn assigns_fresh(names: Names<'_>, call: &CallExpression<'_>) -> bool {
let first = call
.arguments
.first()
.and_then(Argument::as_expression)
.map(Expression::get_inner_expression);
matches!(
call.callee.get_inner_expression(),
Expression::StaticMemberExpression(m) if m.property.name.as_str() == "assign"
) && assigns(names, call)
&& !has_spread(call)
&& matches!(first, Some(Expression::ObjectExpression(_)))
}
pub(crate) fn property_defined(call: &CallExpression<'_>) -> Option<String> {
let Expression::StaticMemberExpression(m) = call.callee.get_inner_expression() else {
return None;
};
if m.property.name.as_str() != "defineProperty" || first_argument_setter(&call.callee).is_none() {
return None;
}
call.arguments
.get(1)
.and_then(Argument::as_expression)
.and_then(static_string)
}
fn foreign(names: Names<'_>, callee: &Expression<'_>) -> bool {
let root = match callee.get_inner_expression() {
Expression::StaticMemberExpression(member) => member.object.get_inner_expression(),
other => other,
};
let Expression::Identifier(id) = root else {
return false;
};
resolved(names.scoping, id).is_none_or(|symbol| names.known.foreign.contains(&symbol))
}
fn is_package(specifier: &str) -> bool {
match specifier.strip_prefix('@') {
Some(scoped) => scoped
.split_once('/')
.is_some_and(|(scope, name)| !scope.is_empty() && !name.is_empty()),
None => specifier
.chars()
.next()
.is_some_and(|first| first.is_ascii_alphanumeric()),
}
}
#[derive(Clone, Copy)]
struct Names<'s> {
scoping: &'s Scoping,
known: &'s Known,
}
#[derive(Default)]
struct Known {
modules: HashMap<SymbolId, (String, Option<String>)>,
builtins: HashSet<SymbolId>,
namespaces: HashSet<SymbolId>,
foreign: HashSet<SymbolId>,
followed: HashSet<SymbolId>,
results: HashMap<SymbolId, Vec<Write>>,
import_paths: HashMap<SymbolId, Vec<String>>,
inert: HashSet<SymbolId>,
}
fn known(program: &Program<'_>, scoping: &Scoping, imports: &dyn Imports) -> Known {
let mut finder = FindKnown {
scoping,
known: Known::default(),
in_const: false,
called: HashSet::default(),
};
finder.visit_program(program);
let called = finder.called;
let mut known = finder.known;
known.followed = known
.modules
.iter()
.filter(|(_, (module, _))| imports.follows(module))
.map(|(&symbol, _)| symbol)
.collect();
known.results = known
.followed
.iter()
.filter(|&&symbol| {
scoping
.get_resolved_reference_ids(symbol)
.iter()
.any(|reference| called.contains(reference))
})
.filter_map(|&symbol| {
let (module, _) = known.modules.get(&symbol)?;
let [export] = known.import_paths.get(&symbol)?.as_slice() else {
return None;
};
Some((symbol, imports.result_places(module, export)?))
})
.collect();
known.foreign = known
.modules
.iter()
.filter(|(_, (module, _))| is_package(module) && !imports.follows(module) && imports.is_installed(module))
.map(|(&symbol, _)| symbol)
.collect();
let mut receivers = MethodReceivers::default();
receivers.visit_program(program);
known.inert = known
.foreign
.iter()
.copied()
.filter(|&symbol| {
scoping
.get_resolved_reference_ids(symbol)
.iter()
.all(|reference| !scoping.get_reference(*reference).is_value() || receivers.0.contains(reference))
})
.collect();
known
}
#[derive(Default)]
struct MethodReceivers(HashSet<ReferenceId>);
impl<'a> Visit<'a> for MethodReceivers {
fn visit_call_expression(&mut self, it: &CallExpression<'a>) {
if let Expression::StaticMemberExpression(member) = it.callee.get_inner_expression()
&& let Expression::Identifier(id) = member.object.get_inner_expression()
&& it.arguments.iter().filter_map(given).all(given_in_full)
{
self.0.extend(id.reference_id.get());
}
walk::walk_call_expression(self, it);
}
}
struct FindKnown<'s> {
scoping: &'s Scoping,
known: Known,
in_const: bool,
called: HashSet<ReferenceId>,
}
impl FindKnown<'_> {
fn bind(&mut self, symbol: Option<SymbolId>, specifier: &str, imported: Option<String>) {
if let Some(symbol) = symbol {
let imported = imported.filter(|name| name != "default");
self.known.modules.insert(symbol, (specifier.to_owned(), imported));
}
}
fn path(&mut self, symbol: Option<SymbolId>, path: Vec<String>) {
if let Some(symbol) = symbol {
self.known.import_paths.insert(symbol, path);
}
}
fn required(&self, init: &Expression<'_>) -> Option<String> {
let Expression::CallExpression(call) = init.get_inner_expression() else {
return None;
};
match call.callee.get_inner_expression() {
Expression::Identifier(id) if id.name.as_str() == "require" && resolved(self.scoping, id).is_none() => call
.arguments
.first()
.and_then(Argument::as_expression)
.and_then(static_string),
_ => None,
}
}
fn creates_require(&self, init: &Expression<'_>) -> bool {
let Expression::CallExpression(call) = init.get_inner_expression() else {
return false;
};
let Expression::Identifier(id) = call.callee.get_inner_expression() else {
return false;
};
resolved(self.scoping, id)
.and_then(|symbol| self.known.modules.get(&symbol))
.is_some_and(|(module, imported)| {
matches!(module.as_str(), "module" | "node:module") && imported.as_deref() == Some("createRequire")
})
}
}
impl<'a> Visit<'a> for FindKnown<'_> {
fn visit_call_expression(&mut self, it: &CallExpression<'a>) {
if let Expression::Identifier(id) = it.callee.get_inner_expression() {
self.called.extend(id.reference_id.get());
}
walk::walk_call_expression(self, it);
}
fn visit_variable_declaration(&mut self, it: &VariableDeclaration<'a>) {
let saved = std::mem::replace(&mut self.in_const, it.kind == VariableDeclarationKind::Const);
walk::walk_variable_declaration(self, it);
self.in_const = saved;
}
fn visit_import_declaration(&mut self, it: &ImportDeclaration<'a>) {
let specifier = crate::js_string(it.source.value);
for item in it.specifiers.iter().flatten() {
match item {
ImportDeclarationSpecifier::ImportSpecifier(s) => {
let imported = crate::collect::module_export_name(&s.imported);
self.path(s.local.symbol_id.get(), vec![imported.clone()]);
self.bind(s.local.symbol_id.get(), &specifier, Some(imported));
}
ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
self.path(s.local.symbol_id.get(), vec!["default".to_owned()]);
self.bind(s.local.symbol_id.get(), &specifier, None);
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
self.path(s.local.symbol_id.get(), Vec::new());
self.bind(s.local.symbol_id.get(), &specifier, None);
self.known.namespaces.extend(s.local.symbol_id.get());
}
}
}
}
fn visit_variable_declarator(&mut self, it: &VariableDeclarator<'a>) {
if let Some(specifier) = it.init.as_ref().and_then(|init| self.required(init)) {
match &it.id {
BindingPattern::BindingIdentifier(id) => self.bind(id.symbol_id.get(), &specifier, None),
BindingPattern::ObjectPattern(pattern) => {
for property in &pattern.properties {
if let (Some(key), BindingPattern::BindingIdentifier(id)) =
(property.key.static_name(), &property.value)
{
self.bind(id.symbol_id.get(), &specifier, Some(crate::js_string(key.as_js_str())));
}
}
}
_ => {}
}
} else if let (true, BindingPattern::BindingIdentifier(id), Some(init)) = (self.in_const, &it.id, &it.init)
&& (creates_builtin(self.scoping, init) || self.creates_require(init))
&& let Some(symbol) = id.symbol_id.get()
{
self.known.builtins.insert(symbol);
}
walk::walk_variable_declarator(self, it);
}
}
const BUILTIN_OBJECTS: &[&str] = &[
"Array", "JSON", "Math", "Number", "Object", "Promise", "Reflect", "String", "console", "process", "require",
];
fn builtin(names: Names<'_>, receiver: &Expression<'_>) -> bool {
match receiver.get_inner_expression() {
Expression::Identifier(id) => match resolved(names.scoping, id) {
None => BUILTIN_OBJECTS.contains(&id.name.as_str()),
Some(symbol) => {
names.known.builtins.contains(&symbol)
|| names
.known
.modules
.get(&symbol)
.is_some_and(|(module, imported)| imported.is_none() && known_module(module).is_some())
}
},
Expression::CallExpression(call) => pure_function(names, call).is_some(),
other => creates_builtin(names.scoping, other),
}
}
fn creates_builtin(scoping: &Scoping, expr: &Expression<'_>) -> bool {
match expr.get_inner_expression() {
Expression::ArrayExpression(_)
| Expression::StringLiteral(_)
| Expression::TemplateLiteral(_)
| Expression::RegExpLiteral(_)
| Expression::ImportMeta(_) => true,
Expression::NewExpression(new) => constructs_builtin(scoping, new),
_ => false,
}
}
fn is_namespace(names: Names<'_>, expr: &Expression<'_>) -> bool {
matches!(
expr.get_inner_expression(),
Expression::Identifier(id) if resolved(names.scoping, id).is_some_and(|symbol| names.known.namespaces.contains(&symbol))
)
}
fn resolved(scoping: &Scoping, id: &IdentifierReference<'_>) -> Option<SymbolId> {
scoping.get_reference(id.reference_id.get()?).symbol_id()
}
type Classes = HashMap<SymbolId, Rc<[SymbolId]>>;
fn classes(links: &[(SymbolId, SymbolId)]) -> Classes {
let mut parent = forest(links);
let mut members: HashMap<SymbolId, Vec<SymbolId>> = HashMap::default();
let symbols: Vec<SymbolId> = parent.keys().copied().collect();
for symbol in symbols {
let root = find(&mut parent, symbol);
members.entry(root).or_default().push(symbol);
}
let mut out = Classes::default();
for (_, mut symbols) in members {
symbols.sort_unstable();
let shared: Rc<[SymbolId]> = symbols.into();
for &symbol in shared.iter() {
out.insert(symbol, Rc::clone(&shared));
}
}
out
}
fn forest(links: &[(SymbolId, SymbolId)]) -> HashMap<SymbolId, SymbolId> {
let mut parent: HashMap<SymbolId, SymbolId> = HashMap::default();
for &(a, b) in links {
let (a, b) = (find(&mut parent, a), find(&mut parent, b));
if a != b {
parent.insert(a, b);
}
}
parent
}
fn find(parent: &mut HashMap<SymbolId, SymbolId>, symbol: SymbolId) -> SymbolId {
let mut root = symbol;
while let Some(&next) = parent.get(&root).filter(|&&next| next != root) {
root = next;
}
parent.insert(symbol, root);
root
}
fn is_inert_package(scoping: &Scoping, known: &Known, expr: &Expression<'_>) -> bool {
let Expression::Identifier(id) = expr.get_inner_expression() else {
return false;
};
resolved(scoping, id).is_some_and(|symbol| known.inert.contains(&symbol))
}
fn is_followed_import(scoping: &Scoping, known: &Known, id: &IdentifierReference<'_>) -> bool {
resolved(scoping, id).is_some_and(|symbol| known.followed.contains(&symbol))
}
fn local_symbol(scoping: &Scoping, id: &IdentifierReference<'_>) -> Option<SymbolId> {
let symbol = scoping.get_reference(id.reference_id.get()?).symbol_id()?;
(!scoping.symbol_flags(symbol).is_import()).then_some(symbol)
}
pub(crate) fn reach<'b, 'a>(expr: &'b Expression<'a>) -> Option<(&'b IdentifierReference<'a>, Vec<String>, bool)> {
let mut keys = Vec::new();
let mut exact = true;
let mut expr = expr.get_inner_expression();
loop {
match expr {
Expression::Identifier(id) => {
keys.reverse();
return Some((id, keys, exact));
}
Expression::StaticMemberExpression(member) => {
keys.push(member.property.name.as_str().to_owned());
expr = member.object.get_inner_expression();
}
other => {
let member = other.as_member_expression()?;
keys.clear();
exact = false;
expr = member.object().get_inner_expression();
}
}
}
}
pub(crate) fn given_parts<'b, 'a>(expr: &'b Expression<'a>) -> Vec<(&'b IdentifierReference<'a>, Vec<String>)> {
let mut parts = Vec::new();
add_given_parts(expr, &mut parts);
parts
}
fn add_given_parts<'b, 'a>(expr: &'b Expression<'a>, parts: &mut Vec<(&'b IdentifierReference<'a>, Vec<String>)>) {
if let Some((id, keys, _)) = reach(expr) {
parts.push((id, keys));
return;
}
match expr.get_inner_expression() {
Expression::ObjectExpression(object) => {
for property in &object.properties {
match property {
ObjectPropertyKind::ObjectProperty(p) => add_given_parts(&p.value, parts),
ObjectPropertyKind::SpreadProperty(s) => add_given_parts(&s.argument, parts),
}
}
}
Expression::ArrayExpression(array) => {
for element in &array.elements {
match element {
ArrayExpressionElement::SpreadElement(s) => add_given_parts(&s.argument, parts),
other => {
if let Some(value) = other.as_expression() {
add_given_parts(value, parts);
}
}
}
}
}
Expression::ConditionalExpression(c) => {
add_given_parts(&c.consequent, parts);
add_given_parts(&c.alternate, parts);
}
Expression::LogicalExpression(l) => {
add_given_parts(&l.left, parts);
add_given_parts(&l.right, parts);
}
Expression::SequenceExpression(sequence) => {
if let Some(last) = sequence.expressions.last() {
add_given_parts(last, parts);
}
}
Expression::AssignmentExpression(a) if a.operator == AssignmentOperator::Assign => {
add_given_parts(&a.right, parts);
}
_ => {}
}
}
fn given_in_full(expr: &Expression<'_>) -> bool {
if reach(expr).is_some() {
return true;
}
match expr.get_inner_expression() {
Expression::StringLiteral(_)
| Expression::NumericLiteral(_)
| Expression::BooleanLiteral(_)
| Expression::NullLiteral(_)
| Expression::BigIntLiteral(_)
| Expression::RegExpLiteral(_)
| Expression::TemplateLiteral(_)
| Expression::BinaryExpression(_)
| Expression::UnaryExpression(_) => true,
Expression::ObjectExpression(object) => object.properties.iter().all(|property| match property {
ObjectPropertyKind::ObjectProperty(p) => {
!p.method && p.kind == PropertyKind::Init && given_in_full(&p.value)
}
ObjectPropertyKind::SpreadProperty(s) => given_in_full(&s.argument),
}),
Expression::ArrayExpression(array) => array.elements.iter().all(|element| match element {
ArrayExpressionElement::SpreadElement(s) => given_in_full(&s.argument),
ArrayExpressionElement::Elision(_) => true,
other => other.as_expression().is_some_and(given_in_full),
}),
Expression::ConditionalExpression(c) => given_in_full(&c.consequent) && given_in_full(&c.alternate),
Expression::LogicalExpression(l) => given_in_full(&l.left) && given_in_full(&l.right),
Expression::SequenceExpression(sequence) => sequence.expressions.last().is_some_and(given_in_full),
Expression::AssignmentExpression(a) => a.operator == AssignmentOperator::Assign && given_in_full(&a.right),
_ => false,
}
}
pub(crate) fn alias_source<'b, 'a>(init: &'b Expression<'a>) -> Option<(&'b IdentifierReference<'a>, Vec<String>)> {
let mut keys = Vec::new();
let mut expr = init.get_inner_expression();
loop {
match expr {
Expression::Identifier(id) => {
keys.reverse();
return Some((id, keys));
}
Expression::StaticMemberExpression(member) => {
keys.push(member.property.name.as_str().to_owned());
expr = member.object.get_inner_expression();
}
_ => return None,
}
}
}
pub(crate) fn alias_names<'b, 'a>(
pattern: &'b BindingPattern<'a>,
) -> Option<Vec<(&'b BindingIdentifier<'a>, Option<String>)>> {
match pattern {
BindingPattern::BindingIdentifier(id) => Some(vec![(&**id, None)]),
BindingPattern::ObjectPattern(object) if object.rest.is_none() => object
.properties
.iter()
.map(|property| match &property.value {
BindingPattern::BindingIdentifier(id) => {
let key = property.key.static_name()?;
Some((&**id, Some(crate::js_string(key.as_js_str()))))
}
_ => None,
})
.collect(),
_ => None,
}
}
pub(crate) fn statement_call<'b, 'a>(expr: &'b Expression<'a>) -> Option<&'b CallExpression<'a>> {
match expr.get_inner_expression() {
Expression::CallExpression(call) => Some(call),
Expression::AwaitExpression(a) => statement_call(&a.argument),
_ => None,
}
}
const PURE_METHODS: &[&str] = &[
"at",
"charAt",
"concat",
"endsWith",
"entries",
"every",
"filter",
"find",
"findIndex",
"findLast",
"findLastIndex",
"flat",
"flatMap",
"forEach",
"get",
"has",
"hasOwnProperty",
"includes",
"indexOf",
"join",
"keys",
"lastIndexOf",
"map",
"match",
"padEnd",
"padStart",
"reduce",
"reduceRight",
"replace",
"replaceAll",
"resolve",
"slice",
"some",
"split",
"startsWith",
"test",
"toLowerCase",
"toReversed",
"toSorted",
"toSpliced",
"toString",
"toUpperCase",
"trim",
"trimEnd",
"trimStart",
"values",
"with",
];
const OWN_RESULT_METHODS: &[&str] = &[
"charAt",
"endsWith",
"every",
"findIndex",
"findLastIndex",
"forEach",
"has",
"hasOwnProperty",
"includes",
"indexOf",
"join",
"lastIndexOf",
"match",
"padEnd",
"padStart",
"push",
"replace",
"replaceAll",
"resolve",
"some",
"split",
"startsWith",
"test",
"toLowerCase",
"toString",
"toUpperCase",
"trim",
"trimEnd",
"trimStart",
"unshift",
];
const CALLBACK_METHODS: &[&str] = &[
"every",
"filter",
"find",
"findIndex",
"findLast",
"findLastIndex",
"flatMap",
"forEach",
"map",
"reduce",
"reduceRight",
"some",
"toSorted",
];
const LIST_ADDERS: &[&str] = &["push", "unshift"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Returns {
Primitive,
Fresh,
Shared,
}
const PURE_FUNCTIONS: &[(&str, &[&str], Returns)] = &[
(
"path",
&[
"basename",
"dirname",
"extname",
"format",
"isAbsolute",
"join",
"normalize",
"relative",
"resolve",
"toNamespacedPath",
],
Returns::Primitive,
),
("path", &["parse"], Returns::Fresh),
("fs", &["existsSync"], Returns::Primitive),
(
"fs",
&["readFileSync", "readdirSync", "realpathSync", "statSync", "lstatSync"],
Returns::Fresh,
),
("url", &["fileURLToPath"], Returns::Primitive),
("url", &["pathToFileURL"], Returns::Fresh),
("module", &["createRequire"], Returns::Fresh),
("JSON", &["stringify"], Returns::Primitive),
("JSON", &["parse"], Returns::Fresh),
("Object", &["keys", "getOwnPropertyNames"], Returns::Fresh),
("Object", &["entries", "values"], Returns::Shared),
("Array", &["isArray"], Returns::Primitive),
("Math", &[], Returns::Primitive),
("Number", &[], Returns::Primitive),
("String", &[], Returns::Primitive),
("console", &[], Returns::Primitive),
("process", &["cwd"], Returns::Primitive),
(
"",
&[
"Boolean",
"Number",
"String",
"decodeURIComponent",
"encodeURIComponent",
"existsSync",
"fileURLToPath",
"parseFloat",
"parseInt",
],
Returns::Primitive,
),
("", &["readFileSync", "readdirSync"], Returns::Fresh),
];
const GLOBAL_OBJECTS: &[&str] = &[
"Array", "JSON", "Math", "Number", "Object", "String", "console", "process",
];
const CALLING_PURE_FUNCTIONS: &[(&str, &str)] = &[("JSON", "parse"), ("JSON", "stringify")];
fn pure_function(names: Names<'_>, call: &CallExpression<'_>) -> Option<Returns> {
let (object, name) = match call.callee.get_inner_expression() {
Expression::Identifier(id) => match resolved(names.scoping, id) {
None => ("", id.name.as_str()),
Some(symbol) => {
let (module, imported) = names.known.modules.get(&symbol)?;
(known_module(module)?, imported.as_deref()?)
}
},
Expression::StaticMemberExpression(member) => {
(module_object(names, &member.object)?, member.property.name.as_str())
}
_ => return None,
};
let returns = PURE_FUNCTIONS
.iter()
.find(|(module, functions, _)| *module == object && (functions.is_empty() || functions.contains(&name)))
.map(|&(_, _, returns)| returns)?;
let calls_back = CALLING_PURE_FUNCTIONS.contains(&(object, name));
let given_no_function = || {
call.arguments.iter().skip(1).all(|argument| {
argument
.as_expression()
.is_some_and(|value| holds_no_function(names, value))
})
};
(!calls_back || (!has_spread(call) && given_no_function())).then_some(returns)
}
fn holds_no_function(names: Names<'_>, expr: &Expression<'_>) -> bool {
function_free(names, expr)
|| matches!(
expr.get_inner_expression(),
Expression::Identifier(id) if id.name.as_str() == "undefined" && resolved(names.scoping, id).is_none()
)
}
fn module_object(names: Names<'_>, object: &Expression<'_>) -> Option<&'static str> {
match object.get_inner_expression() {
Expression::Identifier(id) => match resolved(names.scoping, id) {
None => GLOBAL_OBJECTS
.iter()
.find(|global| **global == id.name.as_str())
.copied(),
Some(symbol) => match names.known.modules.get(&symbol) {
Some((module, None)) => known_module(module),
_ => None,
},
},
Expression::StaticMemberExpression(member)
if matches!(member.property.name.as_str(), "posix" | "win32")
&& module_object(names, &member.object) == Some("path") =>
{
Some("path")
}
_ => None,
}
}
fn known_module(specifier: &str) -> Option<&'static str> {
match specifier.strip_prefix("node:").unwrap_or(specifier) {
"path" | "path/posix" => Some("path"),
"fs" => Some("fs"),
"url" => Some("url"),
"module" => Some("module"),
_ => None,
}
}
fn is_primitive(names: Names<'_>, expr: &Expression<'_>) -> bool {
match expr.get_inner_expression() {
Expression::StringLiteral(_)
| Expression::NumericLiteral(_)
| Expression::BooleanLiteral(_)
| Expression::NullLiteral(_)
| Expression::BigIntLiteral(_)
| Expression::TemplateLiteral(_)
| Expression::BinaryExpression(_)
| Expression::UnaryExpression(_) => true,
Expression::ConditionalExpression(c) => is_primitive(names, &c.consequent) && is_primitive(names, &c.alternate),
Expression::LogicalExpression(l) => is_primitive(names, &l.left) && is_primitive(names, &l.right),
Expression::CallExpression(call) => pure_function(names, call) == Some(Returns::Primitive),
_ => false,
}
}
pub(crate) fn changed_by_call<'p, 'a>(
call: &'p CallExpression<'a>,
pure: bool,
calls_back: bool,
) -> Option<&'p Expression<'a>> {
if pure {
return None;
}
if first_argument_setter(&call.callee).is_some() {
return call.arguments.first().and_then(Argument::as_expression);
}
match call.callee.get_inner_expression() {
Expression::StaticMemberExpression(m) if PURE_METHODS.contains(&m.property.name.as_str()) && !calls_back => {
None
}
callee => callee.as_member_expression().map(MemberExpression::object),
}
}