#[allow(clippy::wildcard_imports, reason = "many AST types used")]
use oxc_ast::ast::*;
use oxc_ast_visit::Visit;
use oxc_ast_visit::walk;
use oxc_semantic::ScopeFlags;
use oxc_span::Span;
use rustc_hash::FxHashMap;
use crate::{
DynamicImportInfo, DynamicImportPattern, ExportInfo, ExportName, ImportInfo, ImportedName,
MemberAccess, ReExportInfo, RequireCallInfo, VisibilityTag,
};
use fallow_types::extract::{
ClassHeritageInfo, LocalTypeDeclaration, PublicSignatureTypeReference,
};
use crate::asset_url::normalize_asset_url;
use crate::html::is_remote_url;
use super::helpers::{
extract_angular_component_metadata, extract_angular_signal_query, extract_class_members,
extract_concat_parts, extract_custom_elements_define, extract_implemented_interface_names,
extract_nested_type_bindings, extract_query_list_element_type, extract_super_class_name,
extract_type_annotation_name, has_angular_class_decorator, has_angular_plural_query_decorator,
is_meta_url_arg, lit_custom_element_decorator, regex_pattern_to_suffix,
ts_import_type_qualifier_root,
};
use super::{
ModuleInfoExtractor, PendingLocalExportSpecifier, SideEffectRegistrationTarget,
try_extract_arrow_wrapped_import, try_extract_dynamic_import, try_extract_import_then_callback,
try_extract_property_callback_import, try_extract_require,
};
#[derive(Default)]
struct SignatureTypeCollector {
refs: Vec<(String, Span)>,
}
impl<'a> Visit<'a> for SignatureTypeCollector {
fn visit_ts_type_reference(&mut self, type_ref: &TSTypeReference<'a>) {
if let Some((name, span)) = type_name_root(&type_ref.type_name) {
self.refs.push((name, span));
}
walk::walk_ts_type_reference(self, type_ref);
}
}
fn type_name_root(name: &TSTypeName<'_>) -> Option<(String, Span)> {
match name {
TSTypeName::IdentifierReference(ident) => Some((ident.name.to_string(), ident.span)),
TSTypeName::QualifiedName(qualified) => type_name_root(&qualified.left),
TSTypeName::ThisExpression(_) => None,
}
}
fn expression_root_name(expr: &Expression<'_>) -> Option<(String, Span)> {
match expr {
Expression::Identifier(ident) => Some((ident.name.to_string(), ident.span)),
Expression::StaticMemberExpression(member) => expression_root_name(&member.object),
_ => None,
}
}
fn is_private_member_key(key: &PropertyKey<'_>) -> bool {
matches!(key, PropertyKey::PrivateIdentifier(_))
}
fn vitest_mock_source(call: &CallExpression<'_>) -> Option<String> {
let Expression::StaticMemberExpression(member) = &call.callee else {
return None;
};
if member.property.name != "mock" {
return None;
}
let Expression::Identifier(object) = &member.object else {
return None;
};
if object.name != "vi" {
return None;
}
call.arguments.first().and_then(|argument| match argument {
Argument::StringLiteral(value) => Some(value.value.to_string()),
Argument::TemplateLiteral(value) if value.expressions.is_empty() => value
.quasis
.first()
.map(|quasi| quasi.value.raw.to_string()),
Argument::ImportExpression(value) => match &value.source {
Expression::StringLiteral(source) => Some(source.value.to_string()),
_ => None,
},
_ => None,
})
}
fn vitest_auto_mock_source(source: &str) -> Option<String> {
if source.is_empty()
|| source.contains("://")
|| source.starts_with("data:")
|| source.split('/').any(|segment| segment == "__mocks__")
{
return None;
}
let (dir, file_name) = source.rsplit_once('/')?;
if file_name.is_empty() {
return None;
}
Some(format!("{dir}/__mocks__/{file_name}"))
}
fn vi_mock_has_factory(call: &CallExpression<'_>) -> bool {
fn is_factory_expression(expr: &Expression<'_>) -> bool {
match expr {
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) => true,
Expression::ParenthesizedExpression(paren) => is_factory_expression(&paren.expression),
_ => false,
}
}
fn is_factory_arg(arg: &Argument<'_>) -> bool {
match arg {
Argument::ArrowFunctionExpression(_) | Argument::FunctionExpression(_) => true,
Argument::ParenthesizedExpression(paren) => is_factory_expression(&paren.expression),
_ => false,
}
}
call.arguments.get(1).is_some_and(is_factory_arg)
}
fn node_module_register_specifier(call: &CallExpression<'_>) -> Option<String> {
match call.arguments.first()? {
Argument::StringLiteral(value) => Some(value.value.to_string()),
Argument::TemplateLiteral(value) if value.expressions.is_empty() => value
.quasis
.first()
.map(|quasi| quasi.value.raw.to_string()),
_ => None,
}
}
const NODE_MODULE_REGISTER_HOOK_EXPORTS: &[&str] = &[
"initialize",
"resolve",
"load",
"globalPreload",
"getFormat",
"getSource",
"transformSource",
];
fn loader_hook_exports_for_source(source: &str) -> Vec<String> {
if source.starts_with("./")
|| source.starts_with("../")
|| source.starts_with('/')
|| source.starts_with("file:")
{
NODE_MODULE_REGISTER_HOOK_EXPORTS
.iter()
.map(|name| (*name).to_string())
.collect()
} else {
Vec::new()
}
}
fn new_url_import_source(expr: &NewExpression<'_>) -> Option<String> {
if let Expression::Identifier(callee) = &expr.callee
&& callee.name == "URL"
&& expr.arguments.len() == 2
&& let Some(Argument::StringLiteral(path_lit)) = expr.arguments.first()
&& is_meta_url_arg(&expr.arguments[1])
&& (path_lit.value.starts_with("./") || path_lit.value.starts_with("../"))
&& !path_lit.value.ends_with('/')
{
Some(path_lit.value.to_string())
} else {
None
}
}
#[derive(Default)]
struct PlaywrightFixtureMemberCollector {
fixture_by_local: FxHashMap<String, String>,
accesses: Vec<MemberAccess>,
}
impl PlaywrightFixtureMemberCollector {
fn new(fixture_by_local: FxHashMap<String, String>) -> Self {
Self {
fixture_by_local,
accesses: Vec::new(),
}
}
}
impl<'a> Visit<'a> for PlaywrightFixtureMemberCollector {
fn visit_static_member_expression(&mut self, expr: &StaticMemberExpression<'a>) {
if let Some(object_dotted) = static_member_object_name(&expr.object)
&& let Some(fixture_path) =
resolve_object_to_fixture_path(&object_dotted, &self.fixture_by_local)
{
self.accesses.push(MemberAccess {
object: fixture_path,
member: expr.property.name.to_string(),
});
return;
}
walk::walk_static_member_expression(self, expr);
}
}
fn extract_binding_local_name<'a>(pattern: &'a BindingPattern<'a>) -> Option<&'a str> {
match pattern {
BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
BindingPattern::AssignmentPattern(assign) => extract_binding_local_name(&assign.left),
_ => None,
}
}
fn extract_object_pattern_bindings(pattern: &ObjectPattern<'_>) -> FxHashMap<String, String> {
let mut bindings = FxHashMap::default();
collect_object_pattern_bindings(pattern, "", &mut bindings);
bindings
}
fn collect_object_pattern_bindings(
pattern: &ObjectPattern<'_>,
path_prefix: &str,
bindings: &mut FxHashMap<String, String>,
) {
for prop in &pattern.properties {
let Some(fixture_name) = prop.key.static_name() else {
continue;
};
let next_path = if path_prefix.is_empty() {
fixture_name.to_string()
} else {
format!("{path_prefix}.{fixture_name}")
};
match &prop.value {
BindingPattern::ObjectPattern(inner) => {
collect_object_pattern_bindings(inner, &next_path, bindings);
}
other => {
if let Some(local_name) = extract_binding_local_name(other) {
bindings.insert(local_name.to_string(), next_path);
}
}
}
}
}
fn resolve_object_to_fixture_path(
object_dotted: &str,
fixture_by_local: &FxHashMap<String, String>,
) -> Option<String> {
let (root, rest) = object_dotted
.split_once('.')
.map_or((object_dotted, ""), |(r, x)| (r, x));
let base = fixture_by_local.get(root)?;
if rest.is_empty() {
Some(base.clone())
} else {
Some(format!("{base}.{rest}"))
}
}
fn playwright_test_callee_name(expr: &Expression<'_>) -> Option<String> {
match expr {
Expression::Identifier(ident) => Some(ident.name.to_string()),
Expression::StaticMemberExpression(member) => playwright_test_callee_name(&member.object),
Expression::CallExpression(call) => playwright_test_callee_name(&call.callee),
_ => None,
}
}
fn extract_function_body_final_return_call<'a, 'b>(
body: &'b oxc_ast::ast::FunctionBody<'a>,
) -> Option<&'b CallExpression<'a>> {
let Statement::ReturnStatement(ret) = body.statements.last()? else {
return None;
};
let Expression::CallExpression(call) = ret.argument.as_ref()? else {
return None;
};
Some(call.as_ref())
}
fn extract_arrow_return_call<'a, 'b>(
arrow: &'b oxc_ast::ast::ArrowFunctionExpression<'a>,
) -> Option<&'b CallExpression<'a>> {
if arrow.expression {
if arrow.body.statements.len() != 1 {
return None;
}
let Statement::ExpressionStatement(stmt) = arrow.body.statements.first()? else {
return None;
};
let Expression::CallExpression(call) = &stmt.expression else {
return None;
};
return Some(call.as_ref());
}
extract_function_body_final_return_call(&arrow.body)
}
fn collect_playwright_fixture_member_uses(
test_name: &str,
arguments: &[Argument<'_>],
) -> Vec<MemberAccess> {
let Some(callback) = arguments.iter().find_map(|arg| match arg {
Argument::ArrowFunctionExpression(arrow) => {
Some((arrow.params.items.first()?, arrow.body.as_ref()))
}
Argument::FunctionExpression(function) => {
Some((function.params.items.first()?, function.body.as_deref()?))
}
_ => None,
}) else {
return Vec::new();
};
let BindingPattern::ObjectPattern(pattern) = &callback.0.pattern else {
return Vec::new();
};
let fixture_by_local = extract_object_pattern_bindings(pattern);
if fixture_by_local.is_empty() {
return Vec::new();
}
let mut collector = PlaywrightFixtureMemberCollector::new(fixture_by_local);
collector.visit_function_body(callback.1);
collector
.accesses
.into_iter()
.map(|access| MemberAccess {
object: format!(
"{}{}:{}",
crate::PLAYWRIGHT_FIXTURE_USE_SENTINEL,
test_name,
access.object
),
member: access.member,
})
.collect()
}
fn playwright_extend_base_name(call: &CallExpression<'_>) -> Option<String> {
let Expression::StaticMemberExpression(member) = &call.callee else {
return None;
};
if member.property.name != "extend" {
return None;
}
let Expression::Identifier(base) = &member.object else {
return None;
};
Some(base.name.to_string())
}
fn collect_fixture_type_bindings_from_type(
ty: &TSType<'_>,
path_prefix: &str,
aliases: &FxHashMap<String, Vec<(String, String)>>,
bindings: &mut Vec<(String, String)>,
) {
match ty {
TSType::TSTypeLiteral(type_lit) => {
for member in &type_lit.members {
let TSSignature::TSPropertySignature(prop) = member else {
continue;
};
let Some(fixture_name) = prop.key.static_name() else {
continue;
};
let Some(type_annotation) = prop.type_annotation.as_deref() else {
continue;
};
let next_path = if path_prefix.is_empty() {
fixture_name.to_string()
} else {
format!("{path_prefix}.{fixture_name}")
};
if let Some((alias_name, _)) =
fixture_type_reference_name(&type_annotation.type_annotation)
&& aliases.contains_key(alias_name.as_str())
{
collect_fixture_type_bindings_from_type(
&type_annotation.type_annotation,
&next_path,
aliases,
bindings,
);
} else if let Some(type_name) = extract_type_annotation_name(type_annotation) {
bindings.push((next_path, type_name));
} else {
collect_fixture_type_bindings_from_type(
&type_annotation.type_annotation,
&next_path,
aliases,
bindings,
);
}
}
}
TSType::TSTypeReference(type_ref) => {
let Some((alias_name, _)) = type_name_root(&type_ref.type_name) else {
return;
};
if let Some(alias_bindings) = aliases.get(alias_name.as_str()) {
for (suffix, type_name) in alias_bindings {
let combined = if path_prefix.is_empty() {
suffix.clone()
} else {
format!("{path_prefix}.{suffix}")
};
bindings.push((combined, type_name.clone()));
}
}
}
TSType::TSIntersectionType(intersection) => {
for branch in &intersection.types {
collect_fixture_type_bindings_from_type(branch, path_prefix, aliases, bindings);
}
}
TSType::TSParenthesizedType(paren) => {
collect_fixture_type_bindings_from_type(
&paren.type_annotation,
path_prefix,
aliases,
bindings,
);
}
_ => {}
}
}
fn fixture_type_reference_name(ty: &TSType<'_>) -> Option<(String, Span)> {
match ty {
TSType::TSTypeReference(type_ref) => type_name_root(&type_ref.type_name),
TSType::TSParenthesizedType(paren) => fixture_type_reference_name(&paren.type_annotation),
_ => None,
}
}
impl ModuleInfoExtractor {
fn record_node_module_register_url_binding(&mut self, name: String, sources: Vec<String>) {
let entry = self
.node_module_register_url_bindings
.entry(name)
.or_default();
for source in sources {
if !entry.contains(&source) {
entry.push(source);
}
}
}
fn node_module_register_url_binding(&self, name: &str) -> Vec<String> {
self.node_module_register_url_bindings
.get(name)
.cloned()
.unwrap_or_default()
}
fn record_local_type_declaration(&mut self, name: &str, span: Span) {
if self
.local_type_declarations
.iter()
.any(|decl| decl.name == name)
{
return;
}
self.local_type_declarations.push(LocalTypeDeclaration {
name: name.to_string(),
span,
});
}
fn record_local_signature_refs(&mut self, owner_name: &str, refs: Vec<(String, Span)>) {
self.local_signature_type_references
.extend(
refs.into_iter()
.map(|(type_name, span)| super::LocalSignatureTypeReference {
owner_name: owner_name.to_string(),
type_name,
span,
}),
);
}
fn record_public_signature_refs(&mut self, export_name: &str, refs: Vec<(String, Span)>) {
self.public_signature_type_references
.extend(
refs.into_iter()
.map(|(type_name, span)| PublicSignatureTypeReference {
export_name: export_name.to_string(),
type_name,
span,
}),
);
}
fn collect_type_refs_from_annotation(annotation: &TSTypeAnnotation<'_>) -> Vec<(String, Span)> {
let mut collector = SignatureTypeCollector::default();
collector.visit_ts_type_annotation(annotation);
collector.refs
}
fn collect_function_signature_refs(function: &Function<'_>) -> Vec<(String, Span)> {
let mut collector = SignatureTypeCollector::default();
if let Some(type_parameters) = function.type_parameters.as_deref() {
collector.visit_ts_type_parameter_declaration(type_parameters);
}
if let Some(this_param) = function.this_param.as_deref() {
collector.visit_ts_this_parameter(this_param);
}
for param in &function.params.items {
if let Some(annotation) = param.type_annotation.as_deref() {
collector.visit_ts_type_annotation(annotation);
}
}
if let Some(rest) = function.params.rest.as_deref()
&& let Some(annotation) = rest.type_annotation.as_deref()
{
collector.visit_ts_type_annotation(annotation);
}
if let Some(return_type) = function.return_type.as_deref() {
collector.visit_ts_type_annotation(return_type);
}
collector.refs
}
fn collect_arrow_signature_refs(arrow: &ArrowFunctionExpression<'_>) -> Vec<(String, Span)> {
let mut collector = SignatureTypeCollector::default();
if let Some(type_parameters) = arrow.type_parameters.as_deref() {
collector.visit_ts_type_parameter_declaration(type_parameters);
}
for param in &arrow.params.items {
if let Some(annotation) = param.type_annotation.as_deref() {
collector.visit_ts_type_annotation(annotation);
}
}
if let Some(rest) = arrow.params.rest.as_deref()
&& let Some(annotation) = rest.type_annotation.as_deref()
{
collector.visit_ts_type_annotation(annotation);
}
if let Some(return_type) = arrow.return_type.as_deref() {
collector.visit_ts_type_annotation(return_type);
}
collector.refs
}
fn collect_variable_signature_refs(declarator: &VariableDeclarator<'_>) -> Vec<(String, Span)> {
let mut refs = Vec::new();
if let Some(annotation) = declarator.type_annotation.as_deref() {
refs.extend(Self::collect_type_refs_from_annotation(annotation));
}
if let Some(init) = &declarator.init {
match init {
Expression::ArrowFunctionExpression(arrow) => {
refs.extend(Self::collect_arrow_signature_refs(arrow));
}
Expression::FunctionExpression(function) => {
refs.extend(Self::collect_function_signature_refs(function));
}
_ => {}
}
}
refs
}
fn collect_class_signature_refs(class: &Class<'_>) -> Vec<(String, Span)> {
let mut collector = SignatureTypeCollector::default();
if let Some(type_parameters) = class.type_parameters.as_deref() {
collector.visit_ts_type_parameter_declaration(type_parameters);
}
if let Some(super_class) = class.super_class.as_ref()
&& let Some((name, span)) = expression_root_name(super_class)
{
collector.refs.push((name, span));
}
if let Some(type_arguments) = class.super_type_arguments.as_deref() {
collector.visit_ts_type_parameter_instantiation(type_arguments);
}
for implemented in &class.implements {
if let Some((name, span)) = type_name_root(&implemented.expression) {
collector.refs.push((name, span));
}
if let Some(type_arguments) = implemented.type_arguments.as_deref() {
collector.visit_ts_type_parameter_instantiation(type_arguments);
}
}
for element in &class.body.body {
match element {
ClassElement::MethodDefinition(method) => {
if matches!(method.accessibility, Some(TSAccessibility::Private))
|| is_private_member_key(&method.key)
{
continue;
}
collector
.refs
.extend(Self::collect_function_signature_refs(&method.value));
}
ClassElement::PropertyDefinition(prop) => {
if matches!(prop.accessibility, Some(TSAccessibility::Private))
|| is_private_member_key(&prop.key)
{
continue;
}
if let Some(annotation) = prop.type_annotation.as_deref() {
collector.visit_ts_type_annotation(annotation);
}
}
ClassElement::AccessorProperty(prop) => {
if matches!(prop.accessibility, Some(TSAccessibility::Private))
|| is_private_member_key(&prop.key)
{
continue;
}
if let Some(annotation) = prop.type_annotation.as_deref() {
collector.visit_ts_type_annotation(annotation);
}
}
ClassElement::TSIndexSignature(index) => {
collector.visit_ts_index_signature(index);
}
ClassElement::StaticBlock(_) => {}
}
}
collector.refs
}
fn collect_interface_signature_refs(iface: &TSInterfaceDeclaration<'_>) -> Vec<(String, Span)> {
let mut collector = SignatureTypeCollector::default();
if let Some(type_parameters) = iface.type_parameters.as_deref() {
collector.visit_ts_type_parameter_declaration(type_parameters);
}
for heritage in &iface.extends {
if let Some((name, span)) = expression_root_name(&heritage.expression) {
collector.refs.push((name, span));
}
if let Some(type_arguments) = heritage.type_arguments.as_deref() {
collector.visit_ts_type_parameter_instantiation(type_arguments);
}
}
collector.visit_ts_interface_body(&iface.body);
collector.refs
}
fn collect_type_alias_signature_refs(
alias: &TSTypeAliasDeclaration<'_>,
) -> Vec<(String, Span)> {
let mut collector = SignatureTypeCollector::default();
if let Some(type_parameters) = alias.type_parameters.as_deref() {
collector.visit_ts_type_parameter_declaration(type_parameters);
}
collector.visit_ts_type(&alias.type_annotation);
collector.refs
}
fn record_typed_binding(&mut self, binding_name: &str, type_annotation: &TSTypeAnnotation<'_>) {
if let Some(type_name) = extract_type_annotation_name(type_annotation)
&& let Some(resolved) = self.resolve_class_type_param(&type_name)
{
self.binding_target_names
.insert(binding_name.to_string(), resolved);
}
for (property_path, type_name) in extract_nested_type_bindings(type_annotation) {
let Some(resolved) = self.resolve_class_type_param(&type_name) else {
continue;
};
self.binding_target_names
.insert(format!("{binding_name}.{property_path}"), resolved);
}
}
fn resolve_class_type_param(&self, type_name: &str) -> Option<String> {
let Some(frame) = self.class_type_param_constraints.last() else {
return Some(type_name.to_string());
};
match frame.get(type_name) {
Some(Some(constraint)) => Some(constraint.clone()),
Some(None) => None,
None => Some(type_name.to_string()),
}
}
fn try_record_fluent_chain_access(&mut self, expr: &CallExpression<'_>) {
let Expression::StaticMemberExpression(member) = &expr.callee else {
return;
};
let Expression::CallExpression(_) = &member.object else {
return;
};
let this_method = member.property.name.as_str();
let mut chain_prefix_reversed: Vec<String> = Vec::new();
let mut current = &member.object;
loop {
let Expression::CallExpression(call) = current else {
return;
};
let Expression::StaticMemberExpression(inner_member) = &call.callee else {
return;
};
if let Expression::Identifier(root_id) = &inner_member.object {
chain_prefix_reversed.reverse();
let chain_prefix = chain_prefix_reversed.join(",");
self.member_accesses.push(MemberAccess {
object: format!(
"{}{}:{}:{}",
crate::FLUENT_CHAIN_SENTINEL,
root_id.name,
inner_member.property.name,
chain_prefix,
),
member: this_method.to_string(),
});
return;
}
chain_prefix_reversed.push(inner_member.property.name.to_string());
current = &inner_member.object;
}
}
fn bind_iterable_callback_parameter(&mut self, expr: &CallExpression<'_>) {
let (receiver_expr, method_name) = match &expr.callee {
Expression::StaticMemberExpression(member) => (&member.object, &member.property.name),
Expression::ChainExpression(chain) => match &chain.expression {
ChainElement::StaticMemberExpression(member) => {
(&member.object, &member.property.name)
}
_ => return,
},
_ => return,
};
if method_name.as_str() != "forEach" {
return;
}
let Some(receiver_name) = static_member_object_name(receiver_expr) else {
return;
};
let Some(element_type) = self.iterable_element_types.get(&receiver_name).cloned() else {
return;
};
let Some(first_arg) = expr.arguments.first() else {
return;
};
let param_name = match first_arg {
Argument::ArrowFunctionExpression(arrow) => {
arrow.params.items.first().and_then(|p| match &p.pattern {
BindingPattern::BindingIdentifier(id) => Some(id.name.to_string()),
_ => None,
})
}
Argument::FunctionExpression(func) => {
func.params.items.first().and_then(|p| match &p.pattern {
BindingPattern::BindingIdentifier(id) => Some(id.name.to_string()),
_ => None,
})
}
_ => None,
};
if let Some(name) = param_name {
self.binding_target_names.insert(name, element_type);
}
}
fn is_named_import_from(&self, local_name: &str, source: &str, imported_name: &str) -> bool {
self.imports.iter().any(|import| {
import.source == source
&& import.local_name == local_name
&& matches!(&import.imported_name, ImportedName::Named(name) if name == imported_name)
})
}
fn try_record_node_module_register(&mut self, expr: &CallExpression<'_>) {
let register_match = match &expr.callee {
Expression::Identifier(ident) => {
self.is_node_module_register(ident.name.as_str(), false)
}
Expression::StaticMemberExpression(member) => {
member.property.name == "register"
&& matches!(&member.object, Expression::Identifier(obj)
if self.is_node_module_register(obj.name.as_str(), true))
}
_ => false,
};
if !register_match {
return;
}
let sources = self.node_module_register_sources(expr);
for source in sources.into_iter().filter(|source| !source.is_empty()) {
let destructured_names = loader_hook_exports_for_source(&source);
self.dynamic_imports.push(DynamicImportInfo {
source,
span: expr.span,
destructured_names,
local_name: None,
is_speculative: false,
});
}
}
fn node_module_register_sources(&self, call: &CallExpression<'_>) -> Vec<String> {
if let Some(source) = node_module_register_specifier(call) {
return vec![source];
}
let Some(first_arg) = call.arguments.first() else {
return Vec::new();
};
first_arg
.as_expression()
.map(|expr| self.node_module_register_sources_from_expression(expr))
.unwrap_or_default()
}
fn node_module_register_sources_from_expression(&self, expr: &Expression<'_>) -> Vec<String> {
match expr {
Expression::Identifier(ident) => {
self.node_module_register_url_binding(ident.name.as_str())
}
Expression::NewExpression(new_expr) => {
new_url_import_source(new_expr).into_iter().collect()
}
Expression::ConditionalExpression(conditional) => {
let mut sources =
self.node_module_register_sources_from_expression(&conditional.consequent);
sources.extend(
self.node_module_register_sources_from_expression(&conditional.alternate),
);
sources.sort();
sources.dedup();
sources
}
Expression::ParenthesizedExpression(paren) => {
self.node_module_register_sources_from_expression(&paren.expression)
}
Expression::TSAsExpression(ts_as) => {
self.node_module_register_sources_from_expression(&ts_as.expression)
}
Expression::TSSatisfiesExpression(ts_sat) => {
self.node_module_register_sources_from_expression(&ts_sat.expression)
}
_ => Vec::new(),
}
}
fn extract_angular_inject_target(&self, call: &CallExpression<'_>) -> Option<String> {
let Expression::Identifier(callee) = &call.callee else {
return None;
};
if !self.is_named_import_from(callee.name.as_str(), "@angular/core", "inject") {
return None;
}
if let Some(type_arguments) = call.type_arguments.as_deref()
&& let Some(TSType::TSTypeReference(type_ref)) = type_arguments.params.first()
&& let Some((type_name, _)) = type_name_root(&type_ref.type_name)
{
return Some(type_name);
}
let Some(Argument::Identifier(target)) = call.arguments.first() else {
return None;
};
Some(target.name.to_string())
}
fn copy_nested_binding_targets(&mut self, source_binding: &str, target_binding: &str) -> bool {
let source_prefix = format!("{source_binding}.");
let target_prefix = format!("{target_binding}.");
let copied: Vec<(String, String)> = self
.binding_target_names
.iter()
.filter_map(|(binding, target)| {
binding
.strip_prefix(&source_prefix)
.map(|suffix| (format!("{target_prefix}{suffix}"), target.clone()))
})
.collect();
let mut changed = false;
for (binding, target) in copied {
changed |= self.insert_binding_target(binding, target);
}
changed
}
fn insert_binding_target(&mut self, binding: String, target: String) -> bool {
if self.binding_target_names.get(&binding) == Some(&target) {
return false;
}
self.binding_target_names.insert(binding, target);
true
}
pub(super) fn resolve_object_binding_candidate(
&mut self,
candidate: &super::ObjectBindingCandidate,
) -> bool {
let mut changed = false;
if self
.namespace_binding_names
.iter()
.any(|name| name == candidate.source_name.as_str())
{
changed |= self.insert_binding_target(
candidate.binding_path.clone(),
candidate.source_name.clone(),
);
} else if let Some(target_name) = self
.binding_target_names
.get(candidate.source_name.as_str())
.cloned()
{
changed |= self.insert_binding_target(candidate.binding_path.clone(), target_name);
}
changed | self.copy_nested_binding_targets(&candidate.source_name, &candidate.binding_path)
}
fn record_object_binding_targets(&mut self, binding_name: &str, obj: &ObjectExpression<'_>) {
self.record_object_binding_targets_at_path(binding_name, obj);
}
fn record_object_binding_targets_at_path(
&mut self,
object_path: &str,
obj: &ObjectExpression<'_>,
) {
for prop in &obj.properties {
let ObjectPropertyKind::ObjectProperty(prop) = prop else {
continue;
};
let Some(key_name) = prop.key.static_name() else {
continue;
};
let binding_path = format!("{object_path}.{key_name}");
match &prop.value {
Expression::Identifier(ident) => {
self.object_binding_candidates
.push(super::ObjectBindingCandidate {
binding_path,
source_name: ident.name.to_string(),
});
}
Expression::ObjectExpression(child) => {
self.record_object_binding_targets_at_path(&binding_path, child);
}
_ => {}
}
}
}
fn collect_playwright_fixture_type_bindings(&self, ty: &TSType<'_>) -> Vec<(String, String)> {
let mut bindings = Vec::new();
collect_fixture_type_bindings_from_type(
ty,
"",
&self.playwright_fixture_types,
&mut bindings,
);
bindings.sort_unstable();
bindings.dedup();
bindings
}
fn record_playwright_fixture_type_alias(&mut self, alias: &TSTypeAliasDeclaration<'_>) {
let bindings = self.collect_playwright_fixture_type_bindings(&alias.type_annotation);
if !bindings.is_empty() {
self.playwright_fixture_types
.insert(alias.id.name.to_string(), bindings);
}
}
fn record_playwright_fixture_definitions(
&mut self,
test_name: &str,
call: &CallExpression<'_>,
) {
let Some(base_name) = playwright_extend_base_name(call) else {
return;
};
if !self.is_named_import_from(base_name.as_str(), "@playwright/test", "test") {
return;
}
let Some(type_arguments) = call.type_arguments.as_deref() else {
return;
};
let mut bindings = Vec::new();
for type_arg in &type_arguments.params {
bindings.extend(self.collect_playwright_fixture_type_bindings(type_arg));
}
bindings.sort_unstable();
bindings.dedup();
self.member_accesses
.extend(
bindings
.into_iter()
.map(|(fixture_name, type_name)| MemberAccess {
object: format!(
"{}{}:{}",
crate::PLAYWRIGHT_FIXTURE_DEF_SENTINEL,
test_name,
fixture_name
),
member: type_name,
}),
);
}
pub(super) fn try_capture_playwright_factory_helper(
&mut self,
test_name: &str,
call: &CallExpression<'_>,
) {
if let Some(base_name) = playwright_extend_base_name(call) {
let Some(type_arguments) = call.type_arguments.as_deref() else {
return;
};
let mut bindings = Vec::new();
for type_arg in &type_arguments.params {
bindings.extend(self.collect_playwright_fixture_type_bindings(type_arg));
}
bindings.sort_unstable();
bindings.dedup();
if bindings.is_empty() {
return;
}
self.pending_playwright_factory_calls
.push(super::PendingPlaywrightFactory {
test_name: test_name.to_string(),
base_name,
type_bindings: bindings,
});
} else if let Expression::Identifier(ident) = &call.callee {
self.pending_playwright_factory_aliases
.push((test_name.to_string(), ident.name.to_string()));
}
}
}
impl<'a> Visit<'a> for ModuleInfoExtractor {
fn visit_formal_parameter(&mut self, param: &FormalParameter<'a>) {
if let BindingPattern::BindingIdentifier(id) = ¶m.pattern
&& let Some(type_annotation) = param.type_annotation.as_deref()
{
self.record_typed_binding(id.name.as_str(), type_annotation);
if param.accessibility.is_some() {
self.record_typed_binding(format!("this.{}", id.name).as_str(), type_annotation);
}
}
walk::walk_formal_parameter(self, param);
}
fn visit_property_definition(&mut self, prop: &PropertyDefinition<'a>) {
if let Some(name) = prop.key.static_name() {
if let Some(type_annotation) = prop.type_annotation.as_deref() {
self.record_typed_binding(format!("this.{name}").as_str(), type_annotation);
if has_angular_plural_query_decorator(&prop.decorators)
&& let Some(element_type) = extract_query_list_element_type(type_annotation)
{
self.iterable_element_types
.insert(format!("this.{name}"), element_type);
}
}
if let Some(Expression::NewExpression(new_expr)) = &prop.value
&& let Expression::Identifier(callee) = &new_expr.callee
&& !super::helpers::is_builtin_constructor(callee.name.as_str())
{
self.binding_target_names
.insert(format!("this.{name}"), callee.name.to_string());
}
if let Some(Expression::CallExpression(call)) = &prop.value
&& let Some(type_name) = self.extract_angular_inject_target(call)
{
self.binding_target_names
.insert(format!("this.{name}"), type_name);
}
if let Some(value) = prop.value.as_ref()
&& let Some(query) = extract_angular_signal_query(value)
{
let call_key = format!("this.{name}()");
if query.plural {
self.iterable_element_types.insert(call_key, query.type_arg);
} else {
self.binding_target_names.insert(call_key, query.type_arg);
}
}
}
walk::walk_property_definition(self, prop);
}
fn visit_block_statement(&mut self, stmt: &BlockStatement<'a>) {
self.block_depth += 1;
walk::walk_block_statement(self, stmt);
self.block_depth -= 1;
}
fn visit_declaration(&mut self, decl: &Declaration<'a>) {
if self.block_depth == 0 && self.function_depth == 0 && self.namespace_depth == 0 {
match decl {
Declaration::VariableDeclaration(var) => {
for declarator in &var.declarations {
for id in declarator.id.get_binding_identifiers() {
self.record_local_declaration_name(&id.name);
}
}
}
Declaration::ClassDeclaration(class) => {
if let Some(id) = class.id.as_ref() {
self.record_local_declaration_name(&id.name);
self.record_local_type_declaration(&id.name, id.span);
let is_angular = has_angular_class_decorator(class);
let instance_bindings =
super::helpers::extract_class_instance_bindings(class);
self.record_local_class_export(
id.name.to_string(),
extract_class_members(class, is_angular),
extract_super_class_name(class),
extract_implemented_interface_names(class),
instance_bindings,
);
let refs = Self::collect_class_signature_refs(class);
self.record_local_signature_refs(&id.name, refs);
}
}
Declaration::FunctionDeclaration(function) => {
if let Some(id) = function.id.as_ref() {
self.record_local_declaration_name(&id.name);
let refs = Self::collect_function_signature_refs(function);
self.record_local_signature_refs(&id.name, refs);
if let Some(body) = function.body.as_deref()
&& let Some(call) = extract_function_body_final_return_call(body)
{
self.try_capture_playwright_factory_helper(id.name.as_str(), call);
}
}
}
Declaration::TSTypeAliasDeclaration(alias) => {
self.record_local_declaration_name(&alias.id.name);
self.record_local_type_declaration(&alias.id.name, alias.id.span);
self.record_playwright_fixture_type_alias(alias);
let refs = Self::collect_type_alias_signature_refs(alias);
self.record_local_signature_refs(&alias.id.name, refs);
}
Declaration::TSInterfaceDeclaration(iface) => {
self.record_local_declaration_name(&iface.id.name);
self.record_local_type_declaration(&iface.id.name, iface.id.span);
let refs = Self::collect_interface_signature_refs(iface);
self.record_local_signature_refs(&iface.id.name, refs);
}
Declaration::TSEnumDeclaration(enumd) => {
self.record_local_declaration_name(&enumd.id.name);
self.record_local_type_declaration(&enumd.id.name, enumd.id.span);
}
Declaration::TSModuleDeclaration(module) => {
if let TSModuleDeclarationName::Identifier(id) = &module.id {
self.record_local_declaration_name(&id.name);
self.record_local_type_declaration(&id.name, id.span);
}
}
_ => {}
}
}
walk::walk_declaration(self, decl);
}
fn visit_function(&mut self, func: &Function<'a>, flags: ScopeFlags) {
self.function_depth += 1;
walk::walk_function(self, func, flags);
self.function_depth -= 1;
}
fn visit_arrow_function_expression(&mut self, expr: &ArrowFunctionExpression<'a>) {
self.function_depth += 1;
walk::walk_arrow_function_expression(self, expr);
self.function_depth -= 1;
}
fn visit_import_declaration(&mut self, decl: &ImportDeclaration<'a>) {
let source = decl.source.value.to_string();
let is_type_only = decl.import_kind.is_type();
let source_span = decl.source.span;
if let Some(specifiers) = &decl.specifiers {
for spec in specifiers {
match spec {
ImportDeclarationSpecifier::ImportSpecifier(s) => {
self.imports.push(ImportInfo {
source: source.clone(),
imported_name: ImportedName::Named(s.imported.name().to_string()),
local_name: s.local.name.to_string(),
is_type_only: is_type_only || s.import_kind.is_type(),
from_style: false,
span: s.span,
source_span,
});
}
ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
self.imports.push(ImportInfo {
source: source.clone(),
imported_name: ImportedName::Default,
local_name: s.local.name.to_string(),
is_type_only,
from_style: false,
span: s.span,
source_span,
});
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
let local = s.local.name.to_string();
self.namespace_binding_names.push(local.clone());
self.imports.push(ImportInfo {
source: source.clone(),
imported_name: ImportedName::Namespace,
local_name: local,
is_type_only,
from_style: false,
span: s.span,
source_span,
});
}
}
}
} else {
self.imports.push(ImportInfo {
source,
imported_name: ImportedName::SideEffect,
local_name: String::new(),
is_type_only: false,
from_style: false,
span: decl.span,
source_span,
});
}
}
fn visit_export_named_declaration(&mut self, decl: &ExportNamedDeclaration<'a>) {
let is_namespace = matches!(&decl.declaration, Some(Declaration::TSModuleDeclaration(_)));
if self.namespace_depth > 0 {
if let Some(declaration) = &decl.declaration {
self.extract_namespace_members(declaration);
}
if is_namespace {
self.namespace_depth += 1;
}
walk::walk_export_named_declaration(self, decl);
if is_namespace {
self.namespace_depth -= 1;
}
return;
}
let is_type_only = decl.export_kind.is_type();
if let Some(source) = &decl.source {
for spec in &decl.specifiers {
self.re_exports.push(ReExportInfo {
source: source.value.to_string(),
imported_name: spec.local.name().to_string(),
exported_name: spec.exported.name().to_string(),
is_type_only: is_type_only || spec.export_kind.is_type(),
span: spec.span,
});
}
} else {
if let Some(declaration) = &decl.declaration {
self.extract_declaration_exports(declaration, is_type_only);
}
for spec in &decl.specifiers {
let local_name_str = spec.local.name().as_str();
let spec_type_only = is_type_only || spec.export_kind.is_type();
self.pending_local_export_specifiers
.push(PendingLocalExportSpecifier {
local_name: local_name_str.to_string(),
exported_name: spec.exported.name().to_string(),
is_type_only: spec_type_only,
span: spec.span,
});
}
}
if is_namespace {
self.namespace_depth += 1;
self.pending_namespace_members.clear();
}
walk::walk_export_named_declaration(self, decl);
if is_namespace {
self.namespace_depth -= 1;
if let Some(ns_export) = self.exports.last_mut() {
ns_export.members = std::mem::take(&mut self.pending_namespace_members);
}
}
}
fn visit_export_default_declaration(&mut self, decl: &ExportDefaultDeclaration<'a>) {
let (members, super_class, implemented_interfaces, instance_bindings) =
if let ExportDefaultDeclarationKind::ClassDeclaration(class) = &decl.declaration {
let is_angular = has_angular_class_decorator(class);
let bindings = super::helpers::extract_class_instance_bindings(class);
(
extract_class_members(class, is_angular),
extract_super_class_name(class),
extract_implemented_interface_names(class),
bindings,
)
} else {
(vec![], None, vec![], vec![])
};
let local_name = match &decl.declaration {
ExportDefaultDeclarationKind::ClassDeclaration(class) => {
class.id.as_ref().map(|id| id.name.to_string())
}
ExportDefaultDeclarationKind::FunctionDeclaration(function) => {
function.id.as_ref().map(|id| id.name.to_string())
}
_ => None,
};
match &decl.declaration {
ExportDefaultDeclarationKind::ClassDeclaration(class) => {
let refs = Self::collect_class_signature_refs(class);
if let Some(id) = class.id.as_ref() {
self.record_local_type_declaration(&id.name, id.span);
self.record_local_signature_refs(&id.name, refs);
} else {
self.record_public_signature_refs("default", refs);
}
}
ExportDefaultDeclarationKind::FunctionDeclaration(function) => {
let refs = Self::collect_function_signature_refs(function);
if let Some(id) = function.id.as_ref() {
self.record_local_signature_refs(&id.name, refs);
} else {
self.record_public_signature_refs("default", refs);
}
}
ExportDefaultDeclarationKind::TSInterfaceDeclaration(iface) => {
self.record_local_type_declaration(&iface.id.name, iface.id.span);
let refs = Self::collect_interface_signature_refs(iface);
self.record_public_signature_refs("default", refs);
}
_ => {}
}
if super_class.is_some()
|| !implemented_interfaces.is_empty()
|| !instance_bindings.is_empty()
{
self.class_heritage.push(ClassHeritageInfo {
export_name: "default".to_string(),
super_class: super_class.clone(),
implements: implemented_interfaces,
instance_bindings,
});
}
self.exports.push(ExportInfo {
name: ExportName::Default,
local_name,
is_type_only: false,
is_side_effect_used: false,
visibility: VisibilityTag::None,
span: decl.span,
members,
super_class,
});
walk::walk_export_default_declaration(self, decl);
}
fn visit_export_all_declaration(&mut self, decl: &ExportAllDeclaration<'a>) {
let exported_name = decl
.exported
.as_ref()
.map_or_else(|| "*".to_string(), |e| e.name().to_string());
self.re_exports.push(ReExportInfo {
source: decl.source.value.to_string(),
imported_name: "*".to_string(),
exported_name,
is_type_only: decl.export_kind.is_type(),
span: decl.span,
});
walk::walk_export_all_declaration(self, decl);
}
fn visit_import_expression(&mut self, expr: &ImportExpression<'a>) {
if self.handled_import_spans.contains(&expr.span) {
walk::walk_import_expression(self, expr);
return;
}
match &expr.source {
Expression::StringLiteral(lit) => {
self.dynamic_imports.push(DynamicImportInfo {
source: lit.value.to_string(),
span: expr.span,
destructured_names: Vec::new(),
local_name: None,
is_speculative: false,
});
}
Expression::TemplateLiteral(tpl)
if !tpl.quasis.is_empty() && !tpl.expressions.is_empty() =>
{
let first_quasi = tpl.quasis[0].value.raw.to_string();
if first_quasi.starts_with("./") || first_quasi.starts_with("../") {
let prefix = if tpl.expressions.len() > 1 {
format!("{first_quasi}**/")
} else {
first_quasi
};
let suffix = if tpl.quasis.len() > 1 {
let last = &tpl.quasis[tpl.quasis.len() - 1];
let s = last.value.raw.to_string();
if s.is_empty() { None } else { Some(s) }
} else {
None
};
self.dynamic_import_patterns.push(DynamicImportPattern {
prefix,
suffix,
span: expr.span,
});
}
}
Expression::TemplateLiteral(tpl)
if !tpl.quasis.is_empty() && tpl.expressions.is_empty() =>
{
let value = tpl.quasis[0].value.raw.to_string();
if !value.is_empty() {
self.dynamic_imports.push(DynamicImportInfo {
source: value,
span: expr.span,
destructured_names: Vec::new(),
local_name: None,
is_speculative: false,
});
}
}
Expression::BinaryExpression(bin)
if bin.operator == oxc_ast::ast::BinaryOperator::Addition =>
{
if let Some((prefix, suffix)) = extract_concat_parts(bin)
&& (prefix.starts_with("./") || prefix.starts_with("../"))
{
self.dynamic_import_patterns.push(DynamicImportPattern {
prefix,
suffix,
span: expr.span,
});
}
}
_ => {}
}
walk::walk_import_expression(self, expr);
}
fn visit_variable_declaration(&mut self, decl: &VariableDeclaration<'a>) {
for declarator in &decl.declarations {
if self.block_depth == 0 && self.function_depth == 0 && self.namespace_depth == 0 {
let refs = Self::collect_variable_signature_refs(declarator);
for id in declarator.id.get_binding_identifiers() {
self.record_local_signature_refs(&id.name, refs.clone());
}
}
if let BindingPattern::BindingIdentifier(id) = &declarator.id
&& let Some(type_annotation) = declarator.type_annotation.as_deref()
{
self.record_typed_binding(id.name.as_str(), type_annotation);
}
let Some(init) = &declarator.init else {
continue;
};
if let BindingPattern::BindingIdentifier(id) = &declarator.id {
let sources = self.node_module_register_sources_from_expression(init);
if !sources.is_empty() {
self.record_node_module_register_url_binding(id.name.to_string(), sources);
}
}
if let BindingPattern::BindingIdentifier(id) = &declarator.id
&& let Expression::CallExpression(call) = init
{
self.record_playwright_fixture_definitions(id.name.as_str(), call);
}
if let BindingPattern::BindingIdentifier(id) = &declarator.id {
let helper_call = match init {
Expression::ArrowFunctionExpression(arrow) => extract_arrow_return_call(arrow),
Expression::FunctionExpression(func) => func
.body
.as_deref()
.and_then(extract_function_body_final_return_call),
_ => None,
};
if let Some(call) = helper_call {
self.try_capture_playwright_factory_helper(id.name.as_str(), call);
}
}
if let BindingPattern::BindingIdentifier(id) = &declarator.id
&& let Expression::ObjectExpression(obj) = init
{
self.record_object_binding_targets(id.name.as_str(), obj);
}
if let Some((call, source)) = try_extract_require(init) {
self.handle_require_declaration(declarator, call, source);
continue;
}
if let Expression::NewExpression(new_expr) = init
&& let Expression::Identifier(callee) = &new_expr.callee
&& let BindingPattern::BindingIdentifier(id) = &declarator.id
&& !super::helpers::is_builtin_constructor(callee.name.as_str())
{
self.binding_target_names
.insert(id.name.to_string(), callee.name.to_string());
}
if let Expression::CallExpression(call) = init
&& let BindingPattern::ArrayPattern(arr_pat) = &declarator.id
&& let Some(Some(BindingPattern::BindingIdentifier(id))) = arr_pat.elements.first()
&& let Some(class_name) =
super::helpers::try_extract_factory_new_class(&call.arguments)
{
self.binding_target_names
.insert(id.name.to_string(), class_name);
}
if let Expression::CallExpression(call) = init
&& let BindingPattern::BindingIdentifier(id) = &declarator.id
&& let Expression::StaticMemberExpression(member) = &call.callee
&& let Expression::Identifier(callee_object) = &member.object
{
self.factory_call_candidates
.push(super::FactoryCallCandidate {
local_name: id.name.to_string(),
callee_object: callee_object.name.to_string(),
callee_method: member.property.name.to_string(),
});
}
if let Expression::Identifier(ident) = init
&& self
.namespace_binding_names
.iter()
.any(|n| n == ident.name.as_str())
{
self.handle_namespace_destructuring(declarator, &ident.name);
continue;
}
let Some((import_expr, source)) = try_extract_dynamic_import(init) else {
continue;
};
self.handle_dynamic_import_declaration(declarator, import_expr, source);
}
walk::walk_variable_declaration(self, decl);
}
fn visit_object_property(&mut self, prop: &ObjectProperty<'a>) {
if let Some((import_expr, source)) = try_extract_property_callback_import(prop) {
self.dynamic_imports.push(DynamicImportInfo {
source: source.to_string(),
span: import_expr.span,
destructured_names: vec!["default".to_string()],
local_name: None,
is_speculative: false,
});
self.handled_import_spans.insert(import_expr.span);
}
walk::walk_object_property(self, prop);
}
fn visit_call_expression(&mut self, expr: &CallExpression<'a>) {
if let Some(test_name) = playwright_test_callee_name(&expr.callee) {
self.member_accesses
.extend(collect_playwright_fixture_member_uses(
test_name.as_str(),
&expr.arguments,
));
}
if let Some((_tag, class_name)) = extract_custom_elements_define(expr) {
self.side_effect_registered_class_names.insert(class_name);
}
self.bind_iterable_callback_parameter(expr);
if let Some(target_source) = vitest_mock_source(expr) {
self.dynamic_imports.push(DynamicImportInfo {
source: target_source.clone(),
span: expr.span,
destructured_names: Vec::new(),
local_name: None,
is_speculative: false,
});
if !vi_mock_has_factory(expr)
&& let Some(mock_source) = vitest_auto_mock_source(&target_source)
{
self.dynamic_imports.push(DynamicImportInfo {
source: mock_source,
span: expr.span,
destructured_names: Vec::new(),
local_name: Some(String::new()),
is_speculative: true,
});
}
}
self.try_record_node_module_register(expr);
if let Expression::Identifier(ident) = &expr.callee
&& ident.name == "require"
&& let Some(Argument::StringLiteral(lit)) = expr.arguments.first()
&& !self.handled_require_spans.contains(&expr.span)
{
self.require_calls.push(RequireCallInfo {
source: lit.value.to_string(),
span: expr.span,
destructured_names: Vec::new(),
local_name: None,
});
}
if let Expression::StaticMemberExpression(member) = &expr.callee
&& let Expression::Identifier(obj) = &member.object
&& obj.name == "Object"
&& matches!(
member.property.name.as_str(),
"values" | "keys" | "entries" | "getOwnPropertyNames"
)
&& let Some(arg_name) = expr.arguments.first().and_then(static_argument_object_name)
{
self.whole_object_uses.push(arg_name);
}
if let Expression::StaticMemberExpression(member) = &expr.callee
&& member.property.name == "glob"
&& matches!(member.object, Expression::MetaProperty(_))
&& let Some(first_arg) = expr.arguments.first()
{
match first_arg {
Argument::StringLiteral(lit) => {
let s = lit.value.to_string();
if s.starts_with("./") || s.starts_with("../") {
self.dynamic_import_patterns.push(DynamicImportPattern {
prefix: s,
suffix: None,
span: expr.span,
});
}
}
Argument::ArrayExpression(arr) => {
for elem in &arr.elements {
if let ArrayExpressionElement::StringLiteral(lit) = elem {
let s = lit.value.to_string();
if s.starts_with("./") || s.starts_with("../") {
self.dynamic_import_patterns.push(DynamicImportPattern {
prefix: s,
suffix: None,
span: expr.span,
});
}
}
}
}
_ => {}
}
}
if let Expression::StaticMemberExpression(member) = &expr.callee
&& member.property.name == "context"
&& let Expression::Identifier(obj) = &member.object
&& obj.name == "require"
&& let Some(Argument::StringLiteral(dir_lit)) = expr.arguments.first()
{
let dir = dir_lit.value.to_string();
if dir.starts_with("./") || dir.starts_with("../") {
let recursive = expr
.arguments
.get(1)
.is_some_and(|arg| matches!(arg, Argument::BooleanLiteral(b) if b.value));
let prefix = if recursive {
format!("{dir}/**/")
} else {
format!("{dir}/")
};
let suffix = expr.arguments.get(2).and_then(|arg| match arg {
Argument::RegExpLiteral(re) => regex_pattern_to_suffix(&re.regex.pattern.text),
_ => None,
});
self.dynamic_import_patterns.push(DynamicImportPattern {
prefix,
suffix,
span: expr.span,
});
}
}
if let Some(then_cb) = try_extract_import_then_callback(expr) {
if let Some(local) = &then_cb.local_name {
self.namespace_binding_names.push(local.clone());
}
self.handled_import_spans.insert(then_cb.import_span);
self.dynamic_imports.push(DynamicImportInfo {
source: then_cb.source,
span: then_cb.import_span,
destructured_names: then_cb.destructured_names,
local_name: then_cb.local_name,
is_speculative: false,
});
}
if let Some((import_expr, source)) = try_extract_arrow_wrapped_import(&expr.arguments) {
self.dynamic_imports.push(DynamicImportInfo {
source: source.to_string(),
span: import_expr.span,
destructured_names: vec!["default".to_string()],
local_name: None,
is_speculative: false,
});
self.handled_import_spans.insert(import_expr.span);
}
self.try_record_fluent_chain_access(expr);
walk::walk_call_expression(self, expr);
}
fn visit_new_expression(&mut self, expr: &oxc_ast::ast::NewExpression<'a>) {
if let Some(source) = new_url_import_source(expr) {
self.dynamic_imports.push(DynamicImportInfo {
source,
span: expr.span,
destructured_names: Vec::new(),
local_name: None,
is_speculative: false,
});
}
walk::walk_new_expression(self, expr);
}
fn visit_ts_import_type(&mut self, node: &oxc_ast::ast::TSImportType<'a>) {
let source = node.source.value.to_string();
let source_span = node.source.span;
let imported_name = node.qualifier.as_ref().map_or_else(
|| ImportedName::SideEffect,
|q| ImportedName::Named(ts_import_type_qualifier_root(q).to_string()),
);
self.imports.push(ImportInfo {
source,
imported_name,
local_name: String::new(),
is_type_only: true,
from_style: false,
span: node.span,
source_span,
});
walk::walk_ts_import_type(self, node);
}
#[expect(
clippy::excessive_nesting,
reason = "CJS export pattern matching requires deep nesting"
)]
fn visit_assignment_expression(&mut self, expr: &AssignmentExpression<'a>) {
if let AssignmentTarget::StaticMemberExpression(member) = &expr.left {
if let Expression::Identifier(obj) = &member.object {
if obj.name == "module" && member.property.name == "exports" {
self.has_cjs_exports = true;
if let Expression::ObjectExpression(obj_expr) = &expr.right {
for prop in &obj_expr.properties {
if let oxc_ast::ast::ObjectPropertyKind::ObjectProperty(p) = prop
&& let Some(name) = p.key.static_name()
{
self.exports.push(ExportInfo {
name: ExportName::Named(name.to_string()),
local_name: None,
is_type_only: false,
visibility: VisibilityTag::None,
span: p.span,
members: vec![],
is_side_effect_used: false,
super_class: None,
});
}
}
}
}
if obj.name == "exports" {
self.has_cjs_exports = true;
self.exports.push(ExportInfo {
name: ExportName::Named(member.property.name.to_string()),
local_name: None,
is_type_only: false,
visibility: VisibilityTag::None,
span: expr.span,
members: vec![],
is_side_effect_used: false,
super_class: None,
});
}
} else if let Expression::StaticMemberExpression(inner) = &member.object
&& let Expression::Identifier(obj) = &inner.object
&& obj.name == "module"
&& inner.property.name == "exports"
{
self.has_cjs_exports = true;
self.exports.push(ExportInfo {
name: ExportName::Named(member.property.name.to_string()),
local_name: None,
is_type_only: false,
visibility: VisibilityTag::None,
span: expr.span,
members: vec![],
is_side_effect_used: false,
super_class: None,
});
}
if matches!(member.object, Expression::ThisExpression(_)) {
self.member_accesses.push(MemberAccess {
object: "this".to_string(),
member: member.property.name.to_string(),
});
if let Expression::NewExpression(new_expr) = &expr.right
&& let Expression::Identifier(callee) = &new_expr.callee
&& !super::helpers::is_builtin_constructor(callee.name.as_str())
{
self.binding_target_names.insert(
format!("this.{}", member.property.name),
callee.name.to_string(),
);
} else if let Expression::Identifier(ident) = &expr.right
&& let Some(target_name) =
self.binding_target_names.get(ident.name.as_str()).cloned()
{
self.binding_target_names
.insert(format!("this.{}", member.property.name), target_name);
}
if let Expression::Identifier(ident) = &expr.right {
self.copy_nested_binding_targets(
ident.name.as_str(),
format!("this.{}", member.property.name).as_str(),
);
}
}
}
walk::walk_assignment_expression(self, expr);
}
fn visit_static_member_expression(&mut self, expr: &StaticMemberExpression<'a>) {
if let Some(object_name) = static_member_object_name(&expr.object) {
self.member_accesses.push(MemberAccess {
object: object_name,
member: expr.property.name.to_string(),
});
}
if matches!(expr.object, Expression::Super(_))
&& let Some(Some(super_local)) = self.class_super_stack.last()
{
self.member_accesses.push(MemberAccess {
object: super_local.clone(),
member: expr.property.name.to_string(),
});
}
walk::walk_static_member_expression(self, expr);
}
fn visit_computed_member_expression(&mut self, expr: &ComputedMemberExpression<'a>) {
if let Expression::Identifier(obj) = &expr.object {
if let Expression::StringLiteral(lit) = &expr.expression {
self.member_accesses.push(MemberAccess {
object: obj.name.to_string(),
member: lit.value.to_string(),
});
} else {
self.whole_object_uses.push(obj.name.to_string());
}
}
walk::walk_computed_member_expression(self, expr);
}
fn visit_ts_qualified_name(&mut self, it: &TSQualifiedName<'a>) {
if let TSTypeName::IdentifierReference(obj) = &it.left {
self.member_accesses.push(MemberAccess {
object: obj.name.to_string(),
member: it.right.name.to_string(),
});
}
walk::walk_ts_qualified_name(self, it);
}
fn visit_ts_mapped_type(&mut self, it: &TSMappedType<'a>) {
if let TSType::TSTypeReference(type_ref) = &it.constraint
&& let TSTypeName::IdentifierReference(ident) = &type_ref.type_name
{
self.whole_object_uses.push(ident.name.to_string());
}
if let TSType::TSTypeOperatorType(op) = &it.constraint
&& op.operator == TSTypeOperatorOperator::Keyof
&& let TSType::TSTypeQuery(query) = &op.type_annotation
&& let TSTypeQueryExprName::IdentifierReference(ident) = &query.expr_name
{
self.whole_object_uses.push(ident.name.to_string());
}
walk::walk_ts_mapped_type(self, it);
}
fn visit_ts_type_reference(&mut self, it: &TSTypeReference<'a>) {
if let TSTypeName::IdentifierReference(name) = &it.type_name
&& name.name == "Record"
&& let Some(type_args) = &it.type_arguments
&& let Some(first_arg) = type_args.params.first()
&& let TSType::TSTypeReference(key_ref) = first_arg
&& let TSTypeName::IdentifierReference(key_ident) = &key_ref.type_name
{
self.whole_object_uses.push(key_ident.name.to_string());
}
walk::walk_ts_type_reference(self, it);
}
fn visit_for_in_statement(&mut self, stmt: &ForInStatement<'a>) {
if let Expression::Identifier(ident) = &stmt.right {
self.whole_object_uses.push(ident.name.to_string());
}
walk::walk_for_in_statement(self, stmt);
}
fn visit_spread_element(&mut self, elem: &SpreadElement<'a>) {
if let Expression::Identifier(ident) = &elem.argument {
self.whole_object_uses.push(ident.name.to_string());
}
walk::walk_spread_element(self, elem);
}
fn visit_class(&mut self, class: &Class<'a>) {
if let Some(decorator) = lit_custom_element_decorator(class) {
if let Some(id) = class.id.as_ref() {
self.record_lit_custom_element_candidate(
decorator,
SideEffectRegistrationTarget::LocalClass(id.name.to_string()),
);
} else if let Some(export) = self.exports.last()
&& matches!(export.name, crate::ExportName::Default)
&& export.local_name.is_none()
{
let export_index = self.exports.len() - 1;
self.record_lit_custom_element_candidate(
decorator,
SideEffectRegistrationTarget::AnonymousDefaultExport(export_index),
);
}
}
if let Some(meta) = extract_angular_component_metadata(class) {
if let Some(ref template_url) = meta.template_url {
self.imports.push(ImportInfo {
source: normalize_asset_url(template_url),
imported_name: ImportedName::SideEffect,
local_name: String::new(),
is_type_only: false,
from_style: false,
span: oxc_span::Span::default(),
source_span: oxc_span::Span::default(),
});
self.has_angular_component_template_url = true;
}
for style_url in &meta.style_urls {
self.imports.push(ImportInfo {
source: normalize_asset_url(style_url),
imported_name: ImportedName::SideEffect,
local_name: String::new(),
is_type_only: false,
from_style: false,
span: oxc_span::Span::default(),
source_span: oxc_span::Span::default(),
});
}
if let Some(ref template) = meta.inline_template {
let refs = crate::sfc_template::angular::collect_angular_template_refs(template);
for name in refs.identifiers {
self.member_accesses.push(MemberAccess {
object: crate::sfc_template::angular::ANGULAR_TPL_SENTINEL.to_string(),
member: name,
});
}
self.member_accesses.extend(refs.member_accesses);
self.inline_template_findings
.push(super::InlineTemplateFinding {
template_source: template.clone(),
decorator_start: meta.decorator_span.start,
});
}
for name in &meta.host_member_refs {
self.member_accesses.push(MemberAccess {
object: crate::sfc_template::angular::ANGULAR_TPL_SENTINEL.to_string(),
member: name.clone(),
});
}
for name in &meta.input_output_members {
self.member_accesses.push(MemberAccess {
object: crate::sfc_template::angular::ANGULAR_TPL_SENTINEL.to_string(),
member: name.clone(),
});
}
}
self.class_super_stack
.push(super::helpers::extract_super_class_name(class));
self.class_type_param_constraints
.push(super::helpers::collect_class_type_param_constraints(class));
walk::walk_class(self, class);
self.class_type_param_constraints.pop();
self.class_super_stack.pop();
}
fn visit_tagged_template_expression(&mut self, expr: &TaggedTemplateExpression<'a>) {
if is_html_tagged_template(&expr.tag) {
for quasi in &expr.quasi.quasis {
let text = quasi
.value
.cooked
.as_ref()
.map_or_else(|| quasi.value.raw.as_str(), |c| c.as_str());
for raw in crate::html::collect_asset_refs(text) {
self.push_html_template_asset_import(&raw);
}
}
}
walk::walk_tagged_template_expression(self, expr);
}
}
fn static_argument_object_name(arg: &Argument<'_>) -> Option<String> {
match arg {
Argument::Identifier(ident) => Some(ident.name.to_string()),
Argument::ThisExpression(_) => Some("this".to_string()),
Argument::StaticMemberExpression(member) => Some(format!(
"{}.{}",
static_member_object_name(&member.object)?,
member.property.name
)),
_ => None,
}
}
fn static_member_object_name(expr: &Expression<'_>) -> Option<String> {
match expr {
Expression::Identifier(obj) => Some(obj.name.to_string()),
Expression::ThisExpression(_) => Some("this".to_string()),
Expression::StaticMemberExpression(member) => Some(format!(
"{}.{}",
static_member_object_name(&member.object)?,
member.property.name
)),
Expression::CallExpression(call) if call.arguments.is_empty() => {
Some(format!("{}()", static_member_object_name(&call.callee)?))
}
Expression::ChainExpression(chain) => match &chain.expression {
ChainElement::CallExpression(call) if call.arguments.is_empty() => {
Some(format!("{}()", static_member_object_name(&call.callee)?))
}
ChainElement::StaticMemberExpression(member) => Some(format!(
"{}.{}",
static_member_object_name(&member.object)?,
member.property.name
)),
_ => None,
},
_ => None,
}
}
fn is_html_tagged_template(tag: &Expression<'_>) -> bool {
matches!(tag, Expression::Identifier(id) if id.name == "html")
}
impl ModuleInfoExtractor {
fn push_html_template_asset_import(&mut self, raw: &str) {
let trimmed = raw.trim();
if trimmed.is_empty() || is_remote_url(trimmed) {
return;
}
self.imports.push(ImportInfo {
source: normalize_asset_url(trimmed),
imported_name: ImportedName::SideEffect,
local_name: String::new(),
is_type_only: false,
from_style: false,
span: oxc_span::Span::default(),
source_span: oxc_span::Span::default(),
});
}
}