From 0e97636fc75405b61d493f163128628ba37afd71 Mon Sep 17 00:00:00 2001 From: "Arend van Beelen jr." Date: Wed, 19 Feb 2025 18:32:00 +0100 Subject: [PATCH 1/2] Implement plugin loading --- .github/workflows/benchmark.yml | 1 + Cargo.lock | 3 + crates/biome_analyze/src/analyzer_plugin.rs | 10 +- crates/biome_analyze/src/lib.rs | 7 +- crates/biome_cli/src/commands/mod.rs | 5 +- crates/biome_configuration/src/plugins.rs | 11 +- crates/biome_css_analyze/src/lib.rs | 18 +- crates/biome_css_analyze/tests/spec_tests.rs | 13 +- crates/biome_js_analyze/src/lib.rs | 40 +-- crates/biome_js_analyze/tests/quick_test.rs | 29 +- crates/biome_js_analyze/tests/spec_tests.rs | 13 +- crates/biome_plugin_loader/Cargo.toml | 4 +- .../src/analyzer_grit_plugin.rs | 12 +- crates/biome_plugin_loader/src/diagnostics.rs | 6 + crates/biome_plugin_loader/src/lib.rs | 94 +++--- .../biome_plugin_loader/src/plugin_cache.rs | 29 ++ crates/biome_service/Cargo.toml | 1 + crates/biome_service/src/file_handlers/css.rs | 86 ++--- .../src/file_handlers/graphql.rs | 1 + .../src/file_handlers/javascript.rs | 9 +- .../biome_service/src/file_handlers/json.rs | 1 + crates/biome_service/src/file_handlers/mod.rs | 8 +- crates/biome_service/src/settings.rs | 8 + crates/biome_service/src/workspace.rs | 12 +- crates/biome_service/src/workspace/client.rs | 7 +- crates/biome_service/src/workspace/server.rs | 66 +++- ...s_are_loaded_and_used_during_analysis.snap | 40 +++ crates/biome_service/tests/workspace.rs | 68 +++- crates/biome_wasm/src/lib.rs | 10 +- .../@biomejs/backend-jsonrpc/src/workspace.ts | 305 +++++++++--------- xtask/bench/src/language.rs | 4 +- xtask/rules_check/src/lib.rs | 4 +- 32 files changed, 602 insertions(+), 323 deletions(-) create mode 100644 crates/biome_plugin_loader/src/plugin_cache.rs create mode 100644 crates/biome_service/tests/snapshots/workspace__test__plugins_are_loaded_and_used_during_analysis.snap diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index b0c9f218a90e..46773c582cef 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -50,6 +50,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Compile + timeout-minutes: 15 run: cargo codspeed build --features codspeed -p xtask_bench - name: Run the benchmarks diff --git a/Cargo.lock b/Cargo.lock index aa903366dbfe..42c3f9f7714b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1218,6 +1218,8 @@ dependencies = [ "grit-pattern-matcher", "grit-util", "insta", + "papaya", + "rustc-hash 2.1.1", "serde", ] @@ -1290,6 +1292,7 @@ dependencies = [ "biome_json_syntax", "biome_package", "biome_parser", + "biome_plugin_loader", "biome_project_layout", "biome_rowan", "biome_string_case", diff --git a/crates/biome_analyze/src/analyzer_plugin.rs b/crates/biome_analyze/src/analyzer_plugin.rs index e7b59220ea27..c8b2edca446a 100644 --- a/crates/biome_analyze/src/analyzer_plugin.rs +++ b/crates/biome_analyze/src/analyzer_plugin.rs @@ -1,10 +1,16 @@ use crate::RuleDiagnostic; use biome_parser::AnyParse; use camino::Utf8PathBuf; -use std::fmt::Debug; +use std::{fmt::Debug, sync::Arc}; + +/// Slice of analyzer plugins that can be cheaply cloned. +pub type AnalyzerPluginSlice<'a> = &'a [Arc>]; + +/// Vector of analyzer plugins that can be cheaply cloned. +pub type AnalyzerPluginVec = Vec>>; /// Definition of an analyzer plugin. -pub trait AnalyzerPlugin: Debug { +pub trait AnalyzerPlugin: Debug + Send + Sync { fn evaluate(&self, root: AnyParse, path: Utf8PathBuf) -> Vec; fn supports_css(&self) -> bool; diff --git a/crates/biome_analyze/src/lib.rs b/crates/biome_analyze/src/lib.rs index 21d7831aa0cf..e567a77b407d 100644 --- a/crates/biome_analyze/src/lib.rs +++ b/crates/biome_analyze/src/lib.rs @@ -5,6 +5,7 @@ use biome_parser::AnyParse; use std::collections::{BTreeMap, BinaryHeap}; use std::fmt::{Debug, Display, Formatter}; use std::ops; +use std::sync::Arc; mod analyzer_plugin; mod categories; @@ -25,7 +26,7 @@ mod visitor; // Re-exported for use in the `declare_group` macro pub use biome_diagnostics::category_concat; -pub use crate::analyzer_plugin::AnalyzerPlugin; +pub use crate::analyzer_plugin::{AnalyzerPlugin, AnalyzerPluginSlice, AnalyzerPluginVec}; pub use crate::categories::{ ActionCategory, OtherActionCategory, RefactorKind, RuleCategories, RuleCategoriesBuilder, RuleCategory, SourceActionKind, SUPPRESSION_INLINE_ACTION_CATEGORY, @@ -72,7 +73,7 @@ pub struct Analyzer<'analyzer, L: Language, Matcher, Break, Diag> { /// List of visitors being run by this instance of the analyzer for each phase phases: BTreeMap + 'analyzer>>>, /// Plugins to be run after the phases for built-in rules. - plugins: Vec>, + plugins: AnalyzerPluginVec, /// Holds the metadata for all the rules statically known to the analyzer metadata: &'analyzer MetadataRegistry, /// Executor for the query matches emitted by the visitors @@ -128,7 +129,7 @@ where } /// Registers an [AnalyzerPlugin] to be executed after the regular phases. - pub fn add_plugin(&mut self, plugin: Box) { + pub fn add_plugin(&mut self, plugin: Arc>) { self.plugins.push(plugin); } diff --git a/crates/biome_cli/src/commands/mod.rs b/crates/biome_cli/src/commands/mod.rs index a0abb5ee7a6f..be5857bdceae 100644 --- a/crates/biome_cli/src/commands/mod.rs +++ b/crates/biome_cli/src/commands/mod.rs @@ -801,13 +801,16 @@ pub(crate) trait CommandRunner: Sized { open_uninitialized: true, })?; - workspace.update_settings(UpdateSettingsParams { + let result = workspace.update_settings(UpdateSettingsParams { project_key, workspace_directory: configuration_path.map(BiomePath::from), configuration, vcs_base_path: vcs_base_path.map(BiomePath::from), gitignore_matches, })?; + for diagnostic in result.diagnostics { + console.log(markup! {{PrintDiagnostic::simple(&diagnostic)}}); + } let execution = self.get_execution(cli_options, console, workspace, project_key)?; diff --git a/crates/biome_configuration/src/plugins.rs b/crates/biome_configuration/src/plugins.rs index 18528637705e..1a7e2ba9e132 100644 --- a/crates/biome_configuration/src/plugins.rs +++ b/crates/biome_configuration/src/plugins.rs @@ -10,6 +10,12 @@ use std::str::FromStr; #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Plugins(pub Vec); +impl Plugins { + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } +} + impl FromStr for Plugins { type Err = String; @@ -36,8 +42,9 @@ impl Deserializable for PluginConfiguration { Deserializable::deserialize(ctx, value, rule_name).map(Self::Path) } else { // TODO: Fix this to allow plugins to receive options. - // Difficulty is that we need a `Deserializable` implementation - // for `serde_json::Value`, since plugin options are untyped. + // We probably need to pass them as `AnyJsonValue` or + // `biome_json_value::JsonValue`, since plugin options are + // untyped. // Also, we don't have a way to configure Grit plugins yet. /*Deserializable::deserialize(value, rule_name, diagnostics) .map(|plugin| Self::PathWithOptions(plugin))*/ diff --git a/crates/biome_css_analyze/src/lib.rs b/crates/biome_css_analyze/src/lib.rs index d473a8297767..9fbdd5cce399 100644 --- a/crates/biome_css_analyze/src/lib.rs +++ b/crates/biome_css_analyze/src/lib.rs @@ -11,7 +11,7 @@ mod utils; pub use crate::registry::visit_registry; use crate::suppression_action::CssSuppressionAction; use biome_analyze::{ - to_analyzer_suppressions, AnalysisFilter, AnalyzerOptions, AnalyzerPlugin, AnalyzerSignal, + to_analyzer_suppressions, AnalysisFilter, AnalyzerOptions, AnalyzerPluginSlice, AnalyzerSignal, AnalyzerSuppression, ControlFlow, LanguageRoot, MatchQueryParams, MetadataRegistry, RuleAction, RuleRegistry, }; @@ -36,7 +36,7 @@ pub fn analyze<'a, F, B>( root: &LanguageRoot, filter: AnalysisFilter, options: &'a AnalyzerOptions, - plugins: Vec>, + plugins: AnalyzerPluginSlice<'a>, emit_signal: F, ) -> (Option, Vec) where @@ -57,7 +57,7 @@ pub fn analyze_with_inspect_matcher<'a, V, F, B>( filter: AnalysisFilter, inspect_matcher: V, options: &'a AnalyzerOptions, - plugins: Vec>, + plugins: AnalyzerPluginSlice<'a>, mut emit_signal: F, ) -> (Option, Vec) where @@ -111,7 +111,7 @@ where for plugin in plugins { if plugin.supports_css() { - analyzer.add_plugin(plugin); + analyzer.add_plugin(plugin.clone()); } } @@ -189,7 +189,7 @@ mod tests { ..AnalysisFilter::default() }, &options, - Vec::new(), + &[], |signal| { if let Some(diag) = signal.diagnostic() { error_ranges.push(diag.location().span.unwrap()); @@ -234,7 +234,7 @@ mod tests { }; let options = AnalyzerOptions::default(); - analyze(&parsed.tree(), filter, &options, Vec::new(), |signal| { + analyze(&parsed.tree(), filter, &options, &[], |signal| { if let Some(diag) = signal.diagnostic() { let error = diag .with_file_path("dummyFile") @@ -274,7 +274,7 @@ a { }; let options = AnalyzerOptions::default(); - analyze(&parsed.tree(), filter, &options, Vec::new(), |signal| { + analyze(&parsed.tree(), filter, &options, &[], |signal| { if let Some(diag) = signal.diagnostic() { let error = diag .with_file_path("dummyFile") @@ -310,7 +310,7 @@ a { }; let options = AnalyzerOptions::default(); - analyze(&parsed.tree(), filter, &options, Vec::new(), |signal| { + analyze(&parsed.tree(), filter, &options, &[], |signal| { if let Some(diag) = signal.diagnostic() { let error = diag .with_file_path("dummyFile") @@ -343,7 +343,7 @@ a { }; let options = AnalyzerOptions::default(); - analyze(&parsed.tree(), filter, &options, Vec::new(), |signal| { + analyze(&parsed.tree(), filter, &options, &[], |signal| { if let Some(diag) = signal.diagnostic() { let code = diag.category().unwrap(); if code != category!("suppressions/unused") { diff --git a/crates/biome_css_analyze/tests/spec_tests.rs b/crates/biome_css_analyze/tests/spec_tests.rs index 9b873185951f..02f38d5ec322 100644 --- a/crates/biome_css_analyze/tests/spec_tests.rs +++ b/crates/biome_css_analyze/tests/spec_tests.rs @@ -1,5 +1,5 @@ use biome_analyze::{ - AnalysisFilter, AnalyzerAction, AnalyzerPlugin, ControlFlow, Never, RuleFilter, + AnalysisFilter, AnalyzerAction, AnalyzerPluginSlice, ControlFlow, Never, RuleFilter, }; use biome_css_parser::{parse_css, CssParserOptions}; use biome_css_syntax::{CssFileSource, CssLanguage}; @@ -14,6 +14,7 @@ use biome_test_utils::{ }; use camino::Utf8Path; use std::ops::Deref; +use std::sync::Arc; use std::{fs::read_to_string, slice}; tests_macros::gen_tests! {"tests/specs/**/*.{css,json,jsonc}", crate::run_test, "module"} @@ -72,7 +73,7 @@ fn run_test(input: &'static str, _: &str, _: &str, _: &str) { input_file, CheckActionType::Lint, parser_options, - Vec::new(), + &[], ); } @@ -90,7 +91,7 @@ fn run_test(input: &'static str, _: &str, _: &str, _: &str) { input_file, CheckActionType::Lint, parser_options, - Vec::new(), + &[], ) }; @@ -116,7 +117,7 @@ pub(crate) fn analyze_and_snap( input_file: &Utf8Path, check_action_type: CheckActionType, parser_options: CssParserOptions, - plugins: Vec>, + plugins: AnalyzerPluginSlice, ) -> usize { let parsed = parse_css(input_code, parser_options); let root = parsed.tree(); @@ -241,7 +242,7 @@ pub(crate) fn run_suppression_test(input: &'static str, _: &str, _: &str, _: &st input_file, CheckActionType::Suppression, CssParserOptions::default(), - Vec::new(), + &[], ); insta::with_settings!({ @@ -288,7 +289,7 @@ fn run_plugin_test(input: &'static str, _: &str, _: &str, _: &str) { &input_path, CheckActionType::Lint, CssParserOptions::default(), - vec![Box::new(plugin)], + &[Arc::new(Box::new(plugin))], ); insta::with_settings!({ diff --git a/crates/biome_js_analyze/src/lib.rs b/crates/biome_js_analyze/src/lib.rs index 799dfafb790e..083b00721860 100644 --- a/crates/biome_js_analyze/src/lib.rs +++ b/crates/biome_js_analyze/src/lib.rs @@ -3,8 +3,8 @@ use crate::suppression_action::JsSuppressionAction; use biome_analyze::{ to_analyzer_suppressions, AnalysisFilter, Analyzer, AnalyzerContext, AnalyzerOptions, - AnalyzerPlugin, AnalyzerSignal, AnalyzerSuppression, ControlFlow, InspectMatcher, LanguageRoot, - MatchQueryParams, MetadataRegistry, RuleAction, RuleRegistry, + AnalyzerPluginSlice, AnalyzerSignal, AnalyzerSuppression, ControlFlow, InspectMatcher, + LanguageRoot, MatchQueryParams, MetadataRegistry, RuleAction, RuleRegistry, }; use biome_aria::AriaRoles; use biome_dependency_graph::DependencyGraph; @@ -74,7 +74,7 @@ pub fn analyze_with_inspect_matcher<'a, V, F, B>( filter: AnalysisFilter, inspect_matcher: V, options: &'a AnalyzerOptions, - plugins: Vec>, + plugins: AnalyzerPluginSlice<'a>, services: JsAnalyzerServices, mut emit_signal: F, ) -> (Option, Vec) @@ -135,7 +135,7 @@ where for plugin in plugins { if plugin.supports_js() { - analyzer.add_plugin(plugin); + analyzer.add_plugin(plugin.clone()); } } @@ -167,7 +167,7 @@ pub fn analyze<'a, F, B>( root: &LanguageRoot, filter: AnalysisFilter, options: &'a AnalyzerOptions, - plugins: Vec>, + plugins: AnalyzerPluginSlice<'a>, services: JsAnalyzerServices, emit_signal: F, ) -> (Option, Vec) @@ -234,7 +234,7 @@ let bar = 33; ..AnalysisFilter::default() }, &options, - Vec::new(), + &[], services, |signal| { if let Some(diag) = signal.diagnostic() { @@ -282,7 +282,7 @@ let bar = 33; &parsed.tree(), AnalysisFilter::default(), &options, - Vec::new(), + &[], Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { @@ -367,7 +367,7 @@ let bar = 33; &parsed.tree(), AnalysisFilter::default(), &options, - Vec::new(), + &[], Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { @@ -438,7 +438,7 @@ let bar = 33; &parsed.tree(), filter, &options, - Vec::new(), + &[], Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { @@ -482,7 +482,7 @@ let bar = 33; &parsed.tree(), filter, &options, - Vec::new(), + &[], Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { @@ -533,7 +533,7 @@ debugger; &parsed.tree(), filter, &options, - Vec::new(), + &[], Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { @@ -579,7 +579,7 @@ debugger; &parsed.tree(), filter, &options, - Vec::new(), + &[], Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { @@ -627,7 +627,7 @@ debugger; &parsed.tree(), filter, &options, - Vec::new(), + &[], Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { @@ -676,7 +676,7 @@ let bar = 33; &parsed.tree(), filter, &options, - Vec::new(), + &[], Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { @@ -723,7 +723,7 @@ let bar = 33; &parsed.tree(), filter, &options, - Vec::new(), + &[], Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { @@ -773,7 +773,7 @@ let c; &parsed.tree(), filter, &options, - Vec::new(), + &[], Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { @@ -824,7 +824,7 @@ debugger; &parsed.tree(), filter, &options, - Vec::new(), + &[], Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { @@ -876,7 +876,7 @@ let d; &parsed.tree(), filter, &options, - Vec::new(), + &[], Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { @@ -919,7 +919,7 @@ const foo0 = function (bar: string) { let services = JsAnalyzerServices::from((Default::default(), Default::default(), JsFileSource::ts())); - analyze(&root, filter, &options, Vec::new(), services, |signal| { + analyze(&root, filter, &options, &[], services, |signal| { if let Some(diag) = signal.diagnostic() { let error = diag .with_file_path("dummyFile") @@ -963,7 +963,7 @@ a == b; &parsed.tree(), filter, &options, - Vec::new(), + &[], Default::default(), |signal| { if let Some(diag) = signal.diagnostic() { diff --git a/crates/biome_js_analyze/tests/quick_test.rs b/crates/biome_js_analyze/tests/quick_test.rs index 2c0010dd6342..8e0066143548 100644 --- a/crates/biome_js_analyze/tests/quick_test.rs +++ b/crates/biome_js_analyze/tests/quick_test.rs @@ -78,24 +78,23 @@ fn analyze( let services = JsAnalyzerServices::from((dependency_graph, project_layout, source_type)); - let (_, errors) = - biome_js_analyze::analyze(&root, filter, &options, Vec::new(), services, |event| { - if let Some(mut diag) = event.diagnostic() { - for action in event.actions() { - diag = diag.add_code_suggestion(CodeSuggestionAdvice::from(action)); - } - - let error = diag.with_severity(Severity::Warning); - diagnostics.push(diagnostic_to_string(file_name, input_code, error)); - return ControlFlow::Continue(()); - } - + let (_, errors) = biome_js_analyze::analyze(&root, filter, &options, &[], services, |event| { + if let Some(mut diag) = event.diagnostic() { for action in event.actions() { - code_fixes.push(code_fix_to_string(input_code, action)); + diag = diag.add_code_suggestion(CodeSuggestionAdvice::from(action)); } - ControlFlow::::Continue(()) - }); + let error = diag.with_severity(Severity::Warning); + diagnostics.push(diagnostic_to_string(file_name, input_code, error)); + return ControlFlow::Continue(()); + } + + for action in event.actions() { + code_fixes.push(code_fix_to_string(input_code, action)); + } + + ControlFlow::::Continue(()) + }); for error in errors { diagnostics.push(diagnostic_to_string(file_name, input_code, error)); diff --git a/crates/biome_js_analyze/tests/spec_tests.rs b/crates/biome_js_analyze/tests/spec_tests.rs index fb623937c32a..b3ef2d91565b 100644 --- a/crates/biome_js_analyze/tests/spec_tests.rs +++ b/crates/biome_js_analyze/tests/spec_tests.rs @@ -1,5 +1,5 @@ use biome_analyze::{ - AnalysisFilter, AnalyzerAction, AnalyzerPlugin, ControlFlow, Never, RuleFilter, + AnalysisFilter, AnalyzerAction, AnalyzerPluginSlice, ControlFlow, Never, RuleFilter, }; use biome_diagnostics::advice::CodeSuggestionAdvice; use biome_fs::OsFileSystem; @@ -17,6 +17,7 @@ use biome_test_utils::{ }; use camino::{Utf8Component, Utf8Path}; use std::ops::Deref; +use std::sync::Arc; use std::{fs::read_to_string, slice}; tests_macros::gen_tests! {"tests/specs/**/*.{cjs,cts,js,jsx,tsx,ts,json,jsonc,svelte}", crate::run_test, "module"} @@ -66,7 +67,7 @@ fn run_test(input: &'static str, _: &str, _: &str, _: &str) { input_file, CheckActionType::Lint, JsParserOptions::default(), - Vec::new(), + &[], ); } @@ -84,7 +85,7 @@ fn run_test(input: &'static str, _: &str, _: &str, _: &str) { input_file, CheckActionType::Lint, JsParserOptions::default(), - Vec::new(), + &[], ) }; @@ -110,7 +111,7 @@ pub(crate) fn analyze_and_snap( input_file: &Utf8Path, check_action_type: CheckActionType, parser_options: JsParserOptions, - plugins: Vec>, + plugins: AnalyzerPluginSlice, ) -> usize { let mut diagnostics = Vec::new(); let mut code_fixes = Vec::new(); @@ -304,7 +305,7 @@ pub(crate) fn run_suppression_test(input: &'static str, _: &str, _: &str, _: &st input_file, CheckActionType::Suppression, JsParserOptions::default(), - Vec::new(), + &[], ); insta::with_settings!({ @@ -351,7 +352,7 @@ fn run_plugin_test(input: &'static str, _: &str, _: &str, _: &str) { &input_path, CheckActionType::Lint, JsParserOptions::default(), - vec![Box::new(plugin)], + &[Arc::new(Box::new(plugin))], ); insta::with_settings!({ diff --git a/crates/biome_plugin_loader/Cargo.toml b/crates/biome_plugin_loader/Cargo.toml index 679b709cda6a..b1fb48cd7260 100644 --- a/crates/biome_plugin_loader/Cargo.toml +++ b/crates/biome_plugin_loader/Cargo.toml @@ -1,7 +1,7 @@ [package] authors.workspace = true categories.workspace = true -description = "biome_plugin_loader2" +description = "Functionality for loading plugins and caching them in memory" edition.workspace = true homepage.workspace = true keywords.workspace = true @@ -24,6 +24,8 @@ biome_rowan = { workspace = true } camino = { workspace = true } grit-pattern-matcher = { workspace = true } grit-util = { workspace = true } +papaya = { workspace = true } +rustc-hash = { workspace = true } serde = { workspace = true } [dev-dependencies] diff --git a/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs b/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs index d71af3a18263..105e2eb74f12 100644 --- a/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs +++ b/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs @@ -12,14 +12,14 @@ use biome_rowan::TextRange; use camino::{Utf8Path, Utf8PathBuf}; use grit_pattern_matcher::{binding::Binding, pattern::ResolvedPattern}; use grit_util::{error::GritPatternError, AnalysisLogs}; -use std::{borrow::Cow, fmt::Debug, rc::Rc}; +use std::{borrow::Cow, fmt::Debug}; use crate::{AnalyzerPlugin, PluginDiagnostic}; /// Definition of an analyzer plugin. -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct AnalyzerGritPlugin { - grit_query: Rc, + grit_query: GritQuery, } impl AnalyzerGritPlugin { @@ -39,11 +39,9 @@ impl AnalyzerGritPlugin { ) .as_predicate()]) .with_path(path); - let query = compile_pattern_with_options(&source, options)?; + let grit_query = compile_pattern_with_options(&source, options)?; - Ok(Self { - grit_query: Rc::new(query), - }) + Ok(Self { grit_query }) } } diff --git a/crates/biome_plugin_loader/src/diagnostics.rs b/crates/biome_plugin_loader/src/diagnostics.rs index 9b6d9d19fe19..eb4cde4c1d9d 100644 --- a/crates/biome_plugin_loader/src/diagnostics.rs +++ b/crates/biome_plugin_loader/src/diagnostics.rs @@ -100,6 +100,12 @@ impl std::fmt::Display for PluginDiagnostic { } } +impl From for biome_diagnostics::serde::Diagnostic { + fn from(error: PluginDiagnostic) -> Self { + biome_diagnostics::serde::Diagnostic::new(error) + } +} + #[derive(Debug, Serialize, Deserialize, Diagnostic)] #[diagnostic( category = "plugin", diff --git a/crates/biome_plugin_loader/src/lib.rs b/crates/biome_plugin_loader/src/lib.rs index 92e7d176811d..8b795dd7e98d 100644 --- a/crates/biome_plugin_loader/src/lib.rs +++ b/crates/biome_plugin_loader/src/lib.rs @@ -1,61 +1,47 @@ mod analyzer_grit_plugin; mod diagnostics; +mod plugin_cache; mod plugin_manifest; pub use analyzer_grit_plugin::AnalyzerGritPlugin; -use biome_analyze::AnalyzerPlugin; +pub use diagnostics::PluginDiagnostic; +pub use plugin_cache::*; + +use std::sync::Arc; + +use biome_analyze::{AnalyzerPlugin, AnalyzerPluginVec}; use biome_console::markup; use biome_deserialize::json::deserialize_from_json_str; -use biome_diagnostics::ResolveError; use biome_fs::FileSystem; use biome_json_parser::JsonParserOptions; -use camino::{Utf8Path, Utf8PathBuf}; -use diagnostics::PluginDiagnostic; +use camino::{Utf8Component, Utf8Path, Utf8PathBuf}; use plugin_manifest::PluginManifest; #[derive(Debug)] pub struct BiomePlugin { - pub analyzer_plugins: Vec>, + pub analyzer_plugins: AnalyzerPluginVec, } impl BiomePlugin { /// Loads a plugin from the given `plugin_path`. /// - /// Base paths are used to resolve relative paths and package specifiers. + /// The base path is used to resolve relative paths. pub fn load( fs: &dyn FileSystem, plugin_path: &str, - relative_resolution_base_path: &Utf8Path, - external_resolution_base_path: &Utf8Path, + base_path: &Utf8Path, ) -> Result { - let plugin_path = if let Some(plugin_path) = plugin_path.strip_prefix("./") { - relative_resolution_base_path.join(plugin_path) - } else if plugin_path.starts_with('.') { - relative_resolution_base_path.join(plugin_path) - } else { - Utf8PathBuf::from_path_buf( - fs.resolve_configuration(plugin_path, external_resolution_base_path) - .map_err(|error| { - PluginDiagnostic::cant_resolve( - external_resolution_base_path.to_string(), - Some(ResolveError::from(error)), - ) - })? - .into_path_buf(), - ) - .expect("Valid UTF-8 path") - }; + let plugin_path = normalize_path(&base_path.join(plugin_path)); // If the plugin path references a `.grit` file directly, treat it as // a single-rule plugin instead of going through the manifest process: if plugin_path - .as_os_str() - .as_encoded_bytes() - .ends_with(b".grit") + .extension() + .is_some_and(|extension| extension == "grit") { let plugin = AnalyzerGritPlugin::load(fs, &plugin_path)?; return Ok(Self { - analyzer_plugins: vec![Box::new(plugin) as Box], + analyzer_plugins: vec![Arc::new(Box::new(plugin) as Box)], }); } @@ -90,7 +76,7 @@ impl BiomePlugin { .map(|rule| { if rule.as_os_str().as_encoded_bytes().ends_with(b".grit") { let plugin = AnalyzerGritPlugin::load(fs, &plugin_path.join(rule))?; - Ok(Box::new(plugin) as Box) + Ok(Arc::new(Box::new(plugin) as Box)) } else { Err(PluginDiagnostic::unsupported_rule_format(markup!( "Unsupported rule format for plugin rule " @@ -105,6 +91,37 @@ impl BiomePlugin { } } +/// Normalizes the given `path` without requiring filesystem access. +fn normalize_path(path: &Utf8Path) -> Utf8PathBuf { + let mut stack = Vec::new(); + + for component in path.components() { + match component { + Utf8Component::ParentDir => { + if stack.last().is_some_and(|last| *last == "..") { + stack.push(".."); + } else { + stack.pop(); + } + } + Utf8Component::CurDir => {} + Utf8Component::RootDir => { + stack.clear(); + stack.push("/"); + } + Utf8Component::Normal(c) => stack.push(c), + _ => {} + } + } + + let mut result = Utf8PathBuf::new(); + for part in stack { + result.push(part); + } + + result +} + #[cfg(test)] mod test { use biome_diagnostics::{print_diagnostic_to_string, Error}; @@ -138,7 +155,7 @@ mod test { fs.insert("/my-plugin/rules/1.grit".into(), r#"`hello`"#); - let plugin = BiomePlugin::load(&fs, "./my-plugin", Utf8Path::new("/"), Utf8Path::new("/")) + let plugin = BiomePlugin::load(&fs, "./my-plugin", Utf8Path::new("/")) .expect("Couldn't load plugin"); assert_eq!(plugin.analyzer_plugins.len(), 1); } @@ -148,7 +165,7 @@ mod test { let mut fs = MemoryFileSystem::default(); fs.insert("/my-plugin/rules/1.grit".into(), r#"`hello`"#); - let error = BiomePlugin::load(&fs, "./my-plugin", Utf8Path::new("/"), Utf8Path::new("/")) + let error = BiomePlugin::load(&fs, "./my-plugin", Utf8Path::new("/")) .expect_err("Plugin loading should've failed"); snap_diagnostic("load_plugin_without_manifest", error.into()); } @@ -164,7 +181,7 @@ mod test { }"#, ); - let error = BiomePlugin::load(&fs, "./my-plugin", Utf8Path::new("/"), Utf8Path::new("/")) + let error = BiomePlugin::load(&fs, "./my-plugin", Utf8Path::new("/")) .expect_err("Plugin loading should've failed"); snap_diagnostic("load_plugin_with_wrong_version", error.into()); } @@ -180,7 +197,7 @@ mod test { }"#, ); - let error = BiomePlugin::load(&fs, "./my-plugin", Utf8Path::new("/"), Utf8Path::new("/")) + let error = BiomePlugin::load(&fs, "./my-plugin", Utf8Path::new("/")) .expect_err("Plugin loading should've failed"); snap_diagnostic("load_plugin_with_wrong_rule_extension", error.into()); } @@ -190,13 +207,8 @@ mod test { let mut fs = MemoryFileSystem::default(); fs.insert("/my-plugin.grit".into(), r#"`hello`"#); - let plugin = BiomePlugin::load( - &fs, - "./my-plugin.grit", - Utf8Path::new("/"), - Utf8Path::new("/"), - ) - .expect("Couldn't load plugin"); + let plugin = BiomePlugin::load(&fs, "./my-plugin.grit", Utf8Path::new("/")) + .expect("Couldn't load plugin"); assert_eq!(plugin.analyzer_plugins.len(), 1); } } diff --git a/crates/biome_plugin_loader/src/plugin_cache.rs b/crates/biome_plugin_loader/src/plugin_cache.rs new file mode 100644 index 000000000000..057eff6988c7 --- /dev/null +++ b/crates/biome_plugin_loader/src/plugin_cache.rs @@ -0,0 +1,29 @@ +use biome_analyze::AnalyzerPluginVec; +use camino::Utf8PathBuf; +use papaya::HashMap; +use rustc_hash::FxBuildHasher; + +use crate::BiomePlugin; + +/// Cache for storing loaded plugins in memory. +/// +/// Plugins are kept in a map from path to plugin instance. This allows for +/// convenient reloading of plugins if they are modified on disk. +#[derive(Debug, Default)] +pub struct PluginCache(HashMap); + +impl PluginCache { + /// Inserts a new plugin into the cache. + pub fn insert_plugin(&self, path: Utf8PathBuf, plugin: BiomePlugin) { + self.0.pin().insert(path, plugin); + } + + /// Returns the loaded analyzer plugins. + pub fn get_analyzer_plugins(&self) -> AnalyzerPluginVec { + let mut plugins = AnalyzerPluginVec::new(); + for plugin in self.0.pin().values() { + plugins.extend_from_slice(&plugin.analyzer_plugins); + } + plugins + } +} diff --git a/crates/biome_service/Cargo.toml b/crates/biome_service/Cargo.toml index 52994408abd4..0a54bc35d3a6 100644 --- a/crates/biome_service/Cargo.toml +++ b/crates/biome_service/Cargo.toml @@ -51,6 +51,7 @@ biome_json_parser = { workspace = true } biome_json_syntax = { workspace = true } biome_package = { workspace = true } biome_parser = { workspace = true } +biome_plugin_loader = { workspace = true } biome_project_layout = { workspace = true } biome_rowan = { workspace = true, features = ["serde"] } biome_string_case = { workspace = true } diff --git a/crates/biome_service/src/file_handlers/css.rs b/crates/biome_service/src/file_handlers/css.rs index 7afaf193dad7..36f768d4f657 100644 --- a/crates/biome_service/src/file_handlers/css.rs +++ b/crates/biome_service/src/file_handlers/css.rs @@ -520,10 +520,13 @@ fn lint(params: LintParams) -> LintResults { let mut process_lint = ProcessLint::new(¶ms); - let (_, analyze_diagnostics) = - analyze(&tree, filter, &analyzer_options, Vec::new(), |signal| { - process_lint.process_signal(signal) - }); + let (_, analyze_diagnostics) = analyze( + &tree, + filter, + &analyzer_options, + ¶ms.plugins, + |signal| process_lint.process_signal(signal), + ); process_lint.into_result(params.parse.into_diagnostics(), analyze_diagnostics) } @@ -542,6 +545,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult { skip, enabled_rules: rules, suppression_reason, + plugins, } = params; let _ = debug_span!("Code actions CSS", range =? range, path =? path).entered(); let tree = parse.tree(); @@ -578,7 +582,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult { info!("CSS runs the analyzer"); - analyze(&tree, filter, &analyzer_options, Vec::new(), |signal| { + analyze(&tree, filter, &analyzer_options, &plugins, |signal| { actions.extend(signal.actions().into_code_action_iter().map(|item| { CodeAction { category: item.category.clone(), @@ -638,48 +642,54 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result { - if action.applicability == Applicability::MaybeIncorrect { - skipped_suggested_fixes += 1; + match params.fix_file_mode { + FixFileMode::SafeFixes => { + if action.applicability == Applicability::MaybeIncorrect { + skipped_suggested_fixes += 1; + } + if action.applicability == Applicability::Always { + errors = errors.saturating_sub(1); + return ControlFlow::Break(action); + } } - if action.applicability == Applicability::Always { - errors = errors.saturating_sub(1); - return ControlFlow::Break(action); + FixFileMode::SafeAndUnsafeFixes => { + if matches!( + action.applicability, + Applicability::Always | Applicability::MaybeIncorrect + ) { + errors = errors.saturating_sub(1); + return ControlFlow::Break(action); + } } - } - FixFileMode::SafeAndUnsafeFixes => { - if matches!( - action.applicability, - Applicability::Always | Applicability::MaybeIncorrect - ) { - errors = errors.saturating_sub(1); - return ControlFlow::Break(action); + FixFileMode::ApplySuppressions => { + // TODO: to implement } } - FixFileMode::ApplySuppressions => { - // TODO: to implement - } } - } - ControlFlow::Continue(()) - }); + ControlFlow::Continue(()) + }, + ); match action { Some(action) => { diff --git a/crates/biome_service/src/file_handlers/graphql.rs b/crates/biome_service/src/file_handlers/graphql.rs index 0e0992e739f4..e18943517153 100644 --- a/crates/biome_service/src/file_handlers/graphql.rs +++ b/crates/biome_service/src/file_handlers/graphql.rs @@ -466,6 +466,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult { skip, suppression_reason, enabled_rules: rules, + plugins: _, } = params; let _ = debug_span!("Code actions GraphQL", range =? range, path =? path).entered(); let tree = parse.tree(); diff --git a/crates/biome_service/src/file_handlers/javascript.rs b/crates/biome_service/src/file_handlers/javascript.rs index 08fd2fdc8583..5492de4d5e73 100644 --- a/crates/biome_service/src/file_handlers/javascript.rs +++ b/crates/biome_service/src/file_handlers/javascript.rs @@ -605,7 +605,7 @@ fn debug_control_flow(parse: AnyParse, cursor: TextSize) -> String { } }, &options, - Vec::new(), + &[], Default::default(), |_| ControlFlow::::Continue(()), ); @@ -672,7 +672,7 @@ pub(crate) fn lint(params: LintParams) -> LintResults { &tree, filter, &analyzer_options, - Vec::new(), + ¶ms.plugins, services, |signal| process_lint.process_signal(signal), ); @@ -694,6 +694,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult { skip, suppression_reason, enabled_rules: rules, + plugins, } = params; let _ = debug_span!("Code actions JavaScript", range =? range, path =? path).entered(); let tree = parse.tree(); @@ -735,7 +736,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult { &tree, filter, &analyzer_options, - Vec::new(), + &plugins, services, |signal| { actions.extend(signal.actions().into_code_action_iter().map(|item| { @@ -814,7 +815,7 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result PullActionsResult { only, enabled_rules: rules, suppression_reason, + plugins: _, } = params; let _ = debug_span!("Code actions JSON", range =? range, path =? path).entered(); diff --git a/crates/biome_service/src/file_handlers/mod.rs b/crates/biome_service/src/file_handlers/mod.rs index f78679c54a40..64e48b060cd3 100644 --- a/crates/biome_service/src/file_handlers/mod.rs +++ b/crates/biome_service/src/file_handlers/mod.rs @@ -13,8 +13,9 @@ use crate::workspace::{ }; use crate::WorkspaceError; use biome_analyze::{ - AnalyzerDiagnostic, AnalyzerOptions, AnalyzerSignal, ControlFlow, GroupCategory, Never, - Queryable, RegistryVisitor, Rule, RuleCategories, RuleCategory, RuleFilter, RuleGroup, + AnalyzerDiagnostic, AnalyzerOptions, AnalyzerPluginVec, AnalyzerSignal, ControlFlow, + GroupCategory, Never, Queryable, RegistryVisitor, Rule, RuleCategories, RuleCategory, + RuleFilter, RuleGroup, }; use biome_configuration::analyzer::{RuleDomainValue, RuleSelector}; use biome_configuration::Rules; @@ -397,6 +398,7 @@ pub struct FixAllParams<'a> { pub(crate) rule_categories: RuleCategories, pub(crate) suppression_reason: Option, pub(crate) enabled_rules: Vec, + pub(crate) plugins: AnalyzerPluginVec, } #[derive(Default)] @@ -463,6 +465,7 @@ pub(crate) struct LintParams<'a> { pub(crate) project_layout: Arc, pub(crate) suppression_reason: Option, pub(crate) enabled_rules: Vec, + pub(crate) plugins: AnalyzerPluginVec, } pub(crate) struct LintResults { @@ -590,6 +593,7 @@ pub(crate) struct CodeActionsParams<'a> { pub(crate) skip: Vec, pub(crate) suppression_reason: Option, pub(crate) enabled_rules: Vec, + pub(crate) plugins: AnalyzerPluginVec, } type Lint = fn(LintParams) -> LintResults; diff --git a/crates/biome_service/src/settings.rs b/crates/biome_service/src/settings.rs index 692daf7cf3cf..49add7a355ec 100644 --- a/crates/biome_service/src/settings.rs +++ b/crates/biome_service/src/settings.rs @@ -9,6 +9,7 @@ use biome_configuration::formatter::{FormatWithErrorsEnabled, FormatterEnabled}; use biome_configuration::html::HtmlConfiguration; use biome_configuration::javascript::JsxRuntime; use biome_configuration::max_size::MaxSize; +use biome_configuration::plugins::Plugins; use biome_configuration::{ push_to_analyzer_assist, push_to_analyzer_rules, BiomeDiagnostic, Configuration, CssConfiguration, FilesConfiguration, FilesIgnoreUnknownEnabled, FormatterConfiguration, @@ -57,6 +58,8 @@ pub struct Settings { pub files: FilesSettings, /// Assist settings pub assist: AssistSettings, + /// Plugin settings. + pub plugins: Plugins, /// overrides pub override_settings: OverrideSettings, } @@ -117,6 +120,11 @@ impl Settings { self.languages.html = html.into() } + // plugin settings + if let Some(plugins) = configuration.plugins { + self.plugins = plugins; + } + // NOTE: keep this last. Computing the overrides require reading the settings computed by the parent settings. if let Some(overrides) = configuration.overrides { self.override_settings = diff --git a/crates/biome_service/src/workspace.rs b/crates/biome_service/src/workspace.rs index f767920922df..8d9d88a9c547 100644 --- a/crates/biome_service/src/workspace.rs +++ b/crates/biome_service/src/workspace.rs @@ -530,6 +530,13 @@ pub struct UpdateSettingsParams { pub workspace_directory: Option, } +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct UpdateSettingsResult { + pub diagnostics: Vec, +} + #[derive(Debug, serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] @@ -1038,7 +1045,10 @@ pub trait Workspace: Send + Sync + RefUnwindSafe { /// This method should not be used in combination with /// `scan_project_folder()`. When scanning is enabled, the server will /// manage project settings on its own. - fn update_settings(&self, params: UpdateSettingsParams) -> Result<(), WorkspaceError>; + fn update_settings( + &self, + params: UpdateSettingsParams, + ) -> Result; /// Closes the project with the given key. /// diff --git a/crates/biome_service/src/workspace/client.rs b/crates/biome_service/src/workspace/client.rs index c296de5c283c..c014a6932f4c 100644 --- a/crates/biome_service/src/workspace/client.rs +++ b/crates/biome_service/src/workspace/client.rs @@ -19,7 +19,7 @@ use super::{ GetSyntaxTreeParams, GetSyntaxTreeResult, OpenFileParams, PullActionsParams, PullActionsResult, PullDiagnosticsParams, PullDiagnosticsResult, RenameParams, RenameResult, ScanProjectFolderParams, ScanProjectFolderResult, SearchPatternParams, SearchResults, - SupportsFeatureParams, UpdateSettingsParams, + SupportsFeatureParams, UpdateSettingsParams, UpdateSettingsResult, }; pub struct WorkspaceClient { @@ -120,7 +120,10 @@ where self.request("biome/is_path_ignored", params) } - fn update_settings(&self, params: UpdateSettingsParams) -> Result<(), WorkspaceError> { + fn update_settings( + &self, + params: UpdateSettingsParams, + ) -> Result { self.request("biome/update_settings", params) } diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs index dc99457ee46c..81a8f22b9750 100644 --- a/crates/biome_service/src/workspace/server.rs +++ b/crates/biome_service/src/workspace/server.rs @@ -7,7 +7,7 @@ use super::{ ParsePatternParams, ParsePatternResult, PatternId, ProjectKey, PullActionsParams, PullActionsResult, PullDiagnosticsParams, PullDiagnosticsResult, RenameResult, ScanProjectFolderParams, ScanProjectFolderResult, SearchPatternParams, SearchResults, - SupportsFeatureParams, UpdateSettingsParams, + SupportsFeatureParams, UpdateSettingsParams, UpdateSettingsResult, }; use crate::diagnostics::FileTooLarge; use crate::file_handlers::{ @@ -22,6 +22,8 @@ use crate::workspace::{ }; use crate::{file_handlers::Features, Workspace, WorkspaceError}; use append_only_vec::AppendOnlyVec; +use biome_analyze::AnalyzerPluginVec; +use biome_configuration::plugins::{PluginConfiguration, Plugins}; use biome_configuration::{BiomeDiagnostic, Configuration}; use biome_dependency_graph::DependencyGraph; use biome_deserialize::json::deserialize_from_json_str; @@ -38,6 +40,7 @@ use biome_json_parser::JsonParserOptions; use biome_json_syntax::JsonFileSource; use biome_package::PackageType; use biome_parser::AnyParse; +use biome_plugin_loader::{BiomePlugin, PluginCache, PluginDiagnostic}; use biome_project_layout::ProjectLayout; use biome_rowan::NodeCache; use camino::{Utf8Path, Utf8PathBuf}; @@ -62,6 +65,9 @@ pub(super) struct WorkspaceServer { /// Dependency graph tracking imports across source files. dependency_graph: Arc, + /// Keeps all loaded plugins in memory, per project. + plugin_caches: Arc>, + /// Stores the document (text content + version number) associated with a URL documents: HashMap, @@ -136,6 +142,7 @@ impl WorkspaceServer { projects: Default::default(), project_layout: Default::default(), dependency_graph: Default::default(), + plugin_caches: Default::default(), documents: Default::default(), file_sources: AppendOnlyVec::default(), patterns: Default::default(), @@ -519,6 +526,41 @@ impl WorkspaceServer { }) } + fn load_plugins( + &self, + project_key: ProjectKey, + base_path: &Utf8Path, + plugins: &Plugins, + ) -> Vec { + let mut diagnostics = Vec::new(); + let plugin_cache = PluginCache::default(); + + for plugin_config in plugins.iter() { + match plugin_config { + PluginConfiguration::Path(plugin_path) => { + match BiomePlugin::load(self.fs.as_ref(), plugin_path, base_path) { + Ok(plugin) => { + plugin_cache.insert_plugin(plugin_path.clone().into(), plugin); + } + Err(diagnostic) => diagnostics.push(diagnostic), + } + } + } + } + + self.plugin_caches.pin().insert(project_key, plugin_cache); + + diagnostics + } + + fn get_analyzer_plugins_for_project(&self, project_key: ProjectKey) -> AnalyzerPluginVec { + self.plugin_caches + .pin() + .get(&project_key) + .map(|cache| cache.get_analyzer_plugins()) + .unwrap_or_default() + } + pub(super) fn update_project_layout_for_paths(&self, paths: &[BiomePath]) { for path in paths { if let Err(error) = self.update_project_layout_for_path(path) { @@ -645,22 +687,35 @@ impl Workspace for WorkspaceServer { /// This function may panic if the internal settings mutex has been poisoned /// by another thread having previously panicked while holding the lock #[tracing::instrument(level = "debug", skip(self))] - fn update_settings(&self, params: UpdateSettingsParams) -> Result<(), WorkspaceError> { + fn update_settings( + &self, + params: UpdateSettingsParams, + ) -> Result { let mut settings = self .projects .get_settings(params.project_key) .ok_or_else(WorkspaceError::no_project)?; + let workspace_directory = params.workspace_directory.map(|p| p.to_path_buf()); + settings.merge_with_configuration( params.configuration, - params.workspace_directory.map(|p| p.to_path_buf()), + workspace_directory.clone(), params.vcs_base_path.map(|p| p.to_path_buf()), params.gitignore_matches.as_slice(), )?; + let diagnostics = self.load_plugins( + params.project_key, + &workspace_directory.unwrap_or_default(), + &settings.plugins, + ); + self.projects.set_settings(params.project_key, settings); - Ok(()) + Ok(UpdateSettingsResult { + diagnostics: diagnostics.into_iter().map(Into::into).collect(), + }) } fn open_file(&self, params: OpenFileParams) -> Result<(), WorkspaceError> { @@ -918,6 +973,7 @@ impl Workspace for WorkspaceServer { project_layout: self.project_layout.clone(), suppression_reason: None, enabled_rules, + plugins: self.get_analyzer_plugins_for_project(project_key), }); ( @@ -998,6 +1054,7 @@ impl Workspace for WorkspaceServer { skip, suppression_reason: None, enabled_rules, + plugins: self.get_analyzer_plugins_for_project(project_key), })) } @@ -1137,6 +1194,7 @@ impl Workspace for WorkspaceServer { rule_categories, suppression_reason, enabled_rules, + plugins: self.get_analyzer_plugins_for_project(project_key), }) } diff --git a/crates/biome_service/tests/snapshots/workspace__test__plugins_are_loaded_and_used_during_analysis.snap b/crates/biome_service/tests/snapshots/workspace__test__plugins_are_loaded_and_used_during_analysis.snap new file mode 100644 index 000000000000..b8700a4cd72d --- /dev/null +++ b/crates/biome_service/tests/snapshots/workspace__test__plugins_are_loaded_and_used_during_analysis.snap @@ -0,0 +1,40 @@ +--- +source: crates/biome_service/tests/workspace.rs +expression: result.diagnostics +--- +[ + Diagnostic { + category: Some( + Category { + name: "plugin", + link: None, + }, + ), + severity: Information, + description: "Prefer object spread instead of `Object.assign()`", + message: "Prefer object spread instead of `Object.assign()`", + advices: Advices { + advices: [], + }, + verbose_advices: Advices { + advices: [], + }, + location: Location { + path: Some( + File( + "/project/a.ts", + ), + ), + span: Some( + 24..38, + ), + source_code: None, + }, + tags: DiagnosticTags( + BitFlags { + bits: 0b0, + }, + ), + source: None, + }, +] diff --git a/crates/biome_service/tests/workspace.rs b/crates/biome_service/tests/workspace.rs index 20bfd6cc82b6..f456b2ef2536 100644 --- a/crates/biome_service/tests/workspace.rs +++ b/crates/biome_service/tests/workspace.rs @@ -2,6 +2,7 @@ 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}; @@ -9,11 +10,12 @@ mod test { use biome_service::projects::ProjectKey; use biome_service::workspace::{ server, CloseFileParams, CloseProjectParams, FileContent, FileGuard, GetFileContentParams, - GetSyntaxTreeParams, OpenFileParams, OpenProjectParams, ScanProjectFolderParams, - UpdateSettingsParams, + 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) { @@ -433,4 +435,66 @@ type User { }) .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_wasm/src/lib.rs b/crates/biome_wasm/src/lib.rs index 286619de5923..e74a5aeac865 100644 --- a/crates/biome_wasm/src/lib.rs +++ b/crates/biome_wasm/src/lib.rs @@ -50,10 +50,16 @@ impl Workspace { } #[wasm_bindgen(js_name = updateSettings)] - pub fn update_settings(&self, params: IUpdateSettingsParams) -> Result<(), Error> { + pub fn update_settings( + &self, + params: IUpdateSettingsParams, + ) -> Result { let params: UpdateSettingsParams = serde_wasm_bindgen::from_value(params.into()).map_err(into_error)?; - self.inner.update_settings(params).map_err(into_error) + let result = self.inner.update_settings(params).map_err(into_error)?; + to_value(&result) + .map(IUpdateSettingsResult::from) + .map_err(into_error) } #[wasm_bindgen(js_name = openProject)] diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index db55de0949b3..0340c217d348 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -3013,157 +3013,8 @@ export type RestrictedModifier = | "protected" | "readonly" | "static"; -export interface OpenProjectParams { - /** - * Whether the folder should be opened as a project, even if no `biome.json` can be found. - */ - openUninitialized: boolean; - /** - * The path to open - */ - path: BiomePath; -} -export interface OpenFileParams { - content: FileContent; - documentFileSource?: DocumentFileSource; - path: BiomePath; - /** - * Set to `true` to persist the node cache used during parsing, in order to speed up subsequent reparsing if the document has been edited. - -This should only be enabled if reparsing is to be expected, such as when the file is opened through the LSP Proxy. - */ - persistNodeCache?: boolean; - projectKey: ProjectKey; - version: number; -} -export type FileContent = - | { content: string; type: "fromClient" } - | { type: "fromServer" }; -export type DocumentFileSource = - | "Unknown" - | { Js: JsFileSource } - | { Json: JsonFileSource } - | { Css: CssFileSource } - | { Graphql: GraphqlFileSource } - | { Html: HtmlFileSource } - | { Grit: GritFileSource }; -export interface JsFileSource { - /** - * Used to mark if the source is being used for an Astro, Svelte or Vue file - */ - embedding_kind: EmbeddingKind; - language: Language; - module_kind: ModuleKind; - variant: LanguageVariant; - version: LanguageVersion; -} -export interface JsonFileSource { - allowComments: boolean; - allowTrailingCommas: boolean; - variant: JsonFileVariant; -} -export interface CssFileSource { - variant: CssVariant; -} -export interface GraphqlFileSource { - variant: GraphqlVariant; -} -export interface HtmlFileSource { - variant: HtmlVariant; -} -export interface GritFileSource { - variant: GritVariant; -} -export type EmbeddingKind = "Astro" | "Vue" | "Svelte" | "None"; -export type Language = - | "javaScript" - | { typeScript: { definition_file: boolean } }; -/** - * Is the source file an ECMAScript Module or Script. Changes the parsing semantic. - */ -export type ModuleKind = "script" | "module"; -export type LanguageVariant = "standard" | "standardRestricted" | "jsx"; -/** - * Enum of the different ECMAScript standard versions. The versions are ordered in increasing order; The newest version comes last. - -Defaults to the latest stable ECMAScript standard. - */ -export type LanguageVersion = "eS2022" | "eSNext"; -/** - * It represents the extension of the file - */ -export type JsonFileVariant = "standard" | "jsonc"; -/** - * The style of CSS contained in the file. - -Currently, Biome only supports plain CSS, and aims to be compatible with the latest Recommendation level standards. - */ -export type CssVariant = "standard"; -/** - * The style of GraphQL contained in the file. - */ -export type GraphqlVariant = "standard"; -export type HtmlVariant = "Standard" | "Astro"; -export type GritVariant = "Standard"; -export interface ChangeFileParams { - content: string; - path: BiomePath; - projectKey: ProjectKey; - version: number; -} -export interface CloseFileParams { - path: BiomePath; - projectKey: ProjectKey; -} -export interface GetSyntaxTreeParams { - path: BiomePath; - projectKey: ProjectKey; -} -export interface GetSyntaxTreeResult { - ast: string; - cst: string; -} -export interface CheckFileSizeParams { - path: BiomePath; - projectKey: ProjectKey; -} -export interface CheckFileSizeResult { - fileSize: number; - limit: number; -} -export interface GetFileContentParams { - path: BiomePath; - projectKey: ProjectKey; -} -export interface GetControlFlowGraphParams { - cursor: TextSize; - path: BiomePath; - projectKey: ProjectKey; -} -export type TextSize = number; -export interface GetFormatterIRParams { - path: BiomePath; - projectKey: ProjectKey; -} -export interface PullDiagnosticsParams { - categories: RuleCategories; - /** - * Rules to apply on top of the configuration - */ - enabledRules?: RuleCode[]; - maxDiagnostics: number; - only?: RuleCode[]; - path: BiomePath; - projectKey: ProjectKey; - skip?: RuleCode[]; -} -export type RuleCategories = RuleCategory[]; -export type RuleCode = string; -export type RuleCategory = "syntax" | "lint" | "action" | "transformation"; -export interface PullDiagnosticsResult { +export interface UpdateSettingsResult { diagnostics: Diagnostic[]; - errors: number; - skippedDiagnostics: number; } /** * Serializable representation for a [Diagnostic](super::Diagnostic). @@ -3611,6 +3462,7 @@ export interface TextEdit { ops: CompressedOp[]; } export type Backtrace = BacktraceFrame[]; +export type TextSize = number; /** * Enumeration of all the supported markup elements */ @@ -3650,6 +3502,157 @@ export interface BacktraceSymbol { lineno?: number; name?: string; } +export interface OpenProjectParams { + /** + * Whether the folder should be opened as a project, even if no `biome.json` can be found. + */ + openUninitialized: boolean; + /** + * The path to open + */ + path: BiomePath; +} +export interface OpenFileParams { + content: FileContent; + documentFileSource?: DocumentFileSource; + path: BiomePath; + /** + * Set to `true` to persist the node cache used during parsing, in order to speed up subsequent reparsing if the document has been edited. + +This should only be enabled if reparsing is to be expected, such as when the file is opened through the LSP Proxy. + */ + persistNodeCache?: boolean; + projectKey: ProjectKey; + version: number; +} +export type FileContent = + | { content: string; type: "fromClient" } + | { type: "fromServer" }; +export type DocumentFileSource = + | "Unknown" + | { Js: JsFileSource } + | { Json: JsonFileSource } + | { Css: CssFileSource } + | { Graphql: GraphqlFileSource } + | { Html: HtmlFileSource } + | { Grit: GritFileSource }; +export interface JsFileSource { + /** + * Used to mark if the source is being used for an Astro, Svelte or Vue file + */ + embedding_kind: EmbeddingKind; + language: Language; + module_kind: ModuleKind; + variant: LanguageVariant; + version: LanguageVersion; +} +export interface JsonFileSource { + allowComments: boolean; + allowTrailingCommas: boolean; + variant: JsonFileVariant; +} +export interface CssFileSource { + variant: CssVariant; +} +export interface GraphqlFileSource { + variant: GraphqlVariant; +} +export interface HtmlFileSource { + variant: HtmlVariant; +} +export interface GritFileSource { + variant: GritVariant; +} +export type EmbeddingKind = "Astro" | "Vue" | "Svelte" | "None"; +export type Language = + | "javaScript" + | { typeScript: { definition_file: boolean } }; +/** + * Is the source file an ECMAScript Module or Script. Changes the parsing semantic. + */ +export type ModuleKind = "script" | "module"; +export type LanguageVariant = "standard" | "standardRestricted" | "jsx"; +/** + * Enum of the different ECMAScript standard versions. The versions are ordered in increasing order; The newest version comes last. + +Defaults to the latest stable ECMAScript standard. + */ +export type LanguageVersion = "eS2022" | "eSNext"; +/** + * It represents the extension of the file + */ +export type JsonFileVariant = "standard" | "jsonc"; +/** + * The style of CSS contained in the file. + +Currently, Biome only supports plain CSS, and aims to be compatible with the latest Recommendation level standards. + */ +export type CssVariant = "standard"; +/** + * The style of GraphQL contained in the file. + */ +export type GraphqlVariant = "standard"; +export type HtmlVariant = "Standard" | "Astro"; +export type GritVariant = "Standard"; +export interface ChangeFileParams { + content: string; + path: BiomePath; + projectKey: ProjectKey; + version: number; +} +export interface CloseFileParams { + path: BiomePath; + projectKey: ProjectKey; +} +export interface GetSyntaxTreeParams { + path: BiomePath; + projectKey: ProjectKey; +} +export interface GetSyntaxTreeResult { + ast: string; + cst: string; +} +export interface CheckFileSizeParams { + path: BiomePath; + projectKey: ProjectKey; +} +export interface CheckFileSizeResult { + fileSize: number; + limit: number; +} +export interface GetFileContentParams { + path: BiomePath; + projectKey: ProjectKey; +} +export interface GetControlFlowGraphParams { + cursor: TextSize; + path: BiomePath; + projectKey: ProjectKey; +} +export interface GetFormatterIRParams { + path: BiomePath; + projectKey: ProjectKey; +} +export interface PullDiagnosticsParams { + categories: RuleCategories; + /** + * Rules to apply on top of the configuration + */ + enabledRules?: RuleCode[]; + maxDiagnostics: number; + only?: RuleCode[]; + path: BiomePath; + projectKey: ProjectKey; + skip?: RuleCode[]; +} +export type RuleCategories = RuleCategory[]; +export type RuleCode = string; +export type RuleCategory = "syntax" | "lint" | "action" | "transformation"; +export interface PullDiagnosticsResult { + diagnostics: Diagnostic[]; + errors: number; + skippedDiagnostics: number; +} export interface PullActionsParams { enabledRules?: RuleCode[]; only?: RuleCode[]; @@ -3846,7 +3849,7 @@ export type RuleDomain = "react" | "test" | "solid" | "next"; export type RuleDomainValue = "all" | "none" | "recommended"; export interface Workspace { fileFeatures(params: SupportsFeatureParams): Promise; - updateSettings(params: UpdateSettingsParams): Promise; + updateSettings(params: UpdateSettingsParams): Promise; openProject(params: OpenProjectParams): Promise; openFile(params: OpenFileParams): Promise; changeFile(params: ChangeFileParams): Promise; diff --git a/xtask/bench/src/language.rs b/xtask/bench/src/language.rs index c68050c6d27a..c5a58b66a539 100644 --- a/xtask/bench/src/language.rs +++ b/xtask/bench/src/language.rs @@ -198,7 +198,7 @@ impl Analyze { root, filter, &options, - Vec::new(), + &[], Default::default(), |event| { black_box(event.diagnostic()); @@ -216,7 +216,7 @@ impl Analyze { ..AnalysisFilter::default() }; let options = AnalyzerOptions::default(); - biome_css_analyze::analyze(root, filter, &options, Vec::new(), |event| { + biome_css_analyze::analyze(root, filter, &options, &[], |event| { black_box(event.diagnostic()); black_box(event.actions()); ControlFlow::::Continue(()) diff --git a/xtask/rules_check/src/lib.rs b/xtask/rules_check/src/lib.rs index 9f664ea93a9c..ee6e7e9ffcad 100644 --- a/xtask/rules_check/src/lib.rs +++ b/xtask/rules_check/src/lib.rs @@ -430,7 +430,7 @@ fn assert_lint( let services = JsAnalyzerServices::from((Default::default(), Default::default(), file_source)); - biome_js_analyze::analyze(&root, filter, &options, vec![], services, |signal| { + biome_js_analyze::analyze(&root, filter, &options, &[], services, |signal| { if let Some(mut diag) = signal.diagnostic() { for action in signal.actions() { if !action.is_suppression() { @@ -522,7 +522,7 @@ fn assert_lint( test, ); - biome_css_analyze::analyze(&root, filter, &options, Vec::new(), |signal| { + biome_css_analyze::analyze(&root, filter, &options, &[], |signal| { if let Some(mut diag) = signal.diagnostic() { for action in signal.actions() { if !action.is_suppression() { From f46d8a115660f2a35f09b8759ea37a851ecab132 Mon Sep 17 00:00:00 2001 From: "Arend van Beelen jr." Date: Thu, 20 Feb 2025 11:26:48 +0100 Subject: [PATCH 2/2] PR feedback --- crates/biome_cli/src/commands/mod.rs | 4 +-- crates/biome_plugin_loader/src/lib.rs | 2 ++ crates/biome_service/src/diagnostics.rs | 34 +++++++++++++++++++- crates/biome_service/src/workspace.rs | 4 +-- crates/biome_service/src/workspace/server.rs | 9 ++++++ 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/crates/biome_cli/src/commands/mod.rs b/crates/biome_cli/src/commands/mod.rs index be5857bdceae..614420e94eac 100644 --- a/crates/biome_cli/src/commands/mod.rs +++ b/crates/biome_cli/src/commands/mod.rs @@ -808,8 +808,8 @@ pub(crate) trait CommandRunner: Sized { vcs_base_path: vcs_base_path.map(BiomePath::from), gitignore_matches, })?; - for diagnostic in result.diagnostics { - console.log(markup! {{PrintDiagnostic::simple(&diagnostic)}}); + for diagnostic in &result.diagnostics { + console.log(markup! {{PrintDiagnostic::simple(diagnostic)}}); } let execution = self.get_execution(cli_options, console, workspace, project_key)?; diff --git a/crates/biome_plugin_loader/src/lib.rs b/crates/biome_plugin_loader/src/lib.rs index 8b795dd7e98d..504991a0082d 100644 --- a/crates/biome_plugin_loader/src/lib.rs +++ b/crates/biome_plugin_loader/src/lib.rs @@ -92,6 +92,8 @@ impl BiomePlugin { } /// Normalizes the given `path` without requiring filesystem access. +/// +/// This only normalizes `.` and `..` entries, but does not resolve symlinks. fn normalize_path(path: &Utf8Path) -> Utf8PathBuf { let mut stack = Vec::new(); diff --git a/crates/biome_service/src/diagnostics.rs b/crates/biome_service/src/diagnostics.rs index a8fafb466ea3..9ae6975c3fb8 100644 --- a/crates/biome_service/src/diagnostics.rs +++ b/crates/biome_service/src/diagnostics.rs @@ -13,6 +13,7 @@ use biome_formatter::{FormatError, PrintError}; use biome_fs::{BiomePath, FileSystemDiagnostic}; use biome_grit_patterns::CompileError; use biome_js_analyze::utils::rename::RenameError; +use biome_plugin_loader::PluginDiagnostic; use camino::Utf8Path; use serde::{Deserialize, Serialize}; use std::error::Error; @@ -52,7 +53,9 @@ pub enum WorkspaceError { FileSystem(FileSystemDiagnostic), /// Raised when there's an issue around the VCS integration Vcs(VcsDiagnostic), - /// Diagnostic raised when a file is protected + /// One or more errors occurred during plugin loading. + PluginErrors(PluginErrors), + /// Diagnostic raised when a file is protected. ProtectedFile(ProtectedFile), /// Error when searching for a pattern SearchError(SearchError), @@ -95,6 +98,10 @@ impl WorkspaceError { }) } + pub fn plugin_errors(diagnostics: Vec) -> Self { + Self::PluginErrors(PluginErrors { diagnostics }) + } + pub fn vcs_disabled() -> Self { Self::Vcs(VcsDiagnostic::DisabledVcs(DisabledVcs {})) } @@ -530,6 +537,31 @@ pub struct NoVcsFolderFound { )] pub struct DisabledVcs {} +#[derive(Debug, Serialize, Deserialize)] +pub struct PluginErrors { + diagnostics: Vec, +} + +impl Diagnostic for PluginErrors { + fn category(&self) -> Option<&'static Category> { + Some(category!("plugin")) + } + + fn severity(&self) -> Severity { + Severity::Error + } + + fn message(&self, fmt: &mut biome_console::fmt::Formatter<'_>) -> std::io::Result<()> { + fmt.write_markup(markup!("Error(s) during loading of plugins:\n"))?; + + for diagnostic in &self.diagnostics { + diagnostic.message(fmt)?; + } + + Ok(()) + } +} + #[derive(Debug, Serialize, Deserialize, Diagnostic)] #[diagnostic( category = "project", diff --git a/crates/biome_service/src/workspace.rs b/crates/biome_service/src/workspace.rs index 8d9d88a9c547..a2dae533e4fd 100644 --- a/crates/biome_service/src/workspace.rs +++ b/crates/biome_service/src/workspace.rs @@ -1042,8 +1042,8 @@ pub trait Workspace: Send + Sync + RefUnwindSafe { /// Updates the global settings for the given project. /// - /// This method should not be used in combination with - /// `scan_project_folder()`. When scanning is enabled, the server will + /// TODO: This method should not be used in combination with + /// `scan_project_folder()`. When scanning is enabled, the server should /// manage project settings on its own. fn update_settings( &self, diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs index 81a8f22b9750..bb1d9d13d37b 100644 --- a/crates/biome_service/src/workspace/server.rs +++ b/crates/biome_service/src/workspace/server.rs @@ -710,6 +710,15 @@ impl Workspace for WorkspaceServer { &workspace_directory.unwrap_or_default(), &settings.plugins, ); + let has_errors = diagnostics + .iter() + .any(|diagnostic| diagnostic.severity() >= Severity::Error); + if has_errors { + // Note we also pass non-error diagnostics here. Filtering them + // might be cleaner, but on the other hand, including them may + // sometimes give a hint as to why an error occurred? + return Err(WorkspaceError::plugin_errors(diagnostics)); + } self.projects.set_settings(params.project_key, settings);