use std::collections::BTreeMap;
use oxc_ast::ast::{
Argument, BindingPattern, CallExpression, Class, Declaration, ExportDefaultDeclarationKind, Expression, Program,
Statement, StaticBlock, VariableDeclarator,
};
use oxc_ast_visit::{Visit, walk};
use oxc_semantic::{Scoping, SymbolId};
use skif_core::{HashMap, HashSet};
use crate::model::{Export, ExportKind, ImportKind, ImportedKind, ModuleInfo, Registration};
use crate::semantic::Analysis;
enum Binding {
Named(String, String),
Namespace(String),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Target {
Local(String),
Default,
}
#[derive(Default)]
struct Found(BTreeMap<Target, Vec<Registration>>);
impl Found {
fn add(&mut self, target: Target, registration: Registration) {
let list = self.0.entry(target).or_default();
if !list.contains(®istration) {
list.push(registration);
}
}
}
pub(crate) fn mark(program: &Program<'_>, analysis: &Analysis<'_>, info: &mut ModuleInfo) {
if info.exports.is_empty() || !may_register(program.source_text) {
return;
}
let bindings = import_bindings(info);
let mut found = Found::default();
let base_classes = top_level_classes(program, &bindings, &mut found);
let mut calls = Calls::default();
calls.visit_program(program);
if calls
.defines
.iter()
.any(|call| calls.is_registry_variable(&call.object))
{
calls = Calls {
scoping: Some(analysis.scoping()),
..Calls::default()
};
calls.visit_program(program);
}
for call in &calls.defines {
match &call.object {
DefineObject::MemberRegistry => {
if let Some(class) = &call.class {
found.add(Target::Local(class.clone()), Registration::Define);
}
}
DefineObject::Registry { name, symbol } if calls.is_registry(name, *symbol) => {
if let Some(class) = &call.class {
found.add(Target::Local(class.clone()), Registration::Define);
}
}
DefineObject::Registry { name, .. } if base_classes.contains_key(name) => {
found.add(Target::Local(name.clone()), base_classes[name].clone());
}
DefineObject::Registry { name, .. } => {
if let (Some(class), Some(Binding::Named(module, imported))) = (&call.first, bindings.get(name)) {
let registration = Registration::BaseClassDefine {
module: module.clone(),
name: imported.clone(),
};
found.add(Target::Local(class.clone()), registration);
}
}
}
}
for (target, registrations) in found.0 {
for export in info.exports.iter_mut().filter(|export| applies(&target, export)) {
for registration in ®istrations {
if !export.registrations.contains(registration) {
export.registrations.push(registration.clone());
}
}
}
}
}
fn may_register(source: &str) -> bool {
let bytes = source.as_bytes();
memchr::memchr(b'@', bytes).is_some()
|| memchr::memmem::find(bytes, b"define").is_some()
|| memchr::memmem::find(bytes, b"\\u").is_some()
}
fn applies(target: &Target, export: &Export) -> bool {
match target {
Target::Local(name) => {
!matches!(export.kind, ExportKind::ReExport | ExportKind::StarReExport)
&& export.from.is_none()
&& export.local_name.as_deref() == Some(name.as_str())
}
Target::Default => export.kind == ExportKind::Default && export.name == "default",
}
}
fn top_level_classes(
program: &Program<'_>,
bindings: &HashMap<String, Binding>,
found: &mut Found,
) -> HashMap<String, Registration> {
let mut base_classes = HashMap::default();
for (class, is_default) in program.body.iter().filter_map(top_level_class) {
let name = class.id.as_ref().map(|id| id.name.as_str().to_owned());
for decorator in &class.decorators {
let Some(registration) = decorator_registration(&decorator.expression, bindings) else {
continue;
};
if let Some(name) = &name {
found.add(Target::Local(name.clone()), registration.clone());
}
if is_default {
found.add(Target::Default, registration);
}
}
if let (Some(name), Some(heritage)) = (name, &class.heritage)
&& let Some(registration) = base_class_registration(&heritage.expression, bindings)
{
base_classes.insert(name, registration);
}
}
base_classes
}
fn top_level_class<'s, 'a>(statement: &'s Statement<'a>) -> Option<(&'s Class<'a>, bool)> {
match statement {
Statement::ClassDeclaration(class) => Some((class, false)),
Statement::ExportDeclaration(export) => match &export.declaration {
Declaration::ClassDeclaration(class) => Some((class, false)),
_ => None,
},
Statement::ExportDefaultDeclaration(export) => match &export.declaration {
ExportDefaultDeclarationKind::ClassDeclaration(class) => Some((class, true)),
_ => None,
},
_ => None,
}
}
fn import_bindings(info: &ModuleInfo) -> HashMap<String, Binding> {
let mut bindings = HashMap::default();
for import in info.imports.iter().filter(|import| import.kind == ImportKind::Static) {
for name in &import.names {
let Some(local) = &name.local else { continue };
let binding = match &name.kind {
ImportedKind::Named(imported) => Binding::Named(import.specifier.clone(), imported.clone()),
ImportedKind::Namespace => Binding::Namespace(import.specifier.clone()),
ImportedKind::Default => continue,
};
bindings.insert(local.clone(), binding);
}
}
bindings
}
fn decorator_registration(expression: &Expression<'_>, bindings: &HashMap<String, Binding>) -> Option<Registration> {
let callee = match expression {
Expression::CallExpression(call) => &call.callee,
Expression::Identifier(_) => expression,
_ => return None,
};
let (module, name) = match callee {
Expression::Identifier(id) => match bindings.get(id.name.as_str())? {
Binding::Named(module, name) => (module.clone(), name.clone()),
Binding::Namespace(_) => return None,
},
Expression::StaticMemberExpression(member) => {
let Expression::Identifier(object) = &member.object else {
return None;
};
match bindings.get(object.name.as_str())? {
Binding::Namespace(module) => (module.clone(), member.property.name.as_str().to_owned()),
Binding::Named(..) => return None,
}
}
_ => return None,
};
Some(Registration::Decorator { module, name })
}
fn base_class_registration(expression: &Expression<'_>, bindings: &HashMap<String, Binding>) -> Option<Registration> {
let named = |name: &str| match bindings.get(name)? {
Binding::Named(module, imported) => Some(Registration::BaseClassDefine {
module: module.clone(),
name: imported.clone(),
}),
Binding::Namespace(_) => None,
};
match expression {
Expression::Identifier(id) => named(id.name.as_str()),
Expression::CallExpression(call) => call.arguments.iter().find_map(|argument| match argument {
Argument::Identifier(id) => named(id.name.as_str()),
_ => None,
}),
_ => None,
}
}
enum DefineObject {
Registry { name: String, symbol: Option<SymbolId> },
MemberRegistry,
}
struct DefineCall {
object: DefineObject,
class: Option<String>,
first: Option<String>,
}
#[derive(Default)]
struct Calls<'s> {
scoping: Option<&'s Scoping>,
defines: Vec<DefineCall>,
registries: HashSet<String>,
registry_symbols: HashSet<SymbolId>,
classes: Vec<Option<String>>,
static_blocks: Vec<Option<String>>,
}
impl Calls<'_> {
fn is_registry_variable(&self, object: &DefineObject) -> bool {
matches!(object, DefineObject::Registry { name, .. } if self.registries.contains(name))
}
fn is_registry(&self, name: &str, symbol: Option<SymbolId>) -> bool {
match (self.scoping, symbol) {
(Some(_), Some(symbol)) => self.registry_symbols.contains(&symbol),
(Some(_), None) => name == "customElements",
(None, _) => name == "customElements" || self.registries.contains(name),
}
}
fn define_call(&self, call: &CallExpression<'_>) -> Option<DefineCall> {
let Expression::StaticMemberExpression(callee) = &call.callee else {
return None;
};
let method = callee.property.name.as_str();
let object = match &callee.object {
Expression::Identifier(id) => DefineObject::Registry {
name: id.name.as_str().to_owned(),
symbol: self
.scoping
.zip(id.reference_id.get())
.and_then(|(scoping, reference)| scoping.get_reference(reference).symbol_id()),
},
Expression::StaticMemberExpression(member) if member.property.name == "customElements" => {
if method != "define" {
return None;
}
DefineObject::MemberRegistry
}
_ => return None,
};
if !matches!(method, "define" | "defineAsync") {
return None;
}
let class = match call.arguments.get(1) {
Some(Argument::Identifier(id)) if method == "define" => Some(id.name.as_str().to_owned()),
Some(Argument::ThisExpression(_)) if method == "define" => self.static_blocks.last().cloned().flatten(),
_ => None,
};
let first = match call.arguments.first() {
Some(Argument::Identifier(id)) if method == "define" => Some(id.name.as_str().to_owned()),
_ => None,
};
Some(DefineCall { object, class, first })
}
}
impl<'a> Visit<'a> for Calls<'_> {
fn visit_variable_declarator(&mut self, it: &VariableDeclarator<'a>) {
if let BindingPattern::BindingIdentifier(id) = &it.id
&& let Some(init) = &it.init
{
let is_registry = match init.get_inner_expression() {
Expression::Identifier(init) => init.name == "customElements",
Expression::StaticMemberExpression(member) => member.property.name == "customElements",
Expression::NewExpression(new) => {
matches!(&new.callee, Expression::Identifier(callee) if callee.name == "CustomElementRegistry")
}
_ => false,
};
if is_registry {
self.registries.insert(id.name.as_str().to_owned());
self.registry_symbols.extend(id.symbol_id.get());
}
}
walk::walk_variable_declarator(self, it);
}
fn visit_call_expression(&mut self, it: &CallExpression<'a>) {
if let Some(call) = self.define_call(it) {
self.defines.push(call);
}
walk::walk_call_expression(self, it);
}
fn visit_class(&mut self, it: &Class<'a>) {
self.classes.push(it.id.as_ref().map(|id| id.name.as_str().to_owned()));
walk::walk_class(self, it);
self.classes.pop();
}
fn visit_static_block(&mut self, it: &StaticBlock<'a>) {
self.static_blocks.push(self.classes.last().cloned().flatten());
walk::walk_static_block(self, it);
self.static_blocks.pop();
}
}