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

  1. Upgrade to the latest Biome version; this panic is an internal bug likely already reported/fixed upstream
  2. 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
  3. 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)
  4. If calling the query in custom code: register `TwSyntaxService` in the ServiceBag and construct the input via the visitor, never by casting arbitrary nodes
  5. 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

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


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)