biomejs/biome · error
TailwindSyntaxVisitor only emits Tailwind class strings
Error message
TailwindSyntaxVisitor only emits Tailwind class strings
What it means
This panic comes from an `.expect()` in `TailwindSyntaxServices::unwrap_match` in biome_tailwind_logic. It fires when the analyzer hands `unwrap_match` a node that does not actually host a Tailwind class string, so `tailwind_class_string(...)` returns `None` even though the `TailwindSyntaxVisitor` is documented to only emit such nodes. It is an internal invariant violation inside Biome's Tailwind lint machinery, not a problem with the user's code or Tailwind classes; a second `.expect()` on the same path panics if `TwSyntaxService` was never registered in the ServiceBag.
Solutions
- Upgrade to the latest Biome version; this panic is an internal bug likely already reported/fixed upstream
- File a Biome issue with the exact file content and framework/file type that triggered the crash, and disable the specific Tailwind lint rule as a workaround
- If you are a contributor: ensure `TailwindSyntaxVisitor` emits only nodes for which `N::tailwind_class_string` returns Some under the same `is_class_attribute` embedding condition used in `unwrap_match` (crates/biome_tailwind_logic/src/syntax_service.rs:264-278)
- If calling the query in custom code: register `TwSyntaxService` in the ServiceBag and construct the input via the visitor, never by casting arbitrary nodes
- Verify the `JsFileSource` embedding kind is consistent for the file being analyzed (class-attribute detection must match between visitor and unwrap phases)
Example fix
// before: unwrap_match trusts any node handed to it
let class_string = node
.tailwind_class_string(services.get_service::<JsFileSource>().is_some_and(|s| s.as_embedding_kind().is_class_attribute()))
.expect("TailwindSyntaxVisitor only emits Tailwind class strings");
// after: fall back gracefully instead of panicking
let class_string = match node.tailwind_class_string(is_class_attribute) {
Some(class_string) => class_string,
None => return Self { node, parse: None }, // or skip the match entirely
}; Defensive patterns
Strategy: validation
Validate before calling
// Before relying on the Tailwind query, confirm the node hosts a class string
// and the required service is registered:
fn can_run_tailwind_query<N: AstNode + TailwindClassStringHost>(
node: &N,
services: &ServiceBag,
) -> bool {
let is_class_attribute = services
.get_service::<JsFileSource>()
.is_some_and(|s| s.as_embedding_kind().is_class_attribute());
node.tailwind_class_string(is_class_attribute).is_some()
&& services.get_service::<TwSyntaxService>().is_some()
} Type guard
fn hosts_class_string<N: AstNode + TailwindClassStringHost>(node: &N, is_class_attribute: bool) -> bool {
node.tailwind_class_string(is_class_attribute).is_some()
} Try / catch
// Rust panics are not catchable via Result; catch_unwind is the only guard,
// and in Biome the correct approach is to avoid the panic by validating first:
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
TailwindSyntax::<N>::unwrap_match(services, node)
}));
match result {
Ok(output) => output,
Err(_) => skip_rule(), // report a bug instead of crashing the analyzer
} Prevention
- Keep `TailwindSyntaxVisitor::visit` and `TailwindClassStringHost::tailwind_class_string` conditions in sync when touching either
- Add a debug assertion/test asserting every visitor-emitted node yields Some from `tailwind_class_string`
- Never call `unwrap_match` directly in tests or tooling without the full analyzer service setup (`TwSyntaxService`, `JsFileSource`)
- When adding new framework embeddings, verify `as_embedding_kind().is_class_attribute()` returns the same value during visitation and during query unwrapping
- Run the tailwind lint suites (`cargo test -p biome_tailwind_logic`) after changing class-string host logic
When it happens
Trigger: Running a Tailwind-enabled lint rule whose query is `TailwindSyntax<N>` when (1) the visitor emits a match whose node's `tailwind_class_string(embedding_is_class_attribute)` returns None (e.g. the embedding-kind check via `JsFileSource.as_embedding_kind().is_class_attribute()` disagrees with what the visitor matched), or (2) `unwrap_match` is invoked outside the normal analyzer pipeline (custom tooling/tests) with an arbitrary node, or (3) `TwSyntaxService` is missing from the ServiceBag because the Tailwind syntax phase was not set up.
Common situations: Encountered by Biome contributors adding new class-string host node types or changing `TailwindClassStringHost` implementations so visitor emission and `tailwind_class_string` disagree; by embedding-kind detection changes (e.g. a framework integration whose class attribute embedding is not flagged as class attribute, flipping the boolean between visitor and unwrap); or by tests calling the query services directly without registering `TwSyntaxService`. End users only see this as a Biome crash report, typically after a Biome version upgrade.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- formatting embedded JavaScript snippets requires the…
- generated helper has unexpected shape
- generated Promise. member has unexpected shape
- token kind cannot be transformed to text
- A list of attribute is expected
AI-assisted analysis of biomejs/biome@17a2560fb5 (2026-09-20).
Data as JSON: /api/errors/aec98c5ae3316cbe.
Report an issue: GitHub.
Appendix: source
Thrown at crates/biome_tailwind_logic/src/syntax_service.rs:273
type Services = TailwindSyntaxServices;
fn build_visitor(
analyzer: &mut impl AddVisitor<Self::Language>,
_: &<Self::Language as Language>::Root,
) {
analyzer.add_visitor(Phases::Syntax, TailwindSyntaxVisitor::<N>::default);
}
fn unwrap_match(services: &ServiceBag, node: &Self::Input) -> Self::Output {
let node = N::unwrap_cast(node.0.clone());
let class_string = node
.tailwind_class_string(
services
.get_service::<JsFileSource>()
.is_some_and(|source| source.as_embedding_kind().is_class_attribute()),
)
// SAFETY: The visitor emits matches only for nodes that host a Tailwind class string.
.expect("TailwindSyntaxVisitor only emits Tailwind class strings");
let parse = services
.get_service::<TwSyntaxService>()
// SAFETY: TailwindSyntaxServices requires this service before the rule can run.
.expect("TwSyntaxService service is not registered")
.parse_for_query(&class_string);
Self { node, parse }
}
}
pub struct TailwindSyntaxVisitor<N: AstNode> {
skip_subtree: Option<SyntaxNode<N::Language>>,
_node: PhantomData<N>,
}
impl<N: AstNode> Default for TailwindSyntaxVisitor<N> {
fn default() -> Self {
Self {View on GitHub (pinned to 17a2560fb5)