Skip to content

Commit 06a0f35

Browse files
fix(cli): improve code actions filtering (#9627)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent e4687fe commit 06a0f35

83 files changed

Lines changed: 1704 additions & 985 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/gentle-comics-win.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#191](https://github.com/biomejs/biome-zed/issues/191): Improved the performance of how the Biome Language Server pulls code actions and diagnostics.
6+
7+
Before, code actions were pulled and computed all at once in one request. This approach couldn't work in big files, and caused Biome to stale and have CPU usage spikes up to 100%.
8+
9+
Now, code actions are pulled and computed lazily, and Biome won't choke anymore in big files.

.changeset/mighty-pillows-count.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed assist diagnostics being invisible when using `--diagnostic-level=error`. Enforced assist violations (e.g. `useSortedKeys`) were filtered out before being promoted to errors, causing `biome check` to incorrectly return success.

.changeset/silly-islands-relate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed duplicate parse errors in `check` and `ci` output. When a file had syntax errors, the same parse error was printed twice and the error count was inflated.

.changeset/smart-radios-end.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Improved the performance of the commands `lint` and `check` when they are called with `--write`.

.changeset/thirty-breads-beg.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed `--diagnostic-level` not fully filtering diagnostics. Setting `--diagnostic-level=error` now correctly excludes warnings and infos from both the output and the summary counts.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_analyze/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ pub use crate::services::{
5656
ExtendedConfigurationProvider, FromServices, ServiceBag, ServicesDiagnostic,
5757
};
5858
pub use crate::signals::{
59-
AnalyzerAction, AnalyzerSignal, AnalyzerTransformation, DiagnosticSignal, PluginSignal,
59+
ActionFilter, ActionMetadata, AnalyzerAction, AnalyzerSignal, AnalyzerTransformation,
60+
DiagnosticSignal, PluginSignal,
6061
};
6162
use crate::suppressions::Suppressions;
6263
pub use crate::syntax::{Ast, SyntaxVisitor};

crates/biome_analyze/src/signals.rs

Lines changed: 155 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use crate::categories::{
22
SUPPRESSION_INLINE_ACTION_CATEGORY, SUPPRESSION_TOP_LEVEL_ACTION_CATEGORY,
33
};
44
use crate::{
5-
AnalyzerDiagnostic, AnalyzerOptions, OtherActionCategory, Queryable, RuleDiagnostic, RuleGroup,
6-
ServiceBag, SuppressionAction,
5+
AnalyzerDiagnostic, AnalyzerOptions, FixKind, GroupCategory, OtherActionCategory, Queryable,
6+
RuleCategory, RuleDiagnostic, RuleGroup, ServiceBag, SuppressionAction,
77
categories::ActionCategory,
88
context::RuleContext,
99
registry::{RuleLanguage, RuleRoot},
@@ -12,15 +12,81 @@ use crate::{
1212
use biome_console::{MarkupBuf, markup};
1313
use biome_diagnostics::{Applicability, CodeSuggestion, Error, advice::CodeSuggestionAdvice};
1414
use biome_rowan::{BatchMutation, Language};
15+
use enumflags2::{BitFlag, BitFlags, bitflags};
1516
use std::iter::FusedIterator;
1617
use std::marker::PhantomData;
1718
use std::vec::IntoIter;
1819

20+
/// Select which action categories should be computed by the analyzer.
21+
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
22+
pub struct ActionFilter(pub BitFlags<ActionKind>);
23+
24+
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
25+
#[bitflags]
26+
#[repr(u8)]
27+
pub enum ActionKind {
28+
RuleFix = 1 << 0,
29+
InlineSuppression = 1 << 1,
30+
ToplevelSuppression = 1 << 2,
31+
}
32+
33+
impl ActionFilter {
34+
pub fn all() -> Self {
35+
let all: BitFlags<ActionKind> = ActionKind::all();
36+
Self(all)
37+
}
38+
39+
pub fn rule_fix() -> Self {
40+
let mut filter = ActionKind::empty();
41+
filter.insert(ActionKind::RuleFix);
42+
Self(filter)
43+
}
44+
45+
pub fn inline_suppression() -> Self {
46+
let mut filter = ActionKind::empty();
47+
filter.insert(ActionKind::InlineSuppression);
48+
Self(filter)
49+
}
50+
51+
pub fn toplevel_suppression() -> Self {
52+
let mut filter = ActionKind::empty();
53+
filter.insert(ActionKind::ToplevelSuppression);
54+
Self(filter)
55+
}
56+
57+
pub fn has_actions(&self) -> bool {
58+
!self.0.is_empty()
59+
}
60+
61+
pub fn is_rule_fix(&self) -> bool {
62+
self.0.contains(ActionKind::RuleFix)
63+
}
64+
65+
pub fn is_inline_suppression(&self) -> bool {
66+
self.0.contains(ActionKind::InlineSuppression)
67+
}
68+
pub fn is_toplevel_suppression(&self) -> bool {
69+
self.0.contains(ActionKind::ToplevelSuppression)
70+
}
71+
}
72+
73+
/// Lightweight description of an available action, without the expensive
74+
/// [`BatchMutation`]. Used by `codeAction/resolve` to defer edit computation.
75+
#[derive(Debug, Clone)]
76+
pub struct ActionMetadata {
77+
pub rule_name: Option<(&'static str, &'static str)>,
78+
pub category: ActionCategory,
79+
pub applicability: Applicability,
80+
}
81+
1982
/// Event raised by the analyzer when a [Rule](crate::Rule)
2083
/// emits a diagnostic, a code action, or both
2184
pub trait AnalyzerSignal<L: Language> {
2285
fn diagnostic(&self) -> Option<AnalyzerDiagnostic>;
23-
fn actions(&self) -> AnalyzerActionIter<L>;
86+
fn actions(&self, filter: ActionFilter) -> AnalyzerActionIter<L>;
87+
/// Returns lightweight metadata about available actions without computing
88+
/// mutations. This is cheap — no [`BatchMutation`] or tree clones.
89+
fn actions_metadata(&self) -> Vec<ActionMetadata>;
2490
fn transformations(&self) -> AnalyzerTransformationIter<L>;
2591
}
2692

@@ -84,12 +150,17 @@ where
84150
Some(AnalyzerDiagnostic::from_error(error))
85151
}
86152

87-
fn actions(&self) -> AnalyzerActionIter<L> {
88-
if let Some(action) = (self.action)() {
89-
AnalyzerActionIter::new([action])
90-
} else {
91-
AnalyzerActionIter::new(vec![])
153+
fn actions(&self, filter: ActionFilter) -> AnalyzerActionIter<L> {
154+
if filter.is_rule_fix()
155+
&& let Some(action) = (self.action)()
156+
{
157+
return AnalyzerActionIter::new([action]);
92158
}
159+
AnalyzerActionIter::new(vec![])
160+
}
161+
162+
fn actions_metadata(&self) -> Vec<ActionMetadata> {
163+
Vec::new()
93164
}
94165

95166
fn transformations(&self) -> AnalyzerTransformationIter<L> {
@@ -128,10 +199,14 @@ impl<L: Language> AnalyzerSignal<L> for PluginSignal<L> {
128199
Some(AnalyzerDiagnostic::from(self.diagnostic.clone()))
129200
}
130201

131-
fn actions(&self) -> AnalyzerActionIter<L> {
202+
fn actions(&self, _filter: ActionFilter) -> AnalyzerActionIter<L> {
132203
AnalyzerActionIter::new(vec![])
133204
}
134205

206+
fn actions_metadata(&self) -> Vec<ActionMetadata> {
207+
Vec::new()
208+
}
209+
135210
fn transformations(&self) -> AnalyzerTransformationIter<L> {
136211
AnalyzerTransformationIter::new(vec![])
137212
}
@@ -429,17 +504,21 @@ where
429504
})
430505
}
431506

432-
fn actions(&self) -> AnalyzerActionIter<RuleLanguage<R>> {
507+
fn actions(&self, filter: ActionFilter) -> AnalyzerActionIter<RuleLanguage<R>> {
433508
let globals = self.options.globals();
434509

435-
let configured_applicability = if let Some(fix_kind) = self.options.rule_fix_kind::<R>() {
436-
match fix_kind {
437-
crate::FixKind::None => {
438-
// The action is disabled
439-
return AnalyzerActionIter::new(vec![]);
510+
// When fix is set to "none", disable the rule fix but still allow
511+
// suppression actions.
512+
let fix_disabled = matches!(self.options.rule_fix_kind::<R>(), Some(FixKind::None));
513+
let configured_applicability = if filter.is_rule_fix() && !fix_disabled {
514+
if let Some(fix_kind) = self.options.rule_fix_kind::<R>() {
515+
match fix_kind {
516+
FixKind::None => None,
517+
FixKind::Safe => Some(Applicability::Always),
518+
FixKind::Unsafe => Some(Applicability::MaybeIncorrect),
440519
}
441-
crate::FixKind::Safe => Some(Applicability::Always),
442-
crate::FixKind::Unsafe => Some(Applicability::MaybeIncorrect),
520+
} else {
521+
None
443522
}
444523
} else {
445524
None
@@ -462,16 +541,21 @@ where
462541
.ok();
463542
let mut actions = Vec::new();
464543
if let Some(ctx) = ctx {
465-
if let Some(action) = R::action(&ctx, &self.state) {
544+
if filter.is_rule_fix()
545+
&& !fix_disabled
546+
&& let Some(action) = R::action(&ctx, &self.state)
547+
{
466548
actions.push(AnalyzerAction {
467549
rule_name: Some((<R::Group as RuleGroup>::NAME, R::METADATA.name)),
468550
applicability: configured_applicability.unwrap_or(action.applicability()),
469551
category: action.category,
470552
mutation: action.mutation,
471553
message: action.message,
472554
});
473-
};
474-
if let Some(text_range) = R::text_range(&ctx, &self.state)
555+
}
556+
557+
if filter.is_inline_suppression()
558+
&& let Some(text_range) = R::text_range(&ctx, &self.state)
475559
&& let Some(suppression_action) = R::inline_suppression(
476560
&ctx,
477561
&text_range,
@@ -489,8 +573,9 @@ where
489573
actions.push(action);
490574
}
491575

492-
if let Some(suppression_action) =
493-
R::top_level_suppression(&ctx, self.suppression_action)
576+
if filter.is_toplevel_suppression()
577+
&& let Some(suppression_action) =
578+
R::top_level_suppression(&ctx, self.suppression_action)
494579
{
495580
let action = AnalyzerAction {
496581
rule_name: Some((<R::Group as RuleGroup>::NAME, R::METADATA.name)),
@@ -508,6 +593,54 @@ where
508593
}
509594
}
510595

596+
fn actions_metadata(&self) -> Vec<ActionMetadata> {
597+
let rule_name = Some((<R::Group as RuleGroup>::NAME, R::METADATA.name));
598+
let rule_category = <<R::Group as RuleGroup>::Category as GroupCategory>::CATEGORY;
599+
let has_suppression = matches!(
600+
rule_category,
601+
RuleCategory::Lint | RuleCategory::Action | RuleCategory::Syntax
602+
);
603+
604+
let mut metadata = Vec::new();
605+
606+
// Rule fix metadata — only if the rule declares a fix
607+
let fix_kind = self.options.rule_fix_kind::<R>();
608+
let is_disabled = matches!(fix_kind, Some(FixKind::None));
609+
if !is_disabled && R::METADATA.fix_kind != FixKind::None {
610+
let applicability = match fix_kind {
611+
Some(FixKind::Safe) => Applicability::Always,
612+
Some(FixKind::Unsafe) => Applicability::MaybeIncorrect,
613+
_ => match R::METADATA.fix_kind {
614+
FixKind::Safe => Applicability::Always,
615+
_ => Applicability::MaybeIncorrect,
616+
},
617+
};
618+
let action_category =
619+
R::METADATA.action_category(rule_category, <R::Group as RuleGroup>::NAME);
620+
metadata.push(ActionMetadata {
621+
rule_name,
622+
category: action_category,
623+
applicability,
624+
});
625+
}
626+
627+
// Suppression metadata
628+
if has_suppression {
629+
metadata.push(ActionMetadata {
630+
rule_name,
631+
category: ActionCategory::Other(OtherActionCategory::InlineSuppression),
632+
applicability: Applicability::Always,
633+
});
634+
metadata.push(ActionMetadata {
635+
rule_name,
636+
category: ActionCategory::Other(OtherActionCategory::ToplevelSuppression),
637+
applicability: Applicability::Always,
638+
});
639+
}
640+
641+
metadata
642+
}
643+
511644
fn transformations(&self) -> AnalyzerTransformationIter<RuleLanguage<R>> {
512645
let globals = self.options.globals();
513646
let options = self.options.rule_options::<R>().unwrap_or_default();

crates/biome_cli/src/commands/search.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use biome_configuration::vcs::VcsConfiguration;
1212
use biome_configuration::{Configuration, FilesConfiguration};
1313
use biome_console::{Console, MarkupBuf};
1414
use biome_deserialize::Merge;
15+
use biome_diagnostics::Severity;
1516
use biome_diagnostics::{Category, DiagnosticExt, category};
1617
use biome_fs::FileSystem;
1718
use biome_grit_patterns::{GritTargetLanguage, JsTargetLanguage};
@@ -140,6 +141,8 @@ impl ProcessFile for SearchProcessFile {
140141
ctx: &Ctx,
141142
workspace_file: &mut WorkspaceFile,
142143
_features_supported: &FeaturesSupported,
144+
_max_diagnostics: u32,
145+
_diagnostic_level: Severity,
143146
) -> Result<FileStatus, Message>
144147
where
145148
Ctx: CrawlerContext,
@@ -175,6 +178,9 @@ impl ProcessFile for SearchProcessFile {
175178
.map(|mat| SearchDiagnostic.with_file_span(mat))
176179
.collect(),
177180
skipped_diagnostics: 0,
181+
errors: 0,
182+
warnings: 0,
183+
infos: 0,
178184
};
179185

180186
Ok(FileStatus::SearchResult(matches_len, search_results))

crates/biome_cli/src/execute/migrate.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::commands::MigrateSubCommand;
22
use crate::diagnostics::MigrationDiagnostic;
33
use crate::runner::diagnostics::{ContentDiffAdvice, MigrateDiffDiagnostic};
44
use crate::{CliDiagnostic, CliSession};
5-
use biome_analyze::AnalysisFilter;
5+
use biome_analyze::{ActionFilter, AnalysisFilter};
66
use biome_configuration::Configuration;
77
use biome_console::fmt::{Display, Formatter};
88
use biome_console::{Console, ConsoleExt, markup};
@@ -375,7 +375,7 @@ fn migrate_file(payload: MigrateFile) -> Result<MigrationFileResult, CliDiagnost
375375
configuration_file_path.as_path(),
376376
is_root,
377377
|signal| {
378-
if let Some(action) = signal.actions().next() {
378+
if let Some(action) = signal.actions(ActionFilter::rule_fix()).next() {
379379
return ControlFlow::Break(action);
380380
}
381381
ControlFlow::Continue(())

0 commit comments

Comments
 (0)