use super::super::Printer;
use super::arg_comments::has_inter_argument_comments_slice;
use super::call_paren_open;
use crate::ast::{self, IdentName};
use smallvec::SmallVec;
use tsv_lang::Span;
use tsv_lang::doc::arena::DocId;
const TEST_CALL_PATTERNS: &[&str] = &[
"it",
"it.only",
"it.skip",
"describe",
"describe.only",
"describe.skip",
"test",
"test.only",
"test.skip",
"test.fixme", "test.step",
"test.describe",
"test.describe.only",
"test.describe.skip",
"test.describe.fixme", "test.describe.parallel",
"test.describe.parallel.only",
"test.describe.serial",
"test.describe.serial.only",
"skip",
"xit",
"xdescribe",
"xtest",
"fit",
"fdescribe",
"ftest",
];
fn get_identifier_name<'a>(expr: &ast::Expression<'a>) -> Option<(IdentName<'a>, u32, u32)> {
if let ast::ExpressionKind::Identifier(id) = &expr.kind {
Some((id.ident_name(), id.span.start, id.span.end))
} else {
None
}
}
pub(super) fn build_test_callee_flat_doc(
expr: &ast::Expression<'_>,
printer: &Printer<'_>,
) -> Option<DocId> {
let parts = get_member_chain_parts(expr)?;
let mut iter = parts.iter().rev();
let &(name, start, mut prev_end) = iter.next()?;
let mut doc = printer.ident_name_doc(name, start);
for &(name, start, end) in iter {
doc = printer.build_dotted_pair_doc(
doc,
printer.ident_name_doc(name, start),
prev_end,
start,
);
prev_end = end;
}
Some(printer.prepend_owned_leading_comment_at(expr.span().start, doc))
}
#[inline(never)]
fn get_member_chain_parts<'a>(
expr: &ast::Expression<'a>,
) -> Option<SmallVec<[(IdentName<'a>, u32, u32); 8]>> {
let mut parts: SmallVec<[(IdentName<'a>, u32, u32); 8]> = SmallVec::new();
let mut expr = expr;
loop {
match &expr.kind {
ast::ExpressionKind::Identifier(id) => {
parts.push((id.ident_name(), id.span.start, id.span.end));
return Some(parts);
}
ast::ExpressionKind::MemberExpression(member) => {
if member.computed || member.optional {
return None;
}
parts.push(get_identifier_name(member.property)?);
expr = member.object;
}
_ => return None,
}
}
}
pub(super) fn test_call_flat_layout_applies(
call: &ast::CallExpression<'_>,
span: Span,
printer: &Printer<'_>,
) -> bool {
is_test_call(call, printer) && test_call_flat_gaps_clear(call, span, printer)
}
pub(super) fn test_call_flat_gaps_clear(
call: &ast::CallExpression<'_>,
span: Span,
printer: &Printer<'_>,
) -> bool {
let paren_open = call_paren_open(printer, call, span);
let Some(last_start) = call.arguments.last().map(|a| a.span().start) else {
return true;
};
!(printer.has_comments_to_emit_between(paren_open, last_start)
&& test_call_gaps_have_comments(call, printer, paren_open))
}
fn test_call_gaps_have_comments(
call: &ast::CallExpression<'_>,
printer: &Printer<'_>,
paren_open: u32,
) -> bool {
call.arguments
.first()
.is_some_and(|first| printer.has_comments_to_emit_between(paren_open, first.span().start))
|| has_inter_argument_comments_slice(call.arguments, printer)
}
pub(super) fn is_test_call(call: &ast::CallExpression<'_>, printer: &Printer<'_>) -> bool {
if call.optional {
return false;
}
let arg_count = call.arguments.len();
if !(2..=3).contains(&arg_count) {
return false;
}
let first_is_string = match &call.arguments[0].kind {
ast::ExpressionKind::Literal(lit) => {
matches!(lit.value, ast::LiteralValue::String { .. })
}
ast::ExpressionKind::TemplateLiteral(_) => true,
_ => false,
};
if !first_is_string {
return false;
}
if arg_count == 3 {
let third_is_number = match &call.arguments[2].kind {
ast::ExpressionKind::Literal(lit) => {
matches!(lit.value, ast::LiteralValue::Number(_))
}
_ => false,
};
if !third_is_number {
return false;
}
}
let callback_ok = match &call.arguments[1].kind {
ast::ExpressionKind::FunctionExpression(func) => arg_count == 2 || func.params.len() <= 1,
ast::ExpressionKind::ArrowFunctionExpression(arrow) => {
arg_count == 2 || (!arrow.body.is_expression() && arrow.params.len() <= 1)
}
_ => false,
};
if !callback_ok {
return false;
}
callee_is_test_pattern(call.callee, printer.source)
}
fn callee_is_test_pattern(callee: &ast::Expression<'_>, source: &str) -> bool {
let mut names = [""; TEST_PATTERN_MAX_DEPTH];
let mut depth = 0;
let mut expr = callee;
loop {
let (id, object) = match &expr.kind {
ast::ExpressionKind::Identifier(id) => (id, None),
ast::ExpressionKind::MemberExpression(member) => {
if member.computed || member.optional {
return false;
}
let ast::ExpressionKind::Identifier(property) = &member.property.kind else {
return false;
};
(property, Some(member.object))
}
_ => return false,
};
let Some(slot) = names.get_mut(depth) else {
return false;
};
*slot = id.ident_name().resolve(id.span.start, source);
depth += 1;
match object {
Some(object) => expr = object,
None => break,
}
}
let names = &mut names[..depth];
names.reverse();
is_test_pattern(names)
}
const TEST_PATTERN_MAX_DEPTH: usize = max_pattern_segments(TEST_CALL_PATTERNS);
const fn max_pattern_segments(patterns: &[&str]) -> usize {
let mut max = 0;
let mut i = 0;
while i < patterns.len() {
let bytes = patterns[i].as_bytes();
let mut segments = 1;
let mut j = 0;
while j < bytes.len() {
if bytes[j] == b'.' {
segments += 1;
}
j += 1;
}
if segments > max {
max = segments;
}
i += 1;
}
max
}
fn is_test_pattern(names: &[&str]) -> bool {
matches!(
names,
["it"
| "describe"
| "test"
| "skip"
| "xit"
| "xdescribe"
| "xtest"
| "fit"
| "fdescribe"
| "ftest"]
| ["it" | "describe", "only" | "skip"]
| ["test", "only" | "skip" | "fixme" | "step" | "describe"]
| [
"test",
"describe",
"only" | "skip" | "fixme" | "parallel" | "serial"
]
| ["test", "describe", "parallel" | "serial", "only"]
)
}
#[cfg(test)]
mod tests {
use super::{TEST_CALL_PATTERNS, TEST_PATTERN_MAX_DEPTH, is_test_pattern};
#[test]
fn is_test_pattern_matches_the_pattern_list() {
let mut alphabet: Vec<&str> = TEST_CALL_PATTERNS
.iter()
.flat_map(|pattern| pattern.split('.'))
.collect();
alphabet.extend(["Test", "a"]);
alphabet.sort_unstable();
alphabet.dedup();
let in_list = |names: &[&str]| {
TEST_CALL_PATTERNS
.iter()
.any(|pattern| pattern.split('.').eq(names.iter().copied()))
};
let mut matched = 0;
let mut frontier: Vec<Vec<&str>> = vec![Vec::new()];
for _ in 0..TEST_PATTERN_MAX_DEPTH {
let mut next = Vec::new();
for names in &frontier {
for &segment in &alphabet {
let mut longer = names.clone();
longer.push(segment);
let expected = in_list(&longer);
assert_eq!(is_test_pattern(&longer), expected, "{}", longer.join("."));
matched += usize::from(expected);
next.push(longer);
}
}
frontier = next;
}
assert_eq!(matched, TEST_CALL_PATTERNS.len());
for pattern in TEST_CALL_PATTERNS {
for &segment in &alphabet {
let mut longer: Vec<&str> = pattern.split('.').collect();
longer.push(segment);
assert_eq!(
is_test_pattern(&longer),
in_list(&longer),
"{}",
longer.join(".")
);
}
}
assert_eq!(
TEST_CALL_PATTERNS
.iter()
.map(|pattern| pattern.split('.').count())
.max(),
Some(TEST_PATTERN_MAX_DEPTH)
);
}
}