use std::sync::LazyLock;
use fallow_types::extract::FunctionComplexity;
use super::build_template_complexity;
use super::engine::{ScanError, TemplateComplexity, find_matching_delimiter};
static MASK_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
crate::static_regex(
r#"(?is)\A\s*---[ \t]*\r?\n.*?\r?\n---|<script\b(?:[^>"']|"[^"]*"|'[^']*')*>.*?</script>|<style\b(?:[^>"']|"[^"]*"|'[^']*')*>.*?</style>|<!--.*?-->"#,
)
});
const ITERATION_CALLS: [&str; 3] = [".map(", ".flatMap(", ".forEach("];
#[must_use]
pub fn compute_astro_template_complexity(source: &str) -> Option<FunctionComplexity> {
let masked = super::mask_ranges(source, &MASK_RE);
let mut complexity = TemplateComplexity::default();
if scan_markup_expressions(&masked, &mut complexity).is_err() {
return None;
}
build_template_complexity(source, &complexity)
}
fn scan_markup_expressions(
masked: &str,
complexity: &mut TemplateComplexity,
) -> Result<(), ScanError> {
let bytes = masked.as_bytes();
let mut offset = 0;
while offset < bytes.len() {
if bytes[offset] != b'{' {
offset += 1;
continue;
}
let close = find_matching_delimiter(masked, offset, b'{', b'}')?;
let expr = &masked[offset + 1..close];
let _ = complexity.add_expression(expr, offset + 1, 0);
for needle in ITERATION_CALLS {
let mut from = 0;
while let Some(pos) = expr[from..].find(needle) {
complexity.add_control_flow(0);
from += pos + needle.len();
}
}
offset = close + 1;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::compute_astro_template_complexity;
#[test]
fn trivial_template_has_no_entry() {
let source = "---\nconst x = 1;\n---\n<h1>{x}</h1>\n";
assert!(compute_astro_template_complexity(source).is_none());
}
#[test]
fn conditional_and_iteration_score() {
let source = "---\nconst items = [];\nconst show = true;\n---\n\
<ul>{show && items.map((i) => <li>{i.name ?? 'x'}</li>)}</ul>\n";
let fc = compute_astro_template_complexity(source).expect("non-trivial template");
assert!(fc.cyclomatic > 1, "cyclomatic should rise: {fc:?}");
assert_eq!(fc.name, "<template>");
}
#[test]
fn frontmatter_braces_are_masked() {
let source = "---\nconst cfg = { a: 1, b: 2 };\n---\n<div>plain</div>\n";
assert!(compute_astro_template_complexity(source).is_none());
}
#[test]
fn malformed_brace_drops_entry() {
let source = "---\n---\n<div>{a && b</div>\n";
assert!(compute_astro_template_complexity(source).is_none());
}
}