use oxc_ast::ast::{
Argument, CallExpression, Expression, TSInterfaceDeclaration, TSType, TSTypeAliasDeclaration,
};
use crate::{
DynamicImportInfo, SemanticFact, VitestModuleMockAction, VitestModuleMockOperationFact,
};
use super::super::{
ModuleInfoExtractor, PendingPlaywrightFactory, PendingVitestMockOperation,
PendingVitestMockProof,
};
use super::visit_helpers::{
collect_fixture_type_bindings_from_members, collect_fixture_type_bindings_from_type,
playwright_extend_base_name, vi_mock_has_factory, vitest_auto_mock_source,
vitest_mock_object_span, vitest_mock_source, vitest_replacement_candidate,
vitest_unmock_object_span, vitest_unmock_source,
};
impl ModuleInfoExtractor {
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
}
pub(super) fn record_playwright_fixture_type_alias(
&mut self,
alias: &TSTypeAliasDeclaration<'_>,
) {
let bindings = self.collect_playwright_fixture_type_bindings(&alias.type_annotation);
self.record_playwright_fixture_type_bindings(alias.id.name.as_str(), bindings);
}
pub(super) fn record_playwright_fixture_interface(
&mut self,
iface: &TSInterfaceDeclaration<'_>,
) {
let mut bindings = Vec::new();
collect_fixture_type_bindings_from_members(
&iface.body.body,
"",
&self.playwright_fixture_types,
&mut bindings,
);
self.record_playwright_fixture_type_bindings(iface.id.name.as_str(), bindings);
}
fn record_playwright_fixture_type_bindings(
&mut self,
type_name: &str,
mut bindings: Vec<(String, String)>,
) {
bindings.sort_unstable();
bindings.dedup();
if bindings.is_empty() {
return;
}
self.playwright_fixture_types
.insert(type_name.to_string(), bindings.clone());
for (fixture_name, fixture_type) in bindings {
self.record_playwright_fixture_type_fact(
type_name.to_string(),
fixture_name.clone(),
fixture_type,
);
}
}
pub(super) 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();
if !bindings.is_empty() {
self.playwright_local_fixture_defs
.insert(test_name.to_string(), bindings.clone());
}
for (fixture_name, type_name) in bindings {
self.record_playwright_fixture_definition_fact(
test_name.to_string(),
fixture_name.clone(),
type_name,
);
}
}
fn record_playwright_fixture_alias(&mut self, test_name: &str, base_name: &str) {
self.record_playwright_fixture_alias_fact(test_name.to_string(), base_name.to_string());
}
pub(super) fn record_playwright_wrapper_aliases(
&mut self,
test_name: &str,
call: &CallExpression<'_>,
) {
if let Some(base_name) = playwright_extend_base_name(call) {
if !self.is_named_import_from(base_name.as_str(), "@playwright/test", "test") {
self.record_playwright_fixture_alias(test_name, &base_name);
}
return;
}
let Expression::Identifier(callee) = &call.callee else {
return;
};
if !self.is_named_import_from(callee.name.as_str(), "@playwright/test", "mergeTests") {
return;
}
let mut base_names: Vec<String> = call
.arguments
.iter()
.filter_map(playwright_merge_argument_base_name)
.collect();
base_names.sort();
base_names.dedup();
for base_name in base_names {
self.record_playwright_fixture_alias(test_name, &base_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) {
self.pending_playwright_factory_aliases
.push((test_name.to_string(), base_name.clone()));
if !self.is_named_import_from(base_name.as_str(), "@playwright/test", "test") {
self.record_playwright_fixture_alias(test_name, &base_name);
}
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(PendingPlaywrightFactory {
test_name: test_name.to_string(),
base_name,
type_bindings: bindings,
});
} else if let Expression::Identifier(callee) = &call.callee
&& self.is_named_import_from(callee.name.as_str(), "@playwright/test", "mergeTests")
{
let mut base_names: Vec<String> = call
.arguments
.iter()
.filter_map(playwright_merge_argument_base_name)
.collect();
base_names.sort();
base_names.dedup();
for base_name in base_names {
self.record_playwright_fixture_alias(test_name, &base_name);
self.pending_playwright_factory_aliases
.push((test_name.to_string(), base_name));
}
} else if let Expression::Identifier(ident) = &call.callee {
self.pending_playwright_factory_aliases
.push((test_name.to_string(), ident.name.to_string()));
}
}
pub(super) fn record_vitest_mock_imports(&mut self, expr: &CallExpression<'_>) {
let Some(target_source) = vitest_mock_source(expr) else {
return;
};
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,
});
}
if let Some(vi_reference_span) = vitest_mock_object_span(expr) {
let proof = vitest_replacement_candidate(expr).map_or(
PendingVitestMockProof::UnprovenMock,
|candidate| PendingVitestMockProof::ClosedFactory {
vi_reference_spans: candidate.factory_vi_reference_spans,
},
);
self.pending_vitest_mock_operations
.push(PendingVitestMockOperation {
source: target_source,
vi_reference_span,
call_start: expr.span.start,
proof,
});
}
}
pub(super) fn record_vitest_unmock(&mut self, expr: &CallExpression<'_>) {
if let Some(source) = vitest_unmock_source(expr)
&& let Some(vi_reference_span) = vitest_unmock_object_span(expr)
{
self.pending_vitest_mock_operations
.push(PendingVitestMockOperation {
source,
vi_reference_span,
call_start: expr.span.start,
proof: PendingVitestMockProof::Unmock,
});
}
}
pub(crate) fn resolve_vitest_mock_operations(
&mut self,
vitest_vi_reference_spans: &rustc_hash::FxHashSet<oxc_span::Span>,
) {
let mut operations: Vec<_> = self
.pending_vitest_mock_operations
.drain(..)
.filter(|operation| vitest_vi_reference_spans.contains(&operation.vi_reference_span))
.collect();
operations.sort_unstable_by_key(|operation| operation.call_start);
self.semantic_facts
.extend(operations.into_iter().map(|operation| {
let action = match operation.proof {
PendingVitestMockProof::ClosedFactory { vi_reference_spans } => {
VitestModuleMockAction::Mock {
factory_replaces_original: vi_reference_spans
.iter()
.all(|span| vitest_vi_reference_spans.contains(span)),
}
}
PendingVitestMockProof::UnprovenMock => VitestModuleMockAction::Mock {
factory_replaces_original: false,
},
PendingVitestMockProof::Unmock => VitestModuleMockAction::Unmock,
};
SemanticFact::VitestModuleMockOperation(VitestModuleMockOperationFact {
source: operation.source,
call_start: operation.call_start,
action,
})
}));
}
}
fn playwright_merge_argument_base_name(argument: &Argument<'_>) -> Option<String> {
match argument {
Argument::Identifier(ident) => Some(ident.name.to_string()),
Argument::CallExpression(call) => match &call.callee {
Expression::Identifier(callee) => Some(callee.name.to_string()),
_ => None,
},
_ => None,
}
}