From d91a51f2d1dd2be4dc00ff5170c554ad6ef7d5e7 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Sun, 30 Nov 2025 09:31:21 +0000 Subject: [PATCH 1/3] refactor: document services --- crates/biome_service/src/file_handlers/astro.rs | 3 ++- crates/biome_service/src/file_handlers/css.rs | 5 ++++- crates/biome_service/src/file_handlers/graphql.rs | 5 ++++- crates/biome_service/src/file_handlers/grit.rs | 3 ++- crates/biome_service/src/file_handlers/html.rs | 1 + crates/biome_service/src/file_handlers/javascript.rs | 3 ++- crates/biome_service/src/file_handlers/json.rs | 5 ++++- crates/biome_service/src/file_handlers/mod.rs | 1 + crates/biome_service/src/file_handlers/svelte.rs | 3 ++- crates/biome_service/src/file_handlers/vue.rs | 3 ++- 10 files changed, 24 insertions(+), 8 deletions(-) diff --git a/crates/biome_service/src/file_handlers/astro.rs b/crates/biome_service/src/file_handlers/astro.rs index 19b1acb7ed30..3004b5071a82 100644 --- a/crates/biome_service/src/file_handlers/astro.rs +++ b/crates/biome_service/src/file_handlers/astro.rs @@ -5,7 +5,7 @@ use crate::file_handlers::{ ParserCapabilities, javascript, }; use crate::settings::Settings; -use crate::workspace::{DocumentFileSource, FixFileResult, PullActionsResult}; +use crate::workspace::{DocumentFileSource, DocumentServices, FixFileResult, PullActionsResult}; use biome_formatter::Printed; use biome_fs::BiomePath; use biome_js_parser::{JsParserOptions, parse_js_with_cache}; @@ -127,6 +127,7 @@ fn parse( ParseResult { any_parse: parse.into(), language: Some(JsFileSource::astro().into()), + services: DocumentServices::none(), } } diff --git a/crates/biome_service/src/file_handlers/css.rs b/crates/biome_service/src/file_handlers/css.rs index 27ed65783561..6987f8d16a21 100644 --- a/crates/biome_service/src/file_handlers/css.rs +++ b/crates/biome_service/src/file_handlers/css.rs @@ -13,7 +13,8 @@ use crate::settings::{ Settings, check_feature_activity, check_override_feature_activity, }; use crate::workspace::{ - CodeAction, DocumentFileSource, FixFileResult, GetSyntaxTreeResult, PullActionsResult, + CodeAction, DocumentFileSource, DocumentServices, FixFileResult, GetSyntaxTreeResult, + PullActionsResult, }; use biome_analyze::options::PreferredQuote; use biome_analyze::{AnalysisFilter, AnalyzerConfiguration, AnalyzerOptions, ControlFlow, Never}; @@ -432,9 +433,11 @@ fn parse( .apply_override_css_parser_options(biome_path, &mut options); let parse = biome_css_parser::parse_css_with_cache(text, cache, options); + let services = DocumentServices::new_css(&parse.tree()); ParseResult { any_parse: parse.into(), language: None, + services, } } diff --git a/crates/biome_service/src/file_handlers/graphql.rs b/crates/biome_service/src/file_handlers/graphql.rs index ed0507ab4bbd..e67b53f675c5 100644 --- a/crates/biome_service/src/file_handlers/graphql.rs +++ b/crates/biome_service/src/file_handlers/graphql.rs @@ -13,7 +13,9 @@ use crate::settings::{ FormatSettings, LanguageListSettings, LanguageSettings, OverrideSettings, ServiceLanguage, Settings, check_feature_activity, check_override_feature_activity, }; -use crate::workspace::{CodeAction, FixFileResult, GetSyntaxTreeResult, PullActionsResult}; +use crate::workspace::{ + CodeAction, DocumentServices, FixFileResult, GetSyntaxTreeResult, PullActionsResult, +}; use biome_analyze::{AnalysisFilter, AnalyzerConfiguration, AnalyzerOptions, ControlFlow, Never}; use biome_configuration::graphql::{ GraphqlAssistConfiguration, GraphqlAssistEnabled, GraphqlFormatterConfiguration, @@ -332,6 +334,7 @@ fn parse( ParseResult { any_parse: parse.into(), language: Some(file_source), + services: DocumentServices::none(), } } diff --git a/crates/biome_service/src/file_handlers/grit.rs b/crates/biome_service/src/file_handlers/grit.rs index 204b5a47b0c9..5e521b20871e 100644 --- a/crates/biome_service/src/file_handlers/grit.rs +++ b/crates/biome_service/src/file_handlers/grit.rs @@ -4,7 +4,7 @@ use super::{ ParserCapabilities, SearchCapabilities, }; use crate::settings::{OverrideSettings, check_feature_activity, check_override_feature_activity}; -use crate::workspace::{FixFileResult, GetSyntaxTreeResult}; +use crate::workspace::{DocumentServices, FixFileResult, GetSyntaxTreeResult}; use crate::{ WorkspaceError, settings::{ServiceLanguage, Settings}, @@ -305,6 +305,7 @@ fn parse( ParseResult { any_parse: parse.into(), language: Some(file_source), + services: DocumentServices::none(), } } diff --git a/crates/biome_service/src/file_handlers/html.rs b/crates/biome_service/src/file_handlers/html.rs index f0ea194ad7e8..81310951485f 100644 --- a/crates/biome_service/src/file_handlers/html.rs +++ b/crates/biome_service/src/file_handlers/html.rs @@ -385,6 +385,7 @@ fn parse( ParseResult { any_parse: parse.into(), language: Some(file_source), + services: DocumentServices::none(), } } diff --git a/crates/biome_service/src/file_handlers/javascript.rs b/crates/biome_service/src/file_handlers/javascript.rs index 87bb01ada258..e360ea540af6 100644 --- a/crates/biome_service/src/file_handlers/javascript.rs +++ b/crates/biome_service/src/file_handlers/javascript.rs @@ -10,7 +10,7 @@ use crate::file_handlers::FixAllParams; use crate::settings::{ OverrideSettings, Settings, check_feature_activity, check_override_feature_activity, }; -use crate::workspace::{DocumentFileSource, PullDiagnosticsAndActionsResult}; +use crate::workspace::{DocumentFileSource, DocumentServices, PullDiagnosticsAndActionsResult}; use crate::{ WorkspaceError, settings::{FormatSettings, LanguageListSettings, LanguageSettings, ServiceLanguage}, @@ -547,6 +547,7 @@ fn parse( ParseResult { any_parse: parse.into(), language: None, + services: DocumentServices::none(), } } diff --git a/crates/biome_service/src/file_handlers/json.rs b/crates/biome_service/src/file_handlers/json.rs index 68cb40adc28f..c9aac5765283 100644 --- a/crates/biome_service/src/file_handlers/json.rs +++ b/crates/biome_service/src/file_handlers/json.rs @@ -12,7 +12,9 @@ use crate::settings::{ FormatSettings, LanguageListSettings, LanguageSettings, OverrideSettings, ServiceLanguage, Settings, check_feature_activity, check_override_feature_activity, }; -use crate::workspace::{CodeAction, FixFileResult, GetSyntaxTreeResult, PullActionsResult}; +use crate::workspace::{ + CodeAction, DocumentServices, FixFileResult, GetSyntaxTreeResult, PullActionsResult, +}; use crate::{WorkspaceError, extension_error}; use biome_analyze::options::PreferredQuote; use biome_analyze::{AnalysisFilter, AnalyzerConfiguration, AnalyzerOptions, ControlFlow, Never}; @@ -393,6 +395,7 @@ fn parse( ParseResult { any_parse: parse.into(), language: Some(file_source), + services: DocumentServices::none(), } } diff --git a/crates/biome_service/src/file_handlers/mod.rs b/crates/biome_service/src/file_handlers/mod.rs index 475fa85bc28f..f76836b20198 100644 --- a/crates/biome_service/src/file_handlers/mod.rs +++ b/crates/biome_service/src/file_handlers/mod.rs @@ -489,6 +489,7 @@ pub struct Capabilities { pub struct ParseResult { pub(crate) any_parse: AnyParse, pub(crate) language: Option, + pub(crate) services: DocumentServices, } pub struct ParseEmbedResult { diff --git a/crates/biome_service/src/file_handlers/svelte.rs b/crates/biome_service/src/file_handlers/svelte.rs index 0c02eedf029b..5ed886c75f91 100644 --- a/crates/biome_service/src/file_handlers/svelte.rs +++ b/crates/biome_service/src/file_handlers/svelte.rs @@ -6,7 +6,7 @@ use crate::file_handlers::{ ParserCapabilities, javascript, }; use crate::settings::Settings; -use crate::workspace::{DocumentFileSource, FixFileResult, PullActionsResult}; +use crate::workspace::{DocumentFileSource, DocumentServices, FixFileResult, PullActionsResult}; use biome_formatter::Printed; use biome_fs::BiomePath; use biome_html_syntax::HtmlLanguage; @@ -138,6 +138,7 @@ fn parse( ParseResult { any_parse: parse.into(), language: Some(file_source.into()), + services: DocumentServices::none(), } } diff --git a/crates/biome_service/src/file_handlers/vue.rs b/crates/biome_service/src/file_handlers/vue.rs index 3658d4311534..5ca0cb8257f3 100644 --- a/crates/biome_service/src/file_handlers/vue.rs +++ b/crates/biome_service/src/file_handlers/vue.rs @@ -6,7 +6,7 @@ use crate::file_handlers::{ ParserCapabilities, javascript, }; use crate::settings::Settings; -use crate::workspace::{DocumentFileSource, FixFileResult, PullActionsResult}; +use crate::workspace::{DocumentFileSource, DocumentServices, FixFileResult, PullActionsResult}; use biome_formatter::Printed; use biome_fs::BiomePath; use biome_html_syntax::HtmlLanguage; @@ -138,6 +138,7 @@ fn parse( ParseResult { any_parse: parse.into(), language: Some(file_source.into()), + services: DocumentServices::none(), } } From 15916d5e30b2cfd5145bd12935bb7197fd48e17d Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Sun, 30 Nov 2025 19:22:45 +0000 Subject: [PATCH 2/3] feat: CSS module graph --- Cargo.lock | 4 + crates/biome_css_syntax/src/import_ext.rs | 27 +++ crates/biome_css_syntax/src/lib.rs | 1 + crates/biome_js_analyze/src/lib.rs | 2 +- .../src/lint/nursery/no_deprecated_imports.rs | 2 +- .../src/lint/nursery/no_unresolved_imports.rs | 2 +- .../src/services/module_graph.rs | 2 +- crates/biome_lsp/src/server.tests.rs | 12 +- crates/biome_module_graph/Cargo.toml | 2 + .../benches/module_graph.rs | 1 - .../biome_module_graph/src/css_module_info.rs | 102 ++++++++ .../src/css_module_info/visitor.rs | 76 ++++++ .../biome_module_graph/src/js_module_info.rs | 2 +- .../src/js_module_info/module_resolver.rs | 2 +- crates/biome_module_graph/src/lib.rs | 6 +- crates/biome_module_graph/src/module_graph.rs | 175 ++++++++++++-- .../tests/fixtures/css/bar.css | 0 .../tests/fixtures/css/foo.css | 0 .../tests/fixtures/css/index.css | 2 + .../tests/fixtures/css/package.json | 4 + crates/biome_module_graph/tests/snap/mod.rs | 53 +++-- crates/biome_module_graph/tests/spec_tests.rs | 218 ++++++++++++------ crates/biome_rowan/src/syntax/node.rs | 12 + crates/biome_ruledoc_utils/src/lib.rs | 2 +- .../biome_service/src/file_handlers/astro.rs | 3 +- crates/biome_service/src/file_handlers/css.rs | 5 +- .../src/file_handlers/graphql.rs | 5 +- .../biome_service/src/file_handlers/grit.rs | 3 +- .../biome_service/src/file_handlers/html.rs | 1 - .../src/file_handlers/javascript.rs | 5 +- .../biome_service/src/file_handlers/json.rs | 5 +- crates/biome_service/src/file_handlers/mod.rs | 1 - .../biome_service/src/file_handlers/svelte.rs | 3 +- crates/biome_service/src/file_handlers/vue.rs | 3 +- .../scanner/workspace_watcher_bridge.tests.rs | 12 +- ..._workspace__tests__debug_module_graph.snap | 60 ++--- crates/biome_service/src/workspace.rs | 4 +- crates/biome_service/src/workspace.tests.rs | 6 +- crates/biome_service/src/workspace/server.rs | 90 +++++--- crates/biome_test_utils/Cargo.toml | 2 + crates/biome_test_utils/src/lib.rs | 26 ++- 41 files changed, 735 insertions(+), 208 deletions(-) create mode 100644 crates/biome_css_syntax/src/import_ext.rs create mode 100644 crates/biome_module_graph/src/css_module_info.rs create mode 100644 crates/biome_module_graph/src/css_module_info/visitor.rs create mode 100644 crates/biome_module_graph/tests/fixtures/css/bar.css create mode 100644 crates/biome_module_graph/tests/fixtures/css/foo.css create mode 100644 crates/biome_module_graph/tests/fixtures/css/index.css create mode 100644 crates/biome_module_graph/tests/fixtures/css/package.json diff --git a/Cargo.lock b/Cargo.lock index 06d597e4cd2a..c76d65436fa8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1314,6 +1314,8 @@ name = "biome_module_graph" version = "0.0.1" dependencies = [ "biome_console", + "biome_css_semantic", + "biome_css_syntax", "biome_deserialize", "biome_diagnostics", "biome_formatter", @@ -1652,6 +1654,8 @@ dependencies = [ "biome_analyze", "biome_configuration", "biome_console", + "biome_css_parser", + "biome_css_syntax", "biome_deserialize", "biome_diagnostics", "biome_formatter", diff --git a/crates/biome_css_syntax/src/import_ext.rs b/crates/biome_css_syntax/src/import_ext.rs new file mode 100644 index 000000000000..016c684b04b4 --- /dev/null +++ b/crates/biome_css_syntax/src/import_ext.rs @@ -0,0 +1,27 @@ +use crate::{AnyCssImportUrl, AnyCssUrlValue}; +use biome_rowan::TokenText; + +impl AnyCssImportUrl { + /// Returns the inner text of specifier: + /// + /// ## Examples + /// + /// ``` + /// use biome_css_factory::make; + /// use biome_css_syntax::AnyCssImportUrl; + /// ``` + pub fn inner_string_text(&self) -> Option { + match self { + Self::CssString(css) => css.inner_string_text().ok(), + Self::CssUrlFunction(url_function) => { + url_function.value().and_then(|value| match value { + AnyCssUrlValue::CssString(css) => css.inner_string_text().ok(), + AnyCssUrlValue::CssUrlValueRaw(raw) => raw + .value_token() + .ok() + .map(|token| token.token_text_trimmed()), + }) + } + } + } +} diff --git a/crates/biome_css_syntax/src/lib.rs b/crates/biome_css_syntax/src/lib.rs index 1658957b52fd..1cfd0a80d941 100644 --- a/crates/biome_css_syntax/src/lib.rs +++ b/crates/biome_css_syntax/src/lib.rs @@ -3,6 +3,7 @@ #[macro_use] mod file_source; mod generated; +mod import_ext; pub mod selector_ext; pub mod stmt_ext; mod string_ext; diff --git a/crates/biome_js_analyze/src/lib.rs b/crates/biome_js_analyze/src/lib.rs index 52e73d556f11..edc9fbb3c82a 100644 --- a/crates/biome_js_analyze/src/lib.rs +++ b/crates/biome_js_analyze/src/lib.rs @@ -159,7 +159,7 @@ where .map(|(path, manifest)| (path, Arc::new(manifest))); let type_resolver = module_graph - .module_info_for_path(file_path.as_ref()) + .js_module_info_for_path(file_path.as_ref()) .map(|module_info| ModuleResolver::for_module(module_info, module_graph.clone())) .map(Arc::new); diff --git a/crates/biome_js_analyze/src/lint/nursery/no_deprecated_imports.rs b/crates/biome_js_analyze/src/lint/nursery/no_deprecated_imports.rs index eeb3390552ea..3630882d34ca 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_deprecated_imports.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_deprecated_imports.rs @@ -125,7 +125,7 @@ fn get_deprecated_imports_from_module_source( target_path: &Utf8Path, module_graph: &ModuleGraph, ) -> Vec { - let Some(module_info) = module_graph.module_info_for_path(target_path) else { + let Some(module_info) = module_graph.js_module_info_for_path(target_path) else { return Vec::new(); }; diff --git a/crates/biome_js_analyze/src/lint/nursery/no_unresolved_imports.rs b/crates/biome_js_analyze/src/lint/nursery/no_unresolved_imports.rs index e84de77b7eaf..f78379a2489c 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_unresolved_imports.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_unresolved_imports.rs @@ -260,6 +260,6 @@ fn get_unresolved_imports_from_module_source( fn has_exported_symbol(import_name: &Text, options: &GetUnresolvedImportsOptions) -> bool { options .target_info - .find_exported_symbol(options.module_graph, import_name.text()) + .find_js_exported_symbol(options.module_graph, import_name.text()) .is_some() } diff --git a/crates/biome_js_analyze/src/services/module_graph.rs b/crates/biome_js_analyze/src/services/module_graph.rs index 0f54fda0ad85..a44c7d10aab8 100644 --- a/crates/biome_js_analyze/src/services/module_graph.rs +++ b/crates/biome_js_analyze/src/services/module_graph.rs @@ -17,7 +17,7 @@ impl ModuleGraphService { } pub fn module_info_for_path(&self, path: &Utf8Path) -> Option { - self.0.module_info_for_path(path) + self.0.js_module_info_for_path(path) } pub fn project_layout(&self) -> &ProjectLayout { diff --git a/crates/biome_lsp/src/server.tests.rs b/crates/biome_lsp/src/server.tests.rs index 8debd2e664e4..d8f321b7f9d9 100644 --- a/crates/biome_lsp/src/server.tests.rs +++ b/crates/biome_lsp/src/server.tests.rs @@ -3838,7 +3838,11 @@ async fn should_open_and_update_nested_files() -> Result<()> { result .data .get(fs.working_directory.join("src").join("a.js").as_str()) - .map(|module_info| module_info.static_import_paths.clone()), + .map(|module_info| module_info + .as_js_module_info() + .unwrap() + .static_import_paths + .clone()), Some(BTreeMap::from([("foo".to_string(), "foo".to_string())])) ); @@ -3865,7 +3869,11 @@ async fn should_open_and_update_nested_files() -> Result<()> { result .data .get(fs.working_directory.join("src").join("a.js").as_str()) - .map(|module_info| module_info.static_import_paths.clone()), + .map(|module_info| module_info + .as_js_module_info() + .unwrap() + .static_import_paths + .clone()), Some(BTreeMap::from([("bar".to_string(), "bar".to_string())])) ); diff --git a/crates/biome_module_graph/Cargo.toml b/crates/biome_module_graph/Cargo.toml index 161aaf4c46ed..409e90d2204c 100644 --- a/crates/biome_module_graph/Cargo.toml +++ b/crates/biome_module_graph/Cargo.toml @@ -16,6 +16,8 @@ workspace = true [dependencies] biome_console = { workspace = true } +biome_css_semantic = { workspace = true } +biome_css_syntax = { workspace = true } biome_diagnostics = { workspace = true } biome_formatter = { workspace = true } biome_fs = { workspace = true } diff --git a/crates/biome_module_graph/benches/module_graph.rs b/crates/biome_module_graph/benches/module_graph.rs index 11f64b7f2423..9da270cb0492 100644 --- a/crates/biome_module_graph/benches/module_graph.rs +++ b/crates/biome_module_graph/benches/module_graph.rs @@ -73,7 +73,6 @@ fn bench_index_d_ts(bencher: Bencher, name: &str) { &fs, &ProjectLayout::default(), &[(&path, root)], - &[], ); divan::black_box(&module_graph); }); diff --git a/crates/biome_module_graph/src/css_module_info.rs b/crates/biome_module_graph/src/css_module_info.rs new file mode 100644 index 000000000000..2e02a1bb0568 --- /dev/null +++ b/crates/biome_module_graph/src/css_module_info.rs @@ -0,0 +1,102 @@ +mod visitor; + +use biome_resolver::ResolvedPath; +use biome_rowan::Text; +use indexmap::IndexMap; +use std::collections::BTreeSet; +use std::ops::{Deref, DerefMut}; +use std::sync::Arc; + +pub(crate) use visitor::CssModuleVisitor; + +/// Information restricted to a single module in the [ModuleGraph]. +#[derive(Clone, Debug)] +pub struct CssModuleInfo(pub(super) Arc); + +impl Deref for CssModuleInfo { + type Target = CssModuleInfoInner; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} + +impl CssModuleInfo { + pub(crate) fn new(imports: CssImports) -> Self { + let info = CssModuleInfoInner { imports }; + Self(Arc::new(info)) + } + + pub(crate) fn dump(&self) -> SerializedCssModuleInfo { + SerializedCssModuleInfo { + imports: self + .0 + .imports + .iter() + .map(|(_, static_import)| static_import.specifier.to_string()) + .collect(), + } + } +} + +#[derive(Clone, Debug)] +pub struct CssModuleInfoInner { + /// Map of all static imports found in the module. + /// + /// Maps from the local imported name to a [JsImport] with the absolute path + /// it resolves to. The resolved path may be looked up as key in the + /// [ModuleGraph::data] map, although it is not required to exist + /// (for instance, if the path is outside the project's scope). + /// + /// Note that re-exports may introduce additional dependencies, because they + /// import another module and immediately re-export from that module. + /// Re-exports are tracked as part of [Self::exports] and + /// [Self::blanket_reexports]. + pub imports: CssImports, +} + +#[derive(Debug, Default, Clone)] +pub struct CssImports(pub(crate) IndexMap); + +impl Deref for CssImports { + type Target = IndexMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for CssImports { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// Represents an import to one or more symbols from an external path. +/// +/// It could point to any kind of resource, such as JavaScript files, CSS files, +/// images, and so on. +#[derive(Clone, Debug, PartialEq, Hash, Eq)] +pub struct CssImport { + /// The specifier for the imported as it appeared in the source text. + pub specifier: Text, + + /// Absolute path of the resource being imported, if it can be resolved. + /// + /// If the import statement referred to a package dependency, the path will + /// point towards the resolved entry point of the package. + /// + /// If `None`, import resolution failed. + pub resolved_path: ResolvedPath, +} + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +pub struct SerializedCssModuleInfo { + /// Map of all static imports found in the module. + /// + /// Maps from the local imported name to the absolute path it resolves to. + pub imports: BTreeSet, +} diff --git a/crates/biome_module_graph/src/css_module_info/visitor.rs b/crates/biome_module_graph/src/css_module_info/visitor.rs new file mode 100644 index 000000000000..a4de1fbb0366 --- /dev/null +++ b/crates/biome_module_graph/src/css_module_info/visitor.rs @@ -0,0 +1,76 @@ +use crate::css_module_info::{CssImport, CssImports, CssModuleInfo}; +use crate::module_graph::ModuleGraphFsProxy; +use biome_css_syntax::{AnyCssImportUrl, CssRoot}; +use biome_resolver::{ResolveOptions, ResolvedPath, resolve}; +use biome_rowan::{AstNode, Text, WalkEvent}; +use camino::Utf8Path; +use std::ops::DerefMut; + +pub const SUPPORTED_EXTENSIONS: &[&str] = &["css"]; + +pub(crate) struct CssModuleVisitor<'a> { + root: CssRoot, + directory: &'a Utf8Path, + fs_proxy: &'a ModuleGraphFsProxy<'a>, +} + +impl<'a> CssModuleVisitor<'a> { + pub(crate) fn new( + root: CssRoot, + directory: &'a Utf8Path, + fs_proxy: &'a ModuleGraphFsProxy, + ) -> Self { + Self { + root, + directory, + fs_proxy, + } + } + + pub(crate) fn visit(self) -> CssModuleInfo { + let mut imports = CssImports::default(); + let iter = self.root.syntax().preorder(); + for event in iter { + match event { + WalkEvent::Enter(node) => { + if let Some(node) = AnyCssImportUrl::cast(node) { + self.visit_any_css_import_url(node, &mut imports); + } + } + WalkEvent::Leave(_) => {} + } + } + + CssModuleInfo::new(imports) + } + + fn visit_any_css_import_url(&self, node: AnyCssImportUrl, imports: &mut CssImports) { + let Some(specifier) = node.inner_string_text() else { + return; + }; + + let resolved_path = self.resolved_path_from_specifier(&specifier); + + let text: Text = specifier.into(); + imports.deref_mut().insert( + text.clone(), + CssImport { + specifier: text, + resolved_path, + }, + ); + } + + fn resolved_path_from_specifier(&self, specifier: &str) -> ResolvedPath { + let options = ResolveOptions { + assume_relative: true, + condition_names: &[], + default_files: &[], + extensions: SUPPORTED_EXTENSIONS, + extension_aliases: &[], + ..Default::default() + }; + let resolved_path = resolve(specifier, self.directory, self.fs_proxy, &options); + ResolvedPath::new(resolved_path) + } +} diff --git a/crates/biome_module_graph/src/js_module_info.rs b/crates/biome_module_graph/src/js_module_info.rs index c86b0ccecbe6..100f6f048fdc 100644 --- a/crates/biome_module_graph/src/js_module_info.rs +++ b/crates/biome_module_graph/src/js_module_info.rs @@ -57,7 +57,7 @@ impl JsModuleInfo { /// Finds an exported symbol by `name`, using the `module_graph` to /// lookup re-exports if necessary. #[inline] - pub fn find_exported_symbol( + pub fn find_js_exported_symbol( &self, module_graph: &ModuleGraph, name: &str, diff --git a/crates/biome_module_graph/src/js_module_info/module_resolver.rs b/crates/biome_module_graph/src/js_module_info/module_resolver.rs index 15ec9bb06ab6..df52749be10b 100644 --- a/crates/biome_module_graph/src/js_module_info/module_resolver.rs +++ b/crates/biome_module_graph/src/js_module_info/module_resolver.rs @@ -169,7 +169,7 @@ impl ModuleResolver { Entry::Occupied(entry) => Some(*entry.get()), Entry::Vacant(entry) => { let path = entry.key().as_path()?; - let module_info = self.module_graph.module_info_for_path(path)?; + let module_info = self.module_graph.js_module_info_for_path(path)?; let module_id = ModuleId::new(self.modules.len()); self.modules.push(module_info); self.types diff --git a/crates/biome_module_graph/src/lib.rs b/crates/biome_module_graph/src/lib.rs index 490d44a07da3..3b6b9fdf2960 100644 --- a/crates/biome_module_graph/src/lib.rs +++ b/crates/biome_module_graph/src/lib.rs @@ -1,5 +1,6 @@ #![deny(clippy::use_self)] +mod css_module_info; mod diagnostics; mod format_module_graph; mod js_module_info; @@ -8,9 +9,12 @@ mod module_graph; pub use biome_js_type_info::ImportSymbol; pub use biome_resolver::ResolvedPath; +pub use css_module_info::{CssImport, CssImports, CssModuleInfo}; pub use diagnostics::ModuleDiagnostic; pub use js_module_info::{ JsExport, JsImport, JsImportPath, JsImportPhase, JsModuleInfo, JsModuleInfoDiagnostic, JsOwnExport, JsReexport, ModuleResolver, SerializedJsModuleInfo, }; -pub use module_graph::{ModuleDependencies, ModuleGraph, SUPPORTED_EXTENSIONS}; +pub use module_graph::{ + ModuleDependencies, ModuleGraph, ModuleInfo, SUPPORTED_EXTENSIONS, SerializedModuleInfo, +}; diff --git a/crates/biome_module_graph/src/module_graph.rs b/crates/biome_module_graph/src/module_graph.rs index c15005f1a8fd..f9c714a6863b 100644 --- a/crates/biome_module_graph/src/module_graph.rs +++ b/crates/biome_module_graph/src/module_graph.rs @@ -10,6 +10,12 @@ mod fs_proxy; use std::{collections::BTreeSet, ops::Deref}; +use crate::css_module_info::{CssModuleInfo, CssModuleVisitor, SerializedCssModuleInfo}; +use crate::{ + JsExport, JsModuleInfo, JsOwnExport, ModuleDiagnostic, SerializedJsModuleInfo, + js_module_info::JsModuleVisitor, +}; +use biome_css_syntax::CssRoot; use biome_fs::BiomePath; use biome_js_syntax::AnyJsRoot; use biome_js_type_info::ImportSymbol; @@ -17,15 +23,10 @@ use biome_jsdoc_comment::JsdocComment; use biome_project_layout::ProjectLayout; use biome_resolver::{FsWithResolverProxy, PathInfo}; use camino::{Utf8Path, Utf8PathBuf}; +pub(crate) use fs_proxy::ModuleGraphFsProxy; use papaya::{HashMap, HashMapRef, LocalGuard}; use rustc_hash::{FxBuildHasher, FxHashSet}; -use crate::{ - JsExport, JsModuleInfo, JsOwnExport, ModuleDiagnostic, js_module_info::JsModuleVisitor, -}; - -pub(crate) use fs_proxy::ModuleGraphFsProxy; - pub const SUPPORTED_EXTENSIONS: &[&str] = &[ "ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs", "json", "node", ]; @@ -41,10 +42,7 @@ pub const SUPPORTED_EXTENSIONS: &[&str] = &[ #[derive(Debug, Default)] pub struct ModuleGraph { /// Cached module info per file. - // TODO: When we want to generalise the module graph across languages, - // we should insert a `ModuleInfo` enum with variants such as - // `Js(JsModuleInfo)` and those for other languages. - data: HashMap, + data: HashMap, /// Cache that tracks the presence of files, directories, and symlinks /// across the project. @@ -59,12 +57,15 @@ impl ModuleGraph { /// Returns the module info, such as imports and exports and their types, /// for the given `path`. - pub fn module_info_for_path(&self, path: &Utf8Path) -> Option { - self.data.pin().get(path).cloned() + pub fn js_module_info_for_path(&self, path: &Utf8Path) -> Option { + self.data.pin().get(path).and_then(|info| match info { + ModuleInfo::Js(module_info) => Some(module_info.clone()), + _ => None, + }) } /// Returns the data of the module graph in test - pub fn data(&self) -> HashMapRef<'_, Utf8PathBuf, JsModuleInfo, FxBuildHasher, LocalGuard<'_>> { + pub fn data(&self) -> HashMapRef<'_, Utf8PathBuf, ModuleInfo, FxBuildHasher, LocalGuard<'_>> { self.data.pin() } @@ -81,7 +82,6 @@ impl ModuleGraph { fs: &dyn FsWithResolverProxy, project_layout: &ProjectLayout, added_or_updated_paths: &[(&BiomePath, AnyJsRoot)], - removed_paths: &[&BiomePath], ) -> (ModuleDependencies, Vec) { // Make sure all directories are registered for the added/updated paths. let path_info = self.path_info.pin(); @@ -122,16 +122,72 @@ impl ModuleGraph { diagnostics.push(diagnostic.clone()); } - modules.insert(path.to_path_buf(), module_info); + modules.insert(path.to_path_buf(), module_info.into()); + } + + (dependencies, diagnostics) + } + + pub fn update_graph_for_css_paths( + &self, + fs: &dyn FsWithResolverProxy, + project_layout: &ProjectLayout, + added_or_updated_paths: &[(&BiomePath, CssRoot)], + _semantic_model: Option<&biome_css_semantic::model::SemanticModel>, + ) -> (ModuleDependencies, Vec) { + // Make sure all directories are registered for the added/updated paths. + let path_info = self.path_info.pin(); + for (path, _) in added_or_updated_paths { + let mut parent = path.parent(); + while let Some(path) = parent { + let mut inserted = false; + path_info.get_or_insert_with(path.to_path_buf(), || { + inserted = true; + fs.path_info(path).ok() + }); + if !inserted { + break; + } + parent = path.parent(); + } } + let fs_proxy = ModuleGraphFsProxy::new(fs, self, project_layout); + let mut dependencies = ModuleDependencies::default(); + let diagnostics = Vec::new(); + + // Traverse all the added and updated paths and insert their module + // info. + let modules = self.data.pin(); + + for (path, root) in added_or_updated_paths { + let directory = path.parent().unwrap_or(path); + let visitor = CssModuleVisitor::new(root.clone(), directory, &fs_proxy); + + let module = visitor.visit(); + + for (_, path) in module.0.imports.deref() { + if let Some(path) = path.resolved_path.as_path() { + dependencies.insert(path.to_path_buf()); + } + } + + modules.insert(path.to_path_buf(), module.into()); + } + + (dependencies, diagnostics) + } + + pub fn update_graph_for_removed_paths(&self, removed_paths: &[&BiomePath]) { + let modules = self.data.pin(); + // Make sure all directories are registered for the added/updated paths. + let path_info = self.path_info.pin(); + // Clean up removed paths. for removed_path in removed_paths { modules.remove(removed_path.as_path()); path_info.remove(removed_path.as_path()); } - - (dependencies, diagnostics) } pub fn get_or_insert_path_info( @@ -195,8 +251,74 @@ impl ModuleGraph { } } +#[derive(Debug)] +pub enum ModuleInfo { + Js(JsModuleInfo), + Css(CssModuleInfo), +} + +impl From for ModuleInfo { + fn from(value: JsModuleInfo) -> Self { + Self::Js(value) + } +} + +impl From for ModuleInfo { + fn from(value: CssModuleInfo) -> Self { + Self::Css(value) + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +pub enum SerializedModuleInfo { + Js(SerializedJsModuleInfo), + Css(SerializedCssModuleInfo), +} + +impl SerializedModuleInfo { + pub fn as_js_module_info(&self) -> Option<&SerializedJsModuleInfo> { + match self { + Self::Js(module) => Some(module), + _ => None, + } + } + + pub fn as_css_module_info(&self) -> Option<&SerializedCssModuleInfo> { + match self { + Self::Css(module) => Some(module), + _ => None, + } + } +} + +impl ModuleInfo { + pub fn dump(&self) -> SerializedModuleInfo { + match self { + Self::Js(module) => SerializedModuleInfo::Js(module.dump()), + Self::Css(module) => SerializedModuleInfo::Css(module.dump()), + } + } + + pub fn as_js_module_info(&self) -> Option<&JsModuleInfo> { + match self { + Self::Js(module) => Some(module), + _ => None, + } + } + + pub fn as_css_module_info(&self) -> Option<&CssModuleInfo> { + match self { + Self::Css(module) => Some(module), + _ => None, + } + } +} + fn find_exported_symbol_with_seen_paths<'a>( - data: &'a HashMapRef, + data: &'a HashMapRef, module: &'a JsModuleInfo, symbol_name: &str, seen_paths: &mut BTreeSet<&'a Utf8Path>, @@ -212,7 +334,16 @@ fn find_exported_symbol_with_seen_paths<'a>( } else { match reexport.import.resolved_path.as_deref() { Ok(path) if seen_paths.insert(path) => data.get(path).and_then(|module| { - find_exported_symbol_with_seen_paths(data, module, symbol_name, seen_paths) + if let ModuleInfo::Js(module) = module { + find_exported_symbol_with_seen_paths( + data, + module, + symbol_name, + seen_paths, + ) + } else { + None + } }), _ => None, } @@ -221,7 +352,11 @@ fn find_exported_symbol_with_seen_paths<'a>( None => module.blanket_reexports.iter().find_map(|reexport| { match reexport.import.resolved_path.as_deref() { Ok(path) if seen_paths.insert(path) => data.get(path).and_then(|module| { - find_exported_symbol_with_seen_paths(data, module, symbol_name, seen_paths) + if let ModuleInfo::Js(module) = module { + find_exported_symbol_with_seen_paths(data, module, symbol_name, seen_paths) + } else { + None + } }), _ => None, } diff --git a/crates/biome_module_graph/tests/fixtures/css/bar.css b/crates/biome_module_graph/tests/fixtures/css/bar.css new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/crates/biome_module_graph/tests/fixtures/css/foo.css b/crates/biome_module_graph/tests/fixtures/css/foo.css new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/crates/biome_module_graph/tests/fixtures/css/index.css b/crates/biome_module_graph/tests/fixtures/css/index.css new file mode 100644 index 000000000000..46ae42a89cc6 --- /dev/null +++ b/crates/biome_module_graph/tests/fixtures/css/index.css @@ -0,0 +1,2 @@ +@import "foo.css"; +@import "./bar.css"; diff --git a/crates/biome_module_graph/tests/fixtures/css/package.json b/crates/biome_module_graph/tests/fixtures/css/package.json new file mode 100644 index 000000000000..f2368af957e0 --- /dev/null +++ b/crates/biome_module_graph/tests/fixtures/css/package.json @@ -0,0 +1,4 @@ +{ + "name": "css", + "version": "0.0.1" +} diff --git a/crates/biome_module_graph/tests/snap/mod.rs b/crates/biome_module_graph/tests/snap/mod.rs index a7c4fe190cf2..ce01583fde61 100644 --- a/crates/biome_module_graph/tests/snap/mod.rs +++ b/crates/biome_module_graph/tests/snap/mod.rs @@ -5,7 +5,7 @@ use biome_js_formatter::context::JsFormatOptions; use biome_js_formatter::format_node; use biome_js_parser::{JsParserOptions, parse}; use biome_js_syntax::JsFileSource; -use biome_module_graph::{JsExport, JsOwnExport, ModuleGraph, ModuleResolver}; +use biome_module_graph::{JsExport, JsOwnExport, ModuleGraph, ModuleInfo, ModuleResolver}; use biome_resolver::ResolvedPath; use biome_rowan::AstNode; use biome_test_utils::{dump_registered_module_types, dump_registered_types}; @@ -88,32 +88,37 @@ impl<'a> ModuleGraphSnapshot<'a> { if let Some(data) = dependency_data.get(file_name.as_path()) { content.push_str("\n\n## Module Info\n\n"); - content.push_str("```\n"); - content.push_str(&data.to_string()); - content.push_str("\n```\n\n"); + match data { + ModuleInfo::Js(data) => { + content.push_str("```\n"); + content.push_str(&data.to_string()); + content.push_str("\n```\n\n"); - let exported_binding_ids: BTreeSet<_> = data - .exports - .values() - .filter_map(JsExport::as_own_export) - .filter_map(|export| match export { - JsOwnExport::Binding(binding_id) => Some(*binding_id), - JsOwnExport::Type(_) => None, - }) - .collect(); - if !exported_binding_ids.is_empty() { - content.push_str("## Exported Bindings\n\n"); - content.push_str("```"); - for binding_id in exported_binding_ids { - content.push_str(&format!( - "\n{binding_id:?} => {}\n", - data.binding(binding_id) - )); + let exported_binding_ids: BTreeSet<_> = data + .exports + .values() + .filter_map(JsExport::as_own_export) + .filter_map(|export| match export { + JsOwnExport::Binding(binding_id) => Some(*binding_id), + JsOwnExport::Type(_) => None, + }) + .collect(); + if !exported_binding_ids.is_empty() { + content.push_str("## Exported Bindings\n\n"); + content.push_str("```"); + for binding_id in exported_binding_ids { + content.push_str(&format!( + "\n{binding_id:?} => {}\n", + data.binding(binding_id) + )); + } + content.push_str("```\n\n"); + } + + dump_registered_module_types(&mut content, &data.types()); } - content.push_str("```\n\n"); + ModuleInfo::Css(_) => {} } - - dump_registered_module_types(&mut content, &data.types()); } } diff --git a/crates/biome_module_graph/tests/spec_tests.rs b/crates/biome_module_graph/tests/spec_tests.rs index dd21d11ff186..362fbcd4409e 100644 --- a/crates/biome_module_graph/tests/spec_tests.rs +++ b/crates/biome_module_graph/tests/spec_tests.rs @@ -14,13 +14,13 @@ use biome_jsdoc_comment::JsdocComment; use biome_json_parser::{JsonParserOptions, parse_json}; use biome_json_value::{JsonObject, JsonString}; use biome_module_graph::{ - ImportSymbol, JsExport, JsImport, JsImportPath, JsImportPhase, JsReexport, ModuleGraph, - ModuleResolver, ResolvedPath, + CssImport, ImportSymbol, JsExport, JsImport, JsImportPath, JsImportPhase, JsReexport, + ModuleGraph, ModuleResolver, ResolvedPath, }; use biome_package::{Dependencies, PackageJson}; use biome_project_layout::ProjectLayout; use biome_rowan::Text; -use biome_test_utils::get_added_paths; +use biome_test_utils::{get_added_paths, get_css_added_paths}; use camino::{Utf8Path, Utf8PathBuf}; use walkdir::WalkDir; @@ -130,10 +130,11 @@ fn test_resolve_relative_import() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = ModuleGraph::default(); - module_graph.update_graph_for_js_paths(&fs, &project_layout, &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &project_layout, &added_paths); let imports = module_graph.data(); let file_imports = imports.get(Utf8Path::new("/src/index.ts")).unwrap(); + let file_imports = file_imports.as_js_module_info().unwrap(); assert_eq!(file_imports.static_imports.len(), 3); assert_eq!( @@ -156,10 +157,11 @@ fn test_resolve_package_import() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = ModuleGraph::default(); - module_graph.update_graph_for_js_paths(&fs, &project_layout, &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &project_layout, &added_paths); let imports = module_graph.data(); let file_imports = imports.get(Utf8Path::new("/src/index.ts")).unwrap(); + let file_imports = file_imports.as_js_module_info().unwrap(); assert_eq!(file_imports.static_imports.len(), 3); assert_eq!( @@ -182,10 +184,11 @@ fn test_import_through_path_alias() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = ModuleGraph::default(); - module_graph.update_graph_for_js_paths(&fs, &project_layout, &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &project_layout, &added_paths); let imports = module_graph.data(); let file_imports = imports.get(Utf8Path::new("/src/index.ts")).unwrap(); + let file_imports = file_imports.as_js_module_info().unwrap(); assert_eq!(file_imports.static_imports.len(), 3); assert_eq!( @@ -257,7 +260,7 @@ fn test_resolve_package_import_in_monorepo_fixtures() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = ModuleGraph::default(); - module_graph.update_graph_for_js_paths(&fs, &project_layout, &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &project_layout, &added_paths); let imports = module_graph.data(); let file_imports = imports @@ -265,6 +268,7 @@ fn test_resolve_package_import_in_monorepo_fixtures() { "{fixtures_path}/frontend/src/index.ts" ))) .unwrap(); + let file_imports = file_imports.as_js_module_info().unwrap(); assert_eq!(file_imports.static_imports.len(), 3); assert_eq!( @@ -304,7 +308,7 @@ fn test_export_referenced_function() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = ModuleGraph::default(); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let snapshot = ModuleGraphSnapshot::new(&module_graph, &fs); @@ -329,7 +333,7 @@ fn test_export_default_function_declaration() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = ModuleGraph::default(); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let snapshot = ModuleGraphSnapshot::new(&module_graph, &fs); snapshot.assert_snapshot("test_export_default_function_declaration"); @@ -357,7 +361,7 @@ fn test_export_const_type_declaration_with_namespace() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = ModuleGraph::default(); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let snapshot = ModuleGraphSnapshot::new(&module_graph, &fs); snapshot.assert_snapshot("test_export_const_type_declaration_with_namespace"); @@ -444,13 +448,11 @@ fn test_resolve_exports() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = ModuleGraph::default(); - module_graph.update_graph_for_js_paths(&fs, &project_layout, &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &project_layout, &added_paths); let dependency_data = module_graph.data(); - let data = dependency_data - .get(Utf8Path::new("/src/index.ts")) - .unwrap() - .clone(); + let data = dependency_data.get(Utf8Path::new("/src/index.ts")).unwrap(); + let data = data.as_js_module_info().unwrap(); let mut exports = data.exports.clone(); // Remove this entry, or the Windows tests fail on the path in the snapshot below: @@ -494,6 +496,7 @@ fn test_resolve_exports() { let data = dependency_data .get(Utf8Path::new("/src/reexports.ts")) .unwrap(); + let data = data.as_js_module_info().unwrap(); assert_eq!(data.exports.len(), 1); assert_eq!( data.exports.get(&Text::new_static("renamed")), @@ -555,7 +558,7 @@ fn test_resolve_export_types() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = ModuleGraph::default(); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let snapshot = ModuleGraphSnapshot::new(&module_graph, &fs); snapshot.assert_snapshot("test_resolve_export_types"); @@ -583,10 +586,10 @@ export const promise = makePromiseCb(); let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -616,10 +619,10 @@ fn test_resolve_generic_mapped_value() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -678,10 +681,10 @@ fn test_resolve_generic_return_value_with_multiple_modules() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -725,10 +728,10 @@ fn test_resolve_import_as_namespace() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -769,10 +772,10 @@ fn test_resolve_nested_function_call_with_namespace_in_return_type() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = ModuleResolver::for_module(index_module, module_graph.clone()); @@ -802,10 +805,10 @@ fn test_resolve_return_value_of_function() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -860,10 +863,10 @@ fn test_resolve_type_of_property_with_getter() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -953,10 +956,10 @@ fn class_this_test_helper(case_name: &str, prefix: &str) { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -1028,10 +1031,10 @@ fn test_resolve_type_of_this_in_object() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -1120,10 +1123,10 @@ fn test_resolve_type_of_this_in_class_wrong_scope() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -1161,7 +1164,7 @@ fn test_resolve_promise_export() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = ModuleGraph::default(); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let snapshot = ModuleGraphSnapshot::new(&module_graph, &fs); snapshot.assert_snapshot("test_resolve_promise_export"); @@ -1191,7 +1194,7 @@ export { A, B }; let added_paths = get_added_paths(&fs, &added_paths); let module_graph = ModuleGraph::default(); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let snapshot = ModuleGraphSnapshot::new(&module_graph, &fs); snapshot.assert_snapshot("test_resolve_merged_types"); @@ -1214,7 +1217,7 @@ export type Foo = Foo.Bar; let added_paths = get_added_paths(&fs, &added_paths); let module_graph = ModuleGraph::default(); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let snapshot = ModuleGraphSnapshot::new(&module_graph, &fs); @@ -1283,7 +1286,7 @@ export const codes: { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = ModuleGraph::default(); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let snapshot = ModuleGraphSnapshot::new(&module_graph, &fs); snapshot.assert_snapshot("test_resolve_recursive_looking_country_info"); @@ -1462,7 +1465,7 @@ export = vfile let added_paths = get_added_paths(&fs, &added_paths); let module_graph = ModuleGraph::default(); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let snapshot = ModuleGraphSnapshot::new(&module_graph, &fs); snapshot.assert_snapshot("test_resolve_recursive_looking_vfile"); @@ -1503,10 +1506,10 @@ fn test_resolve_react_types() { .insert_serialized_tsconfig("/".into(), &tsconfig_json.syntax().as_send().unwrap()); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &project_layout, &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &project_layout, &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -1546,13 +1549,13 @@ fn test_resolve_redis_commander_types() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); // We previously had an issue with `RedisCommander.d.ts` that caused types // to be duplicated. We should look out in this snapshot that method // signatures are registered only once per signature. let redis_commander_module = module_graph - .module_info_for_path(Utf8Path::new("/RedisCommander.d.ts")) + .js_module_info_for_path(Utf8Path::new("/RedisCommander.d.ts")) .expect("module must exist"); let num_registered_signatures = redis_commander_module .types() @@ -1604,10 +1607,10 @@ fn test_resolve_single_reexport() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -1668,10 +1671,10 @@ fn test_resolve_type_of_union_from_imported_module() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -1734,10 +1737,10 @@ fn test_resolve_multiple_reexports() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -1786,7 +1789,7 @@ fn test_resolve_export_type_referencing_imported_type() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = ModuleGraph::default(); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let snapshot = ModuleGraphSnapshot::new(&module_graph, &fs); snapshot.assert_snapshot("test_resolve_export_type_referencing_imported_type"); @@ -1826,10 +1829,10 @@ fn test_resolve_promise_from_imported_function_returning_imported_promise_type() let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -1890,10 +1893,10 @@ fn test_resolve_promise_from_imported_function_returning_reexported_promise_type let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -1945,10 +1948,10 @@ const { mutate } = useSWRConfig(); let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -2006,10 +2009,10 @@ type Intersection = Foo & Bar;"#, let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("/src/index.ts")) + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -2077,10 +2080,10 @@ fn test_resolve_swr_types() { let added_paths = get_added_paths(&fs, &added_paths); let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &project_layout, &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &project_layout, &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new(&format!( + .js_module_info_for_path(Utf8Path::new(&format!( "{fixtures_path}/frontend/src/index.ts" ))) .expect("module must exist"); @@ -2093,7 +2096,7 @@ fn test_resolve_swr_types() { ); let swr_index_module = module_graph - .module_info_for_path(Utf8Path::new(&format!("{swr_path}/dist/index/index.d.mts"))) + .js_module_info_for_path(Utf8Path::new(&format!("{swr_path}/dist/index/index.d.mts"))) .expect("module must exist"); assert_eq!( swr_index_module @@ -2147,10 +2150,10 @@ function f() { let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("index.ts")) + .js_module_info_for_path(Utf8Path::new("index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -2184,10 +2187,10 @@ function g() { let module_graph = Arc::new(ModuleGraph::default()); - module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths); let index_module = module_graph - .module_info_for_path(Utf8Path::new("index.ts")) + .js_module_info_for_path(Utf8Path::new("index.ts")) .expect("module must exist"); let resolver = Arc::new(ModuleResolver::for_module( index_module, @@ -2199,6 +2202,87 @@ function g() { snapshot.assert_snapshot("test_widening_via_assignment_multiple_values"); } +#[test] +fn resolves_css_imports_correctly() { + let fixtures_path = get_fixtures_path(); + + let fs = OsFileSystem::new(fixtures_path.clone()); + + let project_layout = ProjectLayout::default(); + project_layout.insert_node_manifest(format!("{fixtures_path}/css").into(), { + let path = Utf8PathBuf::from(format!("{fixtures_path}/css/package.json")); + deserialize_from_json_str::( + &fs.read_file_from_path(&path) + .expect("package.json must be readable"), + JsonParserOptions::default(), + "package.json", + ) + .into_deserialized() + .expect("package.json must parse") + }); + + // project_layout.insert_node_manifest(format!("{fixtures_path}/shared").into(), { + // let path = Utf8PathBuf::from(format!("{fixtures_path}/shared/package.json")); + // deserialize_from_json_str::( + // &fs.read_file_from_path(&path) + // .expect("package.json must be readable"), + // JsonParserOptions::default(), + // "package.json", + // ) + // .into_deserialized() + // .expect("package.json must parse") + // }); + + // project_layout.insert_node_manifest( + // format!("{fixtures_path}/frontend/node_modules/shared").into(), + // { + // let path = Utf8PathBuf::from(format!( + // "{fixtures_path}/frontend/node_modules/shared/package.json" + // )); + // deserialize_from_json_str::( + // &fs.read_file_from_path(&path) + // .expect("package.json must be readable"), + // JsonParserOptions::default(), + // "package.json", + // ) + // .into_deserialized() + // .expect("package.json must parse") + // }, + // ); + + let added_paths = [ + BiomePath::new(format!("{fixtures_path}/css/index.css")), + BiomePath::new(format!("{fixtures_path}/css/foo.css")), + BiomePath::new(format!("{fixtures_path}/css/bar.css")), + ]; + let added_paths = get_css_added_paths(&fs, &added_paths); + + let module_graph = ModuleGraph::default(); + module_graph.update_graph_for_css_paths(&fs, &project_layout, &added_paths, None); + + let imports = module_graph.data(); + let file_imports = imports + .get(Utf8Path::new(&format!("{fixtures_path}/css/index.css"))) + .unwrap(); + let file_imports = file_imports.as_css_module_info().unwrap(); + + assert_eq!(file_imports.imports.len(), 2); + assert_eq!( + file_imports.imports.get("foo.css"), + Some(&CssImport { + specifier: "foo.css".into(), + resolved_path: ResolvedPath::from_path(format!("{fixtures_path}/css/foo.css")), + }) + ); + assert_eq!( + file_imports.imports.get("./bar.css"), + Some(&CssImport { + specifier: "./bar.css".into(), + resolved_path: ResolvedPath::from_path(format!("{fixtures_path}/css/bar.css")), + }) + ); +} + fn find_files_recursively_in_directory( directory: &Utf8Path, predicate: impl Fn(&Utf8Path) -> bool, diff --git a/crates/biome_rowan/src/syntax/node.rs b/crates/biome_rowan/src/syntax/node.rs index 56527166bc44..8a895484df1c 100644 --- a/crates/biome_rowan/src/syntax/node.rs +++ b/crates/biome_rowan/src/syntax/node.rs @@ -856,6 +856,18 @@ impl SendNode { } } + /// Downcast this handle back into a [SyntaxNode] + /// + /// Returns `None` if the specified language `L` is not the one this node + /// was created with + pub fn into_language_root(self) -> Option + where + N: AstNode, + N::Language: 'static, + { + self.clone().into_node().map(|node| N::unwrap_cast(node)) + } + /// Downcasts this node to a language-specific root node. /// /// ## Panics diff --git a/crates/biome_ruledoc_utils/src/lib.rs b/crates/biome_ruledoc_utils/src/lib.rs index b27652659a97..0f11eb909112 100644 --- a/crates/biome_ruledoc_utils/src/lib.rs +++ b/crates/biome_ruledoc_utils/src/lib.rs @@ -81,7 +81,7 @@ impl AnalyzerServicesBuilder { let module_graph = ModuleGraph::default(); let added_paths = get_added_paths(&fs, &added_paths); - module_graph.update_graph_for_js_paths(&fs, &layout, &added_paths, &[]); + module_graph.update_graph_for_js_paths(&fs, &layout, &added_paths); Self { module_graph: Arc::new(module_graph), diff --git a/crates/biome_service/src/file_handlers/astro.rs b/crates/biome_service/src/file_handlers/astro.rs index 3004b5071a82..19b1acb7ed30 100644 --- a/crates/biome_service/src/file_handlers/astro.rs +++ b/crates/biome_service/src/file_handlers/astro.rs @@ -5,7 +5,7 @@ use crate::file_handlers::{ ParserCapabilities, javascript, }; use crate::settings::Settings; -use crate::workspace::{DocumentFileSource, DocumentServices, FixFileResult, PullActionsResult}; +use crate::workspace::{DocumentFileSource, FixFileResult, PullActionsResult}; use biome_formatter::Printed; use biome_fs::BiomePath; use biome_js_parser::{JsParserOptions, parse_js_with_cache}; @@ -127,7 +127,6 @@ fn parse( ParseResult { any_parse: parse.into(), language: Some(JsFileSource::astro().into()), - services: DocumentServices::none(), } } diff --git a/crates/biome_service/src/file_handlers/css.rs b/crates/biome_service/src/file_handlers/css.rs index 6987f8d16a21..27ed65783561 100644 --- a/crates/biome_service/src/file_handlers/css.rs +++ b/crates/biome_service/src/file_handlers/css.rs @@ -13,8 +13,7 @@ use crate::settings::{ Settings, check_feature_activity, check_override_feature_activity, }; use crate::workspace::{ - CodeAction, DocumentFileSource, DocumentServices, FixFileResult, GetSyntaxTreeResult, - PullActionsResult, + CodeAction, DocumentFileSource, FixFileResult, GetSyntaxTreeResult, PullActionsResult, }; use biome_analyze::options::PreferredQuote; use biome_analyze::{AnalysisFilter, AnalyzerConfiguration, AnalyzerOptions, ControlFlow, Never}; @@ -433,11 +432,9 @@ fn parse( .apply_override_css_parser_options(biome_path, &mut options); let parse = biome_css_parser::parse_css_with_cache(text, cache, options); - let services = DocumentServices::new_css(&parse.tree()); ParseResult { any_parse: parse.into(), language: None, - services, } } diff --git a/crates/biome_service/src/file_handlers/graphql.rs b/crates/biome_service/src/file_handlers/graphql.rs index e67b53f675c5..ed0507ab4bbd 100644 --- a/crates/biome_service/src/file_handlers/graphql.rs +++ b/crates/biome_service/src/file_handlers/graphql.rs @@ -13,9 +13,7 @@ use crate::settings::{ FormatSettings, LanguageListSettings, LanguageSettings, OverrideSettings, ServiceLanguage, Settings, check_feature_activity, check_override_feature_activity, }; -use crate::workspace::{ - CodeAction, DocumentServices, FixFileResult, GetSyntaxTreeResult, PullActionsResult, -}; +use crate::workspace::{CodeAction, FixFileResult, GetSyntaxTreeResult, PullActionsResult}; use biome_analyze::{AnalysisFilter, AnalyzerConfiguration, AnalyzerOptions, ControlFlow, Never}; use biome_configuration::graphql::{ GraphqlAssistConfiguration, GraphqlAssistEnabled, GraphqlFormatterConfiguration, @@ -334,7 +332,6 @@ fn parse( ParseResult { any_parse: parse.into(), language: Some(file_source), - services: DocumentServices::none(), } } diff --git a/crates/biome_service/src/file_handlers/grit.rs b/crates/biome_service/src/file_handlers/grit.rs index 5e521b20871e..204b5a47b0c9 100644 --- a/crates/biome_service/src/file_handlers/grit.rs +++ b/crates/biome_service/src/file_handlers/grit.rs @@ -4,7 +4,7 @@ use super::{ ParserCapabilities, SearchCapabilities, }; use crate::settings::{OverrideSettings, check_feature_activity, check_override_feature_activity}; -use crate::workspace::{DocumentServices, FixFileResult, GetSyntaxTreeResult}; +use crate::workspace::{FixFileResult, GetSyntaxTreeResult}; use crate::{ WorkspaceError, settings::{ServiceLanguage, Settings}, @@ -305,7 +305,6 @@ fn parse( ParseResult { any_parse: parse.into(), language: Some(file_source), - services: DocumentServices::none(), } } diff --git a/crates/biome_service/src/file_handlers/html.rs b/crates/biome_service/src/file_handlers/html.rs index 81310951485f..f0ea194ad7e8 100644 --- a/crates/biome_service/src/file_handlers/html.rs +++ b/crates/biome_service/src/file_handlers/html.rs @@ -385,7 +385,6 @@ fn parse( ParseResult { any_parse: parse.into(), language: Some(file_source), - services: DocumentServices::none(), } } diff --git a/crates/biome_service/src/file_handlers/javascript.rs b/crates/biome_service/src/file_handlers/javascript.rs index e360ea540af6..b9986c522d32 100644 --- a/crates/biome_service/src/file_handlers/javascript.rs +++ b/crates/biome_service/src/file_handlers/javascript.rs @@ -10,7 +10,7 @@ use crate::file_handlers::FixAllParams; use crate::settings::{ OverrideSettings, Settings, check_feature_activity, check_override_feature_activity, }; -use crate::workspace::{DocumentFileSource, DocumentServices, PullDiagnosticsAndActionsResult}; +use crate::workspace::{DocumentFileSource, PullDiagnosticsAndActionsResult}; use crate::{ WorkspaceError, settings::{FormatSettings, LanguageListSettings, LanguageSettings, ServiceLanguage}, @@ -547,7 +547,6 @@ fn parse( ParseResult { any_parse: parse.into(), language: None, - services: DocumentServices::none(), } } @@ -625,7 +624,7 @@ fn debug_type_info( graph: Arc, ) -> Result { let Some(parse) = parse else { - let result = graph.module_info_for_path(path); + let result = graph.js_module_info_for_path(path); return match result { None => Ok(String::new()), // TODO: print correct type info diff --git a/crates/biome_service/src/file_handlers/json.rs b/crates/biome_service/src/file_handlers/json.rs index c9aac5765283..68cb40adc28f 100644 --- a/crates/biome_service/src/file_handlers/json.rs +++ b/crates/biome_service/src/file_handlers/json.rs @@ -12,9 +12,7 @@ use crate::settings::{ FormatSettings, LanguageListSettings, LanguageSettings, OverrideSettings, ServiceLanguage, Settings, check_feature_activity, check_override_feature_activity, }; -use crate::workspace::{ - CodeAction, DocumentServices, FixFileResult, GetSyntaxTreeResult, PullActionsResult, -}; +use crate::workspace::{CodeAction, FixFileResult, GetSyntaxTreeResult, PullActionsResult}; use crate::{WorkspaceError, extension_error}; use biome_analyze::options::PreferredQuote; use biome_analyze::{AnalysisFilter, AnalyzerConfiguration, AnalyzerOptions, ControlFlow, Never}; @@ -395,7 +393,6 @@ fn parse( ParseResult { any_parse: parse.into(), language: Some(file_source), - services: DocumentServices::none(), } } diff --git a/crates/biome_service/src/file_handlers/mod.rs b/crates/biome_service/src/file_handlers/mod.rs index f76836b20198..475fa85bc28f 100644 --- a/crates/biome_service/src/file_handlers/mod.rs +++ b/crates/biome_service/src/file_handlers/mod.rs @@ -489,7 +489,6 @@ pub struct Capabilities { pub struct ParseResult { pub(crate) any_parse: AnyParse, pub(crate) language: Option, - pub(crate) services: DocumentServices, } pub struct ParseEmbedResult { diff --git a/crates/biome_service/src/file_handlers/svelte.rs b/crates/biome_service/src/file_handlers/svelte.rs index 5ed886c75f91..0c02eedf029b 100644 --- a/crates/biome_service/src/file_handlers/svelte.rs +++ b/crates/biome_service/src/file_handlers/svelte.rs @@ -6,7 +6,7 @@ use crate::file_handlers::{ ParserCapabilities, javascript, }; use crate::settings::Settings; -use crate::workspace::{DocumentFileSource, DocumentServices, FixFileResult, PullActionsResult}; +use crate::workspace::{DocumentFileSource, FixFileResult, PullActionsResult}; use biome_formatter::Printed; use biome_fs::BiomePath; use biome_html_syntax::HtmlLanguage; @@ -138,7 +138,6 @@ fn parse( ParseResult { any_parse: parse.into(), language: Some(file_source.into()), - services: DocumentServices::none(), } } diff --git a/crates/biome_service/src/file_handlers/vue.rs b/crates/biome_service/src/file_handlers/vue.rs index 5ca0cb8257f3..3658d4311534 100644 --- a/crates/biome_service/src/file_handlers/vue.rs +++ b/crates/biome_service/src/file_handlers/vue.rs @@ -6,7 +6,7 @@ use crate::file_handlers::{ ParserCapabilities, javascript, }; use crate::settings::Settings; -use crate::workspace::{DocumentFileSource, DocumentServices, FixFileResult, PullActionsResult}; +use crate::workspace::{DocumentFileSource, FixFileResult, PullActionsResult}; use biome_formatter::Printed; use biome_fs::BiomePath; use biome_html_syntax::HtmlLanguage; @@ -138,7 +138,6 @@ fn parse( ParseResult { any_parse: parse.into(), language: Some(file_source.into()), - services: DocumentServices::none(), } } diff --git a/crates/biome_service/src/scanner/workspace_watcher_bridge.tests.rs b/crates/biome_service/src/scanner/workspace_watcher_bridge.tests.rs index 8b75dcb8536c..24851167f490 100644 --- a/crates/biome_service/src/scanner/workspace_watcher_bridge.tests.rs +++ b/crates/biome_service/src/scanner/workspace_watcher_bridge.tests.rs @@ -76,7 +76,11 @@ fn close_modified_file_from_client_before_watcher() { module_graph .data .get(file_path.as_str()) - .map(|module_info| module_info.static_import_paths.clone()), + .map(|module_info| module_info + .as_js_module_info() + .unwrap() + .static_import_paths + .clone()), Some(BTreeMap::from([("fooo".to_string(), "fooo".to_string())])), "index should've updated to the client state" ); @@ -108,7 +112,11 @@ fn close_modified_file_from_client_before_watcher() { module_graph .data .get(file_path.as_str()) - .map(|module_info| module_info.static_import_paths.clone()), + .map(|module_info| module_info + .as_js_module_info() + .unwrap() + .static_import_paths + .clone()), Some(BTreeMap::from([("foo".to_string(), "foo".to_string())])), "index should've reverted to the filesystem state" ); diff --git a/crates/biome_service/src/snapshots/biome_service__workspace__tests__debug_module_graph.snap b/crates/biome_service/src/snapshots/biome_service__workspace__tests__debug_module_graph.snap index 75fa533ab1e6..5b6ca7df119b 100644 --- a/crates/biome_service/src/snapshots/biome_service__workspace__tests__debug_module_graph.snap +++ b/crates/biome_service/src/snapshots/biome_service__workspace__tests__debug_module_graph.snap @@ -4,35 +4,41 @@ expression: result --- GetModuleGraphResult { data: { - "/project/utils.js": SerializedJsModuleInfo { - static_imports: {}, - static_import_paths: {}, - dynamic_imports: {}, - exports: { - "debounce", - "filter", + "/project/utils.js": Js( + SerializedJsModuleInfo { + static_imports: {}, + static_import_paths: {}, + dynamic_imports: {}, + exports: { + "debounce", + "filter", + }, }, - }, - "/project/dynamic.js": SerializedJsModuleInfo { - static_imports: {}, - static_import_paths: {}, - dynamic_imports: {}, - exports: { - "squash", + ), + "/project/dynamic.js": Js( + SerializedJsModuleInfo { + static_imports: {}, + static_import_paths: {}, + dynamic_imports: {}, + exports: { + "squash", + }, }, - }, - "/project/file.js": SerializedJsModuleInfo { - static_imports: { - "debounce": "./utils.js", - "filter": "./utils.js", + ), + "/project/file.js": Js( + SerializedJsModuleInfo { + static_imports: { + "debounce": "./utils.js", + "filter": "./utils.js", + }, + static_import_paths: { + "./utils.js": "./utils.js", + }, + dynamic_imports: { + "./dynamic.js", + }, + exports: {}, }, - static_import_paths: { - "./utils.js": "./utils.js", - }, - dynamic_imports: { - "./dynamic.js", - }, - exports: {}, - }, + ), }, } diff --git a/crates/biome_service/src/workspace.rs b/crates/biome_service/src/workspace.rs index 942ff9957247..15ad16786457 100644 --- a/crates/biome_service/src/workspace.rs +++ b/crates/biome_service/src/workspace.rs @@ -63,7 +63,7 @@ use biome_formatter::Printed; use biome_fs::BiomePath; use biome_grit_patterns::GritTargetLanguage; use biome_js_syntax::{TextRange, TextSize}; -use biome_module_graph::SerializedJsModuleInfo; +use biome_module_graph::SerializedModuleInfo; use biome_resolver::FsWithResolverProxy; use biome_text_edit::TextEdit; use camino::Utf8Path; @@ -1350,7 +1350,7 @@ impl From for FileExitsParams { #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] pub struct GetModuleGraphResult { - pub data: FxHashMap, + pub data: FxHashMap, } pub trait Workspace: Send + Sync + RefUnwindSafe { diff --git a/crates/biome_service/src/workspace.tests.rs b/crates/biome_service/src/workspace.tests.rs index 814af46f24ef..13078106ecff 100644 --- a/crates/biome_service/src/workspace.tests.rs +++ b/crates/biome_service/src/workspace.tests.rs @@ -345,7 +345,11 @@ fn files_loaded_by_the_scanner_are_only_unloaded_when_the_project_is_unregistere module_graph .data .get("/project/a.ts") - .map(|module_info| module_info.static_import_paths.clone()), + .map(|module_info| module_info + .as_js_module_info() + .unwrap() + .static_import_paths + .clone()), Some(BTreeMap::from([( "./b.ts".to_string(), "/project/b.ts".replace('/', std::path::MAIN_SEPARATOR_STR), diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs index ad97a987ae8b..106501980fcf 100644 --- a/crates/biome_service/src/workspace/server.rs +++ b/crates/biome_service/src/workspace/server.rs @@ -21,7 +21,7 @@ use biome_configuration::bool::Bool; use biome_configuration::max_size::MaxSize; use biome_configuration::vcs::VcsClientKind; use biome_configuration::{BiomeDiagnostic, Configuration, ConfigurationPathHint}; -use biome_css_syntax::CssVariant; +use biome_css_syntax::{CssRoot, CssVariant}; use biome_deserialize::json::deserialize_from_json_str; use biome_deserialize::{Deserialized, Merge}; use biome_diagnostics::print_diagnostic_to_string; @@ -41,7 +41,7 @@ use biome_plugin_loader::{BiomePlugin, PluginCache, PluginDiagnostic}; use biome_plugin_loader::{PluginConfiguration, Plugins}; use biome_project_layout::ProjectLayout; use biome_resolver::FsWithResolverProxy; -use biome_rowan::{AstNode, NodeCache, SendNode}; +use biome_rowan::{NodeCache, SendNode}; use camino::{Utf8Path, Utf8PathBuf}; use crossbeam::channel::Sender; use papaya::HashMap; @@ -507,8 +507,8 @@ impl WorkspaceServer { .and_then(Result::ok) .map(|node| node.unwrap_as_send_node()) { - let (dependencies, diagnostics) = - self.update_service_data(&path, UpdateKind::AddedOrChanged(reason, root))?; + let (dependencies, diagnostics) = self + .update_service_data(&path, UpdateKind::AddedOrChanged(reason, root, services))?; Ok(InternalOpenFileResult { dependencies, @@ -541,6 +541,28 @@ impl WorkspaceServer { } } + fn get_parse_and_services( + &self, + path: &Utf8Path, + ) -> Result<(AnyParse, DocumentServices), WorkspaceError> { + let Document { + syntax, services, .. + } = self + .documents + .pin() + .get(path) + .cloned() + .ok_or_else(WorkspaceError::not_found)?; + + match syntax.transpose() { + Ok(syntax) => match syntax { + None => Err(WorkspaceError::not_found()), + Some(syntax) => Ok((syntax.clone(), services.clone())), + }, + Err(FileTooLarge { .. }) => Err(WorkspaceError::file_ignored(path.to_string())), + } + } + fn get_parse_with_snippets_and_services( &self, path: &Utf8Path, @@ -833,7 +855,7 @@ impl WorkspaceServer { .ok_or_else(WorkspaceError::not_found)?; match update_kind { - UpdateKind::AddedOrChanged(_, root) => { + UpdateKind::AddedOrChanged(_, root, _) => { self.project_layout .insert_serialized_node_manifest(package_path, root); } @@ -848,7 +870,7 @@ impl WorkspaceServer { .ok_or_else(WorkspaceError::not_found)?; match update_kind { - UpdateKind::AddedOrChanged(_, root) => { + UpdateKind::AddedOrChanged(_, root, _) => { self.project_layout .insert_serialized_tsconfig(package_path, root); } @@ -873,23 +895,34 @@ impl WorkspaceServer { path: &BiomePath, update_kind: &UpdateKind, ) -> (ModuleDependencies, Vec) { - let (added_or_changed_paths, removed_paths) = match update_kind { - UpdateKind::AddedOrChanged(_, root) => { - let Some(root) = SendNode::into_node(root.clone()).and_then(AnyJsRoot::cast) else { - return Default::default(); - }; - - (&[(path, root)] as &[_], &[] as &[_]) + match update_kind { + UpdateKind::AddedOrChanged(_, root, services) => { + // NOTE: add a new else if branch to handle other language roots + if let Some(js_root) = SendNode::into_language_root::(root.clone()) { + self.module_graph.update_graph_for_js_paths( + self.fs.as_ref(), + &self.project_layout, + &[(path, js_root)], + ) + } else if let (Some(css_root), Some(services)) = ( + SendNode::into_language_root::(root.clone()), + services.as_css_services(), + ) { + self.module_graph.update_graph_for_css_paths( + self.fs.as_ref(), + &self.project_layout, + &[(path, css_root)], + services.semantic_model.as_ref(), + ) + } else { + Default::default() + } } - UpdateKind::Removed => (&[] as &[_], &[path] as &[_]), - }; - - self.module_graph.update_graph_for_js_paths( - self.fs.as_ref(), - &self.project_layout, - added_or_changed_paths, - removed_paths, - ) + UpdateKind::Removed => { + self.module_graph.update_graph_for_removed_paths(&[path]); + (ModuleDependencies::default(), vec![]) + } + } } /// Updates the state of any services relevant to the given `path`. @@ -910,7 +943,7 @@ impl WorkspaceServer { let result = self.update_module_graph_internal(&path, &update_kind); match update_kind { - UpdateKind::AddedOrChanged(OpenFileReason::Index(IndexTrigger::InitialScan), _) => { + UpdateKind::AddedOrChanged(OpenFileReason::Index(IndexTrigger::InitialScan), _, _) => { // We'll send a single signal at the end of the scan. } _ => { @@ -1435,7 +1468,7 @@ impl Workspace for WorkspaceServer { file_source_index: index, syntax: Some(Ok(parsed.any_parse)), embedded_snippets, - services, + services: services.clone(), }; if persist_node_cache { @@ -1454,7 +1487,7 @@ impl Workspace for WorkspaceServer { if self.is_indexed(&path) { let (dependencies, diagnostics) = self.update_service_data( &path, - UpdateKind::AddedOrChanged(OpenFileReason::ClientRequest, root), + UpdateKind::AddedOrChanged(OpenFileReason::ClientRequest, root, services), )?; final_diagnostics.extend( diagnostics @@ -2100,11 +2133,12 @@ impl Workspace for WorkspaceServer { } fn update_module_graph(&self, params: UpdateModuleGraphParams) -> Result<(), WorkspaceError> { - let parsed = self.get_parse(params.path.as_path())?; + let (parsed, services) = self.get_parse_and_services(params.path.as_path())?; let update_kind = match params.update_kind { super::UpdateKind::AddOrUpdate => UpdateKind::AddedOrChanged( OpenFileReason::ClientRequest, parsed.unwrap_into_send_node(), + services, ), super::UpdateKind::Remove => UpdateKind::Removed, }; @@ -2449,14 +2483,14 @@ impl OpenFileReason { /// Kind of update being performed. pub enum UpdateKind { - AddedOrChanged(OpenFileReason, SendNode), + AddedOrChanged(OpenFileReason, SendNode, DocumentServices), Removed, } impl Debug for UpdateKind { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Self::AddedOrChanged(reason, _) => { + Self::AddedOrChanged(reason, _, _) => { f.debug_tuple("AddedOrChanged").field(reason).finish() } Self::Removed => write!(f, "Removed"), diff --git a/crates/biome_test_utils/Cargo.toml b/crates/biome_test_utils/Cargo.toml index eaf359fdd37c..d471752e121d 100644 --- a/crates/biome_test_utils/Cargo.toml +++ b/crates/biome_test_utils/Cargo.toml @@ -18,6 +18,8 @@ ansi_rgb = "0.2.0" biome_analyze = { workspace = true } biome_configuration = { workspace = true } biome_console = { workspace = true } +biome_css_parser = { workspace = true } +biome_css_syntax = { workspace = true } biome_deserialize = { workspace = true } biome_diagnostics = { workspace = true } biome_formatter = { workspace = true } diff --git a/crates/biome_test_utils/src/lib.rs b/crates/biome_test_utils/src/lib.rs index 2a784698f8cf..bed0bba0bc16 100644 --- a/crates/biome_test_utils/src/lib.rs +++ b/crates/biome_test_utils/src/lib.rs @@ -32,6 +32,8 @@ mod bench_case; pub use bench_case::BenchCase; use biome_service::WorkspaceError; use biome_service::configuration::{LoadedConfiguration, load_configuration}; +use biome_css_parser::CssParserOptions; +use biome_css_syntax::CssRoot; pub fn scripts_from_json(extension: &str, input_code: &str) -> Option> { if extension == "json" || extension == "jsonc" { @@ -202,7 +204,7 @@ pub fn module_graph_for_test_file( let fs = OsFileSystem::new(dir); let paths = get_added_paths(&fs, &paths); - module_graph.update_graph_for_js_paths(&fs, project_layout, &paths, &[]); + module_graph.update_graph_for_js_paths(&fs, project_layout, &paths); Arc::new(module_graph) } @@ -231,6 +233,28 @@ pub fn get_added_paths<'a>( .collect() } +/// Loads and parses files from the file system to pass them to service methods. +pub fn get_css_added_paths<'a>( + fs: &dyn FileSystem, + paths: &'a [BiomePath], +) -> Vec<(&'a BiomePath, CssRoot)> { + paths + .iter() + .filter_map(|path| { + let root = fs.read_file_from_path(path).ok().map(|content| { + let parsed = biome_css_parser::parse_css(&content, CssParserOptions::default()); + let diagnostics = parsed.diagnostics(); + assert!( + diagnostics.is_empty(), + "Unexpected diagnostics: {diagnostics:?}\nWhile parsing:\n{content}" + ); + parsed.tree() + })?; + Some((path, root)) + }) + .collect() +} + fn get_js_like_paths_in_dir(dir: &Utf8Path) -> Vec { std::fs::read_dir(dir) .unwrap() From 470944824e0ed5b640f8fb0e3a2535f723bc1421 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 09:11:15 +0000 Subject: [PATCH 3/3] [autofix.ci] apply automated fixes --- crates/biome_test_utils/src/lib.rs | 4 ++-- packages/@biomejs/backend-jsonrpc/src/workspace.ts | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/biome_test_utils/src/lib.rs b/crates/biome_test_utils/src/lib.rs index bed0bba0bc16..a433a43ae3c8 100644 --- a/crates/biome_test_utils/src/lib.rs +++ b/crates/biome_test_utils/src/lib.rs @@ -30,10 +30,10 @@ use similar::{DiffableStr, TextDiff}; mod bench_case; pub use bench_case::BenchCase; -use biome_service::WorkspaceError; -use biome_service::configuration::{LoadedConfiguration, load_configuration}; use biome_css_parser::CssParserOptions; use biome_css_syntax::CssRoot; +use biome_service::WorkspaceError; +use biome_service::configuration::{LoadedConfiguration, load_configuration}; pub fn scripts_from_json(extension: &str, input_code: &str) -> Option> { if extension == "json" || extension == "jsonc" { diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 555e1bf5be8c..bd5c3a500258 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -7865,8 +7865,11 @@ export interface GetSemanticModelParams { } export type GetModuleGraphParams = {}; export interface GetModuleGraphResult { - data: Record; + data: Record; } +export type SerializedModuleInfo = + | { js: SerializedJsModuleInfo } + | { css: SerializedCssModuleInfo }; export interface SerializedJsModuleInfo { /** * Dynamic imports. @@ -7900,6 +7903,14 @@ Maps from the local imported name to the absolute path it resolves to. */ staticImports: Record; } +export interface SerializedCssModuleInfo { + /** + * Map of all static imports found in the module. + +Maps from the local imported name to the absolute path it resolves to. + */ + imports: string[]; +} export interface PullDiagnosticsParams { categories: RuleCategories; /**