use oxc_ast::ast::{
Argument, CallExpression, Expression, TSInterfaceDeclaration, TSType, TSTypeAliasDeclaration,
};
use crate::{
DynamicImportInfo, SemanticFact, VitestModuleMockAction, VitestModuleMockOperationFact,
};
use super::super::{
MockObjectProvenance, ModuleInfoExtractor, PendingPlaywrightFactory,
PendingVitestMockOperation, PendingVitestMockProof,
};
use super::visit_helpers::{
collect_fixture_type_bindings_from_members, collect_fixture_type_bindings_from_type,
mock_method_object_span, mock_object_is_literal_global, mock_replacement_candidate,
mock_static_target_source, playwright_extend_base_name, vi_mock_has_factory,
vitest_auto_mock_source,
};
use crate::parse::MockApiReferenceSpans;
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((object_span, provenance)) = mock_method_object_span(expr, "mock") else {
return;
};
let Some(target_source) = mock_static_target_source(expr) else {
return;
};
let has_factory = vi_mock_has_factory(expr);
let literal_global = mock_object_is_literal_global(expr);
if literal_global {
self.push_mock_credit_edges(&target_source, expr.span, has_factory);
}
let proof = mock_replacement_candidate(expr).map_or(
PendingVitestMockProof::UnprovenMock,
|candidate| PendingVitestMockProof::ClosedFactory {
binding_requirement_spans: candidate.binding_requirement_spans,
namespace_requirement_spans: candidate.namespace_requirement_spans,
},
);
self.pending_vitest_mock_operations
.push(PendingVitestMockOperation {
source: target_source,
object_span,
provenance,
call_span: expr.span,
has_factory,
needs_deferred_edges: !literal_global,
proof,
});
}
fn push_mock_credit_edges(
&mut self,
target_source: &str,
span: oxc_span::Span,
has_factory: bool,
) {
self.dynamic_imports.push(DynamicImportInfo {
source: target_source.to_string(),
span,
destructured_names: Vec::new(),
local_name: None,
is_speculative: false,
});
if !has_factory && let Some(mock_source) = vitest_auto_mock_source(target_source) {
self.dynamic_imports.push(DynamicImportInfo {
source: mock_source,
span,
destructured_names: Vec::new(),
local_name: Some(String::new()),
is_speculative: true,
});
}
}
pub(super) fn record_vitest_unmock(&mut self, expr: &CallExpression<'_>) {
if let Some((object_span, provenance)) = mock_method_object_span(expr, "unmock")
&& let Some(source) = mock_static_target_source(expr)
{
self.pending_vitest_mock_operations
.push(PendingVitestMockOperation {
source,
object_span,
provenance,
call_span: expr.span,
has_factory: false,
needs_deferred_edges: false,
proof: PendingVitestMockProof::Unmock,
});
}
}
pub(crate) fn resolve_vitest_mock_operations(&mut self, spans: &MockApiReferenceSpans) {
let mut operations: Vec<_> = self
.pending_vitest_mock_operations
.drain(..)
.filter(|operation| match operation.provenance {
MockObjectProvenance::Binding => {
spans.mock_bindings.contains(&operation.object_span)
}
MockObjectProvenance::VitestNamespace => {
spans.vitest_namespaces.contains(&operation.object_span)
}
})
.collect();
operations.sort_unstable_by_key(|operation| operation.call_span.start);
let deferred_edges: Vec<(String, oxc_span::Span, bool)> = operations
.iter()
.filter(|operation| {
operation.needs_deferred_edges
&& !matches!(operation.proof, PendingVitestMockProof::Unmock)
})
.map(|operation| {
(
operation.source.clone(),
operation.call_span,
operation.has_factory,
)
})
.collect();
for (source, span, has_factory) in deferred_edges {
self.push_mock_credit_edges(&source, span, has_factory);
}
self.semantic_facts
.extend(operations.into_iter().map(|operation| {
let action = match operation.proof {
PendingVitestMockProof::ClosedFactory {
binding_requirement_spans,
namespace_requirement_spans,
} => VitestModuleMockAction::Mock {
factory_replaces_original: binding_requirement_spans
.iter()
.all(|span| spans.mock_bindings.contains(span))
&& namespace_requirement_spans
.iter()
.all(|span| spans.vitest_namespaces.contains(span)),
},
PendingVitestMockProof::UnprovenMock => VitestModuleMockAction::Mock {
factory_replaces_original: false,
},
PendingVitestMockProof::Unmock => VitestModuleMockAction::Unmock,
};
SemanticFact::VitestModuleMockOperation(VitestModuleMockOperationFact {
source: operation.source,
call_start: operation.call_span.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,
}
}