diff --git a/docs/locals-query-processing-framing.md b/docs/locals-query-processing-framing.md index 79013b6..4cba836 100644 --- a/docs/locals-query-processing-framing.md +++ b/docs/locals-query-processing-framing.md @@ -221,7 +221,9 @@ cross-layer merge precedence remain unchanged after captures are selected. ### Q#LQ6 - Performance boundary -Local analysis is linear in the locals-query captures for one settled layer. +The locals-query capture walk is linear for one settled layer. Like upstream, +resolving references scans visible definitions newest-first, so the worst case +is $O(\text{captures} + \text{references} \times \text{definitions})$. It runs only for a language whose highlight query actually contains a `local` predicate, and only once when a fresh parse bundle settles. diff --git a/src/syntax.rs b/src/syntax.rs index 2538641..6c91115 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -2666,6 +2666,32 @@ mod tests { } } + #[test] + fn local_sensitive_builtin_highlights_have_compilable_locals_queries() { + let registry = SyntaxRegistry::new(); + for entry in BUILTIN_LANGUAGES { + let Some(highlights) = registry.highlights_query(entry.name) else { + continue; + }; + if !query_uses_local_predicates(&highlights) { + continue; + } + assert!( + entry + .locals_query + .iter() + .any(|fragment| !fragment.trim().is_empty()), + "`{}` highlights use a local predicate but ship no locals query", + entry.name + ); + assert!( + registry.locals_query(entry.name).is_some(), + "`{}` highlights use a local predicate but its locals query does not compile", + entry.name + ); + } + } + #[test] fn javascript_local_predicates_distinguish_lexical_scope() { let registry = SyntaxRegistry::new();