Skip to content

Commit b1f8cbd

Browse files
authored
refactor(analyze): integrate plugin as a syntax visitor (#7369)
1 parent 91a0fc7 commit b1f8cbd

23 files changed

+599
-358
lines changed

.changeset/fresh-terms-carry.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Range suppressions are now supported for Grit plugins.
6+
7+
For JavaScript, you can suppress a plugin as follows:
8+
9+
```js
10+
// biome-ignore-start lint/plugin/preferObjectSpread: reason
11+
Object.assign({ foo: 'bar'}, baz);
12+
// biome-ignore-end lint/plugin/preferObjectSpread: reason
13+
```
14+
15+
For CSS, you can suppress a plugin as follows:
16+
17+
```css
18+
body {
19+
/* biome-ignore-start lint/plugin/useLowercaseColors: reason */
20+
color: #FFF;
21+
/* biome-ignore-end lint/plugin/useLowercaseColors: reason */
22+
}
23+
```

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
use crate::RuleDiagnostic;
2-
use biome_parser::AnyParse;
31
use camino::Utf8PathBuf;
2+
use rustc_hash::FxHashSet;
3+
use std::hash::Hash;
44
use std::{fmt::Debug, sync::Arc};
55

6+
use biome_rowan::{AnySyntaxNode, Language, RawSyntaxKind, SyntaxKind, SyntaxNode, WalkEvent};
7+
8+
use crate::matcher::SignalRuleKey;
9+
use crate::{DiagnosticSignal, RuleCategory, RuleDiagnostic, SignalEntry, Visitor, VisitorContext};
10+
611
/// Slice of analyzer plugins that can be cheaply cloned.
712
pub type AnalyzerPluginSlice<'a> = &'a [Arc<Box<dyn AnalyzerPlugin>>];
813

@@ -11,9 +16,108 @@ pub type AnalyzerPluginVec = Vec<Arc<Box<dyn AnalyzerPlugin>>>;
1116

1217
/// Definition of an analyzer plugin.
1318
pub trait AnalyzerPlugin: Debug + Send + Sync {
14-
fn evaluate(&self, root: AnyParse, path: Arc<Utf8PathBuf>) -> Vec<RuleDiagnostic>;
19+
fn language(&self) -> PluginTargetLanguage;
20+
21+
fn query(&self) -> Vec<RawSyntaxKind>;
22+
23+
fn evaluate(&self, node: AnySyntaxNode, path: Arc<Utf8PathBuf>) -> Vec<RuleDiagnostic>;
24+
}
25+
26+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
27+
pub enum PluginTargetLanguage {
28+
JavaScript,
29+
Css,
30+
}
31+
32+
/// A syntax visitor that queries nodes and evaluates in a plugin.
33+
/// Based on [`crate::SyntaxVisitor`].
34+
pub struct PluginVisitor<L: Language> {
35+
query: FxHashSet<L::Kind>,
36+
plugin: Arc<Box<dyn AnalyzerPlugin>>,
37+
skip_subtree: Option<SyntaxNode<L>>,
38+
}
39+
40+
impl<L> PluginVisitor<L>
41+
where
42+
L: Language + 'static,
43+
L::Kind: Eq + Hash,
44+
{
45+
/// Creates a syntax visitor from the plugin.
46+
///
47+
/// # Safety
48+
/// Do not forget to check the plugin is targeted for the language `L`.
49+
pub unsafe fn new_unchecked(plugin: Arc<Box<dyn AnalyzerPlugin>>) -> Self {
50+
let query = plugin.query().into_iter().map(L::Kind::from_raw).collect();
51+
52+
Self {
53+
query,
54+
plugin,
55+
skip_subtree: None,
56+
}
57+
}
58+
}
59+
60+
impl<L> Visitor for PluginVisitor<L>
61+
where
62+
L: Language + 'static,
63+
L::Kind: Eq + Hash,
64+
{
65+
type Language = L;
66+
67+
fn visit(
68+
&mut self,
69+
event: &WalkEvent<SyntaxNode<Self::Language>>,
70+
ctx: VisitorContext<Self::Language>,
71+
) {
72+
let node = match event {
73+
WalkEvent::Enter(node) => node,
74+
WalkEvent::Leave(node) => {
75+
if let Some(skip_subtree) = &self.skip_subtree
76+
&& skip_subtree == node
77+
{
78+
self.skip_subtree = None;
79+
}
80+
81+
return;
82+
}
83+
};
84+
85+
if self.skip_subtree.is_some() {
86+
return;
87+
}
88+
89+
if let Some(range) = ctx.range
90+
&& node.text_range_with_trivia().ordering(range).is_ne()
91+
{
92+
self.skip_subtree = Some(node.clone());
93+
return;
94+
}
95+
96+
// TODO: Integrate to [`VisitorContext::match_query`]?
97+
let kind = node.kind();
98+
if !self.query.contains(&kind) {
99+
return;
100+
}
101+
102+
let signals = self
103+
.plugin
104+
.evaluate(node.clone().into(), ctx.options.file_path.clone())
105+
.into_iter()
106+
.map(|diagnostic| {
107+
let name = diagnostic
108+
.subcategory
109+
.clone()
110+
.unwrap_or_else(|| "anonymous".into());
15111

16-
fn supports_css(&self) -> bool;
112+
SignalEntry {
113+
text_range: diagnostic.span().unwrap_or_default(),
114+
signal: Box::new(DiagnosticSignal::new(move || diagnostic.clone())),
115+
rule: SignalRuleKey::Plugin(name.into()),
116+
category: RuleCategory::Lint,
117+
instances: Default::default(),
118+
}
119+
});
17120

18-
fn supports_js(&self) -> bool;
121+
ctx.signal_queue.extend(signals);
122+
}
19123
}

crates/biome_analyze/src/lib.rs

Lines changed: 52 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
#![deny(clippy::use_self, rustdoc::broken_intra_doc_links)]
22

33
use biome_console::markup;
4-
use biome_parser::AnyParse;
54
use rustc_hash::FxHashSet;
65
use std::collections::{BTreeMap, BinaryHeap};
76
use std::fmt::{Debug, Display, Formatter};
87
use std::ops;
98
use std::str::FromStr;
10-
use std::sync::Arc;
119

1210
mod analyzer_plugin;
1311
mod categories;
@@ -29,13 +27,16 @@ mod visitor;
2927
// Re-exported for use in the `declare_group` macro
3028
pub use biome_diagnostics::category_concat;
3129

32-
pub use crate::analyzer_plugin::{AnalyzerPlugin, AnalyzerPluginSlice, AnalyzerPluginVec};
30+
pub use crate::analyzer_plugin::{
31+
AnalyzerPlugin, AnalyzerPluginSlice, AnalyzerPluginVec, PluginTargetLanguage, PluginVisitor,
32+
};
3333
pub use crate::categories::{
3434
ActionCategory, OtherActionCategory, RefactorKind, RuleCategories, RuleCategoriesBuilder,
3535
RuleCategory, SUPPRESSION_INLINE_ACTION_CATEGORY, SUPPRESSION_TOP_LEVEL_ACTION_CATEGORY,
3636
SourceActionKind,
3737
};
3838
pub use crate::diagnostics::{AnalyzerDiagnostic, AnalyzerSuppressionDiagnostic, RuleError};
39+
use crate::matcher::SignalRuleKey;
3940
pub use crate::matcher::{InspectMatcher, MatchQueryParams, QueryMatcher, RuleKey, SignalEntry};
4041
pub use crate::options::{AnalyzerConfiguration, AnalyzerOptions, AnalyzerRules};
4142
pub use crate::query::{AddVisitor, QueryKey, QueryMatch, Queryable};
@@ -76,8 +77,6 @@ pub use suppression_action::{ApplySuppression, SuppressionAction};
7677
pub struct Analyzer<'analyzer, L: Language, Matcher, Break, Diag> {
7778
/// List of visitors being run by this instance of the analyzer for each phase
7879
phases: BTreeMap<Phases, Vec<Box<dyn Visitor<Language = L> + 'analyzer>>>,
79-
/// Plugins to be run after the phases for built-in rules.
80-
plugins: AnalyzerPluginVec,
8180
/// Holds the metadata for all the rules statically known to the analyzer
8281
metadata: &'analyzer MetadataRegistry,
8382
/// Executor for the query matches emitted by the visitors
@@ -114,7 +113,6 @@ where
114113
) -> Self {
115114
Self {
116115
phases: BTreeMap::new(),
117-
plugins: Vec::new(),
118116
metadata,
119117
query_matcher,
120118
parse_suppression_comment,
@@ -132,15 +130,9 @@ where
132130
self.phases.entry(phase).or_default().push(visitor);
133131
}
134132

135-
/// Registers an [AnalyzerPlugin] to be executed after the regular phases.
136-
pub fn add_plugin(&mut self, plugin: Arc<Box<dyn AnalyzerPlugin>>) {
137-
self.plugins.push(plugin);
138-
}
139-
140133
pub fn run(self, mut ctx: AnalyzerContext<L>) -> Option<Break> {
141134
let Self {
142135
phases,
143-
plugins,
144136
mut query_matcher,
145137
parse_suppression_comment,
146138
mut emit_signal,
@@ -193,54 +185,6 @@ where
193185
}
194186
}
195187

196-
for plugin in plugins {
197-
let root: AnyParse = ctx.root.syntax().as_send().expect("not a root node").into();
198-
let diagnostics = plugin.evaluate(root, ctx.options.file_path.clone());
199-
for diagnostic in diagnostics {
200-
let name = diagnostic
201-
.subcategory
202-
.clone()
203-
.unwrap_or_else(|| "anonymous".into());
204-
205-
// 1. Check for top level suppression:
206-
if suppressions.top_level_suppression.suppressed_plugin(&name)
207-
|| suppressions
208-
.top_level_suppression
209-
.suppresses_category(RuleCategory::Lint)
210-
{
211-
break;
212-
}
213-
214-
let suppression = diagnostic.span.and_then(|text_range| {
215-
// 2. Check for range suppression is not supported because
216-
// plugins are handled separately after the basic analyze
217-
// phases. At this point, we have read to the end of the
218-
// file, all `// biome-ignore-end` comments are
219-
// processed, thus all range suppressions are cleared.
220-
221-
// 3. Check for line suppression:
222-
suppressions
223-
.overlapping_line_suppressions(&text_range)
224-
.iter_mut()
225-
.find(|s| {
226-
s.text_range.contains(text_range.start())
227-
&& (s.suppressed_categories.contains(RuleCategory::Lint)
228-
|| s.suppress_all_plugins
229-
|| s.suppressed_plugins.contains(&name))
230-
})
231-
});
232-
233-
if let Some(suppression) = suppression {
234-
suppression.did_suppress_signal = true;
235-
} else {
236-
let signal = DiagnosticSignal::new(|| diagnostic.clone());
237-
if let ControlFlow::Break(br) = (emit_signal)(&signal) {
238-
return Some(br);
239-
}
240-
}
241-
}
242-
}
243-
244188
for range_suppression in suppressions.range_suppressions.suppressions {
245189
if range_suppression.did_suppress_signal {
246190
continue;
@@ -252,8 +196,8 @@ where
252196
range_suppression.start_comment_range,
253197
"Suppression comment has no effect because another suppression comment suppresses the same rule.",
254198
).note(
255-
markup!{"This is the suppression comment that was used."}.to_owned(),
256-
range
199+
markup! {"This is the suppression comment that was used."}.to_owned(),
200+
range,
257201
)
258202
});
259203
if let ControlFlow::Break(br) = (emit_signal)(&signal) {
@@ -274,8 +218,8 @@ where
274218
suppression.comment_span,
275219
"Suppression comment has no effect because another suppression comment suppresses the same rule.",
276220
).note(
277-
markup!{"This is the suppression comment that was used."}.to_owned(),
278-
range
221+
markup! {"This is the suppression comment that was used."}.to_owned(),
222+
range,
279223
)
280224
} else {
281225
AnalyzerSuppressionDiagnostic::new(
@@ -456,22 +400,35 @@ where
456400
if self
457401
.suppressions
458402
.top_level_suppression
459-
.contains_rule_key(&entry.category, &entry.rule)
460-
|| self
461-
.suppressions
462-
.top_level_suppression
463-
.suppressed_categories
464-
.contains(entry.category)
403+
.suppressed_categories
404+
.contains(entry.category)
465405
{
466406
self.signal_queue.pop();
467407
continue;
468408
}
469409

470-
if self.suppressions.range_suppressions.suppress_rule(
471-
&entry.category,
472-
&entry.rule,
473-
&entry.text_range,
474-
) {
410+
let is_suppressed = match &entry.rule {
411+
SignalRuleKey::Rule(rule) => {
412+
self.suppressions
413+
.top_level_suppression
414+
.contains_rule_key(&entry.category, rule)
415+
|| self.suppressions.range_suppressions.suppress_rule(
416+
&entry.category,
417+
rule,
418+
&entry.text_range,
419+
)
420+
}
421+
SignalRuleKey::Plugin(plugin) => {
422+
self.suppressions
423+
.top_level_suppression
424+
.suppressed_plugin(plugin)
425+
|| self
426+
.suppressions
427+
.range_suppressions
428+
.suppress_plugin(plugin.as_ref(), &entry.text_range)
429+
}
430+
};
431+
if is_suppressed {
475432
self.signal_queue.pop();
476433
continue;
477434
}
@@ -497,17 +454,28 @@ where
497454
let (is_match, is_exhaustive) =
498455
if suppression.suppressed_categories.contains(entry.category) {
499456
(true, true)
500-
} else if !suppression.matches_rule(&entry.category, &entry.rule) {
501-
(false, false)
502457
} else {
503-
match suppression.suppressed_instance.as_ref() {
504-
None => (true, true),
505-
Some(v) => {
506-
let matches_instance = instances
507-
.get_or_insert_with(|| entry.instances.iter().collect())
508-
.remove(v);
509-
(matches_instance, false)
458+
match &entry.rule {
459+
SignalRuleKey::Rule(rule)
460+
if suppression.matches_rule(&entry.category, rule) =>
461+
{
462+
match suppression.suppressed_instance.as_ref() {
463+
None => (true, true),
464+
Some(v) => {
465+
let matches_instance = instances
466+
.get_or_insert_with(|| entry.instances.iter().collect())
467+
.remove(v);
468+
(matches_instance, false)
469+
}
470+
}
471+
}
472+
SignalRuleKey::Plugin(plugin)
473+
if suppression.suppress_all_plugins
474+
|| suppression.suppressed_plugins.contains(plugin.as_ref()) =>
475+
{
476+
(true, true)
510477
}
478+
_ => (false, false),
511479
}
512480
};
513481
if is_match {

0 commit comments

Comments
 (0)