From 7dcf53b195a97eb0fe034a31cbb89489a77d4e25 Mon Sep 17 00:00:00 2001 From: "Arend van Beelen jr." Date: Mon, 24 Feb 2025 09:50:25 +0100 Subject: [PATCH 1/2] chore: workspace cleanups --- Cargo.lock | 14 +- Cargo.toml | 1 + crates/biome_json_value/Cargo.toml | 2 +- crates/biome_package/Cargo.toml | 2 +- ..._are_loaded_and_used_during_analysis.snap} | 2 +- crates/biome_service/src/workspace.rs | 9 +- crates/biome_service/src/workspace.tests.rs | 507 ++++++++++++++++++ crates/biome_service/src/workspace/scanner.rs | 98 ++-- crates/biome_service/src/workspace/server.rs | 29 +- crates/biome_service/tests/workspace.rs | 500 ----------------- crates/biome_text_size/Cargo.toml | 2 +- 11 files changed, 582 insertions(+), 584 deletions(-) rename crates/biome_service/{tests/snapshots/workspace__test__plugins_are_loaded_and_used_during_analysis.snap => src/snapshots/biome_service__workspace__tests__plugins_are_loaded_and_used_during_analysis.snap} (94%) create mode 100644 crates/biome_service/src/workspace.tests.rs delete mode 100644 crates/biome_service/tests/workspace.rs diff --git a/Cargo.lock b/Cargo.lock index e7c4333602cd..f3e34813d746 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1454,9 +1454,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "bitvec" @@ -2280,7 +2280,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", "libgit2-sys", "log", @@ -2312,7 +2312,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "ignore", "walkdir", ] @@ -2705,7 +2705,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", ] @@ -3214,7 +3214,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "getopts", "memchr", "pulldown-cmark-escape", @@ -3570,7 +3570,7 @@ version = "0.38.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys 0.4.15", diff --git a/Cargo.toml b/Cargo.toml index 56e644a73cdf..64a6dc56a744 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -211,6 +211,7 @@ serde_ini = "0.2.0" serde_json = "1.0.139" similar = "2.7.0" smallvec = { version = "1.13.2", features = ["union", "const_new", "serde"] } +static_assertions = "1.1" syn = "1.0.109" termcolor = "1.4.1" terminal_size = "0.4.1" diff --git a/crates/biome_json_value/Cargo.toml b/crates/biome_json_value/Cargo.toml index 4744e1465fc2..532ebaf1712a 100644 --- a/crates/biome_json_value/Cargo.toml +++ b/crates/biome_json_value/Cargo.toml @@ -21,7 +21,7 @@ biome_rowan = { workspace = true } indexmap = { workspace = true } oxc_resolver = { workspace = true, optional = true } rustc-hash = { workspace = true } -static_assertions = "1.1.0" +static_assertions = { workspace = true } [features] oxc_resolver = ["dep:oxc_resolver"] diff --git a/crates/biome_package/Cargo.toml b/crates/biome_package/Cargo.toml index a37b31b25ef5..0cb1884fb194 100644 --- a/crates/biome_package/Cargo.toml +++ b/crates/biome_package/Cargo.toml @@ -30,7 +30,7 @@ node-semver = "2.1.0" oxc_resolver = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true } -static_assertions = "1.1.0" +static_assertions = { workspace = true } [dev-dependencies] insta = { workspace = true } diff --git a/crates/biome_service/tests/snapshots/workspace__test__plugins_are_loaded_and_used_during_analysis.snap b/crates/biome_service/src/snapshots/biome_service__workspace__tests__plugins_are_loaded_and_used_during_analysis.snap similarity index 94% rename from crates/biome_service/tests/snapshots/workspace__test__plugins_are_loaded_and_used_during_analysis.snap rename to crates/biome_service/src/snapshots/biome_service__workspace__tests__plugins_are_loaded_and_used_during_analysis.snap index b8700a4cd72d..36cfb461db52 100644 --- a/crates/biome_service/tests/snapshots/workspace__test__plugins_are_loaded_and_used_during_analysis.snap +++ b/crates/biome_service/src/snapshots/biome_service__workspace__tests__plugins_are_loaded_and_used_during_analysis.snap @@ -1,5 +1,5 @@ --- -source: crates/biome_service/tests/workspace.rs +source: crates/biome_service/src/workspace.tests.rs expression: result.diagnostics --- [ diff --git a/crates/biome_service/src/workspace.rs b/crates/biome_service/src/workspace.rs index a2dae533e4fd..6475c7dd0d8a 100644 --- a/crates/biome_service/src/workspace.rs +++ b/crates/biome_service/src/workspace.rs @@ -1383,9 +1383,6 @@ impl Drop for FileGuard<'_, W> { } } -#[test] -fn test_order() { - for items in FileFeaturesResult::PROTECTED_FILES.windows(2) { - assert!(items[0] < items[1], "{} < {}", items[0], items[1]); - } -} +#[cfg(test)] +#[path = "workspace.tests.rs"] +mod tests; diff --git a/crates/biome_service/src/workspace.tests.rs b/crates/biome_service/src/workspace.tests.rs new file mode 100644 index 000000000000..b7b4ec206390 --- /dev/null +++ b/crates/biome_service/src/workspace.tests.rs @@ -0,0 +1,507 @@ +use std::num::NonZeroU64; + +use biome_analyze::RuleCategories; +use biome_configuration::analyzer::{RuleGroup, RuleSelector}; +use biome_configuration::plugins::{PluginConfiguration, Plugins}; +use biome_configuration::{Configuration, FilesConfiguration}; +use biome_fs::{BiomePath, MemoryFileSystem}; +use biome_js_syntax::{JsFileSource, TextSize}; +use camino::Utf8PathBuf; +use insta::assert_debug_snapshot; + +use crate::file_handlers::DocumentFileSource; +use crate::projects::ProjectKey; +use crate::{Workspace, WorkspaceError}; + +use super::{ + server, CloseFileParams, CloseProjectParams, FileContent, FileFeaturesResult, FileGuard, + GetFileContentParams, GetSyntaxTreeParams, OpenFileParams, OpenProjectParams, + PullDiagnosticsParams, ScanProjectFolderParams, UpdateSettingsParams, +}; + +fn create_server() -> (Box, ProjectKey) { + let workspace = server(Box::new(MemoryFileSystem::default())); + let project_key = workspace + .open_project(OpenProjectParams { + path: Default::default(), + open_uninitialized: true, + }) + .unwrap(); + + (workspace, project_key) +} + +#[test] +fn debug_control_flow() { + const SOURCE: &str = "function test () { return; }"; + const GRAPH: &str = "flowchart TB + block_0[\"block_0
Return(JS_RETURN_STATEMENT 19..26)
Return\"]\n\n"; + + let (workspace, project_key) = create_server(); + let file = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + project_key, + path: BiomePath::new("file.js"), + content: FileContent::FromClient(SOURCE.into()), + version: 0, + document_file_source: Some(DocumentFileSource::from(JsFileSource::default())), + persist_node_cache: false, + }, + ) + .unwrap(); + + let cfg = file.get_control_flow_graph(TextSize::from(20)).unwrap(); + + assert_eq!(cfg, GRAPH); +} + +#[test] +fn recognize_typescript_definition_file() { + let (workspace, project_key) = create_server(); + + let file = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + project_key, + path: BiomePath::new("file.d.ts"), + // the following code snippet can be correctly parsed in .d.ts file but not in .ts file + content: FileContent::FromClient("export const foo: number".into()), + version: 0, + document_file_source: None, + persist_node_cache: false, + }, + ) + .unwrap(); + + assert!(file.format_file().is_ok()); +} + +#[test] +fn correctly_handle_json_files() { + let (workspace, project_key) = create_server(); + + // ".json" file + let json_file = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + project_key, + path: BiomePath::new("a.json"), + content: FileContent::FromClient(r#"{"a": 42}"#.into()), + version: 0, + document_file_source: None, + persist_node_cache: false, + }, + ) + .unwrap(); + assert!(json_file.format_file().is_ok()); + + // ".json" file doesn't allow comments + let json_file_with_comments = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + project_key, + path: BiomePath::new("b.json"), + content: FileContent::FromClient(r#"{"a": 42}//comment"#.into()), + version: 0, + document_file_source: None, + persist_node_cache: false, + }, + ) + .unwrap(); + assert!(json_file_with_comments.format_file().is_err()); + + // ".json" file doesn't allow trailing commas + let json_file_with_trailing_commas = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + project_key, + path: BiomePath::new("c.json"), + content: FileContent::FromClient(r#"{"a": 42,}"#.into()), + version: 0, + document_file_source: None, + persist_node_cache: false, + }, + ) + .unwrap(); + assert!(json_file_with_trailing_commas.format_file().is_err()); + + // ".jsonc" file allows comments + let jsonc_file = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + project_key, + path: BiomePath::new("d.jsonc"), + content: FileContent::FromClient(r#"{"a": 42}//comment"#.into()), + version: 0, + document_file_source: None, + persist_node_cache: false, + }, + ) + .unwrap(); + assert!(jsonc_file.format_file().is_ok()); + + // ".jsonc" file allow trailing commas + let jsonc_file = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + project_key, + path: BiomePath::new("e.jsonc"), + content: FileContent::FromClient(r#"{"a": 42,}"#.into()), + version: 0, + document_file_source: None, + persist_node_cache: false, + }, + ) + .unwrap(); + assert!(jsonc_file.format_file().is_ok()); + + // well-known json-with-comments file allows comments + let well_known_json_with_comments_file = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + project_key, + path: BiomePath::new(".eslintrc.json"), + content: FileContent::FromClient(r#"{"a": 42}//comment"#.into()), + version: 0, + document_file_source: None, + persist_node_cache: false, + }, + ) + .unwrap(); + assert!(well_known_json_with_comments_file.format_file().is_ok()); + + // well-known json-with-comments file allows comments + let well_known_json_with_comments_file = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + project_key, + path: BiomePath::new("project/.vscode/settings.json"), + content: FileContent::FromClient(r#"{"a": 42}//comment"#.into()), + version: 0, + document_file_source: None, + persist_node_cache: false, + }, + ) + .unwrap(); + assert!(well_known_json_with_comments_file.format_file().is_ok()); + + // well-known json-with-comments file doesn't allow trailing commas + let well_known_json_with_comments_file_with_trailing_commas = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + project_key, + path: BiomePath::new("dir/.eslintrc.json"), + content: FileContent::FromClient(r#"{"a": 42,}"#.into()), + version: 0, + document_file_source: None, + persist_node_cache: false, + }, + ) + .unwrap(); + assert!(well_known_json_with_comments_file_with_trailing_commas + .format_file() + .is_err()); + + // well-known json-with-comments-and-trailing-commas file allows comments and trailing commas + let well_known_json_with_comments_and_trailing_commas_file = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + project_key, + path: BiomePath::new("tsconfig.json"), + content: FileContent::FromClient(r#"{"a": 42,}//comment"#.into()), + version: 0, + document_file_source: None, + persist_node_cache: false, + }, + ) + .unwrap(); + assert!(well_known_json_with_comments_and_trailing_commas_file + .format_file() + .is_ok()); +} + +#[test] +fn correctly_parses_graphql_files() { + let (workspace, project_key) = create_server(); + + let graphql_file = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + project_key, + path: BiomePath::new("file.graphql"), + content: FileContent::FromClient( + r#"type Query { + me: User +} + +type User { + id: ID + name: String +}"# + .into(), + ), + version: 0, + document_file_source: None, + persist_node_cache: false, + }, + ) + .unwrap(); + let result = graphql_file.get_syntax_tree(); + assert!(result.is_ok()); + let syntax = result.unwrap().ast; + + assert!(syntax.starts_with("GraphqlRoot")) +} + +#[test] +fn correctly_pulls_lint_diagnostics() { + let (workspace, project_key) = create_server(); + + let graphql_file = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + project_key, + path: BiomePath::new("file.graphql"), + content: FileContent::FromClient( + r#"query { + member @deprecated(abc: 123) +}"# + .into(), + ), + version: 0, + document_file_source: None, + persist_node_cache: false, + }, + ) + .unwrap(); + let result = graphql_file.pull_diagnostics( + RuleCategories::all(), + 10, + vec![RuleSelector::Rule( + RuleGroup::Nursery.as_str(), + "useDeprecatedReason", + )], + vec![], + ); + assert!(result.is_ok()); + let diagnostics = result.unwrap().diagnostics; + assert_eq!(diagnostics.len(), 1) +} + +#[test] +fn pull_grit_debug_info() { + let (workspace, project_key) = create_server(); + + let grit_file = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + project_key, + path: BiomePath::new("file.grit"), + content: FileContent::FromClient( + r#"`function ($args) { $body }` where { + $args <: contains `x` +}"# + .into(), + ), + version: 0, + document_file_source: None, + persist_node_cache: false, + }, + ) + .unwrap(); + let result = grit_file.get_syntax_tree(); + assert!(result.is_ok()); + let syntax = result.unwrap().ast; + + assert!(syntax.starts_with("GritRoot")) +} + +#[test] +fn files_loaded_by_the_scanner_are_only_unloaded_when_the_project_is_unregistered() { + const FILE_A_CONTENT: &[u8] = b"import { bar } from './b.ts';\nfunction foo() {}"; + const FILE_B_CONTENT: &[u8] = b"import { foo } from './a.ts';\nfunction bar() {}"; + + let mut fs = MemoryFileSystem::default(); + fs.insert(Utf8PathBuf::from("/project/a.ts"), FILE_A_CONTENT); + fs.insert(Utf8PathBuf::from("/project/b.ts"), FILE_B_CONTENT); + + let workspace = server(Box::new(fs)); + let project_key = workspace + .open_project(OpenProjectParams { + path: Utf8PathBuf::from("/project").into(), + open_uninitialized: true, + }) + .unwrap(); + + workspace + .scan_project_folder(ScanProjectFolderParams { + project_key, + path: None, + }) + .unwrap(); + + macro_rules! assert_file_a_content { + () => { + assert_eq!( + workspace + .get_file_content(GetFileContentParams { + project_key, + path: BiomePath::new("/project/a.ts"), + }) + .unwrap(), + String::from_utf8(FILE_A_CONTENT.to_vec()).unwrap(), + ); + }; + } + + assert_file_a_content!(); + + workspace + .open_file(OpenFileParams { + project_key, + path: BiomePath::new("/project/a.ts"), + content: FileContent::FromServer, + version: 0, + document_file_source: None, + persist_node_cache: false, + }) + .unwrap(); + + assert_file_a_content!(); + + workspace + .close_file(CloseFileParams { + project_key, + path: BiomePath::new("/project/a.ts"), + }) + .unwrap(); + + assert_file_a_content!(); + + workspace + .close_project(CloseProjectParams { project_key }) + .unwrap(); + + assert!(workspace + .get_file_content(GetFileContentParams { + project_key, + path: BiomePath::new("/project/a.ts"), + }) + .is_err_and(|error| matches!(error, WorkspaceError::NotFound(_)))); +} + +#[test] +fn too_large_files_are_tracked_but_not_parsed() { + const FILE_CONTENT: &[u8] = b"console.log(`I'm YUUUGE!`);"; + + let mut fs = MemoryFileSystem::default(); + fs.insert(Utf8PathBuf::from("/project/a.ts"), FILE_CONTENT); + + let workspace = server(Box::new(fs)); + let project_key = workspace + .open_project(OpenProjectParams { + path: Utf8PathBuf::from("/project").into(), + open_uninitialized: true, + }) + .unwrap(); + + workspace + .update_settings(UpdateSettingsParams { + project_key, + configuration: Configuration { + files: Some(FilesConfiguration { + max_size: Some(NonZeroU64::new(10).unwrap().into()), + ..Default::default() + }), + ..Default::default() + }, + vcs_base_path: None, + gitignore_matches: Vec::new(), + workspace_directory: None, + }) + .unwrap(); + + workspace + .scan_project_folder(ScanProjectFolderParams { + project_key, + path: None, + }) + .unwrap(); + + assert!(workspace + .get_syntax_tree(GetSyntaxTreeParams { + project_key, + path: BiomePath::new("/project/a.ts"), + }) + .is_err_and(|error| matches!(error, WorkspaceError::FileIgnored(_)))); +} + +#[test] +fn plugins_are_loaded_and_used_during_analysis() { + const PLUGIN_CONTENT: &[u8] = br#" +`Object.assign($args)` where { + register_diagnostic( + span = $args, + message = "Prefer object spread instead of `Object.assign()`" + ) +} +"#; + + const FILE_CONTENT: &[u8] = b"const a = Object.assign({ foo: 'bar' });"; + + let mut fs = MemoryFileSystem::default(); + fs.insert(Utf8PathBuf::from("/project/plugin.grit"), PLUGIN_CONTENT); + fs.insert(Utf8PathBuf::from("/project/a.ts"), FILE_CONTENT); + + let workspace = server(Box::new(fs)); + let project_key = workspace + .open_project(OpenProjectParams { + path: Utf8PathBuf::from("/project").into(), + open_uninitialized: true, + }) + .unwrap(); + + workspace + .update_settings(UpdateSettingsParams { + project_key, + configuration: Configuration { + plugins: Some(Plugins(vec![PluginConfiguration::Path( + "./plugin.grit".to_string(), + )])), + ..Default::default() + }, + vcs_base_path: None, + gitignore_matches: Vec::new(), + workspace_directory: Some(BiomePath::new("/project")), + }) + .unwrap(); + + workspace + .scan_project_folder(ScanProjectFolderParams { + project_key, + path: None, + }) + .unwrap(); + + let result = workspace + .pull_diagnostics(PullDiagnosticsParams { + project_key, + path: BiomePath::new("/project/a.ts"), + categories: RuleCategories::default(), + max_diagnostics: 10, + only: Vec::new(), + skip: Vec::new(), + enabled_rules: Vec::new(), + }) + .unwrap(); + assert_debug_snapshot!(result.diagnostics); + assert_eq!(result.errors, 0); +} + +#[test] +fn test_order() { + for items in FileFeaturesResult::PROTECTED_FILES.windows(2) { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); + } +} diff --git a/crates/biome_service/src/workspace/scanner.rs b/crates/biome_service/src/workspace/scanner.rs index f6a68cdcb06d..2794dbed32ed 100644 --- a/crates/biome_service/src/workspace/scanner.rs +++ b/crates/biome_service/src/workspace/scanner.rs @@ -3,10 +3,9 @@ use biome_diagnostics::{Diagnostic as _, Error, Severity}; use biome_fs::{BiomePath, PathInterner, TraversalContext, TraversalScope}; use camino::Utf8Path; use crossbeam::channel::{unbounded, Receiver, Sender}; -use rayon::ThreadPoolBuilder; use std::collections::BTreeSet; use std::panic::catch_unwind; -use std::sync::{Once, RwLock}; +use std::sync::RwLock; use std::thread; use std::time::{Duration, Instant}; use tracing::instrument; @@ -26,61 +25,48 @@ pub(crate) struct ScanResult { pub duration: Duration, } -#[instrument(level = "debug", skip(workspace))] -pub(crate) fn scan( - workspace: &WorkspaceServer, - project_key: ProjectKey, - folder: &Utf8Path, -) -> Result { - init_thread_pool(); - - let (interner, _path_receiver) = PathInterner::new(); - let (diagnostics_sender, diagnostics_receiver) = unbounded(); - - let collector = DiagnosticsCollector::new(); - - let (duration, diagnostics) = thread::scope(|scope| { - let handler = thread::Builder::new() - .name("biome::scanner".to_string()) - .spawn_scoped(scope, || collector.run(diagnostics_receiver)) - .expect("failed to spawn scanner thread"); - - // The traversal context is scoped to ensure all the channels it - // contains are properly closed once scanning finishes. - let duration = scan_folder( - folder, - ScanContext { - workspace, - project_key, - interner, - diagnostics_sender, - evaluated_paths: Default::default(), - }, - ); - - // Wait for the collector thread to finish. - let diagnostics = handler.join().unwrap(); - - (duration, diagnostics) - }); - - Ok(ScanResult { - diagnostics, - duration, - }) -} +impl WorkspaceServer { + #[instrument(level = "debug", skip(self))] + pub(super) fn scan( + &self, + project_key: ProjectKey, + folder: &Utf8Path, + ) -> Result { + let (interner, _path_receiver) = PathInterner::new(); + let (diagnostics_sender, diagnostics_receiver) = unbounded(); + + let collector = DiagnosticsCollector::new(); + + let (duration, diagnostics) = thread::scope(|scope| { + let handler = thread::Builder::new() + .name("biome::scanner".to_string()) + .spawn_scoped(scope, || collector.run(diagnostics_receiver)) + .expect("failed to spawn scanner thread"); + + // The traversal context is scoped to ensure all the channels it + // contains are properly closed once scanning finishes. + let duration = scan_folder( + folder, + ScanContext { + workspace: self, + project_key, + interner, + diagnostics_sender, + evaluated_paths: Default::default(), + }, + ); -/// Sets up the global Rayon thread pool the first time it's called. -/// -/// This is used to assign friendly debug names to the threads of the pool. -fn init_thread_pool() { - static INIT_ONCE: Once = Once::new(); - INIT_ONCE.call_once(|| { - ThreadPoolBuilder::new() - .thread_name(|index| format!("biome::workspace_worker_{index}")) - .build_global() - .expect("failed to initialize the global thread pool"); - }); + // Wait for the collector thread to finish. + let diagnostics = handler.join().unwrap(); + + (duration, diagnostics) + }); + + Ok(ScanResult { + diagnostics, + duration, + }) + } } /// Initiates the filesystem traversal tasks from the provided path and runs it to completion. diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs index 321d2a868871..a874a2395a2e 100644 --- a/crates/biome_service/src/workspace/server.rs +++ b/crates/biome_service/src/workspace/server.rs @@ -1,4 +1,3 @@ -use super::scanner::scan; use super::{ ChangeFileParams, CheckFileSizeParams, CheckFileSizeResult, CloseFileParams, CloseProjectParams, FeatureName, FileContent, FixFileParams, FixFileResult, FormatFileParams, @@ -45,10 +44,11 @@ use biome_project_layout::ProjectLayout; use biome_rowan::NodeCache; use camino::{Utf8Path, Utf8PathBuf}; use papaya::HashMap; +use rayon::ThreadPoolBuilder; use rustc_hash::{FxBuildHasher, FxHashMap}; use std::panic::RefUnwindSafe; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, Once}; use tracing::{info, instrument, warn}; pub(super) struct WorkspaceServer { @@ -137,6 +137,8 @@ impl WorkspaceServer { /// [Default] to disallow instances of [Workspace] from being created /// outside a [crate::App] pub(crate) fn new(fs: Box) -> Self { + init_thread_pool(); + Self { features: Features::new(), projects: Default::default(), @@ -748,15 +750,7 @@ impl Workspace for WorkspaceServer { .or_else(|| self.projects.get_project_path(params.project_key)) .ok_or_else(WorkspaceError::no_project)?; - // TODO: Need to register a file watcher. This should happen before we - // start scanning, or we might miss changes that happened during - // the scan. - - // TODO: If a watcher is registered, we can also skip the scanning. - // **But** if we are using a polling backend for the watching, we - // probably want to force a poll at this moment. - - let result = scan(self, params.project_key, &path)?; + let result = self.scan(params.project_key, &path)?; Ok(ScanProjectFolderResult { diagnostics: result.diagnostics, @@ -1277,6 +1271,19 @@ impl Workspace for WorkspaceServer { } } +/// Sets up the global Rayon thread pool the first time it's called. +/// +/// This is used to assign friendly debug names to the threads of the pool. +fn init_thread_pool() { + static INIT_ONCE: Once = Once::new(); + INIT_ONCE.call_once(|| { + ThreadPoolBuilder::new() + .thread_name(|index| format!("biome::workspace_worker_{index}")) + .build_global() + .expect("failed to initialize the global thread pool"); + }); +} + /// Generates a pattern ID that we can use as "handle" for referencing /// previously parsed search queries. fn make_search_pattern_id() -> PatternId { diff --git a/crates/biome_service/tests/workspace.rs b/crates/biome_service/tests/workspace.rs deleted file mode 100644 index f456b2ef2536..000000000000 --- a/crates/biome_service/tests/workspace.rs +++ /dev/null @@ -1,500 +0,0 @@ -#[cfg(test)] -mod test { - use biome_analyze::RuleCategories; - use biome_configuration::analyzer::{RuleGroup, RuleSelector}; - use biome_configuration::plugins::{PluginConfiguration, Plugins}; - use biome_configuration::{Configuration, FilesConfiguration}; - use biome_fs::{BiomePath, MemoryFileSystem}; - use biome_js_syntax::{JsFileSource, TextSize}; - use biome_service::file_handlers::DocumentFileSource; - use biome_service::projects::ProjectKey; - use biome_service::workspace::{ - server, CloseFileParams, CloseProjectParams, FileContent, FileGuard, GetFileContentParams, - GetSyntaxTreeParams, OpenFileParams, OpenProjectParams, PullDiagnosticsParams, - ScanProjectFolderParams, UpdateSettingsParams, - }; - use biome_service::{Workspace, WorkspaceError}; - use camino::Utf8PathBuf; - use insta::assert_debug_snapshot; - use std::num::NonZeroU64; - - fn create_server() -> (Box, ProjectKey) { - let workspace = server(Box::new(MemoryFileSystem::default())); - let project_key = workspace - .open_project(OpenProjectParams { - path: Default::default(), - open_uninitialized: true, - }) - .unwrap(); - - (workspace, project_key) - } - - #[test] - fn debug_control_flow() { - const SOURCE: &str = "function test () { return; }"; - const GRAPH: &str = "flowchart TB - block_0[\"block_0
Return(JS_RETURN_STATEMENT 19..26)
Return\"]\n\n"; - - let (workspace, project_key) = create_server(); - let file = FileGuard::open( - workspace.as_ref(), - OpenFileParams { - project_key, - path: BiomePath::new("file.js"), - content: FileContent::FromClient(SOURCE.into()), - version: 0, - document_file_source: Some(DocumentFileSource::from(JsFileSource::default())), - persist_node_cache: false, - }, - ) - .unwrap(); - - let cfg = file.get_control_flow_graph(TextSize::from(20)).unwrap(); - - assert_eq!(cfg, GRAPH); - } - - #[test] - fn recognize_typescript_definition_file() { - let (workspace, project_key) = create_server(); - - let file = FileGuard::open( - workspace.as_ref(), - OpenFileParams { - project_key, - path: BiomePath::new("file.d.ts"), - // the following code snippet can be correctly parsed in .d.ts file but not in .ts file - content: FileContent::FromClient("export const foo: number".into()), - version: 0, - document_file_source: None, - persist_node_cache: false, - }, - ) - .unwrap(); - - assert!(file.format_file().is_ok()); - } - - #[test] - fn correctly_handle_json_files() { - let (workspace, project_key) = create_server(); - - // ".json" file - let json_file = FileGuard::open( - workspace.as_ref(), - OpenFileParams { - project_key, - path: BiomePath::new("a.json"), - content: FileContent::FromClient(r#"{"a": 42}"#.into()), - version: 0, - document_file_source: None, - persist_node_cache: false, - }, - ) - .unwrap(); - assert!(json_file.format_file().is_ok()); - - // ".json" file doesn't allow comments - let json_file_with_comments = FileGuard::open( - workspace.as_ref(), - OpenFileParams { - project_key, - path: BiomePath::new("b.json"), - content: FileContent::FromClient(r#"{"a": 42}//comment"#.into()), - version: 0, - document_file_source: None, - persist_node_cache: false, - }, - ) - .unwrap(); - assert!(json_file_with_comments.format_file().is_err()); - - // ".json" file doesn't allow trailing commas - let json_file_with_trailing_commas = FileGuard::open( - workspace.as_ref(), - OpenFileParams { - project_key, - path: BiomePath::new("c.json"), - content: FileContent::FromClient(r#"{"a": 42,}"#.into()), - version: 0, - document_file_source: None, - persist_node_cache: false, - }, - ) - .unwrap(); - assert!(json_file_with_trailing_commas.format_file().is_err()); - - // ".jsonc" file allows comments - let jsonc_file = FileGuard::open( - workspace.as_ref(), - OpenFileParams { - project_key, - path: BiomePath::new("d.jsonc"), - content: FileContent::FromClient(r#"{"a": 42}//comment"#.into()), - version: 0, - document_file_source: None, - persist_node_cache: false, - }, - ) - .unwrap(); - assert!(jsonc_file.format_file().is_ok()); - - // ".jsonc" file allow trailing commas - let jsonc_file = FileGuard::open( - workspace.as_ref(), - OpenFileParams { - project_key, - path: BiomePath::new("e.jsonc"), - content: FileContent::FromClient(r#"{"a": 42,}"#.into()), - version: 0, - document_file_source: None, - persist_node_cache: false, - }, - ) - .unwrap(); - assert!(jsonc_file.format_file().is_ok()); - - // well-known json-with-comments file allows comments - let well_known_json_with_comments_file = FileGuard::open( - workspace.as_ref(), - OpenFileParams { - project_key, - path: BiomePath::new(".eslintrc.json"), - content: FileContent::FromClient(r#"{"a": 42}//comment"#.into()), - version: 0, - document_file_source: None, - persist_node_cache: false, - }, - ) - .unwrap(); - assert!(well_known_json_with_comments_file.format_file().is_ok()); - - // well-known json-with-comments file allows comments - let well_known_json_with_comments_file = FileGuard::open( - workspace.as_ref(), - OpenFileParams { - project_key, - path: BiomePath::new("project/.vscode/settings.json"), - content: FileContent::FromClient(r#"{"a": 42}//comment"#.into()), - version: 0, - document_file_source: None, - persist_node_cache: false, - }, - ) - .unwrap(); - assert!(well_known_json_with_comments_file.format_file().is_ok()); - - // well-known json-with-comments file doesn't allow trailing commas - let well_known_json_with_comments_file_with_trailing_commas = FileGuard::open( - workspace.as_ref(), - OpenFileParams { - project_key, - path: BiomePath::new("dir/.eslintrc.json"), - content: FileContent::FromClient(r#"{"a": 42,}"#.into()), - version: 0, - document_file_source: None, - persist_node_cache: false, - }, - ) - .unwrap(); - assert!(well_known_json_with_comments_file_with_trailing_commas - .format_file() - .is_err()); - - // well-known json-with-comments-and-trailing-commas file allows comments and trailing commas - let well_known_json_with_comments_and_trailing_commas_file = FileGuard::open( - workspace.as_ref(), - OpenFileParams { - project_key, - path: BiomePath::new("tsconfig.json"), - content: FileContent::FromClient(r#"{"a": 42,}//comment"#.into()), - version: 0, - document_file_source: None, - persist_node_cache: false, - }, - ) - .unwrap(); - assert!(well_known_json_with_comments_and_trailing_commas_file - .format_file() - .is_ok()); - } - - #[test] - fn correctly_parses_graphql_files() { - let (workspace, project_key) = create_server(); - - let graphql_file = FileGuard::open( - workspace.as_ref(), - OpenFileParams { - project_key, - path: BiomePath::new("file.graphql"), - content: FileContent::FromClient( - r#"type Query { - me: User -} - -type User { - id: ID - name: String -}"# - .into(), - ), - version: 0, - document_file_source: None, - persist_node_cache: false, - }, - ) - .unwrap(); - let result = graphql_file.get_syntax_tree(); - assert!(result.is_ok()); - let syntax = result.unwrap().ast; - - assert!(syntax.starts_with("GraphqlRoot")) - } - - #[test] - fn correctly_pulls_lint_diagnostics() { - let (workspace, project_key) = create_server(); - - let graphql_file = FileGuard::open( - workspace.as_ref(), - OpenFileParams { - project_key, - path: BiomePath::new("file.graphql"), - content: FileContent::FromClient( - r#"query { - member @deprecated(abc: 123) -}"# - .into(), - ), - version: 0, - document_file_source: None, - persist_node_cache: false, - }, - ) - .unwrap(); - let result = graphql_file.pull_diagnostics( - RuleCategories::all(), - 10, - vec![RuleSelector::Rule( - RuleGroup::Nursery.as_str(), - "useDeprecatedReason", - )], - vec![], - ); - assert!(result.is_ok()); - let diagnostics = result.unwrap().diagnostics; - assert_eq!(diagnostics.len(), 1) - } - - #[test] - fn pull_grit_debug_info() { - let (workspace, project_key) = create_server(); - - let grit_file = FileGuard::open( - workspace.as_ref(), - OpenFileParams { - project_key, - path: BiomePath::new("file.grit"), - content: FileContent::FromClient( - r#"`function ($args) { $body }` where { - $args <: contains `x` -}"# - .into(), - ), - version: 0, - document_file_source: None, - persist_node_cache: false, - }, - ) - .unwrap(); - let result = grit_file.get_syntax_tree(); - assert!(result.is_ok()); - let syntax = result.unwrap().ast; - - assert!(syntax.starts_with("GritRoot")) - } - - #[test] - fn files_loaded_by_the_scanner_are_only_unloaded_when_the_project_is_unregistered() { - const FILE_A_CONTENT: &[u8] = b"import { bar } from './b.ts';\nfunction foo() {}"; - const FILE_B_CONTENT: &[u8] = b"import { foo } from './a.ts';\nfunction bar() {}"; - - let mut fs = MemoryFileSystem::default(); - fs.insert(Utf8PathBuf::from("/project/a.ts"), FILE_A_CONTENT); - fs.insert(Utf8PathBuf::from("/project/b.ts"), FILE_B_CONTENT); - - let workspace = server(Box::new(fs)); - let project_key = workspace - .open_project(OpenProjectParams { - path: Utf8PathBuf::from("/project").into(), - open_uninitialized: true, - }) - .unwrap(); - - workspace - .scan_project_folder(ScanProjectFolderParams { - project_key, - path: None, - }) - .unwrap(); - - macro_rules! assert_file_a_content { - () => { - assert_eq!( - workspace - .get_file_content(GetFileContentParams { - project_key, - path: BiomePath::new("/project/a.ts"), - }) - .unwrap(), - String::from_utf8(FILE_A_CONTENT.to_vec()).unwrap(), - ); - }; - } - - assert_file_a_content!(); - - workspace - .open_file(OpenFileParams { - project_key, - path: BiomePath::new("/project/a.ts"), - content: FileContent::FromServer, - version: 0, - document_file_source: None, - persist_node_cache: false, - }) - .unwrap(); - - assert_file_a_content!(); - - workspace - .close_file(CloseFileParams { - project_key, - path: BiomePath::new("/project/a.ts"), - }) - .unwrap(); - - assert_file_a_content!(); - - workspace - .close_project(CloseProjectParams { project_key }) - .unwrap(); - - assert!(workspace - .get_file_content(GetFileContentParams { - project_key, - path: BiomePath::new("/project/a.ts"), - }) - .is_err_and(|error| matches!(error, WorkspaceError::NotFound(_)))); - } - - #[test] - fn too_large_files_are_tracked_but_not_parsed() { - const FILE_CONTENT: &[u8] = b"console.log(`I'm YUUUGE!`);"; - - let mut fs = MemoryFileSystem::default(); - fs.insert(Utf8PathBuf::from("/project/a.ts"), FILE_CONTENT); - - let workspace = server(Box::new(fs)); - let project_key = workspace - .open_project(OpenProjectParams { - path: Utf8PathBuf::from("/project").into(), - open_uninitialized: true, - }) - .unwrap(); - - workspace - .update_settings(UpdateSettingsParams { - project_key, - configuration: Configuration { - files: Some(FilesConfiguration { - max_size: Some(NonZeroU64::new(10).unwrap().into()), - ..Default::default() - }), - ..Default::default() - }, - vcs_base_path: None, - gitignore_matches: Vec::new(), - workspace_directory: None, - }) - .unwrap(); - - workspace - .scan_project_folder(ScanProjectFolderParams { - project_key, - path: None, - }) - .unwrap(); - - assert!(workspace - .get_syntax_tree(GetSyntaxTreeParams { - project_key, - path: BiomePath::new("/project/a.ts"), - }) - .is_err_and(|error| matches!(error, WorkspaceError::FileIgnored(_)))); - } - - #[test] - fn plugins_are_loaded_and_used_during_analysis() { - const PLUGIN_CONTENT: &[u8] = br#" -`Object.assign($args)` where { - register_diagnostic( - span = $args, - message = "Prefer object spread instead of `Object.assign()`" - ) -} -"#; - - const FILE_CONTENT: &[u8] = b"const a = Object.assign({ foo: 'bar' });"; - - let mut fs = MemoryFileSystem::default(); - fs.insert(Utf8PathBuf::from("/project/plugin.grit"), PLUGIN_CONTENT); - fs.insert(Utf8PathBuf::from("/project/a.ts"), FILE_CONTENT); - - let workspace = server(Box::new(fs)); - let project_key = workspace - .open_project(OpenProjectParams { - path: Utf8PathBuf::from("/project").into(), - open_uninitialized: true, - }) - .unwrap(); - - workspace - .update_settings(UpdateSettingsParams { - project_key, - configuration: Configuration { - plugins: Some(Plugins(vec![PluginConfiguration::Path( - "./plugin.grit".to_string(), - )])), - ..Default::default() - }, - vcs_base_path: None, - gitignore_matches: Vec::new(), - workspace_directory: Some(BiomePath::new("/project")), - }) - .unwrap(); - - workspace - .scan_project_folder(ScanProjectFolderParams { - project_key, - path: None, - }) - .unwrap(); - - let result = workspace - .pull_diagnostics(PullDiagnosticsParams { - project_key, - path: BiomePath::new("/project/a.ts"), - categories: RuleCategories::default(), - max_diagnostics: 10, - only: Vec::new(), - skip: Vec::new(), - enabled_rules: Vec::new(), - }) - .unwrap(); - assert_debug_snapshot!(result.diagnostics); - assert_eq!(result.errors, 0); - } -} diff --git a/crates/biome_text_size/Cargo.toml b/crates/biome_text_size/Cargo.toml index e4e682ccdf55..98d9b258ff8a 100644 --- a/crates/biome_text_size/Cargo.toml +++ b/crates/biome_text_size/Cargo.toml @@ -20,7 +20,7 @@ serde = ["dep:serde"] [dev-dependencies] serde_test = "1.0.177" -static_assertions = "1.1" +static_assertions = { workspace = true } [[test]] name = "serde" From 348afda026e82fa16024f651645aefae9a0ebbef Mon Sep 17 00:00:00 2001 From: "Arend van Beelen jr." Date: Mon, 24 Feb 2025 11:33:32 +0100 Subject: [PATCH 2/2] There's no thread pool in WASM --- crates/biome_service/src/workspace/server.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs index a874a2395a2e..9faedd2d54a0 100644 --- a/crates/biome_service/src/workspace/server.rs +++ b/crates/biome_service/src/workspace/server.rs @@ -1275,13 +1275,16 @@ impl Workspace for WorkspaceServer { /// /// This is used to assign friendly debug names to the threads of the pool. fn init_thread_pool() { - static INIT_ONCE: Once = Once::new(); - INIT_ONCE.call_once(|| { - ThreadPoolBuilder::new() - .thread_name(|index| format!("biome::workspace_worker_{index}")) - .build_global() - .expect("failed to initialize the global thread pool"); - }); + #[cfg(not(target_family = "wasm"))] + { + static INIT_ONCE: Once = Once::new(); + INIT_ONCE.call_once(|| { + ThreadPoolBuilder::new() + .thread_name(|index| format!("biome::workspace_worker_{index}")) + .build_global() + .expect("failed to initialize the global thread pool"); + }); + } } /// Generates a pattern ID that we can use as "handle" for referencing