From 6fbd0b65c0112775bbe7aba23398ec982a5bb9f5 Mon Sep 17 00:00:00 2001 From: Naoki Ikeguchi Date: Tue, 14 Apr 2026 00:15:24 +0900 Subject: [PATCH] fix(parser/css): parse conditional block correctly inside embedded snippets Co-authored-by: Codex --- .changeset/fast-zebras-move.md | 5 + .../src/syntax/block/conditional_block.rs | 7 +- .../src/workspace/server.tests.rs | 132 ++++++++++++++++++ 3 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 .changeset/fast-zebras-move.md diff --git a/.changeset/fast-zebras-move.md b/.changeset/fast-zebras-move.md new file mode 100644 index 000000000000..ad992404c116 --- /dev/null +++ b/.changeset/fast-zebras-move.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Fixed [#9113](https://github.com/biomejs/biome/issues/9113): Biome now parses and formats `@media` and other conditional blocks correctly inside embedded CSS snippets. diff --git a/crates/biome_css_parser/src/syntax/block/conditional_block.rs b/crates/biome_css_parser/src/syntax/block/conditional_block.rs index 45aec7dfd5c3..016a07cee08d 100644 --- a/crates/biome_css_parser/src/syntax/block/conditional_block.rs +++ b/crates/biome_css_parser/src/syntax/block/conditional_block.rs @@ -1,6 +1,7 @@ -use crate::parser::CssParser; +use biome_css_syntax::EmbeddingKind; use biome_parser::CompletedMarker; +use crate::parser::CssParser; use crate::syntax::block::{parse_declaration_or_rule_list_block, parse_rule_block}; /// @@ -18,7 +19,9 @@ use crate::syntax::block::{parse_declaration_or_rule_list_block, parse_rule_bloc /// #[inline] pub(crate) fn parse_conditional_block(p: &mut CssParser) -> CompletedMarker { - if p.state_mut().is_nesting_block { + if p.state().is_nesting_block + || matches!(p.source_type.as_embedding_kind(), EmbeddingKind::Styled) + { parse_declaration_or_rule_list_block(p) } else { parse_rule_block(p) diff --git a/crates/biome_service/src/workspace/server.tests.rs b/crates/biome_service/src/workspace/server.tests.rs index 57d584c8de69..da37e247a7aa 100644 --- a/crates/biome_service/src/workspace/server.tests.rs +++ b/crates/biome_service/src/workspace/server.tests.rs @@ -762,6 +762,138 @@ const PortfolioIcon = styled.div` "); } +#[test] +fn issue_9113() { + const FILE_PATH: &str = "/project/file.ts"; + const FILE_CONTENT: &str = r#"import styled from 'styled-components'; + +const Wrapper = styled.div` + height: 20px; + + @media screen and (min-width: 768px) { + height: 40px; + } +`; + +const Container = styled.div` + display: grid; + grid-template-rows: auto; + grid-gap: 2px; + margin: 4px 4px 0; + + /* top level seems fine */ + grid-template-columns: repeat(3, 1fr); + + @media (min-width: 480px) { + grid-template-columns: repeat(4, 1fr); + } + + @media (min-width: 640px) { + grid-template-columns: repeat(5, 1fr); + } + + @media (min-width: 780px) { + grid-template-columns: repeat(6, 1fr); + } +`;"#; + + let fs = MemoryFileSystem::default(); + fs.insert(Utf8PathBuf::from(FILE_PATH), FILE_CONTENT); + + let (workspace, project_key) = setup_workspace_and_open_project(fs, "/"); + + workspace + .update_settings(UpdateSettingsParams { + project_key, + workspace_directory: None, + configuration: Configuration { + javascript: Some(JsConfiguration { + experimental_embedded_snippets_enabled: Some(true.into()), + ..Default::default() + }), + ..Default::default() + }, + extended_configurations: vec![], + module_graph_resolution_kind: ModuleGraphResolutionKind::None, + }) + .unwrap(); + + workspace + .open_file(OpenFileParams { + project_key, + path: BiomePath::new(FILE_PATH), + content: FileContent::FromServer, + document_file_source: None, + persist_node_cache: false, + inline_config: None, + }) + .unwrap(); + + let result = workspace + .pull_diagnostics(PullDiagnosticsParams { + project_key, + path: BiomePath::new(FILE_PATH), + only: vec![], + skip: vec![], + enabled_rules: vec![], + categories: Default::default(), + include_code_fix: false, + inline_config: None, + max_diagnostics: None, + diagnostic_level: Severity::Error, + enforce_assist: false, + }) + .unwrap(); + + assert!( + result.diagnostics.is_empty(), + "Expected no diagnostics for embedded CSS, got: {:#?}", + result.diagnostics + ); + + let result = workspace + .format_file(FormatFileParams { + project_key, + path: Utf8PathBuf::from(FILE_PATH).into(), + inline_config: None, + }) + .unwrap(); + + insta::assert_snapshot!(result.as_code(), @r#" + import styled from "styled-components"; + + const Wrapper = styled.div` + height: 20px; + + @media screen and (min-width: 768px) { + height: 40px; + } + `; + + const Container = styled.div` + display: grid; + grid-template-rows: auto; + grid-gap: 2px; + margin: 4px 4px 0; + + /* top level seems fine */ + grid-template-columns: repeat(3, 1fr); + + @media (min-width: 480px) { + grid-template-columns: repeat(4, 1fr); + } + + @media (min-width: 640px) { + grid-template-columns: repeat(5, 1fr); + } + + @media (min-width: 780px) { + grid-template-columns: repeat(6, 1fr); + } + `; + "#); +} + #[test] fn format_js_with_embedded_graphql() { const FILE_PATH: &str = "/project/file.js";