Skip to content

Commit

Permalink
PoC: loadable plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
arendjr committed Sep 24, 2024
1 parent a3483e4 commit ccce38f
Show file tree
Hide file tree
Showing 24 changed files with 284 additions and 103 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 35 additions & 33 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -171,39 +171,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.14", 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.5.0", features = ["serde"] }
insta = "1.40.0"
natord = "1.0.9"
oxc_resolver = "1.11.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.10.6"
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.14", 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.3"
grit-util = "0.3"
ignore = "0.4.23"
indexmap = { version = "2.5.0", features = ["serde"] }
insta = "1.40.0"
natord = "1.0.9"
oxc_resolver = "1.11.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.10.6"
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"
Expand Down
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"style": {
"noNonNullAssertion": "off"
}
}
},
"plugins": ["plugin-example.grit"]
},
"organizeImports": {
"enabled": true
Expand Down
3 changes: 3 additions & 0 deletions crates/biome_analyze/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ biome_console = { workspace = true }
biome_deserialize = { workspace = true, optional = true }
biome_deserialize_macros = { workspace = true, optional = true }
biome_diagnostics = { workspace = true }
biome_grit_patterns = { workspace = true }
biome_rowan = { workspace = true }
enumflags2 = { workspace = true }
grit-util = { workspace = true }
oxc_resolver = { workspace = true }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"], optional = true }
Expand Down
49 changes: 47 additions & 2 deletions crates/biome_analyze/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
#![deny(rustdoc::broken_intra_doc_links)]

use biome_grit_patterns::{compile_pattern, GritTargetLanguage, JsTargetLanguage};
use oxc_resolver::{ResolveOptions, Resolver};
use std::cmp::Ordering;
use std::collections::{BTreeMap, BinaryHeap};
use std::fmt::{Debug, Display, Formatter};
use std::ops;
use std::path::Path;
use tracing::trace;

mod categories;
pub mod context;
mod diagnostics;
mod matcher;
pub mod options;
mod plugin;
mod query;
mod registry;
mod rule;
Expand Down Expand Up @@ -55,16 +59,23 @@ use biome_rowan::{
AstNode, BatchMutation, Direction, Language, SyntaxElement, SyntaxToken, TextLen, TextRange,
TextSize, TokenAtOffset, TriviaPiece, TriviaPieceKind, WalkEvent,
};
use plugin::{AnalyzerPlugin, PluginError};

/// The analyzer is the main entry point into the `biome_analyze` infrastructure.
/// Its role is to run a collection of [Visitor]s over a syntax tree, with each
/// visitor implementing various analysis over this syntax tree to generate
/// auxiliary data structures as well as emit "query match" events to be
/// processed by lint rules and in turn emit "analyzer signals" in the form of
/// diagnostics, code actions or both
/// diagnostics, code actions or both.
/// The analyzer also has support for plugins, although do not (as of yet)
/// support the same visitor pattern. This makes them slower to execute, but
/// otherwise they act the same for consumers of the analyzer. They respect the
/// same suppression comments, and report signals in the same format.
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<AnalyzerPlugin>,
/// Holds the metadata for all the rules statically known to the analyzer
metadata: &'analyzer MetadataRegistry,
/// Executor for the query matches emitted by the visitors
Expand All @@ -86,7 +97,7 @@ pub struct AnalyzerContext<'a, L: Language> {

impl<'analyzer, L, Matcher, Break, Diag> Analyzer<'analyzer, L, Matcher, Break, Diag>
where
L: Language,
L: Language + 'static,
Matcher: QueryMatcher<L>,
Diag: Diagnostic + Clone + Send + Sync + 'static,
{
Expand All @@ -101,6 +112,7 @@ where
) -> Self {
Self {
phases: BTreeMap::new(),
plugins: Vec::new(),
metadata,
query_matcher,
parse_suppression_comment,
Expand All @@ -118,9 +130,32 @@ where
self.phases.entry(phase).or_default().push(visitor);
}

/// Registers an [AnalyzerPlugin] to be executed after the built-in rules.
///
/// The plugin is loaded based on the given `identifier`, which can be a
/// path or another loadable identifier.
pub fn load_plugin(&mut self, plugin_id: &str) -> Result<(), PluginError> {
// TODO: Fix resolver, this didn't work yet.
/*let resolver = Resolver::new(ResolveOptions {
extensions: vec![".grit".to_string()],
..ResolveOptions::default()
});
let resolution = resolver.resolve(Path::new("."), plugin_id)?;*/
let source = std::fs::read_to_string(plugin_id)?;
let query = compile_pattern(
&source,
Some(Path::new(plugin_id)),
// TODO: Target language should be determined dynamically.
GritTargetLanguage::JsTargetLanguage(JsTargetLanguage),
)?;
self.plugins.push(query.into());
Ok(())
}

pub fn run(self, mut ctx: AnalyzerContext<L>) -> Option<Break> {
let Self {
phases,
plugins,
metadata,
mut query_matcher,
parse_suppression_comment,
Expand Down Expand Up @@ -173,6 +208,16 @@ where
}
}

for plugin in plugins {
for diagnostic in plugin.evaluate::<L>(&ctx.root, ctx.options.file_path.clone()) {
let signal = DiagnosticSignal::new(|| diagnostic.clone());

if let ControlFlow::Break(br) = (emit_signal)(&signal) {
return Some(br);
}
}
}

for suppression in line_suppressions {
if suppression.did_suppress_signal {
continue;
Expand Down
3 changes: 3 additions & 0 deletions crates/biome_analyze/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ pub struct AnalyzerConfiguration {
/// A list of rules and their options
pub rules: AnalyzerRules,

/// A list of plugins that should be loaded.
pub plugins: Vec<String>,

/// A collections of bindings that the analyzers should consider as "external".
///
/// For example, lint rules should ignore them.
Expand Down
110 changes: 110 additions & 0 deletions crates/biome_analyze/src/plugin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use crate::{rule::RuleAdvice, RuleDiagnostic};
use biome_console::markup;
use biome_diagnostics::{
adapters::{IoError, ResolveError},
category, Diagnostic, DiagnosticTags,
};
use biome_grit_patterns::{CompileError, GritQuery, GritQueryResult, GritTargetFile};
use biome_rowan::{AstNode, Language, TextRange};
use std::{fmt::Debug, path::PathBuf, rc::Rc};

/// Definition of an analyzer plugin.
#[derive(Clone, Debug)]
pub struct AnalyzerPlugin {
grit_query: Rc<GritQuery>,
}

impl From<GritQuery> for AnalyzerPlugin {
fn from(grit_query: GritQuery) -> Self {
Self {
grit_query: Rc::new(grit_query),
}
}
}

impl AnalyzerPlugin {
pub fn evaluate<L: Language + 'static>(
&self,
root: &L::Root,
path: PathBuf,
) -> Vec<RuleDiagnostic> {
let name: &str = self.grit_query.name.as_deref().unwrap_or("anonymous");

let file = GritTargetFile {
parse: root.syntax().as_send().expect("not a root node").into(),
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 {
category: category!("plugin"),
span: match_
.ranges
.into_iter()
.next()
.map(RangeExt::to_text_range),
// TODO: Plugin should be able to provide its own message
message: markup!(<Emphasis>{name}</Emphasis>" matched").into(),
tags: DiagnosticTags::empty(),
rule_advice: RuleAdvice::default(),
})
.chain(logs.iter().map(|log| RuleDiagnostic {
category: category!("plugin"),
span: log.range.map(RangeExt::to_text_range),
message: markup!(<Emphasis>{name}</Emphasis>" logged: "<Info>{log.message}</Info>)
.into(),
tags: DiagnosticTags::VERBOSE,
rule_advice: RuleAdvice::default(),
}))
.collect(),
Err(error) => vec![RuleDiagnostic {
category: category!("plugin"),
span: None,
message: markup!(<Emphasis>{name}</Emphasis>" errored: "<Error>{error.to_string()}</Error>)
.into(),
tags: DiagnosticTags::empty(),
rule_advice: RuleAdvice::default(),
}],
}
}
}

#[derive(Debug, Diagnostic)]
pub enum PluginError {
Compile(CompileError),
Io(IoError),
Resolve(ResolveError),
}

impl From<CompileError> for PluginError {
fn from(error: CompileError) -> Self {
Self::Compile(error)
}
}

impl From<std::io::Error> for PluginError {
fn from(error: std::io::Error) -> Self {
Self::Io(error.into())
}
}

impl From<oxc_resolver::ResolveError> for PluginError {
fn from(error: oxc_resolver::ResolveError) -> Self {
Self::Resolve(error.into())
}
}

trait RangeExt {
fn to_text_range(self) -> TextRange;
}

impl RangeExt for grit_util::Range {
fn to_text_range(self) -> TextRange {
TextRange::new(self.start_byte.into(), self.end_byte.into())
}
}
8 changes: 4 additions & 4 deletions crates/biome_analyze/src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -910,7 +910,7 @@ pub trait Rule: RuleMeta + Sized {
}

/// Diagnostic object returned by a single analysis rule
#[derive(Debug, Diagnostic)]
#[derive(Clone, Debug, Diagnostic)]
pub struct RuleDiagnostic {
#[category]
pub(crate) category: &'static Category,
Expand All @@ -925,7 +925,7 @@ pub struct RuleDiagnostic {
pub(crate) rule_advice: RuleAdvice,
}

#[derive(Debug, Default)]
#[derive(Clone, Debug, Default)]
/// It contains possible advices to show when printing a diagnostic that belong to the rule
pub struct RuleAdvice {
pub(crate) details: Vec<Detail>,
Expand All @@ -934,7 +934,7 @@ pub struct RuleAdvice {
pub(crate) code_suggestion_list: Vec<CodeSuggestionAdvice<MarkupBuf>>,
}

#[derive(Debug, Default)]
#[derive(Clone, Debug, Default)]
pub struct SuggestionList {
pub(crate) message: MarkupBuf,
pub(crate) list: Vec<MarkupBuf>,
Expand Down Expand Up @@ -976,7 +976,7 @@ impl Advices for RuleAdvice {
}
}

#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct Detail {
pub log_category: LogCategory,
pub message: MarkupBuf,
Expand Down
Loading

0 comments on commit ccce38f

Please sign in to comment.