feat(core): implement workspace plugin loading (#5160)

This commit is contained in:
Arend van Beelen jr. 2025-02-20 14:00:37 +01:00 committed by GitHub
parent 5741b80349
commit a7ebbf0db0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 653 additions and 331 deletions

View file

@ -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

3
Cargo.lock generated
View file

@ -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",

View file

@ -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<Box<dyn AnalyzerPlugin>>];
/// Vector of analyzer plugins that can be cheaply cloned.
pub type AnalyzerPluginVec = Vec<Arc<Box<dyn AnalyzerPlugin>>>;
/// Definition of an analyzer plugin.
pub trait AnalyzerPlugin: Debug {
pub trait AnalyzerPlugin: Debug + Send + Sync {
fn evaluate(&self, root: AnyParse, path: Utf8PathBuf) -> Vec<RuleDiagnostic>;
fn supports_css(&self) -> bool;

View file

@ -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<Phases, Vec<Box<dyn Visitor<Language = L> + 'analyzer>>>,
/// Plugins to be run after the phases for built-in rules.
plugins: Vec<Box<dyn AnalyzerPlugin>>,
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<dyn AnalyzerPlugin>) {
pub fn add_plugin(&mut self, plugin: Arc<Box<dyn AnalyzerPlugin>>) {
self.plugins.push(plugin);
}

View file

@ -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)?;

View file

@ -10,6 +10,12 @@ use std::str::FromStr;
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Plugins(pub Vec<PluginConfiguration>);
impl Plugins {
pub fn iter(&self) -> impl Iterator<Item = &PluginConfiguration> {
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))*/

View file

@ -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<CssLanguage>,
filter: AnalysisFilter,
options: &'a AnalyzerOptions,
plugins: Vec<Box<dyn AnalyzerPlugin>>,
plugins: AnalyzerPluginSlice<'a>,
emit_signal: F,
) -> (Option<B>, Vec<Error>)
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<Box<dyn AnalyzerPlugin>>,
plugins: AnalyzerPluginSlice<'a>,
mut emit_signal: F,
) -> (Option<B>, Vec<Error>)
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") {

View file

@ -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<Box<dyn AnalyzerPlugin>>,
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!({

View file

@ -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<Box<dyn AnalyzerPlugin>>,
plugins: AnalyzerPluginSlice<'a>,
services: JsAnalyzerServices,
mut emit_signal: F,
) -> (Option<B>, Vec<DiagnosticError>)
@ -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<JsLanguage>,
filter: AnalysisFilter,
options: &'a AnalyzerOptions,
plugins: Vec<Box<dyn AnalyzerPlugin>>,
plugins: AnalyzerPluginSlice<'a>,
services: JsAnalyzerServices,
emit_signal: F,
) -> (Option<B>, Vec<DiagnosticError>)
@ -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() {

View file

@ -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::<Never>::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::<Never>::Continue(())
});
for error in errors {
diagnostics.push(diagnostic_to_string(file_name, input_code, error));

View file

@ -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<Box<dyn AnalyzerPlugin>>,
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!({

View file

@ -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]

View file

@ -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<GritQuery>,
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 })
}
}

View file

@ -100,6 +100,12 @@ impl std::fmt::Display for PluginDiagnostic {
}
}
impl From<PluginDiagnostic> for biome_diagnostics::serde::Diagnostic {
fn from(error: PluginDiagnostic) -> Self {
biome_diagnostics::serde::Diagnostic::new(error)
}
}
#[derive(Debug, Serialize, Deserialize, Diagnostic)]
#[diagnostic(
category = "plugin",

View file

@ -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<Box<dyn AnalyzerPlugin>>,
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<Self, PluginDiagnostic> {
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<dyn AnalyzerPlugin>],
analyzer_plugins: vec![Arc::new(Box::new(plugin) as Box<dyn AnalyzerPlugin>)],
});
}
@ -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<dyn AnalyzerPlugin>)
Ok(Arc::new(Box::new(plugin) as Box<dyn AnalyzerPlugin>))
} else {
Err(PluginDiagnostic::unsupported_rule_format(markup!(
"Unsupported rule format for plugin rule "
@ -105,6 +91,39 @@ 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();
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 +157,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 +167,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 +183,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 +199,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 +209,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);
}
}

View file

@ -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<Utf8PathBuf, BiomePlugin, FxBuildHasher>);
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
}
}

View file

@ -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 }

View file

@ -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<PluginDiagnostic>) -> 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<PluginDiagnostic>,
}
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",

View file

@ -520,10 +520,13 @@ fn lint(params: LintParams) -> LintResults {
let mut process_lint = ProcessLint::new(&params);
let (_, analyze_diagnostics) =
analyze(&tree, filter, &analyzer_options, Vec::new(), |signal| {
process_lint.process_signal(signal)
});
let (_, analyze_diagnostics) = analyze(
&tree,
filter,
&analyzer_options,
&params.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<FixFileResult, WorkspaceEr
let mut errors: u16 = 0;
loop {
let (action, _) = analyze(&tree, filter, &analyzer_options, Vec::new(), |signal| {
let current_diagnostic = signal.diagnostic();
let (action, _) = analyze(
&tree,
filter,
&analyzer_options,
&params.plugins,
|signal| {
let current_diagnostic = signal.diagnostic();
if let Some(diagnostic) = current_diagnostic.as_ref() {
if is_diagnostic_error(diagnostic, rules.as_deref()) {
errors += 1;
}
}
for action in signal.actions() {
// suppression actions should not be part of the fixes (safe or suggested)
if action.is_suppression() {
continue;
}
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);
}
}
FixFileMode::SafeAndUnsafeFixes => {
if matches!(
action.applicability,
Applicability::Always | Applicability::MaybeIncorrect
) {
errors = errors.saturating_sub(1);
return ControlFlow::Break(action);
}
}
FixFileMode::ApplySuppressions => {
// TODO: to implement
if let Some(diagnostic) = current_diagnostic.as_ref() {
if is_diagnostic_error(diagnostic, rules.as_deref()) {
errors += 1;
}
}
}
ControlFlow::Continue(())
});
for action in signal.actions() {
// suppression actions should not be part of the fixes (safe or suggested)
if action.is_suppression() {
continue;
}
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);
}
}
FixFileMode::SafeAndUnsafeFixes => {
if matches!(
action.applicability,
Applicability::Always | Applicability::MaybeIncorrect
) {
errors = errors.saturating_sub(1);
return ControlFlow::Break(action);
}
}
FixFileMode::ApplySuppressions => {
// TODO: to implement
}
}
}
ControlFlow::Continue(())
},
);
match action {
Some(action) => {

View file

@ -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();

View file

@ -605,7 +605,7 @@ fn debug_control_flow(parse: AnyParse, cursor: TextSize) -> String {
}
},
&options,
Vec::new(),
&[],
Default::default(),
|_| ControlFlow::<Never>::Continue(()),
);
@ -672,7 +672,7 @@ pub(crate) fn lint(params: LintParams) -> LintResults {
&tree,
filter,
&analyzer_options,
Vec::new(),
&params.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<FixFileResult, WorkspaceEr
&tree,
filter,
&analyzer_options,
Vec::new(),
&params.plugins,
services,
|signal| {
let current_diagnostic = signal.diagnostic();

View file

@ -568,6 +568,7 @@ fn code_actions(params: CodeActionsParams) -> PullActionsResult {
only,
enabled_rules: rules,
suppression_reason,
plugins: _,
} = params;
let _ = debug_span!("Code actions JSON", range =? range, path =? path).entered();

View file

@ -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<String>,
pub(crate) enabled_rules: Vec<RuleSelector>,
pub(crate) plugins: AnalyzerPluginVec,
}
#[derive(Default)]
@ -463,6 +465,7 @@ pub(crate) struct LintParams<'a> {
pub(crate) project_layout: Arc<ProjectLayout>,
pub(crate) suppression_reason: Option<String>,
pub(crate) enabled_rules: Vec<RuleSelector>,
pub(crate) plugins: AnalyzerPluginVec,
}
pub(crate) struct LintResults {
@ -590,6 +593,7 @@ pub(crate) struct CodeActionsParams<'a> {
pub(crate) skip: Vec<RuleSelector>,
pub(crate) suppression_reason: Option<String>,
pub(crate) enabled_rules: Vec<RuleSelector>,
pub(crate) plugins: AnalyzerPluginVec,
}
type Lint = fn(LintParams) -> LintResults;

View file

@ -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 =

View file

@ -530,6 +530,13 @@ pub struct UpdateSettingsParams {
pub workspace_directory: Option<BiomePath>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct UpdateSettingsResult {
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
@ -1035,10 +1042,13 @@ 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, params: UpdateSettingsParams) -> Result<(), WorkspaceError>;
fn update_settings(
&self,
params: UpdateSettingsParams,
) -> Result<UpdateSettingsResult, WorkspaceError>;
/// Closes the project with the given key.
///

View file

@ -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<T> {
@ -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<UpdateSettingsResult, WorkspaceError> {
self.request("biome/update_settings", params)
}

View file

@ -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<DependencyGraph>,
/// Keeps all loaded plugins in memory, per project.
plugin_caches: Arc<HashMap<ProjectKey, PluginCache>>,
/// Stores the document (text content + version number) associated with a URL
documents: HashMap<Utf8PathBuf, Document, FxBuildHasher>,
@ -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<PluginDiagnostic> {
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,44 @@ 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<UpdateSettingsResult, WorkspaceError> {
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,
);
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);
Ok(())
Ok(UpdateSettingsResult {
diagnostics: diagnostics.into_iter().map(Into::into).collect(),
})
}
fn open_file(&self, params: OpenFileParams) -> Result<(), WorkspaceError> {
@ -918,6 +982,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 +1063,7 @@ impl Workspace for WorkspaceServer {
skip,
suppression_reason: None,
enabled_rules,
plugins: self.get_analyzer_plugins_for_project(project_key),
}))
}
@ -1137,6 +1203,7 @@ impl Workspace for WorkspaceServer {
rule_categories,
suppression_reason,
enabled_rules,
plugins: self.get_analyzer_plugins_for_project(project_key),
})
}

View file

@ -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<DiagnosticTag> {
bits: 0b0,
},
),
source: None,
},
]

View file

@ -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<dyn Workspace>, 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);
}
}

View file

@ -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<IUpdateSettingsResult, Error> {
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)]

View file

@ -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<FileFeaturesResult>;
updateSettings(params: UpdateSettingsParams): Promise<void>;
updateSettings(params: UpdateSettingsParams): Promise<UpdateSettingsResult>;
openProject(params: OpenProjectParams): Promise<ProjectKey>;
openFile(params: OpenFileParams): Promise<void>;
changeFile(params: ChangeFileParams): Promise<void>;

View file

@ -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::<Never>::Continue(())

View file

@ -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() {