use std::path::Path;
use fallow_types::discover::FileId;
use fallow_types::extract::ModuleInfo;
use crate::parse_source_to_module;
const DEEP: usize = 20_000;
fn parse(path: &str, source: &str) -> ModuleInfo {
parse_source_to_module(FileId(0), Path::new(path), source, 0, true)
}
fn assert_degraded(info: &ModuleInfo) {
assert!(
info.parse_panicked,
"a too-deep file must be a failed parse"
);
assert!(info.parse_error_count > 0);
assert!(info.exports.is_empty());
assert!(info.imports.is_empty());
}
#[test]
fn deeply_nested_parens_degrade_instead_of_overflowing() {
let source = format!(
"import a from './a';\nexport const x = {}1{};\n",
"(".repeat(DEEP),
")".repeat(DEEP)
);
let info = parse("src/index.mjs", &source);
assert_degraded(&info);
assert_eq!(
info.line_offsets.len(),
3,
"line offsets still describe the source"
);
}
#[test]
fn deeply_nested_arrays_in_a_glimmer_file_degrade() {
let source = format!("export default {};\n", "[".repeat(DEEP));
assert_degraded(&parse("src/App.gts", &source));
}
#[test]
fn deep_nesting_inside_a_vue_script_degrades() {
let source = format!(
"<script setup>\nconst x = {}{};\n</script>\n",
"{a:".repeat(DEEP),
"}".repeat(DEEP)
);
assert_degraded(&parse("src/App.vue", &source));
}
#[test]
fn closers_in_literals_do_not_hide_real_nesting() {
for unit in ["(')',", "(\")\",", "(`)`,", "(/)/,", "(/*)*/", "(//)\n"] {
let source = format!("f{};\n", unit.repeat(DEEP));
assert_degraded(&parse("src/index.js", &source));
}
}
#[test]
fn template_substitutions_count_as_nesting() {
let source = format!("const s = {}{};\n", "`${".repeat(DEEP), "}`".repeat(DEEP));
assert_degraded(&parse("src/index.js", &source));
}
#[test]
fn ordinary_nesting_still_parses() {
let depth = 200;
let source = format!(
"import a from './a';\nexport const x = {}a{};\n",
"[".repeat(depth),
"]".repeat(depth)
);
let info = parse("src/index.ts", &source);
assert!(!info.parse_panicked);
assert_eq!(info.parse_error_count, 0);
assert_eq!(info.exports.len(), 1);
assert_eq!(info.imports.len(), 1);
}
#[test]
fn brackets_inside_literals_do_not_count_as_nesting() {
let openers = "(".repeat(DEEP);
let source = format!(
"export const a = '{openers}';\nexport const b = \"{openers}\";\n\
export const c = `{openers}`;\nexport const d = /\\({{3}}[(]/;\n\
// {openers}\n/* {openers} */\nexport const e = 1 / (2) / 3;\n"
);
let info = parse("src/index.js", &source);
assert!(!info.parse_panicked);
assert_eq!(info.exports.len(), 5);
}
fn deep_custom_property() -> String {
format!(".a{{--x:{}1{};}}\n", "(".repeat(DEEP), ")".repeat(DEEP))
}
#[test]
fn deep_css_after_a_protocol_relative_url_degrades() {
let source = format!(
".b{{background:url(//cdn.example.com/a.png)}}{}",
deep_custom_property()
);
assert_degraded(&parse("src/styles.css", &source));
}
#[test]
fn css_module_exports_skip_a_too_deep_stylesheet() {
let source = format!(".keep{{color:red}}\n{}", deep_custom_property());
let exports = crate::css::extract_css_module_exports(&source, false);
assert!(
exports
.iter()
.any(|export| export.name.to_string() == "keep")
);
}
#[test]
fn css_analytics_skip_a_too_deep_stylesheet() {
assert!(crate::css_metrics::compute_css_analytics(&deep_custom_property()).is_none());
}
#[test]
fn a_hash_or_at_url_prefix_does_not_hide_nesting() {
for prefix in ["#url(", "@url(", "url("] {
let source = format!(".a{{--x:{prefix}{}}}", "(".repeat(DEEP));
assert!(
crate::parse_style_bounded(&source, || ()).is_none(),
"{prefix} hid the nesting"
);
}
}
const DEFAULT_DEPTH: usize = 40;
fn assert_extracts_in_time(source: String) {
let (done, finished) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let info = parse("src/index.ts", &source);
let _ = done.send(info.parse_error_count);
});
let errors = finished
.recv_timeout(std::time::Duration::from_secs(20))
.expect("extraction must end in time");
assert_eq!(errors, 0);
}
#[test]
fn nested_array_destructure_defaults_cost_linear_work() {
let mut pattern = String::from("{ m }");
for _ in 0..DEFAULT_DEPTH {
pattern = format!("[{pattern} = [s]]");
}
assert_extracts_in_time(format!(
"declare const s: any;\nconst {pattern} = [s];\nfor (const {pattern} of [[s]]) {{}}\n"
));
}
#[test]
fn nested_object_destructure_assignment_defaults_cost_linear_work() {
let mut pattern = String::from("{ m }");
for _ in 0..DEFAULT_DEPTH {
pattern = format!("{{ k: {pattern} = s }}");
}
assert_extracts_in_time(format!(
"declare const s: any;\nlet m: unknown;\n({pattern} = s);\n"
));
}
const WIDE_ELEMENTS: usize = 1_500;
fn reads_service_member(info: &ModuleInfo, member: &str) -> bool {
info.member_accesses
.iter()
.any(|access| access.object == "Service" && access.member == member)
}
#[test]
fn wide_for_of_destructure_credits_the_last_element() {
let others = "{ viaWideForOf: () => {} }, ".repeat(WIDE_ELEMENTS);
let source = format!(
"export class Service {{ viaWideForOf() {{}} }}\nconst service = new Service();\n\
for (const {{ viaWideForOf }} of [{others}service]) viaWideForOf();\n"
);
let info = parse("src/index.ts", &source);
assert!(reads_service_member(&info, "viaWideForOf"));
}
#[test]
fn wide_array_destructure_credits_the_last_element() {
let holes = ", ".repeat(WIDE_ELEMENTS);
let others = "{}, ".repeat(WIDE_ELEMENTS);
let wrapped = "[{}], ".repeat(WIDE_ELEMENTS);
let source = format!(
"export class Service {{ viaWideArray() {{}} viaWideNested() {{}} }}\n\
const service = new Service();\n\
const [{holes}{{ viaWideArray }}] = [{others}service];\n\
for (const [{{ viaWideNested }}] of [{wrapped}[service]]) viaWideNested();\n\
viaWideArray();\n"
);
let info = parse("src/index.ts", &source);
assert!(reads_service_member(&info, "viaWideArray"));
assert!(reads_service_member(&info, "viaWideNested"));
}
const SPREAD_DEPTH: usize = 20;
#[test]
fn nested_spread_sources_cost_bounded_work() {
let mut pattern = String::from("{ m }");
let mut source = String::from("s");
for _ in 0..SPREAD_DEPTH {
pattern = format!("{{ k: {pattern} }}");
source = format!("{{ ...a, ...b, k: {source} }}");
}
assert_extracts_in_time(format!(
"declare const s: any, a: any, b: any;\nconst {pattern} = {source};\n"
));
}
const WIDE_BY_WIDE: usize = 2_000;
#[test]
fn wide_pattern_over_wide_source_records_bounded_entries() {
let keys = (0..WIDE_BY_WIDE)
.map(|index| format!("m{index}"))
.collect::<Vec<_>>()
.join(", ");
let elements = "service, ".repeat(WIDE_BY_WIDE);
let source = format!(
"export class Service {{}}\nconst service = new Service();\n\
for (const {{ {keys} }} of [{elements}]) {{}}\n"
);
let info = parse("src/index.ts", &source);
assert!(
info.member_accesses.len() < WIDE_BY_WIDE * WIDE_BY_WIDE / 4,
"one destructure recorded {} member accesses",
info.member_accesses.len()
);
}