diff --git a/Cargo.lock b/Cargo.lock index 3dc7556e0e67..85b03a20c24d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,6 +132,7 @@ dependencies = [ "biome_parser", "biome_rowan", "biome_suppression", + "camino", "enumflags2", "indexmap", "rustc-hash 2.1.0", @@ -799,6 +800,7 @@ dependencies = [ "biome_js_syntax", "biome_package", "biome_plugin_loader", + "biome_project_layout", "biome_rowan", "biome_string_case", "biome_suppression", @@ -1177,6 +1179,17 @@ dependencies = [ "serde", ] +[[package]] +name = "biome_project_layout" +version = "0.0.1" +dependencies = [ + "biome_package", + "biome_parser", + "camino", + "papaya", + "rustc-hash 2.1.0", +] + [[package]] name = "biome_rowan" version = "0.5.7" @@ -1234,6 +1247,7 @@ dependencies = [ "biome_json_syntax", "biome_package", "biome_parser", + "biome_project_layout", "biome_rowan", "biome_string_case", "biome_text_edit", @@ -1291,6 +1305,7 @@ dependencies = [ "biome_formatter", "biome_json_parser", "biome_package", + "biome_project_layout", "biome_rowan", "biome_service", "camino", diff --git a/Cargo.toml b/Cargo.toml index 668a6c094bb1..3f5207897d45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -148,6 +148,7 @@ biome_markdown_factory = { version = "0.0.1", path = "./crates/biome_markd biome_markdown_parser = { version = "0.0.1", path = "./crates/biome_markdown_parser" } biome_markdown_syntax = { version = "0.0.1", path = "./crates/biome_markdown_syntax" } biome_plugin_loader = { version = "0.0.1", path = "./crates/biome_plugin_loader" } +biome_project_layout = { version = "0.0.1", path = "./crates/biome_project_layout" } biome_ungrammar = { version = "0.3.1", path = "./crates/biome_ungrammar" } biome_yaml_factory = { version = "0.0.1", path = "./crates/biome_yaml_factory" } biome_yaml_parser = { version = "0.0.1", path = "./crates/biome_yaml_parser" } diff --git a/benchmark/package.json b/benchmark/package.json index 022cbbdc95fb..e4f33c8f3543 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -12,6 +12,7 @@ }, "devDependencies": { "@typescript-eslint/eslint-plugin": "8.18.1", + "@typescript-eslint/parser": "8.3.0", "dprint": "0.48.0", "eslint": "9.17.0", "prettier": "3.3.3" diff --git a/biome.json b/biome.json index aac6d02b41d9..ded692c24334 100644 --- a/biome.json +++ b/biome.json @@ -52,6 +52,9 @@ "enabled": true, "rules": { "recommended": true, + "correctness": { + "noUndeclaredDependencies": "error" + }, "style": { "noNonNullAssertion": "off", "useNodejsImportProtocol": "error" diff --git a/crates/biome_analyze/Cargo.toml b/crates/biome_analyze/Cargo.toml index 98cf8e0c0905..df83a2e465ec 100644 --- a/crates/biome_analyze/Cargo.toml +++ b/crates/biome_analyze/Cargo.toml @@ -20,6 +20,7 @@ biome_diagnostics = { workspace = true } biome_parser = { workspace = true } biome_rowan = { workspace = true } biome_suppression = { workspace = true } +camino = { workspace = true } enumflags2 = { workspace = true } indexmap = { workspace = true } rustc-hash = { workspace = true } @@ -27,7 +28,6 @@ schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"], optional = true } tracing = { workspace = true } - [features] schema = ["dep:schemars", "biome_console/schema", "serde"] serde = ["dep:serde", "dep:biome_deserialize", "dep:biome_deserialize_macros"] diff --git a/crates/biome_analyze/src/analyzer_plugin.rs b/crates/biome_analyze/src/analyzer_plugin.rs index fb7536c84e1f..e7b59220ea27 100644 --- a/crates/biome_analyze/src/analyzer_plugin.rs +++ b/crates/biome_analyze/src/analyzer_plugin.rs @@ -1,10 +1,11 @@ use crate::RuleDiagnostic; use biome_parser::AnyParse; -use std::{fmt::Debug, path::PathBuf}; +use camino::Utf8PathBuf; +use std::fmt::Debug; /// Definition of an analyzer plugin. pub trait AnalyzerPlugin: Debug { - fn evaluate(&self, root: AnyParse, path: PathBuf) -> Vec<RuleDiagnostic>; + fn evaluate(&self, root: AnyParse, path: Utf8PathBuf) -> Vec<RuleDiagnostic>; fn supports_css(&self) -> bool; diff --git a/crates/biome_analyze/src/context.rs b/crates/biome_analyze/src/context.rs index c2fbf3701d12..e3650eea31f3 100644 --- a/crates/biome_analyze/src/context.rs +++ b/crates/biome_analyze/src/context.rs @@ -2,8 +2,8 @@ use crate::options::{JsxRuntime, PreferredQuote}; use crate::{registry::RuleRoot, FromServices, Queryable, Rule, RuleKey, ServiceBag}; use crate::{GroupCategory, RuleCategory, RuleGroup, RuleMetadata}; use biome_diagnostics::{Error, Result}; +use camino::Utf8Path; use std::ops::Deref; -use std::path::Path; type RuleQueryResult<R> = <<R as Rule>::Query as Queryable>::Output; type RuleServiceBag<R> = <<R as Rule>::Query as Queryable>::Services; @@ -14,7 +14,7 @@ pub struct RuleContext<'a, R: Rule> { bag: &'a ServiceBag, services: RuleServiceBag<R>, globals: &'a [&'a str], - file_path: &'a Path, + file_path: &'a Utf8Path, options: &'a R::Options, preferred_quote: &'a PreferredQuote, jsx_runtime: Option<JsxRuntime>, @@ -30,7 +30,7 @@ where root: &'a RuleRoot<R>, services: &'a ServiceBag, globals: &'a [&'a str], - file_path: &'a Path, + file_path: &'a Utf8Path, options: &'a R::Options, preferred_quote: &'a PreferredQuote, jsx_runtime: Option<JsxRuntime>, @@ -159,7 +159,7 @@ where } /// The file path of the current file - pub fn file_path(&self) -> &Path { + pub fn file_path(&self) -> &Utf8Path { self.file_path } diff --git a/crates/biome_analyze/src/options.rs b/crates/biome_analyze/src/options.rs index 5fe34c9cf188..b1639cb387dc 100644 --- a/crates/biome_analyze/src/options.rs +++ b/crates/biome_analyze/src/options.rs @@ -1,9 +1,9 @@ +use camino::Utf8PathBuf; use rustc_hash::FxHashMap; use crate::{FixKind, Rule, RuleKey}; use std::any::{Any, TypeId}; use std::fmt::Debug; -use std::path::PathBuf; /// A convenient new type data structure to store the options that belong to a rule #[derive(Debug)] @@ -98,14 +98,14 @@ pub struct AnalyzerOptions { pub(crate) configuration: AnalyzerConfiguration, /// The file that is being analyzed - pub(crate) file_path: PathBuf, + pub file_path: Utf8PathBuf, /// Suppression reason used when applying a suppression code action pub(crate) suppression_reason: Option<String>, } impl AnalyzerOptions { - pub fn with_file_path(mut self, file_path: impl Into<PathBuf>) -> Self { + pub fn with_file_path(mut self, file_path: impl Into<Utf8PathBuf>) -> Self { self.file_path = file_path.into(); self } diff --git a/crates/biome_cli/tests/commands/lint.rs b/crates/biome_cli/tests/commands/lint.rs index 10d2d2fe1183..aa5d349a0e1b 100644 --- a/crates/biome_cli/tests/commands/lint.rs +++ b/crates/biome_cli/tests/commands/lint.rs @@ -5,7 +5,9 @@ use crate::configs::{ CONFIG_LINTER_SUPPRESSED_GROUP, CONFIG_LINTER_SUPPRESSED_RULE, CONFIG_LINTER_UPGRADE_DIAGNOSTIC, CONFIG_RECOMMENDED_GROUP, }; -use crate::snap_test::{assert_file_contents, markup_to_string, SnapshotPayload}; +use crate::snap_test::{ + assert_cli_snapshot_with_redactor, assert_file_contents, markup_to_string, SnapshotPayload, +}; use crate::{ assert_cli_snapshot, run_cli, run_cli_with_dyn_fs, run_cli_with_server_workspace, FORMATTED, LINT_ERROR, PARSE_ERROR, @@ -3879,13 +3881,16 @@ fn linter_finds_package_json_for_no_undeclared_dependencies() { &mut console, Args::from(["lint", file.as_str()].as_slice()), ); - assert_cli_snapshot(SnapshotPayload::new( - module_path!(), - "linter_finds_package_json_for_no_undeclared_dependencies", - fs, - console, - result, - )); + assert_cli_snapshot_with_redactor( + SnapshotPayload::new( + module_path!(), + "linter_finds_package_json_for_no_undeclared_dependencies", + fs, + console, + result, + ), + |content| content.replace("frontend\\", "frontend/"), + ); } #[test] @@ -3934,13 +3939,16 @@ fn linter_finds_nested_package_json_for_no_undeclared_dependencies() { &mut console, Args::from(["lint", file.as_str()].as_slice()), ); - assert_cli_snapshot(SnapshotPayload::new( - module_path!(), - "linter_finds_nested_package_json_for_no_undeclared_dependencies", - fs, - console, - result, - )); + assert_cli_snapshot_with_redactor( + SnapshotPayload::new( + module_path!(), + "linter_finds_nested_package_json_for_no_undeclared_dependencies", + fs, + console, + result, + ), + |content| content.replace("frontend\\", "frontend/"), + ); } #[test] diff --git a/crates/biome_cli/tests/snap_test.rs b/crates/biome_cli/tests/snap_test.rs index 014428ed699f..b7d572e81dc7 100644 --- a/crates/biome_cli/tests/snap_test.rs +++ b/crates/biome_cli/tests/snap_test.rs @@ -12,6 +12,7 @@ use camino::{Utf8Path, Utf8PathBuf}; use regex::Regex; use std::borrow::Cow; use std::collections::BTreeMap; +use std::convert::identity; use std::env::{current_exe, temp_dir}; use std::fmt::Write as _; use std::path::MAIN_SEPARATOR; @@ -405,6 +406,17 @@ impl<'a> SnapshotPayload<'a> { /// Function used to snapshot a session test of the a CLI run. pub fn assert_cli_snapshot(payload: SnapshotPayload<'_>) { + assert_cli_snapshot_with_redactor(payload, identity) +} + +/// Used to snapshot a session test of the a CLI run. +/// +/// Takes a custom `redactor` that allows the snapshotted content to be +/// normalized so it remains stable across test runs. +pub fn assert_cli_snapshot_with_redactor( + payload: SnapshotPayload<'_>, + redactor: impl FnOnce(String) -> String, +) { let module_path = payload.module_path.to_owned(); let test_name = payload.test_name; let cli_snapshot = CliSnapshot::from(payload); @@ -416,9 +428,9 @@ pub fn assert_cli_snapshot(payload: SnapshotPayload<'_>) { insta::with_settings!({ prepend_module_to_snapshot => false, - snapshot_path => snapshot_path + snapshot_path => snapshot_path, }, { - insta::assert_snapshot!(test_name, content); + insta::assert_snapshot!(test_name, redactor(content)); }); } diff --git a/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_nested_package_json_for_no_undeclared_dependencies.snap b/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_nested_package_json_for_no_undeclared_dependencies.snap index 7ef880891a32..67dc83f282ff 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_nested_package_json_for_no_undeclared_dependencies.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_nested_package_json_for_no_undeclared_dependencies.snap @@ -47,7 +47,7 @@ import 'react-dom' ```block frontend/file1.js:1:8 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i The current dependency isn't specified in your package.json. + i Dependency react-dom isn't specified in frontend/package.json. > 1 │ import 'react-dom' │ ^^^^^^^^^^^ diff --git a/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_package_json_for_no_undeclared_dependencies.snap b/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_package_json_for_no_undeclared_dependencies.snap index 06520b6470ff..adae399919f1 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_package_json_for_no_undeclared_dependencies.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_package_json_for_no_undeclared_dependencies.snap @@ -37,7 +37,7 @@ import 'react-dom' ```block frontend/file1.js:1:8 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i The current dependency isn't specified in your package.json. + i Dependency react-dom isn't specified in frontend/package.json. > 1 │ import 'react-dom' │ ^^^^^^^^^^^ diff --git a/crates/biome_css_analyze/Cargo.toml b/crates/biome_css_analyze/Cargo.toml index 6e3cdd8b6d32..1f3046c84b7d 100644 --- a/crates/biome_css_analyze/Cargo.toml +++ b/crates/biome_css_analyze/Cargo.toml @@ -23,6 +23,7 @@ biome_diagnostics = { workspace = true } biome_rowan = { workspace = true } biome_string_case = { workspace = true } biome_suppression = { workspace = true } +camino = { workspace = true } regex = { workspace = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } diff --git a/crates/biome_grit_patterns/src/grit_context.rs b/crates/biome_grit_patterns/src/grit_context.rs index e05c4c08c0a3..3ac76f3c3d35 100644 --- a/crates/biome_grit_patterns/src/grit_context.rs +++ b/crates/biome_grit_patterns/src/grit_context.rs @@ -314,7 +314,7 @@ fn new_file_owner( /// that can use the Biome workspace. #[derive(Clone, Debug)] pub struct GritTargetFile { - pub path: PathBuf, + pub path: Utf8PathBuf, pub parse: AnyParse, } @@ -323,9 +323,6 @@ impl GritTargetFile { let parser = target_language.get_parser(); let parse = parser.parse_with_path(source, &path); - Self { - parse, - path: path.into(), - } + Self { parse, path } } } diff --git a/crates/biome_grit_patterns/src/grit_query.rs b/crates/biome_grit_patterns/src/grit_query.rs index 29569187878c..73bd5f85e707 100644 --- a/crates/biome_grit_patterns/src/grit_query.rs +++ b/crates/biome_grit_patterns/src/grit_query.rs @@ -82,8 +82,12 @@ impl GritQuery { let var_registry = VarRegistry::from_locations(&self.variable_locations); - let file_registry = - FileRegistry::new_from_paths(files.iter().map(|file| &file.path).collect()); + // FIXME: Can be simplified when https://github.com/getgrit/gritql/pull/594/files is released. + let paths: Vec<PathBuf> = files + .iter() + .map(|file| file.path.clone().into_std_path_buf()) + .collect(); + let file_registry = FileRegistry::new_from_paths(paths.iter().collect()); let binding = FilePattern::Single(file_ptr); let mut state = State::new(var_registry.into(), file_registry); diff --git a/crates/biome_js_analyze/Cargo.toml b/crates/biome_js_analyze/Cargo.toml index 29b3db59022b..037f094b6f44 100644 --- a/crates/biome_js_analyze/Cargo.toml +++ b/crates/biome_js_analyze/Cargo.toml @@ -24,6 +24,7 @@ biome_js_factory = { workspace = true } biome_js_semantic = { workspace = true } biome_js_syntax = { workspace = true } biome_package = { workspace = true } +biome_project_layout = { workspace = true } biome_rowan = { workspace = true } biome_string_case = { workspace = true } biome_suppression = { workspace = true } diff --git a/crates/biome_js_analyze/src/lib.rs b/crates/biome_js_analyze/src/lib.rs index c2f6446cdb44..b342e9c0fd75 100644 --- a/crates/biome_js_analyze/src/lib.rs +++ b/crates/biome_js_analyze/src/lib.rs @@ -9,7 +9,7 @@ use biome_analyze::{ use biome_aria::AriaRoles; use biome_diagnostics::Error as DiagnosticError; use biome_js_syntax::{JsFileSource, JsLanguage}; -use biome_package::PackageJson; +use biome_project_layout::ProjectLayout; use biome_rowan::TextRange; use biome_suppression::{parse_suppression_comment, SuppressionDiagnostic}; use std::ops::Deref; @@ -53,7 +53,7 @@ pub fn analyze_with_inspect_matcher<'a, V, F, B>( options: &'a AnalyzerOptions, plugins: Vec<Box<dyn AnalyzerPlugin>>, source_type: JsFileSource, - manifest: Option<PackageJson>, + project_layout: Arc<ProjectLayout>, mut emit_signal: F, ) -> (Option<B>, Vec<DiagnosticError>) where @@ -116,8 +116,11 @@ where } services.insert_service(Arc::new(AriaRoles)); - services.insert_service(Arc::new(manifest)); services.insert_service(source_type); + + services.insert_service(project_layout.get_node_manifest_for_path(&options.file_path)); + services.insert_service(project_layout); + ( analyzer.run(AnalyzerContext { root: root.clone(), @@ -138,7 +141,7 @@ pub fn analyze<'a, F, B>( options: &'a AnalyzerOptions, plugins: Vec<Box<dyn AnalyzerPlugin>>, source_type: JsFileSource, - manifest: Option<&'a PackageJson>, + project_layout: Arc<ProjectLayout>, emit_signal: F, ) -> (Option<B>, Vec<DiagnosticError>) where @@ -152,7 +155,7 @@ where options, plugins, source_type, - manifest.cloned(), + project_layout, emit_signal, ) } @@ -167,7 +170,7 @@ mod tests { use biome_package::{Dependencies, PackageJson}; use std::slice; - use crate::{analyze, AnalysisFilter, ControlFlow}; + use super::*; // #[ignore] #[test] @@ -191,6 +194,7 @@ let bar = 33; let mut dependencies = Dependencies::default(); dependencies.add("buffer", "latest"); + analyze( &parsed.tree(), AnalysisFilter { @@ -200,10 +204,7 @@ let bar = 33; &options, Vec::new(), JsFileSource::tsx(), - Some(&PackageJson { - dependencies, - ..Default::default() - }), + project_layout_with_top_level_dependencies(dependencies), |signal| { if let Some(diag) = signal.diagnostic() { error_ranges.push(diag.location().span.unwrap()); @@ -252,7 +253,7 @@ let bar = 33; &options, Vec::new(), JsFileSource::js_module(), - None, + Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { let error = diag @@ -338,7 +339,7 @@ let bar = 33; &options, Vec::new(), JsFileSource::js_module(), - None, + Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { let span = diag.get_span(); @@ -410,7 +411,7 @@ let bar = 33; &options, Vec::new(), JsFileSource::js_module(), - None, + Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { let code = diag.category().unwrap(); @@ -455,7 +456,7 @@ let bar = 33; &options, Vec::new(), JsFileSource::js_module(), - None, + Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { let error = diag @@ -507,7 +508,7 @@ debugger; &options, Vec::new(), JsFileSource::js_module(), - None, + Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { let error = diag @@ -554,7 +555,7 @@ debugger; &options, Vec::new(), JsFileSource::js_module(), - None, + Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { let error = diag @@ -603,7 +604,7 @@ debugger; &options, Vec::new(), JsFileSource::js_module(), - None, + Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { let error = diag @@ -653,7 +654,7 @@ let bar = 33; &options, Vec::new(), JsFileSource::js_module(), - None, + Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { let code = diag.category().unwrap(); @@ -701,7 +702,7 @@ let bar = 33; &options, Vec::new(), JsFileSource::js_module(), - None, + Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { let error = diag @@ -752,7 +753,7 @@ let c; &options, Vec::new(), JsFileSource::js_module(), - None, + Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { let code = diag.category().unwrap(); @@ -804,7 +805,7 @@ debugger; &options, Vec::new(), JsFileSource::js_module(), - None, + Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { has_diagnostics = true; @@ -857,7 +858,7 @@ let d; &options, Vec::new(), JsFileSource::js_module(), - None, + Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { let error = diag @@ -905,7 +906,7 @@ a == b; &options, Vec::new(), JsFileSource::js_module(), - None, + Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { has_diagnostics = true; @@ -920,4 +921,18 @@ a == b; ); assert!(has_diagnostics, "must have diagnostics"); } + + fn project_layout_with_top_level_dependencies( + dependencies: Dependencies, + ) -> Arc<ProjectLayout> { + let manifest = PackageJson { + dependencies, + ..Default::default() + }; + + let project_layout = ProjectLayout::default(); + project_layout.insert_node_manifest("/".into(), manifest); + + Arc::new(project_layout) + } } diff --git a/crates/biome_js_analyze/src/lint/correctness/no_undeclared_dependencies.rs b/crates/biome_js_analyze/src/lint/correctness/no_undeclared_dependencies.rs index 04aa58ebb8c7..cc159de1d870 100644 --- a/crates/biome_js_analyze/src/lint/correctness/no_undeclared_dependencies.rs +++ b/crates/biome_js_analyze/src/lint/correctness/no_undeclared_dependencies.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use biome_analyze::{context::RuleContext, declare_lint_rule, Rule, RuleDiagnostic, RuleSource}; use biome_console::markup; use biome_deserialize::{ @@ -8,6 +6,7 @@ use biome_deserialize::{ use biome_deserialize_macros::Deserializable; use biome_js_syntax::{AnyJsImportClause, AnyJsImportLike}; use biome_rowan::AstNode; +use camino::Utf8Path; use crate::{globals::is_node_builtin_module, services::manifest::Manifest}; @@ -164,7 +163,7 @@ impl schemars::JsonSchema for DependencyAvailability { } impl DependencyAvailability { - fn is_available(&self, path: &Path) -> bool { + fn is_available(&self, path: &Utf8Path) -> bool { match self { Self::Bool(b) => *b, Self::Patterns(globs) => { @@ -270,11 +269,24 @@ impl Rule for NoUndeclaredDependencies { is_optional_dependency_available, } = state; + let Some(package_path) = ctx.package_path.as_ref() else { + return Some(RuleDiagnostic::new( + rule_category!(), + ctx.query().range(), + markup! { + "Dependency "<Emphasis>{package_name}</Emphasis>" cannot be verified because no package.json file was found." + }, + )); + }; + + let mut manifest_path = package_path.clone(); + manifest_path.push("package.json"); + let diag = RuleDiagnostic::new( rule_category!(), ctx.query().range(), markup! { - "The current dependency isn't specified in your package.json." + "Dependency "<Emphasis>{package_name}</Emphasis>" isn't specified in "<Emphasis>{manifest_path.as_str()}</Emphasis>"." }, ); diff --git a/crates/biome_js_analyze/src/lint/correctness/use_import_extensions.rs b/crates/biome_js_analyze/src/lint/correctness/use_import_extensions.rs index a95205e70474..a2bb73639694 100644 --- a/crates/biome_js_analyze/src/lint/correctness/use_import_extensions.rs +++ b/crates/biome_js_analyze/src/lint/correctness/use_import_extensions.rs @@ -149,7 +149,7 @@ impl Rule for UseImportExtensions { fn run(ctx: &RuleContext<Self>) -> Self::Signals { let node = ctx.query(); - let file_ext = ctx.file_path().extension().and_then(|ext| ext.to_str())?; + let file_ext = ctx.file_path().extension()?; let custom_suggested_imports = &ctx.options().suggested_extensions; diff --git a/crates/biome_js_analyze/src/lint/nursery/no_common_js.rs b/crates/biome_js_analyze/src/lint/nursery/no_common_js.rs index a6a2ee742598..bf1c3feb65fe 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_common_js.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_common_js.rs @@ -70,7 +70,7 @@ impl Rule for NoCommonJs { fn run(ctx: &RuleContext<Self>) -> Self::Signals { let file_ext = ctx.file_path().extension(); // cjs and cts files can only use CommonJs modules - if file_ext.is_some_and(|file_ext| matches!(file_ext.as_encoded_bytes(), b"cjs" | b"cts")) { + if file_ext.is_some_and(|file_ext| matches!(file_ext.as_bytes(), b"cjs" | b"cts")) { return None; } diff --git a/crates/biome_js_analyze/src/lint/nursery/no_document_import_in_page.rs b/crates/biome_js_analyze/src/lint/nursery/no_document_import_in_page.rs index 681af0811362..8ed42489833a 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_document_import_in_page.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_document_import_in_page.rs @@ -70,8 +70,8 @@ impl Rule for NoDocumentImportInPage { return None; } - let file_name = path.file_stem()?.to_str()?; - let parent_name = path.parent()?.file_stem()?.to_str()?; + let file_name = path.file_stem()?; + let parent_name = path.parent()?.file_stem()?; if parent_name == "_document" || file_name == "_document" { return None; diff --git a/crates/biome_js_analyze/src/lint/nursery/no_head_import_in_document.rs b/crates/biome_js_analyze/src/lint/nursery/no_head_import_in_document.rs index 2747dbe23c27..dd6a5392dbd2 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_head_import_in_document.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_head_import_in_document.rs @@ -83,14 +83,14 @@ impl Rule for NoHeadImportInDocument { return None; } - let file_name = path.file_stem()?.to_str()?; + let file_name = path.file_stem()?; // pages/_document.(jsx|tsx) if file_name == "_document" { return Some(()); } - let parent_name = path.parent()?.file_stem()?.to_str()?; + let parent_name = path.parent()?.file_stem()?; // pages/_document/index.(jsx|tsx) if parent_name == "_document" && file_name == "index" { @@ -101,7 +101,7 @@ impl Rule for NoHeadImportInDocument { } fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> { - let path = ctx.file_path().to_str()?.split("pages").nth(1)?; + let path = ctx.file_path().as_str().split("pages").nth(1)?; let path = if cfg!(debug_assertions) { path.replace(MAIN_SEPARATOR, "/") } else { diff --git a/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs b/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs index a4a7d5de5517..fdc00c4b9385 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs @@ -141,7 +141,7 @@ impl Rule for UseComponentExportOnlyModules { type Options = UseComponentExportOnlyModulesOptions; fn run(ctx: &RuleContext<Self>) -> Self::Signals { - if let Some(file_name) = ctx.file_path().file_name().and_then(|x| x.to_str()) { + if let Some(file_name) = ctx.file_path().file_name() { if !JSX_FILE_EXT.iter().any(|ext| file_name.ends_with(ext)) { return Vec::new().into_boxed_slice(); } diff --git a/crates/biome_js_analyze/src/lint/style/use_filenaming_convention.rs b/crates/biome_js_analyze/src/lint/style/use_filenaming_convention.rs index 4d2246c7c3bb..6a7818503cad 100644 --- a/crates/biome_js_analyze/src/lint/style/use_filenaming_convention.rs +++ b/crates/biome_js_analyze/src/lint/style/use_filenaming_convention.rs @@ -156,7 +156,7 @@ impl Rule for UseFilenamingConvention { type Options = Box<FilenamingConventionOptions>; fn run(ctx: &RuleContext<Self>) -> Self::Signals { - let file_name = ctx.file_path().file_name()?.to_str()?; + let file_name = ctx.file_path().file_name()?; let options = ctx.options(); if options.require_ascii && !file_name.is_ascii() { return Some(FileNamingConventionState::Ascii); @@ -273,7 +273,7 @@ impl Rule for UseFilenamingConvention { } fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { - let file_name = ctx.file_path().file_name()?.to_str()?; + let file_name = ctx.file_path().file_name()?; let options = ctx.options(); match state { FileNamingConventionState::Ascii => { diff --git a/crates/biome_js_analyze/src/services/manifest.rs b/crates/biome_js_analyze/src/services/manifest.rs index ba39c5a78ca5..1c383ae5d688 100644 --- a/crates/biome_js_analyze/src/services/manifest.rs +++ b/crates/biome_js_analyze/src/services/manifest.rs @@ -5,45 +5,39 @@ use biome_analyze::{ use biome_js_syntax::{AnyJsRoot, JsLanguage, JsSyntaxNode}; use biome_package::PackageJson; use biome_rowan::AstNode; -use std::sync::Arc; +use camino::Utf8PathBuf; #[derive(Debug, Clone)] pub struct ManifestServices { - pub(crate) manifest: Arc<Option<PackageJson>>, + pub(crate) package_path: Option<Utf8PathBuf>, + pub(crate) manifest: Option<PackageJson>, } impl ManifestServices { pub(crate) fn name(&self) -> Option<&str> { - self.manifest - .as_ref() - .as_ref() - .and_then(|pkg| pkg.name.as_deref()) + self.manifest.as_ref().and_then(|pkg| pkg.name.as_deref()) } pub(crate) fn is_dependency(&self, specifier: &str) -> bool { self.manifest - .as_ref() .as_ref() .is_some_and(|pkg| pkg.dependencies.contains(specifier)) } pub(crate) fn is_dev_dependency(&self, specifier: &str) -> bool { self.manifest - .as_ref() .as_ref() .is_some_and(|pkg| pkg.dev_dependencies.contains(specifier)) } pub(crate) fn is_peer_dependency(&self, specifier: &str) -> bool { self.manifest - .as_ref() .as_ref() .is_some_and(|pkg| pkg.peer_dependencies.contains(specifier)) } pub(crate) fn is_optional_dependency(&self, specifier: &str) -> bool { self.manifest - .as_ref() .as_ref() .is_some_and(|pkg| pkg.optional_dependencies.contains(specifier)) } @@ -54,12 +48,19 @@ impl FromServices for ManifestServices { rule_key: &RuleKey, services: &ServiceBag, ) -> biome_diagnostics::Result<Self, MissingServicesDiagnostic> { - let manifest: &Arc<Option<PackageJson>> = services.get_service().ok_or_else(|| { - MissingServicesDiagnostic::new(rule_key.rule_name(), &["PackageJson"]) - })?; + let manifest_info: &Option<(Utf8PathBuf, PackageJson)> = + services.get_service().ok_or_else(|| { + MissingServicesDiagnostic::new(rule_key.rule_name(), &["PackageJson"]) + })?; + + let (package_path, manifest) = match manifest_info { + Some((package_path, manifest)) => (Some(package_path.clone()), Some(manifest.clone())), + None => (None, None), + }; Ok(Self { - manifest: manifest.clone(), + package_path, + manifest, }) } } @@ -70,7 +71,7 @@ impl Phase for ManifestServices { } } -/// Query type usable by lint rules **that uses the semantic model** to match on specific [AstNode] types +/// Query type usable by lint rules **that uses the package manifest** and matches on specific [AstNode] types. #[derive(Clone)] pub struct Manifest<N>(pub N); diff --git a/crates/biome_js_analyze/src/syntax/correctness/no_type_only_import_attributes.rs b/crates/biome_js_analyze/src/syntax/correctness/no_type_only_import_attributes.rs index 84a58e732c90..17ce8db23613 100644 --- a/crates/biome_js_analyze/src/syntax/correctness/no_type_only_import_attributes.rs +++ b/crates/biome_js_analyze/src/syntax/correctness/no_type_only_import_attributes.rs @@ -40,7 +40,7 @@ impl Rule for NoTypeOnlyImportAttributes { fn run(ctx: &RuleContext<Self>) -> Self::Signals { let extension = ctx.file_path().extension()?; - if extension.as_encoded_bytes() == b"cts" { + if extension.as_bytes() == b"cts" { // Ignore `*.cts` return None; } diff --git a/crates/biome_js_analyze/tests/quick_test.rs b/crates/biome_js_analyze/tests/quick_test.rs index f1f1aac23f4a..e01eb91e23b1 100644 --- a/crates/biome_js_analyze/tests/quick_test.rs +++ b/crates/biome_js_analyze/tests/quick_test.rs @@ -4,8 +4,8 @@ use biome_diagnostics::{DiagnosticExt, Severity}; use biome_js_parser::{parse, JsParserOptions}; use biome_js_syntax::JsFileSource; use biome_test_utils::{ - code_fix_to_string, create_analyzer_options, diagnostic_to_string, load_manifest, - parse_test_path, scripts_from_json, + code_fix_to_string, create_analyzer_options, diagnostic_to_string, parse_test_path, + project_layout_with_node_manifest, scripts_from_json, }; use camino::Utf8Path; use std::ops::Deref; @@ -71,7 +71,7 @@ fn analyze( let mut diagnostics = Vec::new(); let mut code_fixes = Vec::new(); let options = create_analyzer_options(input_file, &mut diagnostics); - let manifest = load_manifest(input_file, &mut diagnostics); + let project_layout = project_layout_with_node_manifest(input_file, &mut diagnostics); let (_, errors) = biome_js_analyze::analyze( &root, @@ -79,7 +79,7 @@ fn analyze( &options, Vec::new(), source_type, - manifest.as_ref(), + project_layout, |event| { if let Some(mut diag) = event.diagnostic() { for action in event.actions() { diff --git a/crates/biome_js_analyze/tests/spec_tests.rs b/crates/biome_js_analyze/tests/spec_tests.rs index f62324e256d5..5854b5585d9c 100644 --- a/crates/biome_js_analyze/tests/spec_tests.rs +++ b/crates/biome_js_analyze/tests/spec_tests.rs @@ -11,8 +11,8 @@ use biome_plugin_loader::AnalyzerGritPlugin; use biome_rowan::AstNode; use biome_test_utils::{ assert_errors_are_absent, code_fix_to_string, create_analyzer_options, diagnostic_to_string, - has_bogus_nodes_or_empty_slots, load_manifest, parse_test_path, register_leak_checker, - scripts_from_json, write_analyzer_snapshot, CheckActionType, + has_bogus_nodes_or_empty_slots, parse_test_path, project_layout_with_node_manifest, + register_leak_checker, scripts_from_json, write_analyzer_snapshot, CheckActionType, }; use camino::Utf8Path; use std::ops::Deref; @@ -113,9 +113,9 @@ pub(crate) fn analyze_and_snap( ) -> usize { let mut diagnostics = Vec::new(); let mut code_fixes = Vec::new(); - let manifest = load_manifest(input_file, &mut diagnostics); + let project_layout = project_layout_with_node_manifest(input_file, &mut diagnostics); - if let Some(manifest) = &manifest { + if let Some((_, manifest)) = project_layout.get_node_manifest_for_path(input_file) { if manifest.r#type == Some(PackageType::Commonjs) && // At the moment we treat JS and JSX at the same way (source_type.file_extension() == "js" || source_type.file_extension() == "jsx" ) @@ -123,6 +123,7 @@ pub(crate) fn analyze_and_snap( source_type.set_module_kind(ModuleKind::Script) } } + let parsed = parse(input_code, source_type, parser_options.clone()); let root = parsed.tree(); @@ -134,7 +135,7 @@ pub(crate) fn analyze_and_snap( &options, plugins, source_type, - manifest.as_ref(), + project_layout, |event| { if let Some(mut diag) = event.diagnostic() { for action in event.actions() { diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js.snap b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js.snap index f1ebda526d86..07baece7026f 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js.snap +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js.snap @@ -1,7 +1,6 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs expression: invalid.js -snapshot_kind: text --- # Input ```js @@ -19,7 +18,7 @@ require("@testing-library/react"); ``` invalid.js:1:8 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! The current dependency isn't specified in your package.json. + ! Dependency notInstalled isn't specified in package.json. > 1 │ import "notInstalled"; │ ^^^^^^^^^^^^^^ @@ -36,7 +35,7 @@ invalid.js:1:8 lint/correctness/noUndeclaredDependencies ━━━━━━━ ``` invalid.js:2:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! The current dependency isn't specified in your package.json. + ! Dependency notInstalled isn't specified in package.json. 1 │ import "notInstalled"; > 2 │ import("notInstalled"); @@ -54,7 +53,7 @@ invalid.js:2:1 lint/correctness/noUndeclaredDependencies ━━━━━━━ ``` invalid.js:3:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! The current dependency isn't specified in your package.json. + ! Dependency notInstalled isn't specified in package.json. 1 │ import "notInstalled"; 2 │ import("notInstalled"); @@ -73,7 +72,7 @@ invalid.js:3:1 lint/correctness/noUndeclaredDependencies ━━━━━━━ ``` invalid.js:5:8 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! The current dependency isn't specified in your package.json. + ! Dependency @testing-library/react isn't specified in package.json. 3 │ require("notInstalled") 4 │ @@ -92,7 +91,7 @@ invalid.js:5:8 lint/correctness/noUndeclaredDependencies ━━━━━━━ ``` invalid.js:6:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! The current dependency isn't specified in your package.json. + ! Dependency @testing-library/react isn't specified in package.json. 5 │ import "@testing-library/react"; > 6 │ import("@testing-library/react"); @@ -110,7 +109,7 @@ invalid.js:6:1 lint/correctness/noUndeclaredDependencies ━━━━━━━ ``` invalid.js:7:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! The current dependency isn't specified in your package.json. + ! Dependency @testing-library/react isn't specified in package.json. 5 │ import "@testing-library/react"; 6 │ import("@testing-library/react"); diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.js.snap b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.js.snap index 5c70dc6205a2..f04ab49cdb0f 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.js.snap +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.js.snap @@ -1,7 +1,6 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs expression: invalid.test.js -snapshot_kind: text --- # Input ```js @@ -19,7 +18,7 @@ require("@testing-library/react"); ``` invalid.test.js:1:8 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! The current dependency isn't specified in your package.json. + ! Dependency notInstalled isn't specified in package.json. > 1 │ import "notInstalled"; │ ^^^^^^^^^^^^^^ @@ -36,7 +35,7 @@ invalid.test.js:1:8 lint/correctness/noUndeclaredDependencies ━━━━━━ ``` invalid.test.js:2:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! The current dependency isn't specified in your package.json. + ! Dependency notInstalled isn't specified in package.json. 1 │ import "notInstalled"; > 2 │ import("notInstalled"); @@ -54,7 +53,7 @@ invalid.test.js:2:1 lint/correctness/noUndeclaredDependencies ━━━━━━ ``` invalid.test.js:3:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! The current dependency isn't specified in your package.json. + ! Dependency notInstalled isn't specified in package.json. 1 │ import "notInstalled"; 2 │ import("notInstalled"); @@ -73,7 +72,7 @@ invalid.test.js:3:1 lint/correctness/noUndeclaredDependencies ━━━━━━ ``` invalid.test.js:5:8 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! The current dependency isn't specified in your package.json. + ! Dependency @testing-library/react isn't specified in package.json. 3 │ require("notInstalled") 4 │ @@ -92,7 +91,7 @@ invalid.test.js:5:8 lint/correctness/noUndeclaredDependencies ━━━━━━ ``` invalid.test.js:6:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! The current dependency isn't specified in your package.json. + ! Dependency @testing-library/react isn't specified in package.json. 5 │ import "@testing-library/react"; > 6 │ import("@testing-library/react"); @@ -110,7 +109,7 @@ invalid.test.js:6:1 lint/correctness/noUndeclaredDependencies ━━━━━━ ``` invalid.test.js:7:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! The current dependency isn't specified in your package.json. + ! Dependency @testing-library/react isn't specified in package.json. 5 │ import "@testing-library/react"; 6 │ import("@testing-library/react"); diff --git a/crates/biome_package/src/lib.rs b/crates/biome_package/src/lib.rs index 14432ceae4eb..931f8e074289 100644 --- a/crates/biome_package/src/lib.rs +++ b/crates/biome_package/src/lib.rs @@ -28,8 +28,8 @@ pub trait Manifest: Default + Debug { pub trait Package { type Manifest: Manifest; - /// Use this function to prepare the package, like loading the manifest. - fn deserialize_manifest(&mut self, root: &PackageRoot<Self>); + /// Inserts a manifest into the package, taking care of deserialization. + fn insert_serialized_manifest(&mut self, root: &PackageRoot<Self>); fn manifest(&self) -> Option<&Self::Manifest> { None diff --git a/crates/biome_package/src/node_js_package/mod.rs b/crates/biome_package/src/node_js_package/mod.rs index 64f2ccfbfe40..bc1cf23d8909 100644 --- a/crates/biome_package/src/node_js_package/mod.rs +++ b/crates/biome_package/src/node_js_package/mod.rs @@ -37,11 +37,11 @@ pub(crate) type ProjectLanguageRoot<M> = <<M as Manifest>::Language as Language> impl Package for NodeJsPackage { type Manifest = PackageJson; - fn deserialize_manifest(&mut self, content: &ProjectLanguageRoot<Self::Manifest>) { - let manifest = Self::Manifest::deserialize_manifest(content); - let (package, deserialize_diagnostics) = manifest.consume(); - self.manifest = package.unwrap_or_default(); - self.diagnostics = deserialize_diagnostics + fn insert_serialized_manifest(&mut self, content: &ProjectLanguageRoot<Self::Manifest>) { + let deserialized = Self::Manifest::deserialize_manifest(content); + let (manifest, diagnostics) = deserialized.consume(); + self.manifest = manifest.unwrap_or_default(); + self.diagnostics = diagnostics .into_iter() .map(biome_diagnostics::serde::Diagnostic::new) .collect(); diff --git a/crates/biome_package/tests/manifest_spec_tests.rs b/crates/biome_package/tests/manifest_spec_tests.rs index 2b375a921633..4c9b399920a3 100644 --- a/crates/biome_package/tests/manifest_spec_tests.rs +++ b/crates/biome_package/tests/manifest_spec_tests.rs @@ -24,7 +24,7 @@ fn run_invalid_manifests(input: &'static str, _: &str, _: &str, _: &str) { match input_file.extension().map(OsStr::as_encoded_bytes) { Some(b"json") => { let parsed = parse_json(input_code.as_str(), JsonParserOptions::default()); - package.deserialize_manifest(&parsed.tree()); + package.insert_serialized_manifest(&parsed.tree()); } _ => { panic!("Extension not supported"); diff --git a/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs b/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs index b2d08c723b13..d71af3a18263 100644 --- a/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs +++ b/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs @@ -9,10 +9,10 @@ use biome_grit_patterns::{ }; use biome_parser::AnyParse; use biome_rowan::TextRange; -use camino::Utf8Path; +use camino::{Utf8Path, Utf8PathBuf}; use grit_pattern_matcher::{binding::Binding, pattern::ResolvedPattern}; use grit_util::{error::GritPatternError, AnalysisLogs}; -use std::{borrow::Cow, fmt::Debug, path::PathBuf, rc::Rc}; +use std::{borrow::Cow, fmt::Debug, rc::Rc}; use crate::{AnalyzerPlugin, PluginDiagnostic}; @@ -48,7 +48,7 @@ impl AnalyzerGritPlugin { } impl AnalyzerPlugin for AnalyzerGritPlugin { - fn evaluate(&self, root: AnyParse, path: PathBuf) -> Vec<RuleDiagnostic> { + fn evaluate(&self, root: AnyParse, path: Utf8PathBuf) -> Vec<RuleDiagnostic> { let name: &str = self.grit_query.name.as_deref().unwrap_or("anonymous"); let file = GritTargetFile { parse: root, path }; diff --git a/crates/biome_project_layout/Cargo.toml b/crates/biome_project_layout/Cargo.toml new file mode 100644 index 000000000000..a22b40f33515 --- /dev/null +++ b/crates/biome_project_layout/Cargo.toml @@ -0,0 +1,22 @@ + +[package] +authors.workspace = true +categories.workspace = true +description = "Data structure for tracking projects and their packages" +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +name = "biome_project_layout" +repository.workspace = true +version = "0.0.1" + +[lints] +workspace = true + +[dependencies] +biome_package = { workspace = true } +biome_parser = { workspace = true } +camino = { workspace = true } +papaya = { workspace = true } +rustc-hash = { workspace = true } diff --git a/crates/biome_project_layout/src/lib.rs b/crates/biome_project_layout/src/lib.rs new file mode 100644 index 000000000000..b9d256276fb5 --- /dev/null +++ b/crates/biome_project_layout/src/lib.rs @@ -0,0 +1,3 @@ +mod project_layout; + +pub use project_layout::ProjectLayout; diff --git a/crates/biome_project_layout/src/project_layout.rs b/crates/biome_project_layout/src/project_layout.rs new file mode 100644 index 000000000000..23bc9e248578 --- /dev/null +++ b/crates/biome_project_layout/src/project_layout.rs @@ -0,0 +1,160 @@ +use biome_package::{NodeJsPackage, Package, PackageJson}; +use biome_parser::AnyParse; +use camino::{Utf8Path, Utf8PathBuf}; +use papaya::HashMap; +use rustc_hash::FxBuildHasher; + +/// The layout used across all open projects. +/// +/// Projects are comprised of zero or more packages. This arrangement is +/// intended to reflect the common usage of monorepos, where a single repository +/// may host many packages, and each package is allowed to have its own +/// settings. +/// +/// For Biome, a project is where the **top-level** configuration file is, while +/// packages may have their own nested configuration files. +/// +/// As a data structure, the project layout is simply a flat mapping from paths +/// to package data. This means that in order to lookup the package that is +/// most relevant for a given file, we may need to do multiple lookups from the +/// most-specific possible package path to the least. This means performance +/// degrades linearly with the depth of the path of a file. For now though, this +/// approach makes it very easy for us to invalidate part of the layout when +/// there are file system changes. +#[derive(Debug, Default)] +pub struct ProjectLayout(HashMap<Utf8PathBuf, PackageData, FxBuildHasher>); + +/// The information tracked for each package. +/// +/// Because Biome is intended to support multiple kinds of JavaScript projects, +/// the term "package" is somewhat loosely defined. It may be an NPM package, +/// a JSR package, or simply a directory with its own nested `biome.json`. +#[derive(Debug, Default)] +pub struct PackageData { + /// The settings of the package. + /// + /// Usually inferred from a configuration file, e.g. `biome.json`. + // TODO: Uncomment this. + // Probably best done when Ema has finished with https://github.com/biomejs/biome/pull/4845 + //settings: Settings, + + /// Optional Node.js-specific package information, if relevant for the + /// package. + node_package: Option<NodeJsPackage>, +} + +impl ProjectLayout { + /// Returns the `package.json` that should be used for the given `path`, + /// together with the absolute path of the manifest file. + pub fn get_node_manifest_for_path( + &self, + path: &Utf8Path, + ) -> Option<(Utf8PathBuf, PackageJson)> { + // Note I also tried an alternative approach where instead of iterating + // over all entries and finding the closest match, I would do repeated + // lookups like this: + // + // ```rs + // let packages = self.0.pin(); + // path.ancestors().skip(1).find_map(|package_path| { + // packages + // .get(package_path) + // .and_then(|data| data.node_package.as_ref()) + // .map(|node_package| (package_path.to_path_buf(), node_package.manifest.clone())) + // }) + // ``` + // + // Contrary to what I expected however, the below implementation + // appeared significantly faster (tested on the `unleash` repository). + + let mut result: Option<(&Utf8PathBuf, &PackageJson)> = None; + + let packages = self.0.pin(); + for (package_path, data) in packages.iter() { + let Some(node_manifest) = data + .node_package + .as_ref() + .map(|node_package| &node_package.manifest) + else { + continue; + }; + + let is_closest_match = path.strip_prefix(package_path).is_ok() + && result.is_none_or(|(matched_package_path, _)| { + package_path.as_str().len() > matched_package_path.as_str().len() + }); + + if is_closest_match { + result = Some((package_path, node_manifest)); + } + } + + result.map(|(package_path, package_json)| (package_path.clone(), package_json.clone())) + } + + pub fn insert_node_manifest(&self, path: Utf8PathBuf, manifest: PackageJson) { + self.0.pin().update_or_insert_with( + path, + |data| { + let mut node_js_package = NodeJsPackage { + manifest: Default::default(), + diagnostics: Default::default(), + tsconfig: data + .node_package + .as_ref() + .map(|package| package.tsconfig.clone()) + .unwrap_or_default(), + }; + node_js_package.manifest = manifest.clone(); + + PackageData { + node_package: Some(node_js_package), + } + }, + || { + let node_js_package = NodeJsPackage { + manifest: manifest.clone(), + ..Default::default() + }; + + PackageData { + node_package: Some(node_js_package), + } + }, + ); + } + + pub fn insert_serialized_node_manifest(&self, path: Utf8PathBuf, manifest: AnyParse) { + self.0.pin().update_or_insert_with( + path, + |data| { + let mut node_js_package = NodeJsPackage { + manifest: Default::default(), + diagnostics: Default::default(), + tsconfig: data + .node_package + .as_ref() + .map(|package| package.tsconfig.clone()) + .unwrap_or_default(), + }; + node_js_package.insert_serialized_manifest(&manifest.tree()); + + PackageData { + node_package: Some(node_js_package), + } + }, + || { + let mut node_js_package = NodeJsPackage::default(); + node_js_package.insert_serialized_manifest(&manifest.tree()); + + PackageData { + node_package: Some(node_js_package), + } + }, + ); + } + + pub fn remove_package(&self, path: &Utf8Path) { + self.0.pin().remove(path); + } +} diff --git a/crates/biome_service/Cargo.toml b/crates/biome_service/Cargo.toml index a25238e95b0d..e235dc6b8de9 100644 --- a/crates/biome_service/Cargo.toml +++ b/crates/biome_service/Cargo.toml @@ -49,6 +49,7 @@ biome_json_parser = { workspace = true } biome_json_syntax = { workspace = true } biome_package = { workspace = true } biome_parser = { workspace = true } +biome_project_layout = { workspace = true } biome_rowan = { workspace = true, features = ["serde"] } biome_string_case = { workspace = true } biome_text_edit = { workspace = true } diff --git a/crates/biome_service/src/file_handlers/css.rs b/crates/biome_service/src/file_handlers/css.rs index 91c788c976f1..c05a52058307 100644 --- a/crates/biome_service/src/file_handlers/css.rs +++ b/crates/biome_service/src/file_handlers/css.rs @@ -326,7 +326,7 @@ fn lint(params: LintParams) -> LintResults { .with_skip(¶ms.skip) .with_path(params.path.as_path()) .with_enabled_rules(¶ms.enabled_rules) - .with_manifest(params.manifest.as_ref()) + .with_project_layout(params.project_layout.clone()) .finish(); let filter = AnalysisFilter { @@ -353,7 +353,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult { range, workspace, path, - manifest, + project_layout, language, only, skip, @@ -379,7 +379,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult { .with_skip(&skip) .with_path(path.as_path()) .with_enabled_rules(&rules) - .with_manifest(manifest.as_ref()) + .with_project_layout(project_layout) .finish(); let filter = AnalysisFilter { @@ -437,7 +437,7 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceEr .with_skip(¶ms.skip) .with_path(params.biome_path.as_path()) .with_enabled_rules(¶ms.enabled_rules) - .with_manifest(params.manifest.as_ref()) + .with_project_layout(params.project_layout) .finish(); let filter = AnalysisFilter { diff --git a/crates/biome_service/src/file_handlers/graphql.rs b/crates/biome_service/src/file_handlers/graphql.rs index 39376ef00262..08ce24885cdd 100644 --- a/crates/biome_service/src/file_handlers/graphql.rs +++ b/crates/biome_service/src/file_handlers/graphql.rs @@ -304,7 +304,7 @@ fn lint(params: LintParams) -> LintResults { .with_skip(¶ms.skip) .with_path(params.path.as_path()) .with_enabled_rules(¶ms.enabled_rules) - .with_manifest(params.manifest.as_ref()) + .with_project_layout(params.project_layout.clone()) .finish(); let filter = AnalysisFilter { @@ -331,7 +331,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult { range, workspace, path, - manifest, + project_layout, language, only, skip, @@ -360,7 +360,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult { .with_skip(&skip) .with_path(path.as_path()) .with_enabled_rules(&rules) - .with_manifest(manifest.as_ref()) + .with_project_layout(project_layout) .finish(); let filter = AnalysisFilter { @@ -418,7 +418,7 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceEr .with_skip(¶ms.skip) .with_path(params.biome_path.as_path()) .with_enabled_rules(¶ms.enabled_rules) - .with_manifest(params.manifest.as_ref()) + .with_project_layout(params.project_layout) .finish(); let filter = AnalysisFilter { diff --git a/crates/biome_service/src/file_handlers/javascript.rs b/crates/biome_service/src/file_handlers/javascript.rs index bb9a66224dd5..49bff1cf059d 100644 --- a/crates/biome_service/src/file_handlers/javascript.rs +++ b/crates/biome_service/src/file_handlers/javascript.rs @@ -395,7 +395,7 @@ fn debug_control_flow(parse: AnyParse, cursor: TextSize) -> String { &options, Vec::new(), JsFileSource::default(), - None, + Default::default(), |_| ControlFlow::<Never>::Continue(()), ); @@ -445,7 +445,7 @@ pub(crate) fn lint(params: LintParams) -> LintResults { .with_skip(¶ms.skip) .with_path(params.path.as_path()) .with_enabled_rules(¶ms.enabled_rules) - .with_manifest(params.manifest.as_ref()) + .with_project_layout(params.project_layout.clone()) .finish(); let filter = AnalysisFilter { @@ -463,7 +463,7 @@ pub(crate) fn lint(params: LintParams) -> LintResults { &analyzer_options, Vec::new(), file_source, - params.manifest.as_ref(), + params.project_layout, |signal| process_lint.process_signal(signal), ); @@ -477,7 +477,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult { range, workspace, path, - manifest, + project_layout, language, only, skip, @@ -496,7 +496,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult { .with_skip(&skip) .with_path(path.as_path()) .with_enabled_rules(&rules) - .with_manifest(manifest.as_ref()) + .with_project_layout(project_layout.clone()) .finish(); let filter = AnalysisFilter { @@ -524,7 +524,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult { &analyzer_options, Vec::new(), source_type, - manifest.as_ref(), + project_layout, |signal| { actions.extend(signal.actions().into_code_action_iter().map(|item| { trace!("Pulled action category {:?}", item.category); @@ -569,7 +569,7 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceEr .with_skip(¶ms.skip) .with_path(params.biome_path.as_path()) .with_enabled_rules(¶ms.enabled_rules) - .with_manifest(params.manifest.as_ref()) + .with_project_layout(params.project_layout.clone()) .finish(); let filter = AnalysisFilter { @@ -598,7 +598,7 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceEr &analyzer_options, Vec::new(), file_source, - params.manifest.as_ref(), + params.project_layout.clone(), |signal| { let current_diagnostic = signal.diagnostic(); diff --git a/crates/biome_service/src/file_handlers/json.rs b/crates/biome_service/src/file_handlers/json.rs index 1d7ffa303047..d9c905ce543a 100644 --- a/crates/biome_service/src/file_handlers/json.rs +++ b/crates/biome_service/src/file_handlers/json.rs @@ -348,7 +348,7 @@ fn lint(params: LintParams) -> LintResults { .with_skip(¶ms.skip) .with_path(params.path.as_path()) .with_enabled_rules(¶ms.enabled_rules) - .with_manifest(params.manifest.as_ref()) + .with_project_layout(params.project_layout.clone()) .finish(); let filter = AnalysisFilter { @@ -390,7 +390,7 @@ fn code_actions(params: CodeActionsParams) -> PullActionsResult { range, workspace, path, - manifest, + project_layout, language, skip, only, @@ -412,7 +412,7 @@ fn code_actions(params: CodeActionsParams) -> PullActionsResult { .with_skip(&skip) .with_path(path.as_path()) .with_enabled_rules(&rules) - .with_manifest(manifest.as_ref()) + .with_project_layout(project_layout) .finish(); let filter = AnalysisFilter { @@ -474,7 +474,7 @@ fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceError> { .with_skip(¶ms.skip) .with_path(params.biome_path.as_path()) .with_enabled_rules(¶ms.enabled_rules) - .with_manifest(params.manifest.as_ref()) + .with_project_layout(params.project_layout) .finish(); let filter = AnalysisFilter { diff --git a/crates/biome_service/src/file_handlers/mod.rs b/crates/biome_service/src/file_handlers/mod.rs index b3fe1fc21595..365bb3ed7785 100644 --- a/crates/biome_service/src/file_handlers/mod.rs +++ b/crates/biome_service/src/file_handlers/mod.rs @@ -37,8 +37,8 @@ use biome_js_syntax::{ }; use biome_json_analyze::METADATA as json_metadata; use biome_json_syntax::{JsonFileSource, JsonLanguage}; -use biome_package::PackageJson; use biome_parser::AnyParse; +use biome_project_layout::ProjectLayout; use biome_rowan::{FileSourceError, NodeCache}; use biome_string_case::StrLikeExtension; @@ -48,6 +48,7 @@ use html::HtmlFileHandler; pub use javascript::JsFormatterSettings; use rustc_hash::FxHashSet; use std::borrow::Cow; +use std::sync::Arc; use tracing::instrument; mod astro; @@ -387,7 +388,7 @@ pub struct FixAllParams<'a> { /// Whether it should format the code action pub(crate) should_format: bool, pub(crate) biome_path: &'a BiomePath, - pub(crate) manifest: Option<PackageJson>, + pub(crate) project_layout: Arc<ProjectLayout>, pub(crate) document_file_source: DocumentFileSource, pub(crate) only: Vec<RuleSelector>, pub(crate) skip: Vec<RuleSelector>, @@ -450,7 +451,7 @@ pub(crate) struct LintParams<'a> { pub(crate) only: Vec<RuleSelector>, pub(crate) skip: Vec<RuleSelector>, pub(crate) categories: RuleCategories, - pub(crate) manifest: Option<PackageJson>, + pub(crate) project_layout: Arc<ProjectLayout>, pub(crate) suppression_reason: Option<String>, pub(crate) enabled_rules: Vec<RuleSelector>, } @@ -573,7 +574,7 @@ pub(crate) struct CodeActionsParams<'a> { pub(crate) range: Option<TextRange>, pub(crate) workspace: &'a WorkspaceSettingsHandle, pub(crate) path: &'a BiomePath, - pub(crate) manifest: Option<PackageJson>, + pub(crate) project_layout: Arc<ProjectLayout>, pub(crate) language: DocumentFileSource, pub(crate) only: Vec<RuleSelector>, pub(crate) skip: Vec<RuleSelector>, @@ -791,7 +792,7 @@ pub(crate) fn search( ) -> Result<Vec<TextRange>, WorkspaceError> { let result = query .execute(GritTargetFile { - path: path.as_std_path().to_path_buf(), + path: path.to_path_buf(), parse, }) .map_err(|err| { @@ -936,7 +937,7 @@ struct LintVisitor<'a, 'b> { skip: Option<&'b [RuleSelector]>, settings: Option<&'b Settings>, path: Option<&'b Utf8Path>, - manifest: Option<&'b PackageJson>, + project_layout: Arc<ProjectLayout>, analyzer_options: &'b mut AnalyzerOptions, } @@ -946,7 +947,7 @@ impl<'a, 'b> LintVisitor<'a, 'b> { skip: Option<&'b [RuleSelector]>, settings: Option<&'b Settings>, path: Option<&'b Utf8Path>, - manifest: Option<&'b PackageJson>, + project_layout: Arc<ProjectLayout>, analyzer_options: &'b mut AnalyzerOptions, ) -> Self { Self { @@ -956,7 +957,7 @@ impl<'a, 'b> LintVisitor<'a, 'b> { skip, settings, path, - manifest, + project_layout, analyzer_options, } } @@ -977,7 +978,10 @@ impl<'a, 'b> LintVisitor<'a, 'b> { .is_none_or(|d| d.is_empty()); if no_only && no_domains { - if let Some(manifest) = self.manifest { + if let Some((_, manifest)) = self + .path + .and_then(|path| self.project_layout.get_node_manifest_for_path(path)) + { for domain in R::METADATA.domains { self.analyzer_options .push_globals(domain.globals().iter().map(|s| Box::from(*s)).collect()); @@ -1361,7 +1365,7 @@ pub(crate) struct AnalyzerVisitorBuilder<'a> { skip: Option<&'a [RuleSelector]>, path: Option<&'a Utf8Path>, enabled_rules: Option<&'a [RuleSelector]>, - manifest: Option<&'a PackageJson>, + project_layout: Arc<ProjectLayout>, analyzer_options: AnalyzerOptions, } @@ -1373,7 +1377,7 @@ impl<'b> AnalyzerVisitorBuilder<'b> { skip: None, path: None, enabled_rules: None, - manifest: None, + project_layout: Default::default(), analyzer_options, } } @@ -1403,8 +1407,8 @@ impl<'b> AnalyzerVisitorBuilder<'b> { } #[must_use] - pub(crate) fn with_manifest(mut self, manifest: Option<&'b PackageJson>) -> Self { - self.manifest = manifest; + pub(crate) fn with_project_layout(mut self, project_layout: Arc<ProjectLayout>) -> Self { + self.project_layout = project_layout; self } @@ -1434,7 +1438,7 @@ impl<'b> AnalyzerVisitorBuilder<'b> { self.skip, self.settings, self.path, - self.manifest, + self.project_layout, &mut analyzer_options, ); diff --git a/crates/biome_service/src/lib.rs b/crates/biome_service/src/lib.rs index 754fe6c435a4..1ae2c352a94d 100644 --- a/crates/biome_service/src/lib.rs +++ b/crates/biome_service/src/lib.rs @@ -2,7 +2,6 @@ pub mod documentation; pub mod file_handlers; pub mod matcher; -pub mod project_layout; pub mod projects; pub mod settings; pub mod workspace; diff --git a/crates/biome_service/src/project_layout.rs b/crates/biome_service/src/project_layout.rs deleted file mode 100644 index a9cfa3a8d06a..000000000000 --- a/crates/biome_service/src/project_layout.rs +++ /dev/null @@ -1,106 +0,0 @@ -use biome_package::{NodeJsPackage, Package, PackageJson}; -use biome_parser::AnyParse; -use camino::{Utf8Path, Utf8PathBuf}; -use papaya::HashMap; -use rustc_hash::FxBuildHasher; - -/// The layout used across all open projects. -/// -/// Projects are comprised of zero or more packages. This arrangement is -/// intended to reflect the common usage of monorepos, where a single repository -/// may host many packages, and each package is allowed to have its own -/// settings. -/// -/// For Biome, a project is where the **top-level** configuration file is, while -/// packages may have their own nested configuration files. -/// -/// As a data structure, the project layout is simply a flat mapping from paths -/// to package data. This means that in order to lookup the package that is -/// most relevant for a given file, we may need to do a dumb iteration over all -/// entries to find which is the closest match. This means performance becomes -/// O(N) with the number of open packages, so if this becomes a bottleneck, we -/// may want to reconsider this approach. For now though, it makes sense because -/// it makes it very easy for us to invalidate part of the layout when there are -/// file system changes. -#[derive(Debug, Default)] -pub struct ProjectLayout(HashMap<Utf8PathBuf, PackageData, FxBuildHasher>); - -/// The information tracked for each package. -/// -/// Because Biome is intended to support multiple kinds of JavaScript projects, -/// the term "package" is somewhat loosely defined. It may be an NPM package, -/// a JSR package, or simply a directory with its own nested `biome.json`. -#[derive(Debug, Default)] -pub struct PackageData { - /// The settings of the package. - /// - /// Usually inferred from a configuration file, e.g. `biome.json`. - // TODO: Uncomment this. - // Probably best done when Ema has finished with https://github.com/biomejs/biome/pull/4845 - //settings: Settings, - - /// Optional Node.js-specific package information, if relevant for the - /// package. - node_package: Option<NodeJsPackage>, -} - -impl ProjectLayout { - pub fn get_node_manifest_for_path(&self, path: &Utf8Path) -> Option<PackageJson> { - self.0 - .pin() - .iter() - .fold( - None::<(&Utf8PathBuf, PackageJson)>, - |result, (package_path, data)| { - let node_manifest = data - .node_package - .as_ref() - .map(|node_package| &node_package.manifest)?; - if path.strip_prefix(package_path).is_err() { - return None; - } - - result - .is_none_or(|(matched_package_path, _)| { - package_path.as_str().len() > matched_package_path.as_str().len() - }) - .then(|| (package_path, node_manifest.clone())) - }, - ) - .map(|(_, package_json)| package_json) - } - - pub fn insert_node_manifest(&self, path: Utf8PathBuf, manifest: AnyParse) { - self.0.pin().update_or_insert_with( - path, - |data| { - let mut node_js_package = NodeJsPackage { - manifest: Default::default(), - diagnostics: Default::default(), - tsconfig: data - .node_package - .as_ref() - .map(|package| package.tsconfig.clone()) - .unwrap_or_default(), - }; - node_js_package.deserialize_manifest(&manifest.tree()); - - PackageData { - node_package: Some(node_js_package), - } - }, - || { - let mut node_js_package = NodeJsPackage::default(); - node_js_package.deserialize_manifest(&manifest.tree()); - - PackageData { - node_package: Some(node_js_package), - } - }, - ); - } - - pub fn remove_package(&self, path: &Utf8Path) { - self.0.pin().remove(path); - } -} diff --git a/crates/biome_service/src/workspace/scanner.rs b/crates/biome_service/src/workspace/scanner.rs index 164e7eed9232..ef727f8d3a49 100644 --- a/crates/biome_service/src/workspace/scanner.rs +++ b/crates/biome_service/src/workspace/scanner.rs @@ -185,7 +185,7 @@ impl<'app> TraversalContext for ScanContext<'app> { } fn can_handle(&self, path: &BiomePath) -> bool { - DocumentFileSource::try_from_path(path).is_ok() + path.is_dir() || DocumentFileSource::try_from_path(path).is_ok() } fn handle_path(&self, path: BiomePath) { diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs index d85c07cc75af..d2a4722952c3 100644 --- a/crates/biome_service/src/workspace/server.rs +++ b/crates/biome_service/src/workspace/server.rs @@ -14,7 +14,6 @@ use crate::file_handlers::{ Capabilities, CodeActionsParams, DocumentFileSource, FixAllParams, LintParams, ParseResult, }; use crate::is_dir; -use crate::project_layout::ProjectLayout; use crate::projects::Projects; use crate::workspace::{ FileFeaturesResult, GetFileContentParams, IsPathIgnoredParams, RageEntry, RageParams, @@ -35,15 +34,16 @@ use biome_grit_patterns::{compile_pattern_with_options, CompilePatternOptions, G use biome_js_syntax::ModuleKind; use biome_json_parser::JsonParserOptions; use biome_json_syntax::JsonFileSource; -use biome_package::{PackageJson, PackageType}; +use biome_package::PackageType; use biome_parser::AnyParse; +use biome_project_layout::ProjectLayout; use biome_rowan::NodeCache; use camino::{Utf8Path, Utf8PathBuf}; use papaya::HashMap; use rustc_hash::{FxBuildHasher, FxHashMap}; use std::panic::RefUnwindSafe; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; use tracing::{debug, info, info_span, warn}; pub(super) struct WorkspaceServer { @@ -55,7 +55,7 @@ pub(super) struct WorkspaceServer { projects: Projects, /// The layout of projects and their internal packages. - project_layout: ProjectLayout, + project_layout: Arc<ProjectLayout>, /// Stores the document (text content + version number) associated with a URL documents: HashMap<Utf8PathBuf, Document, FxBuildHasher>, @@ -238,20 +238,6 @@ impl WorkspaceServer { } } - /// Returns the parsed `package.json` for a given path. - /// - /// ## Errors - /// - /// - If no document is found in the workspace. Usually, you'll have to call - /// [WorkspaceServer::set_manifest_for_project] to store said document. - #[tracing::instrument(level = "trace", skip(self))] - fn get_node_manifest_for_path( - &self, - path: &Utf8Path, - ) -> Result<Option<PackageJson>, WorkspaceError> { - Ok(self.project_layout.get_node_manifest_for_path(path)) - } - /// Returns a previously inserted file source by index. /// /// File sources can be inserted using `insert_source()`. @@ -299,10 +285,19 @@ impl WorkspaceServer { ) -> Result<(), WorkspaceError> { let path: Utf8PathBuf = path.into(); let mut source = document_file_source.unwrap_or(DocumentFileSource::from_path(&path)); - let manifest = self.get_node_manifest_for_path(&path)?; + let manifest = if opened_by_scanner { + // FIXME: It doesn't make sense to retrieve the manifest when the + // file is opened by the scanner, because it means the + // project layout isn't yet initialized anyway. But that + // highlights an issue with the CommonJS check below, since + // we can't seem to set this correctly now. + None + } else { + self.project_layout.get_node_manifest_for_path(&path) + }; if let DocumentFileSource::Js(js) = &mut source { - if let Some(manifest) = manifest { + if let Some((_, manifest)) = manifest { if manifest.r#type == Some(PackageType::Commonjs) && js.file_extension() == "js" { js.set_module_kind(ModuleKind::Script); } @@ -526,7 +521,7 @@ impl WorkspaceServer { .ok_or_else(WorkspaceError::not_found)?; let parsed = self.get_parse(path)?; self.project_layout - .insert_node_manifest(package_path, parsed); + .insert_serialized_node_manifest(package_path, parsed); } Ok(()) @@ -853,7 +848,6 @@ impl Workspace for WorkspaceServer { }: PullDiagnosticsParams, ) -> Result<PullDiagnosticsResult, WorkspaceError> { let parse = self.get_parse(&path)?; - let manifest = self.get_node_manifest_for_path(&path)?; let (diagnostics, errors, skipped_diagnostics) = if let Some(lint) = self.get_file_capabilities(&path).analyzer.lint { info_span!("Pulling diagnostics", categories =? categories).in_scope(|| { @@ -866,7 +860,7 @@ impl Workspace for WorkspaceServer { skip, language: self.get_file_source(&path), categories, - manifest, + project_layout: self.project_layout.clone(), suppression_reason: None, enabled_rules, }); @@ -923,14 +917,13 @@ impl Workspace for WorkspaceServer { .ok_or_else(self.build_capability_error(&path))?; let parse = self.get_parse(&path)?; - let manifest = self.get_node_manifest_for_path(&path)?; let language = self.get_file_source(&path); Ok(code_actions(CodeActionsParams { parse, range, workspace: &self.projects.get_settings(project_key).into(), path: &path, - manifest, + project_layout: self.project_layout.clone(), language, only, skip, @@ -1030,7 +1023,6 @@ impl Workspace for WorkspaceServer { .ok_or_else(self.build_capability_error(&path))?; let parse = self.get_parse(&path)?; - let manifest = self.get_node_manifest_for_path(&path)?; let language = self.get_file_source(&path); fix_all(FixAllParams { parse, @@ -1038,7 +1030,7 @@ impl Workspace for WorkspaceServer { workspace: self.projects.get_settings(project_key).into(), should_format, biome_path: &path, - manifest, + project_layout: self.project_layout.clone(), document_file_source: language, only, skip, diff --git a/crates/biome_test_utils/Cargo.toml b/crates/biome_test_utils/Cargo.toml index 6ac61a65b172..37064da24c26 100644 --- a/crates/biome_test_utils/Cargo.toml +++ b/crates/biome_test_utils/Cargo.toml @@ -14,21 +14,22 @@ version = "0.0.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -biome_analyze = { workspace = true } -biome_configuration = { workspace = true } -biome_console = { workspace = true } -biome_deserialize = { workspace = true } -biome_diagnostics = { workspace = true } -biome_formatter = { workspace = true } -biome_json_parser = { workspace = true } -biome_package = { workspace = true } -biome_rowan = { workspace = true } -biome_service = { workspace = true } -camino = { workspace = true } -countme = { workspace = true, features = ["enable"] } -json_comments = "0.2.2" -serde_json = { workspace = true } -similar = { workspace = true } +biome_analyze = { workspace = true } +biome_configuration = { workspace = true } +biome_console = { workspace = true } +biome_deserialize = { workspace = true } +biome_diagnostics = { workspace = true } +biome_formatter = { workspace = true } +biome_json_parser = { workspace = true } +biome_package = { workspace = true } +biome_project_layout = { workspace = true } +biome_rowan = { workspace = true } +biome_service = { workspace = true } +camino = { workspace = true } +countme = { workspace = true, features = ["enable"] } +json_comments = "0.2.2" +serde_json = { workspace = true } +similar = { workspace = true } [lints] workspace = true diff --git a/crates/biome_test_utils/src/lib.rs b/crates/biome_test_utils/src/lib.rs index 2c50e80b7d0a..69ff36973ccc 100644 --- a/crates/biome_test_utils/src/lib.rs +++ b/crates/biome_test_utils/src/lib.rs @@ -7,6 +7,7 @@ use biome_diagnostics::termcolor::Buffer; use biome_diagnostics::{DiagnosticExt, Error, PrintDiagnostic}; use biome_json_parser::{JsonParserOptions, ParseDiagnostic}; use biome_package::PackageJson; +use biome_project_layout::ProjectLayout; use biome_rowan::{SyntaxKind, SyntaxNode, SyntaxSlot}; use biome_service::configuration::to_analyzer_rules; use biome_service::file_handlers::DocumentFileSource; @@ -17,7 +18,7 @@ use json_comments::StripComments; use similar::{DiffableStr, TextDiff}; use std::ffi::c_int; use std::fmt::Write; -use std::sync::Once; +use std::sync::{Arc, Once}; pub fn scripts_from_json(extension: &str, input_code: &str) -> Option<Vec<String>> { if extension == "json" || extension == "jsonc" { @@ -157,7 +158,10 @@ where } } -pub fn load_manifest(input_file: &Utf8Path, diagnostics: &mut Vec<String>) -> Option<PackageJson> { +pub fn project_layout_with_node_manifest( + input_file: &Utf8Path, + diagnostics: &mut Vec<String>, +) -> Arc<ProjectLayout> { let options_file = input_file.with_extension("package.json"); if let Ok(json) = std::fs::read_to_string(options_file.clone()) { let deserialized = biome_deserialize::json::deserialize_from_json_str::<PackageJson>( @@ -176,10 +180,15 @@ pub fn load_manifest(input_file: &Utf8Path, diagnostics: &mut Vec<String>) -> Op .collect::<Vec<_>>(), ); } else { - return deserialized.into_deserialized(); + let project_layout = ProjectLayout::default(); + project_layout.insert_node_manifest( + Utf8PathBuf::new(), + deserialized.into_deserialized().unwrap_or_default(), + ); + return Arc::new(project_layout); } } - None + Default::default() } pub fn diagnostic_to_string(name: &str, source: &str, diag: Error) -> String { diff --git a/knope.toml b/knope.toml index 90da859221cc..c59fe4c448df 100644 --- a/knope.toml +++ b/knope.toml @@ -230,6 +230,10 @@ versioned_files = ["crates/biome_glob/Cargo.toml"] changelog = "crates/biome_syntax_codegen/CHANGELOG.md" versioned_files = ["crates/biome_syntax_codegen/Cargo.toml"] +[packages.biome_project_layout] +changelog = "crates/biome_project_layout/CHANGELOG.md" +versioned_files = ["crates/biome_project_layout/Cargo.toml"] + ## End of crates. DO NOT CHANGE! # Workflow to create a changeset diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72da6d163350..f2d95c0f3d3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,9 @@ importers: '@typescript-eslint/eslint-plugin': specifier: 8.18.1 version: 8.18.1(@typescript-eslint/parser@8.3.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2))(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2) + '@typescript-eslint/parser': + specifier: 8.3.0 + version: 8.3.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2) dprint: specifier: 0.48.0 version: 0.48.0 diff --git a/xtask/bench/src/language.rs b/xtask/bench/src/language.rs index 963bed655d27..cc919b8cfb5c 100644 --- a/xtask/bench/src/language.rs +++ b/xtask/bench/src/language.rs @@ -200,7 +200,7 @@ impl Analyze { &options, Vec::new(), JsFileSource::default(), - None, + Default::default(), |event| { black_box(event.diagnostic()); black_box(event.actions()); diff --git a/xtask/rules_check/src/lib.rs b/xtask/rules_check/src/lib.rs index 5bb533c8b2ef..f20ab885c2bc 100644 --- a/xtask/rules_check/src/lib.rs +++ b/xtask/rules_check/src/lib.rs @@ -432,7 +432,7 @@ fn assert_lint( &options, vec![], file_source, - None, + Default::default(), |signal| { if let Some(mut diag) = signal.diagnostic() { for action in signal.actions() {