{"record":{"id":"aec98c5ae3316cbe","repo":"biomejs/biome","slug":"tailwindsyntaxvisitor-only-emits-tailwind-class-strings","errorCode":null,"errorMessage":"TailwindSyntaxVisitor only emits Tailwind class strings","messagePattern":"TailwindSyntaxVisitor only emits Tailwind class strings","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/biome_tailwind_logic/src/syntax_service.rs","lineNumber":273,"sourceCode":"    type Services = TailwindSyntaxServices;\n\n    fn build_visitor(\n        analyzer: &mut impl AddVisitor<Self::Language>,\n        _: &<Self::Language as Language>::Root,\n    ) {\n        analyzer.add_visitor(Phases::Syntax, TailwindSyntaxVisitor::<N>::default);\n    }\n\n    fn unwrap_match(services: &ServiceBag, node: &Self::Input) -> Self::Output {\n        let node = N::unwrap_cast(node.0.clone());\n        let class_string = node\n            .tailwind_class_string(\n                services\n                    .get_service::<JsFileSource>()\n                    .is_some_and(|source| source.as_embedding_kind().is_class_attribute()),\n            )\n            // SAFETY: The visitor emits matches only for nodes that host a Tailwind class string.\n            .expect(\"TailwindSyntaxVisitor only emits Tailwind class strings\");\n        let parse = services\n            .get_service::<TwSyntaxService>()\n            // SAFETY: TailwindSyntaxServices requires this service before the rule can run.\n            .expect(\"TwSyntaxService service is not registered\")\n            .parse_for_query(&class_string);\n\n        Self { node, parse }\n    }\n}\n\npub struct TailwindSyntaxVisitor<N: AstNode> {\n    skip_subtree: Option<SyntaxNode<N::Language>>,\n    _node: PhantomData<N>,\n}\n\nimpl<N: AstNode> Default for TailwindSyntaxVisitor<N> {\n    fn default() -> Self {\n        Self {","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/biomejs/biome/blob/17a2560fb5e5981fb5c568efca717882b68a8bb5/crates/biome_tailwind_logic/src/syntax_service.rs#L255-L291","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)"],"exampleFix":"// before: unwrap_match trusts any node handed to it\nlet class_string = node\n    .tailwind_class_string(services.get_service::<JsFileSource>().is_some_and(|s| s.as_embedding_kind().is_class_attribute()))\n    .expect(\"TailwindSyntaxVisitor only emits Tailwind class strings\");\n\n// after: fall back gracefully instead of panicking\nlet class_string = match node.tailwind_class_string(is_class_attribute) {\n    Some(class_string) => class_string,\n    None => return Self { node, parse: None }, // or skip the match entirely\n};","handlingStrategy":"validation","validationCode":"// Before relying on the Tailwind query, confirm the node hosts a class string\n// and the required service is registered:\nfn can_run_tailwind_query<N: AstNode + TailwindClassStringHost>(\n    node: &N,\n    services: &ServiceBag,\n) -> bool {\n    let is_class_attribute = services\n        .get_service::<JsFileSource>()\n        .is_some_and(|s| s.as_embedding_kind().is_class_attribute());\n    node.tailwind_class_string(is_class_attribute).is_some()\n        && services.get_service::<TwSyntaxService>().is_some()\n}","typeGuard":"fn hosts_class_string<N: AstNode + TailwindClassStringHost>(node: &N, is_class_attribute: bool) -> bool {\n    node.tailwind_class_string(is_class_attribute).is_some()\n}","tryCatchPattern":"// Rust panics are not catchable via Result; catch_unwind is the only guard,\n// and in Biome the correct approach is to avoid the panic by validating first:\nlet result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n    TailwindSyntax::<N>::unwrap_match(services, node)\n}));\nmatch result {\n    Ok(output) => output,\n    Err(_) => skip_rule(), // report a bug instead of crashing the analyzer\n}","preventionTips":["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"],"tags":["rust","panic","lint","tailwind","invariant"],"backgroundTag":"internal-invariant-violation","analyzedSha":"17a2560fb5e5981fb5c568efca717882b68a8bb5","analyzedAt":"2026-09-20T04:52:15.514Z","contentChangedAt":"2026-09-20T04:52:15.514Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}