diff --git a/Cargo.lock b/Cargo.lock index 1c42219256eb..a76061e33dab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1047,6 +1047,25 @@ dependencies = [ "unicode-bom", ] +[[package]] +name = "biome_plugin_loader" +version = "0.0.1" +dependencies = [ + "biome_analyze", + "biome_console", + "biome_deserialize", + "biome_deserialize_macros", + "biome_diagnostics", + "biome_fs", + "biome_grit_patterns", + "biome_json_parser", + "biome_parser", + "biome_rowan", + "grit-util", + "insta", + "serde", +] + [[package]] name = "biome_project" version = "0.5.7" diff --git a/Cargo.toml b/Cargo.toml index 411a3c969405..c21f03ad19c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,6 +144,7 @@ biome_json_syntax = { version = "0.5.7", path = "./crates/biome_json_ biome_markdown_factory = { version = "0.0.1", path = "./crates/biome_markdown_factory" } 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_yaml_factory = { version = "0.0.1", path = "./crates/biome_yaml_factory" } biome_yaml_parser = { version = "0.0.1", path = "./crates/biome_yaml_parser" } biome_yaml_syntax = { version = "0.0.1", path = "./crates/biome_yaml_syntax" } @@ -171,39 +172,41 @@ biome_ungrammar = { path = "./crates/biome_ungrammar" } tests_macros = { path = "./crates/tests_macros" } # Crates needed in the workspace -anyhow = "1.0.89" -bpaf = { version = "0.9.15", features = ["derive"] } -countme = "3.0.1" -crossbeam = "0.8.4" -dashmap = "6.1.0" -enumflags2 = "0.7.10" -getrandom = "0.2.15" -ignore = "0.4.23" -indexmap = { version = "2.6.0", features = ["serde"] } -insta = "1.40.0" -natord = "1.0.9" -oxc_resolver = "1.12.0" -proc-macro2 = "1.0.86" -quickcheck = "1.0.3" -quickcheck_macros = "1.0.0" -quote = "1.0.37" -rayon = "1.10.0" -regex = "1.11.0" -rustc-hash = "1.1.0" -schemars = { version = "0.8.21", features = ["indexmap2", "smallvec"] } -serde = { version = "1.0.210", features = ["derive"] } -serde_ini = "0.2.0" -serde_json = "1.0.128" -similar = "2.6.0" -slotmap = "1.0.7" -smallvec = { version = "1.13.2", features = ["union", "const_new", "serde"] } -syn = "1.0.109" -termcolor = "1.4.1" -tokio = "1.40.0" -tracing = { version = "0.1.40", default-features = false, features = ["std"] } -tracing-subscriber = "0.3.18" -unicode-bom = "2.0.3" -unicode-width = "0.1.12" +anyhow = "1.0.89" +bpaf = { version = "0.9.15", features = ["derive"] } +countme = "3.0.1" +crossbeam = "0.8.4" +dashmap = "6.1.0" +enumflags2 = "0.7.10" +getrandom = "0.2.15" +grit-pattern-matcher = "0.4" +grit-util = "0.4" +ignore = "0.4.23" +indexmap = { version = "2.6.0", features = ["serde"] } +insta = "1.40.0" +natord = "1.0.9" +oxc_resolver = "1.12.0" +proc-macro2 = "1.0.86" +quickcheck = "1.0.3" +quickcheck_macros = "1.0.0" +quote = "1.0.37" +rayon = "1.10.0" +regex = "1.11.0" +rustc-hash = "1.1.0" +schemars = { version = "0.8.21", features = ["indexmap2", "smallvec"] } +serde = { version = "1.0.210", features = ["derive"] } +serde_ini = "0.2.0" +serde_json = "1.0.128" +similar = "2.6.0" +slotmap = "1.0.7" +smallvec = { version = "1.13.2", features = ["union", "const_new", "serde"] } +syn = "1.0.109" +termcolor = "1.4.1" +tokio = "1.40.0" +tracing = { version = "0.1.40", default-features = false, features = ["std"] } +tracing-subscriber = "0.3.18" +unicode-bom = "2.0.3" +unicode-width = "0.1.12" [profile.dev.package.biome_wasm] debug = true opt-level = "s" diff --git a/crates/biome_analyze/src/rule.rs b/crates/biome_analyze/src/rule.rs index 594c75327733..e69561e1effb 100644 --- a/crates/biome_analyze/src/rule.rs +++ b/crates/biome_analyze/src/rule.rs @@ -1033,6 +1033,14 @@ impl RuleDiagnostic { self } + /// Marks this diagnostic as verbose. + /// + /// The diagnostic will only be shown when using the `--verbose` argument. + pub fn verbose(mut self) -> Self { + self.tags |= DiagnosticTags::VERBOSE; + self + } + /// Attaches a label to this [`RuleDiagnostic`]. /// /// The given span has to be in the file that was provided while creating this [`RuleDiagnostic`]. diff --git a/crates/biome_configuration/src/diagnostics.rs b/crates/biome_configuration/src/diagnostics.rs index 6ab8ad36be50..d7e889b80759 100644 --- a/crates/biome_configuration/src/diagnostics.rs +++ b/crates/biome_configuration/src/diagnostics.rs @@ -184,8 +184,8 @@ pub struct InvalidIgnorePattern { #[derive(Debug, Serialize, Deserialize, Diagnostic)] #[diagnostic( - category = "configuration", - severity = Error, + category = "configuration", + severity = Error, )] pub struct CantLoadExtendFile { #[location(resource)] @@ -217,8 +217,8 @@ impl CantLoadExtendFile { #[derive(Debug, Serialize, Deserialize, Diagnostic)] #[diagnostic( - category = "configuration", - severity = Error, + category = "configuration", + severity = Error, )] pub struct InvalidConfiguration { #[message] diff --git a/crates/biome_configuration/src/lib.rs b/crates/biome_configuration/src/lib.rs index e24d2c37dd12..dca6ed1a4255 100644 --- a/crates/biome_configuration/src/lib.rs +++ b/crates/biome_configuration/src/lib.rs @@ -13,6 +13,7 @@ pub mod javascript; pub mod json; pub mod organize_imports; mod overrides; +pub mod plugins; pub mod vcs; use crate::analyzer::assists::{ @@ -58,6 +59,7 @@ pub use overrides::{ OverrideAssistsConfiguration, OverrideFormatterConfiguration, OverrideLinterConfiguration, OverrideOrganizeImportsConfiguration, OverridePattern, Overrides, }; +use plugins::Plugins; use serde::{Deserialize, Serialize}; use std::fmt::Debug; use std::num::NonZeroU64; @@ -132,6 +134,10 @@ pub struct Configuration { #[partial(bpaf(hide))] pub overrides: Overrides, + /// List of plugins to load. + #[partial(bpaf(hide))] + pub plugins: Plugins, + /// Specific configuration for assists #[partial(type, bpaf(external(partial_assists_configuration), optional))] pub assists: AssistsConfiguration, diff --git a/crates/biome_configuration/src/plugins.rs b/crates/biome_configuration/src/plugins.rs new file mode 100644 index 000000000000..01ed9f7b6338 --- /dev/null +++ b/crates/biome_configuration/src/plugins.rs @@ -0,0 +1,48 @@ +use biome_deserialize::{ + Deserializable, DeserializableType, DeserializableValue, DeserializationDiagnostic, +}; +use biome_deserialize_macros::{Deserializable, Merge}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +#[derive(Clone, Debug, Default, Deserialize, Deserializable, Eq, Merge, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Plugins(pub Vec); + +impl FromStr for Plugins { + type Err = String; + + fn from_str(_s: &str) -> Result { + Ok(Self::default()) + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields, untagged)] +pub enum PluginConfiguration { + Path(String), + // TODO: PathWithOptions(PluginPathWithOptions), +} + +impl Deserializable for PluginConfiguration { + fn deserialize( + value: &impl DeserializableValue, + rule_name: &str, + diagnostics: &mut Vec, + ) -> Option { + if value.visitable_type()? == DeserializableType::Str { + Deserializable::deserialize(value, rule_name, diagnostics).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. + // Also, we don't have a way to configure Grit plugins yet. + /*Deserializable::deserialize(value, rule_name, diagnostics) + .map(|plugin| Self::PathWithOptions(plugin))*/ + None + } + } +} diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index c5b9fa49e2d5..75853c7dd867 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -351,6 +351,7 @@ define_categories! { "assists", "migrate", "deserialize", + "plugin", "project", "search", "internalError/io", diff --git a/crates/biome_fs/src/fs.rs b/crates/biome_fs/src/fs.rs index cd96482437ad..34ad9b744252 100644 --- a/crates/biome_fs/src/fs.rs +++ b/crates/biome_fs/src/fs.rs @@ -139,7 +139,7 @@ pub trait FileSystem: Send + Sync + RefUnwindSafe { /// This method logs an error message and returns a `FileSystemDiagnostic` error in two scenarios: /// - If the file cannot be opened, possibly due to incorrect path or permission issues. /// - If the file is opened but its content cannot be read, potentially due to the file being damaged. - fn read_file_from_path(&self, file_path: &PathBuf) -> Result { + fn read_file_from_path(&self, file_path: &Path) -> Result { match self.open_with_options(file_path, OpenOptions::default().read(true)) { Ok(mut file) => { let mut content = String::new(); diff --git a/crates/biome_grit_patterns/Cargo.toml b/crates/biome_grit_patterns/Cargo.toml index 90460cbf845d..e995a4cb41ca 100644 --- a/crates/biome_grit_patterns/Cargo.toml +++ b/crates/biome_grit_patterns/Cargo.toml @@ -1,7 +1,7 @@ [package] authors.workspace = true categories.workspace = true -description = "Biome implementing for matching Grit Patterns" +description = "Biome implementation for Grit Patterns" edition.workspace = true homepage.workspace = true keywords.workspace = true @@ -21,8 +21,8 @@ biome_js_syntax = { workspace = true } biome_parser = { workspace = true } biome_rowan = { workspace = true } biome_string_case = { workspace = true } -grit-pattern-matcher = { version = "0.4" } -grit-util = { version = "0.4" } +grit-pattern-matcher = { workspace = true } +grit-util = { workspace = true } path-absolutize = { version = "3.1.1", optional = false, features = ["use_unix_paths_on_wasm"] } rand = { version = "0.8.5" } regex = { workspace = true } diff --git a/crates/biome_js_analyze/src/lint/nursery/use_collapsed_if.rs b/crates/biome_js_analyze/src/lint/nursery/use_collapsed_if.rs index c9a2291babd6..fb7a951ec51a 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_collapsed_if.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_collapsed_if.rs @@ -1,4 +1,7 @@ -use biome_analyze::{context::RuleContext, declare_lint_rule, ActionCategory, Ast, FixKind, QueryMatch, Rule, RuleDiagnostic, RuleSource}; +use biome_analyze::{ + context::RuleContext, declare_lint_rule, ActionCategory, Ast, FixKind, QueryMatch, Rule, + RuleDiagnostic, RuleSource, +}; use biome_console::markup; use biome_js_factory::make; use biome_js_syntax::parentheses::NeedsParentheses; diff --git a/crates/biome_parser/src/lib.rs b/crates/biome_parser/src/lib.rs index 5c708d83c225..0e79ee868151 100644 --- a/crates/biome_parser/src/lib.rs +++ b/crates/biome_parser/src/lib.rs @@ -657,6 +657,15 @@ pub struct AnyParse { pub(crate) diagnostics: Vec, } +impl From for AnyParse { + fn from(root: SendNode) -> Self { + Self { + root, + diagnostics: Vec::new(), + } + } +} + impl AnyParse { pub fn new(root: SendNode, diagnostics: Vec) -> AnyParse { AnyParse { root, diagnostics } diff --git a/crates/biome_plugin_loader/Cargo.toml b/crates/biome_plugin_loader/Cargo.toml new file mode 100644 index 000000000000..164ff7e17d50 --- /dev/null +++ b/crates/biome_plugin_loader/Cargo.toml @@ -0,0 +1,32 @@ + +[package] +authors.workspace = true +categories.workspace = true +description = "biome_plugin_loader2" +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +name = "biome_plugin_loader" +repository.workspace = true +version = "0.0.1" + +[dependencies] +biome_analyze = { workspace = true } +biome_console = { workspace = true } +biome_deserialize = { workspace = true } +biome_deserialize_macros = { workspace = true } +biome_diagnostics = { workspace = true } +biome_fs = { workspace = true } +biome_grit_patterns = { workspace = true } +biome_json_parser = { workspace = true } +biome_parser = { workspace = true } +biome_rowan = { workspace = true } +grit-util = { workspace = true } +serde = { workspace = true } + +[dev-dependencies] +insta = { workspace = true } + +[lints] +workspace = true diff --git a/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs b/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs new file mode 100644 index 000000000000..9df88d38ba7f --- /dev/null +++ b/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs @@ -0,0 +1,81 @@ +use std::{ + fmt::Debug, + path::{Path, PathBuf}, + rc::Rc, +}; + +use biome_analyze::RuleDiagnostic; +use biome_console::markup; +use biome_diagnostics::category; +use biome_fs::FileSystem; +use biome_grit_patterns::{ + compile_pattern, GritQuery, GritQueryResult, GritTargetFile, GritTargetLanguage, + JsTargetLanguage, +}; +use biome_parser::AnyParse; +use biome_rowan::TextRange; + +use crate::{AnalyzerPlugin, PluginDiagnostic}; + +/// Definition of an analyzer plugin. +#[derive(Clone, Debug)] +pub struct AnalyzerGritPlugin { + grit_query: Rc, +} + +impl AnalyzerGritPlugin { + pub fn load(fs: &dyn FileSystem, path: &Path) -> Result { + let source = fs.read_file_from_path(path)?; + let query = compile_pattern( + &source, + Some(path), + // TODO: Target language should be determined dynamically. + GritTargetLanguage::JsTargetLanguage(JsTargetLanguage), + )?; + + Ok(Self { + grit_query: Rc::new(query), + }) + } +} + +impl AnalyzerPlugin for AnalyzerGritPlugin { + fn evaluate(&self, root: AnyParse, path: PathBuf) -> Vec { + let name: &str = self.grit_query.name.as_deref().unwrap_or("anonymous"); + + let file = GritTargetFile { parse: root, path }; + match self.grit_query.execute(file) { + Ok((results, logs)) => results + .into_iter() + .filter_map(|result| match result { + GritQueryResult::Match(match_) => Some(match_), + GritQueryResult::Rewrite(_) | GritQueryResult::CreateFile(_) => None, + }) + .map(|match_| { + RuleDiagnostic::new( + category!("plugin"), + match_.ranges.into_iter().next().map(from_grit_range), + markup!({name}" matched"), + ) + }) + .chain(logs.iter().map(|log| { + RuleDiagnostic::new( + category!("plugin"), + log.range.map(from_grit_range), + markup!({name}" logged: "{log.message}), + ) + .verbose() + })) + .collect(), + Err(error) => vec![RuleDiagnostic::new( + category!("plugin"), + None::, + markup!({name}" errored: "{error.to_string()}), + )], + } + } +} + +fn from_grit_range(range: grit_util::Range) -> TextRange { + TextRange::new(range.start_byte.into(), range.end_byte.into()) +} diff --git a/crates/biome_plugin_loader/src/analyzer_plugin.rs b/crates/biome_plugin_loader/src/analyzer_plugin.rs new file mode 100644 index 000000000000..9f6a61027e5c --- /dev/null +++ b/crates/biome_plugin_loader/src/analyzer_plugin.rs @@ -0,0 +1,8 @@ +use biome_analyze::RuleDiagnostic; +use biome_parser::AnyParse; +use std::{fmt::Debug, path::PathBuf}; + +/// Definition of an analyzer plugin. +pub trait AnalyzerPlugin: Debug { + fn evaluate(&self, root: AnyParse, path: PathBuf) -> Vec; +} diff --git a/crates/biome_plugin_loader/src/diagnostics.rs b/crates/biome_plugin_loader/src/diagnostics.rs new file mode 100644 index 000000000000..4deeee22207c --- /dev/null +++ b/crates/biome_plugin_loader/src/diagnostics.rs @@ -0,0 +1,198 @@ +use biome_console::fmt::Display; +use biome_console::markup; +use biome_deserialize::DeserializationDiagnostic; +use biome_diagnostics::adapters::ResolveError; +use biome_diagnostics::{Diagnostic, Error, MessageAndDescription}; +use biome_fs::FileSystemDiagnostic; +use biome_grit_patterns::CompileError; +use biome_rowan::SyntaxError; +use serde::{Deserialize, Serialize}; +use std::fmt::{Debug, Formatter}; + +/// Series of errors that can be thrown while loading a plugin. +#[derive(Deserialize, Diagnostic, Serialize)] +pub enum PluginDiagnostic { + /// Thrown when a plugin can't be resolved from `node_modules`. + CantResolve(CantResolve), + + /// Error compiling the plugin + Compile(CompileDiagnostic), + + /// Error thrown when deserializing a plugin manifest, such as: + /// - syntax error + /// - incorrect fields + /// - incorrect values + Deserialization(DeserializationDiagnostic), + + /// Error loading the plugin from the file system. + FileSystem(FileSystemDiagnostic), + + /// When something is wrong with the manifest. + InvalidManifest(InvalidManifest), + + /// When an analyzer rule plugin uses an unsupported file format. + UnsupportedRuleFormat(UnsupportedRuleFormat), +} + +impl From for PluginDiagnostic { + fn from(value: CompileError) -> Self { + PluginDiagnostic::Compile(CompileDiagnostic { + source: Some(Error::from(value)), + }) + } +} + +impl From for PluginDiagnostic { + fn from(value: DeserializationDiagnostic) -> Self { + PluginDiagnostic::Deserialization(value) + } +} + +impl From for PluginDiagnostic { + fn from(value: FileSystemDiagnostic) -> Self { + PluginDiagnostic::FileSystem(value) + } +} + +impl From for PluginDiagnostic { + fn from(_: SyntaxError) -> Self { + PluginDiagnostic::Deserialization(DeserializationDiagnostic::new(markup! {"Syntax Error"})) + } +} + +impl PluginDiagnostic { + pub fn cant_resolve(path: impl Display, source: Option) -> Self { + Self::CantResolve(CantResolve { + message: MessageAndDescription::from( + markup! { + "Failed to resolve the plugin manifest from "{path} + } + .to_owned(), + ), + source: source.map(Error::from), + }) + } + + pub fn invalid_manifest(message: impl Display, source: Option) -> Self { + Self::InvalidManifest(InvalidManifest { + message: MessageAndDescription::from(markup! {{message}}.to_owned()), + source, + }) + } + + pub fn unsupported_rule_format(message: impl Display) -> Self { + Self::UnsupportedRuleFormat(UnsupportedRuleFormat { + message: MessageAndDescription::from(markup! {{message}}.to_owned()), + }) + } +} + +impl Debug for PluginDiagnostic { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self, f) + } +} + +impl std::fmt::Display for PluginDiagnostic { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.description(f) + } +} + +#[derive(Debug, Serialize, Deserialize, Diagnostic)] +#[diagnostic( + category = "plugin", + message = "Error compiling plugin", + severity = Error, +)] +pub struct CompileDiagnostic { + #[serde(skip)] + #[source] + source: Option, +} + +#[derive(Debug, Serialize, Deserialize, Diagnostic)] +#[diagnostic( + category = "plugin", + severity = Error, +)] +pub struct InvalidManifest { + #[message] + #[description] + message: MessageAndDescription, + + #[serde(skip)] + #[source] + source: Option, +} + +#[derive(Debug, Serialize, Deserialize, Diagnostic)] +#[diagnostic( + category = "plugin", + severity = Error, +)] +pub struct CantResolve { + #[message] + #[description] + message: MessageAndDescription, + + #[serde(skip)] + #[source] + source: Option, +} + +#[derive(Debug, Serialize, Deserialize, Diagnostic)] +#[diagnostic( + category = "plugin", + severity = Error, +)] +pub struct UnsupportedRuleFormat { + #[message] + #[description] + pub message: MessageAndDescription, +} + +#[cfg(test)] +mod test { + use crate::plugin_manifest::PluginManifest; + + use biome_deserialize::json::deserialize_from_json_str; + use biome_diagnostics::{print_diagnostic_to_string, Error}; + use biome_json_parser::JsonParserOptions; + + fn snap_diagnostic(test_name: &str, diagnostic: Error) { + let content = print_diagnostic_to_string(&diagnostic); + + insta::with_settings!({ + prepend_module_to_snapshot => false, + }, { + insta::assert_snapshot!(test_name, content); + }); + } + + #[test] + fn deserialization_error() { + let content = "{}"; + let result = + deserialize_from_json_str::(content, JsonParserOptions::default(), ""); + + assert!(result.has_errors()); + for diagnostic in result.into_diagnostics() { + snap_diagnostic("deserialization_error", diagnostic) + } + } + + #[test] + fn deserialization_quick_check() { + let content = r#"{ + "version": 1, + "rules": [ + "./rules/my-rule.grit" + ] +}"#; + let _result = + deserialize_from_json_str::(content, JsonParserOptions::default(), "") + .into_deserialized() + .unwrap_or_default(); + } +} diff --git a/crates/biome_plugin_loader/src/lib.rs b/crates/biome_plugin_loader/src/lib.rs new file mode 100644 index 000000000000..30ae44715f08 --- /dev/null +++ b/crates/biome_plugin_loader/src/lib.rs @@ -0,0 +1,192 @@ +mod analyzer_grit_plugin; +mod analyzer_plugin; +mod diagnostics; +mod plugin_manifest; + +use std::path::Path; + +use analyzer_grit_plugin::AnalyzerGritPlugin; +pub use analyzer_plugin::AnalyzerPlugin; +use biome_console::markup; +use biome_deserialize::json::deserialize_from_json_str; +use biome_diagnostics::adapters::ResolveError; +use biome_fs::FileSystem; +use biome_json_parser::JsonParserOptions; +use diagnostics::PluginDiagnostic; +use plugin_manifest::PluginManifest; + +#[derive(Debug)] +pub struct BiomePlugin { + pub analyzer_plugins: Vec>, +} + +impl BiomePlugin { + /// Loads a plugin from the given `plugin_path`. + /// + /// Base paths are used to resolve relative paths and package specifiers. + pub fn load( + fs: &dyn FileSystem, + plugin_path: &str, + relative_resolution_base_path: &Path, + external_resolution_base_path: &Path, + ) -> 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 { + fs.resolve_configuration(plugin_path, external_resolution_base_path) + .map_err(|error| { + PluginDiagnostic::cant_resolve( + external_resolution_base_path.display().to_string(), + Some(ResolveError::from(error)), + ) + })? + .into_path_buf() + }; + + // 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") + { + let plugin = AnalyzerGritPlugin::load(fs, &plugin_path)?; + return Ok(Self { + analyzer_plugins: vec![Box::new(plugin) as Box], + }); + } + + let manifest_path = plugin_path.join("biome-manifest.jsonc"); + if !fs.path_is_file(&manifest_path) { + return Err(PluginDiagnostic::cant_resolve( + manifest_path.display().to_string(), + None, + )); + } + + let manifest_content = fs.read_file_from_path(&manifest_path)?; + let (manifest, errors) = deserialize_from_json_str::( + &manifest_content, + JsonParserOptions::default().with_allow_comments(), + "", + ) + .consume(); + + let Some(manifest) = manifest else { + return Err(PluginDiagnostic::invalid_manifest( + markup!("Cannot load plugin manifest "{manifest_path.display().to_string()}), + errors.into_iter().next(), + )); + }; + + let plugin = Self { + analyzer_plugins: manifest + .rules + .into_iter() + .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) + } else { + Err(PluginDiagnostic::unsupported_rule_format(markup!( + "Unsupported rule format for plugin rule " + {rule.display().to_string()} + ))) + } + }) + .collect::>()?, + }; + + Ok(plugin) + } +} + +#[cfg(test)] +mod test { + use biome_diagnostics::{print_diagnostic_to_string, Error}; + use biome_fs::MemoryFileSystem; + + use super::*; + + fn snap_diagnostic(test_name: &str, diagnostic: Error) { + let content = print_diagnostic_to_string(&diagnostic); + + insta::with_settings!({ + prepend_module_to_snapshot => false, + }, { + insta::assert_snapshot!(test_name, content); + }); + } + + #[test] + fn load_plugin() { + let mut fs = MemoryFileSystem::default(); + fs.insert( + "/my-plugin/biome-manifest.jsonc".into(), + r#"{ + "version": 1, + "rules": ["rules/1.grit"] +}"#, + ); + + fs.insert("/my-plugin/rules/1.grit".into(), r#"`hello`"#); + + let plugin = BiomePlugin::load(&fs, "./my-plugin", Path::new("/"), Path::new("/")) + .expect("Couldn't load plugin"); + assert_eq!(plugin.analyzer_plugins.len(), 1); + } + + #[test] + fn load_plugin_without_manifest() { + let mut fs = MemoryFileSystem::default(); + fs.insert("/my-plugin/rules/1.grit".into(), r#"`hello`"#); + + let error = BiomePlugin::load(&fs, "./my-plugin", Path::new("/"), Path::new("/")) + .expect_err("Plugin loading should've failed"); + snap_diagnostic("load_plugin_without_manifest", error.into()); + } + + #[test] + fn load_plugin_with_wrong_version() { + let mut fs = MemoryFileSystem::default(); + fs.insert( + "/my-plugin/biome-manifest.jsonc".into(), + r#"{ + "version": 2, + "rules": ["rules/1.grit"] +}"#, + ); + + let error = BiomePlugin::load(&fs, "./my-plugin", Path::new("/"), Path::new("/")) + .expect_err("Plugin loading should've failed"); + snap_diagnostic("load_plugin_with_wrong_version", error.into()); + } + + #[test] + fn load_plugin_with_wrong_rule_extension() { + let mut fs = MemoryFileSystem::default(); + fs.insert( + "/my-plugin/biome-manifest.jsonc".into(), + r#"{ + "version": 1, + "rules": ["rules/1.js"] +}"#, + ); + + let error = BiomePlugin::load(&fs, "./my-plugin", Path::new("/"), Path::new("/")) + .expect_err("Plugin loading should've failed"); + snap_diagnostic("load_plugin_with_wrong_rule_extension", error.into()); + } + + #[test] + fn load_single_rule_plugin() { + let mut fs = MemoryFileSystem::default(); + fs.insert("/my-plugin.grit".into(), r#"`hello`"#); + + let plugin = BiomePlugin::load(&fs, "./my-plugin.grit", Path::new("/"), Path::new("/")) + .expect("Couldn't load plugin"); + assert_eq!(plugin.analyzer_plugins.len(), 1); + } +} diff --git a/crates/biome_plugin_loader/src/plugin_manifest.rs b/crates/biome_plugin_loader/src/plugin_manifest.rs new file mode 100644 index 000000000000..a5cea9426c29 --- /dev/null +++ b/crates/biome_plugin_loader/src/plugin_manifest.rs @@ -0,0 +1,34 @@ +use biome_console::markup; +use biome_deserialize::DeserializationDiagnostic; +use biome_deserialize_macros::Deserializable; +use biome_rowan::TextRange; + +use std::path::PathBuf; + +#[derive(Clone, Debug, Default, Deserializable, Eq, PartialEq)] +pub struct PluginManifest { + #[deserializable(required, validate = "supported_version")] + pub version: u8, + + pub rules: Vec, +} + +// There's only one manifest version now. +pub fn supported_version( + value: &u8, + name: &str, + range: TextRange, + diagnostics: &mut Vec, +) -> bool { + if *value == 1 { + true + } else { + diagnostics.push( + DeserializationDiagnostic::new(markup! { + {name}" must be 1" + }) + .with_range(range), + ); + false + } +} diff --git a/crates/biome_plugin_loader/src/snapshots/deserialization_error.snap b/crates/biome_plugin_loader/src/snapshots/deserialization_error.snap new file mode 100644 index 000000000000..184db309e438 --- /dev/null +++ b/crates/biome_plugin_loader/src/snapshots/deserialization_error.snap @@ -0,0 +1,10 @@ +--- +source: crates/biome_plugin_loader/src/diagnostics.rs +expression: content +--- +deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × The key `version` is missing. + + > 1 │ {} + │ ^^ diff --git a/crates/biome_plugin_loader/src/snapshots/load_plugin_with_wrong_rule_extension.snap b/crates/biome_plugin_loader/src/snapshots/load_plugin_with_wrong_rule_extension.snap new file mode 100644 index 000000000000..842e02e23241 --- /dev/null +++ b/crates/biome_plugin_loader/src/snapshots/load_plugin_with_wrong_rule_extension.snap @@ -0,0 +1,7 @@ +--- +source: crates/biome_plugin_loader/src/lib.rs +expression: content +--- +plugin ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unsupported rule format for plugin rule rules/1.js diff --git a/crates/biome_plugin_loader/src/snapshots/load_plugin_with_wrong_version.snap b/crates/biome_plugin_loader/src/snapshots/load_plugin_with_wrong_version.snap new file mode 100644 index 000000000000..b46543c1158a --- /dev/null +++ b/crates/biome_plugin_loader/src/snapshots/load_plugin_with_wrong_version.snap @@ -0,0 +1,10 @@ +--- +source: crates/biome_plugin_loader/src/lib.rs +expression: content +--- +plugin ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Cannot load plugin manifest /my-plugin/biome-manifest.jsonc + + Caused by: + version must be 1 diff --git a/crates/biome_plugin_loader/src/snapshots/load_plugin_without_manifest.snap b/crates/biome_plugin_loader/src/snapshots/load_plugin_without_manifest.snap new file mode 100644 index 000000000000..26c1c053806e --- /dev/null +++ b/crates/biome_plugin_loader/src/snapshots/load_plugin_without_manifest.snap @@ -0,0 +1,7 @@ +--- +source: crates/biome_plugin_loader/src/lib.rs +expression: content +--- +plugin ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Failed to resolve the plugin manifest from /my-plugin/biome-manifest.jsonc diff --git a/crates/biome_service/src/file_handlers/css.rs b/crates/biome_service/src/file_handlers/css.rs index 96616e9fc98c..c92685b385a6 100644 --- a/crates/biome_service/src/file_handlers/css.rs +++ b/crates/biome_service/src/file_handlers/css.rs @@ -144,9 +144,8 @@ impl ServiceLanguage for CssLanguage { rules: global .map(|g| to_analyzer_rules(g, file_path.as_path())) .unwrap_or_default(), - globals: Vec::new(), preferred_quote, - jsx_runtime: None, + ..Default::default() }; AnalyzerOptions { diff --git a/crates/biome_service/src/file_handlers/json.rs b/crates/biome_service/src/file_handlers/json.rs index cf72190b2498..b9f1b5695e10 100644 --- a/crates/biome_service/src/file_handlers/json.rs +++ b/crates/biome_service/src/file_handlers/json.rs @@ -139,7 +139,7 @@ impl ServiceLanguage for JsonLanguage { .unwrap_or_default(), globals: vec![], preferred_quote: PreferredQuote::Double, - jsx_runtime: Default::default(), + ..Default::default() }; AnalyzerOptions { configuration, diff --git a/crates/biome_test_utils/src/lib.rs b/crates/biome_test_utils/src/lib.rs index 6191e2eccc95..3e80195c4787 100644 --- a/crates/biome_test_utils/src/lib.rs +++ b/crates/biome_test_utils/src/lib.rs @@ -1,5 +1,5 @@ use biome_analyze::options::{JsxRuntime, PreferredQuote}; -use biome_analyze::{AnalyzerAction, AnalyzerConfiguration, AnalyzerOptions, AnalyzerRules}; +use biome_analyze::{AnalyzerAction, AnalyzerConfiguration, AnalyzerOptions}; use biome_configuration::PartialConfiguration; use biome_console::fmt::{Formatter, Termcolor}; use biome_console::markup; @@ -39,10 +39,9 @@ pub fn create_analyzer_options( // file with the same name as the test but with extension ".options.json" // that configures that specific rule. let mut analyzer_configuration = AnalyzerConfiguration { - rules: AnalyzerRules::default(), - globals: vec![], preferred_quote: PreferredQuote::Double, jsx_runtime: Some(JsxRuntime::Transparent), + ..Default::default() }; let options_file = input_file.with_extension("options.json"); if let Ok(json) = std::fs::read_to_string(options_file.clone()) { diff --git a/knope.toml b/knope.toml index 641743ff4b08..c6bea5437b69 100644 --- a/knope.toml +++ b/knope.toml @@ -212,6 +212,10 @@ versioned_files = ["crates/biome_graphql_semantic/Cargo.toml"] changelog = "crates/biome_css_semantic/CHANGELOG.md" versioned_files = ["crates/biome_css_semantic/Cargo.toml"] +[packages.biome_plugin_loader] +changelog = "crates/biome_plugin_loader/CHANGELOG.md" +versioned_files = ["crates/biome_plugin_loader/Cargo.toml"] + ## End of crates. DO NOT CHANGE! # Workflow to create a changeset diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 98aa521a3abd..1004cc666964 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -97,6 +97,10 @@ export interface PartialConfiguration { * A list of granular patterns that should be applied only to a sub set of files */ overrides?: Overrides; + /** + * List of plugins to load. + */ + plugins?: Plugins; /** * The configuration of the VCS integration */ @@ -305,6 +309,7 @@ export interface PartialOrganizeImports { include?: StringSet; } export type Overrides = OverridePattern[]; +export type Plugins = PluginConfiguration[]; /** * Set of properties to integrate Biome with a VCS software. */ @@ -656,6 +661,7 @@ export interface OverridePattern { */ organizeImports?: OverrideOrganizeImportsConfiguration; } +export type PluginConfiguration = string; export type VcsClientKind = "git"; /** * A list of rules that belong to this group @@ -3104,6 +3110,7 @@ export type Category = | "assists" | "migrate" | "deserialize" + | "plugin" | "project" | "search" | "internalError/io" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 4a4f61340b45..b1d0a83f4fef 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -76,6 +76,10 @@ "description": "A list of granular patterns that should be applied only to a sub set of files", "anyOf": [{ "$ref": "#/definitions/Overrides" }, { "type": "null" }] }, + "plugins": { + "description": "List of plugins to load.", + "anyOf": [{ "$ref": "#/definitions/Plugins" }, { "type": "null" }] + }, "vcs": { "description": "The configuration of the VCS integration", "anyOf": [ @@ -2613,6 +2617,11 @@ }, "additionalProperties": false }, + "PluginConfiguration": { "anyOf": [{ "type": "string" }] }, + "Plugins": { + "type": "array", + "items": { "$ref": "#/definitions/PluginConfiguration" } + }, "QuoteProperties": { "type": "string", "enum": ["asNeeded", "preserve"] }, "QuoteStyle": { "type": "string", "enum": ["double", "single"] }, "Regex": { "type": "string" },