use std::collections::{BTreeSet, HashMap, HashSet};
use oxc_ast::ast::{
BindingPattern, ComputedMemberExpression, Expression, IdentifierReference, StaticMemberExpression, TSQualifiedName,
TSTypeName, VariableDeclarator,
};
use oxc_ast_visit::{Visit, walk};
use crate::model::ModuleInfo;
#[derive(Default)]
pub(crate) struct Slot {
pub members: BTreeSet<String>,
pub uses: u32,
pub member_uses: u32,
}
impl Slot {
pub(crate) const fn escapes(&self) -> bool {
self.uses > self.member_uses
}
}
pub(crate) type Uses = HashMap<String, Slot>;
struct Tracker<'e> {
slots: Uses,
open: bool,
excluded: &'e HashSet<(u32, u32)>,
}
impl Tracker<'_> {
fn slot(&mut self, local: &str) -> Option<&mut Slot> {
if self.open {
Some(self.slots.entry(local.to_owned()).or_default())
} else {
self.slots.get_mut(local)
}
}
fn member(&mut self, local: &str, member: &str) {
if let Some(slot) = self.slot(local) {
slot.members.insert(member.to_string());
slot.member_uses += 1;
}
}
}
impl<'a> Visit<'a> for Tracker<'_> {
fn visit_identifier_reference(&mut self, it: &IdentifierReference<'a>) {
if self.excluded.contains(&(it.span.start, it.span.end)) {
return;
}
if let Some(slot) = self.slot(it.name.as_str()) {
slot.uses += 1;
}
}
fn visit_static_member_expression(&mut self, it: &StaticMemberExpression<'a>) {
if let Expression::Identifier(id) = &it.object {
self.member(id.name.as_str(), it.property.name.as_str());
}
walk::walk_static_member_expression(self, it);
}
fn visit_computed_member_expression(&mut self, it: &ComputedMemberExpression<'a>) {
if let Expression::Identifier(id) = &it.object
&& let Expression::StringLiteral(s) = &it.expression
&& let Some(member) = s.value.as_str()
{
self.member(id.name.as_str(), member);
}
walk::walk_computed_member_expression(self, it);
}
fn visit_ts_qualified_name(&mut self, it: &TSQualifiedName<'a>) {
if let TSTypeName::IdentifierReference(id) = &it.left {
self.member(id.name.as_str(), it.right.name.as_str());
}
walk::walk_ts_qualified_name(self, it);
}
fn visit_variable_declarator(&mut self, it: &VariableDeclarator<'a>) {
if let Some(Expression::Identifier(id)) = it.init.as_ref().map(Expression::get_inner_expression)
&& let BindingPattern::ObjectPattern(pattern) = &it.id
&& pattern.rest.is_none()
{
let keys: Option<Vec<String>> = pattern
.properties
.iter()
.map(|p| p.key.static_name().map(|k| crate::js_string(k.as_js_str())))
.collect();
if let Some(keys) = keys
&& let Some(slot) = self.slot(id.name.as_str())
{
slot.members.extend(keys);
slot.member_uses += 1;
}
}
walk::walk_variable_declarator(self, it);
}
}
pub(crate) fn track(program: &oxc_ast::ast::Program<'_>, info: &mut ModuleInfo, excluded: &HashSet<(u32, u32)>) {
let mut slots = HashMap::new();
for import in &info.imports {
for n in &import.names {
if let Some(local) = &n.local {
slots.insert(local.clone(), Slot::default());
}
}
}
if slots.is_empty() {
return;
}
let mut tracker = Tracker {
slots,
open: false,
excluded,
};
tracker.visit_program(program);
for import in &mut info.imports {
for n in &mut import.names {
let Some(slot) = n.local.as_ref().and_then(|l| tracker.slots.get(l)) else {
continue;
};
n.accessed_members = slot.members.iter().cloned().collect();
n.escapes |= slot.escapes();
}
}
}
pub(crate) fn uses(program: &oxc_ast::ast::Program<'_>) -> Uses {
let excluded = HashSet::new();
let mut tracker = Tracker {
slots: Uses::new(),
open: true,
excluded: &excluded,
};
tracker.visit_program(program);
tracker.slots
}
pub(crate) fn merge_uses(into: &mut Uses, from: &Uses) {
for (name, slot) in from {
let target = into.entry(name.clone()).or_default();
target.members.extend(slot.members.iter().cloned());
target.uses += slot.uses;
target.member_uses += slot.member_uses;
}
}
pub(crate) fn credit(info: &mut ModuleInfo, uses: &Uses) {
for import in &mut info.imports {
for n in &mut import.names {
let Some(slot) = n.local.as_ref().and_then(|l| uses.get(l)) else {
continue;
};
n.accessed_members.extend(slot.members.iter().cloned());
n.accessed_members.sort();
n.accessed_members.dedup();
n.escapes |= slot.escapes();
}
}
}
struct LocalReads<'w> {
wanted: &'w HashSet<String>,
reads: HashMap<String, BTreeSet<String>>,
}
impl LocalReads<'_> {
fn read(&mut self, local: &str, member: &str) -> bool {
if !self.wanted.contains(local) {
return false;
}
self.reads
.entry(local.to_owned())
.or_default()
.insert(member.to_owned());
true
}
}
impl<'a> Visit<'a> for LocalReads<'_> {
fn visit_static_member_expression(&mut self, it: &StaticMemberExpression<'a>) {
if let Expression::Identifier(id) = &it.object
&& self.read(id.name.as_str(), it.property.name.as_str())
{
return;
}
walk::walk_static_member_expression(self, it);
}
fn visit_computed_member_expression(&mut self, it: &ComputedMemberExpression<'a>) {
if let Expression::Identifier(id) = &it.object
&& let Expression::StringLiteral(s) = &it.expression
&& let Some(member) = s.value.as_str()
&& self.read(id.name.as_str(), member)
{
return;
}
walk::walk_computed_member_expression(self, it);
}
fn visit_ts_type_name(&mut self, it: &TSTypeName<'a>) {
if let TSTypeName::QualifiedName(name) = it {
self.visit_ts_qualified_name(name);
}
}
fn visit_ts_qualified_name(&mut self, it: &TSQualifiedName<'a>) {
if let TSTypeName::IdentifierReference(id) = &it.left {
self.read(id.name.as_str(), it.right.name.as_str());
} else {
self.visit_ts_type_name(&it.left);
}
}
fn visit_variable_declarator(&mut self, it: &VariableDeclarator<'a>) {
if let Some(Expression::Identifier(id)) = it.init.as_ref().map(Expression::get_inner_expression)
&& self.wanted.contains(id.name.as_str())
&& let BindingPattern::ObjectPattern(pattern) = &it.id
&& pattern.rest.is_none()
&& let Some(keys) = pattern
.properties
.iter()
.map(|p| p.key.static_name().map(|k| crate::js_string(k.as_js_str())))
.collect::<Option<Vec<String>>>()
{
for key in keys {
self.read(id.name.as_str(), &key);
}
self.visit_binding_pattern(&it.id);
return;
}
walk::walk_variable_declarator(self, it);
}
}
pub(crate) fn mark_local_reads(program: &oxc_ast::ast::Program<'_>, info: &mut ModuleInfo) {
let wanted: HashSet<String> = info
.exports
.iter()
.filter(|export| !export.enum_members.is_empty() || !export.namespace_members.is_empty())
.filter_map(|export| export.local_name.clone())
.collect();
if wanted.is_empty() {
return;
}
let mut tracker = LocalReads {
wanted: &wanted,
reads: HashMap::new(),
};
tracker.visit_program(program);
let mut namespaces: HashMap<String, BTreeSet<String>> = HashMap::new();
for export in info
.exports
.iter()
.filter(|export| !export.namespace_members.is_empty())
{
if let Some(local) = &export.local_name {
namespaces
.entry(local.clone())
.or_default()
.extend(export.namespace_members.iter().map(|member| member.name.clone()));
}
}
if !namespaces.is_empty() {
for (name, members) in crate::semantic::namespace_bare_reads(program, &namespaces) {
tracker.reads.entry(name).or_default().extend(members);
}
}
for export in &mut info.exports {
let Some(local) = export.local_name.as_deref() else {
continue;
};
let reads = tracker
.reads
.get(local)
.map(|members| members.iter().cloned().collect())
.unwrap_or_default();
export.members_read_in_file = reads;
}
}