Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 67 additions & 5 deletions crates/biome_analyze/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,43 @@ pub enum RuleError {
ReplacedRootWithNonRootError {
rule_name: Option<(Cow<'static, str>, Cow<'static, str>)>,
},
/// The rules listed below caused an infinite loop when applying fixes to the file.
ConflictingRuleFixesError {
rules: Vec<(Cow<'static, str>, Cow<'static, str>)>,
},
}

impl Diagnostic for RuleError {}
impl Diagnostic for RuleError {
fn category(&self) -> Option<&'static Category> {
Some(category!("internalError/panic"))
}

fn severity(&self) -> Severity {
Severity::Error
}

fn description(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
fmt,
"An internal error occurred when analyzing this file.\n\n{}\n\nThis is likely a bug in Biome, not an error in your code. Please consider filing an issue on GitHub with a reproduction of this error.",
self
)?;
Ok(())
}

fn message(&self, fmt: &mut biome_console::fmt::Formatter<'_>) -> std::io::Result<()> {
fmt.write_markup(markup! {
"An internal error occurred when analyzing this file."
})?;
fmt.write_markup(markup! {
{self}
})?;
fmt.write_markup(markup! {
"This is likely a bug in Biome, not an error in your code. Please consider filing an issue on "<Hyperlink href="https://github.com/biomejs/biome/issues/new/choose">"GitHub"</Hyperlink>" with a reproduction of this error."
})?;
Comment on lines +268 to +276
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add paragraph breaks to the diagnostic message

Right now each write_markup call runs back-to-back, so the rendered text comes out as one long sentence (…file.The…This…). A couple of explicit separators will fix the readability.

         fmt.write_markup(markup! {
             "An internal error occurred when analyzing this file."
         })?;
+        fmt.write_markup(markup! {
+            "\n\n"
+        })?;
         fmt.write_markup(markup! {
             {self}
         })?;
+        fmt.write_markup(markup! {
+            "\n\n"
+        })?;
         fmt.write_markup(markup! {
             "This is likely a bug in Biome, not an error in your code. Please consider filing an issue on "<Hyperlink href="https://github.com/biomejs/biome/issues/new/choose">"GitHub"</Hyperlink>" with a reproduction of this error."
         })?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fmt.write_markup(markup! {
"An internal error occurred when analyzing this file."
})?;
fmt.write_markup(markup! {
{self}
})?;
fmt.write_markup(markup! {
"This is likely a bug in Biome, not an error in your code. Please consider filing an issue on "<Hyperlink href="https://github.com/biomejs/biome/issues/new/choose">"GitHub"</Hyperlink>" with a reproduction of this error."
})?;
fmt.write_markup(markup! {
"An internal error occurred when analyzing this file."
})?;
fmt.write_markup(markup! {
"\n\n"
})?;
fmt.write_markup(markup! {
{self}
})?;
fmt.write_markup(markup! {
"\n\n"
})?;
fmt.write_markup(markup! {
"This is likely a bug in Biome, not an error in your code. Please consider filing an issue on "<Hyperlink href="https://github.com/biomejs/biome/issues/new/choose">"GitHub"</Hyperlink>" with a reproduction of this error."
})?;
🤖 Prompt for AI Agents
In crates/biome_analyze/src/diagnostics.rs around lines 268 to 276, the three
consecutive fmt.write_markup calls produce concatenated text with no spacing;
add explicit paragraph breaks between the messages (for example insert an empty
paragraph or newline markup between the first and second and between the second
and third write_markup calls) so the rendered diagnostic shows separate
sentences/paragraphs rather than a single run-on line.

Ok(())
}
}

impl std::fmt::Display for RuleError {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
Expand All @@ -252,13 +286,27 @@ impl std::fmt::Display for RuleError {
} => {
std::write!(
fmt,
"the rule '{group}/{rule}' replaced the root of the file with a non-root node."
"The rule '{group}/{rule}' replaced the root of the file with a non-root node."
)
}
Self::ReplacedRootWithNonRootError { rule_name: None } => {
std::write!(
fmt,
"a code action replaced the root of the file with a non-root node."
"A code action replaced the root of the file with a non-root node."
)
}
Self::ConflictingRuleFixesError { rules } => {
if rules.is_empty() {
return std::write!(fmt, "conflicting rule fixes detected");
}
let rules_list = rules
.iter()
.map(|(group, rule)| format!("'{group}/{rule}'"))
.collect::<Vec<_>>()
.join(", ");
std::write!(
fmt,
"The rules {rules_list} caused an infinite loop when applying fixes to the file."
)
}
}
Expand All @@ -273,13 +321,27 @@ impl biome_console::fmt::Display for RuleError {
} => {
std::write!(
fmt,
"the rule '{group}/{rule}' replaced the root of the file with a non-root node."
"The rule '{group}/{rule}' replaced the root of the file with a non-root node."
)
}
Self::ReplacedRootWithNonRootError { rule_name: None } => {
std::write!(
fmt,
"a code action replaced the root of the file with a non-root node."
"A code action replaced the root of the file with a non-root node."
)
}
Self::ConflictingRuleFixesError { rules } => {
if rules.is_empty() {
return std::write!(fmt, "Conflicting rule fixes detected.");
}
let rules_list = rules
.iter()
.map(|(group, rule)| format!("'{group}/{rule}'"))
.collect::<Vec<_>>()
.join(", ");
std::write!(
fmt,
"The rules {rules_list} caused an infinite loop when applying fixes to the file."
)
}
}
Expand Down
24 changes: 24 additions & 0 deletions crates/biome_service/src/file_handlers/css.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::settings::{
FormatSettings, LanguageListSettings, LanguageSettings, OverrideSettings, ServiceLanguage,
Settings, check_feature_activity, check_override_feature_activity,
};
use crate::utils::growth_guard::GrowthGuard;
use crate::workspace::{
CodeAction, DocumentFileSource, FixAction, FixFileMode, FixFileResult, GetSyntaxTreeResult,
PullActionsResult,
Expand Down Expand Up @@ -42,6 +43,7 @@ use biome_rowan::{AstNode, NodeCache};
use biome_rowan::{TextRange, TextSize, TokenAtOffset};
use camino::Utf8Path;
use std::borrow::Cow;
use std::collections::HashSet;
use tracing::{debug_span, error, info, trace_span};

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -634,6 +636,7 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceEr
let mut actions = Vec::new();
let mut skipped_suggested_fixes = 0;
let mut errors: u16 = 0;
let mut growth_guard = GrowthGuard::new(tree.syntax().text_range_with_trivia().len().into());

loop {
let (action, _) = analyze(
Expand Down Expand Up @@ -708,6 +711,27 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceEr
.map(|(group, rule)| (Cow::Borrowed(group), Cow::Borrowed(rule))),
range,
});

// Check for runaway edit growth
let curr_len: u32 = tree.syntax().text_range_with_trivia().len().into();
if !growth_guard.check(curr_len) {
// In order to provide a useful diagnostic, we want to flag the rules that caused the conflict.
// We can do this by inspecting the last few fixes that were applied.
// We limit it to the last 10 fixes. If there is a chain of conflicting fixes longer than that, something is **really** fucked up.

let mut seen_rules = HashSet::new();
for action in actions.iter().rev().take(10) {
if let Some((group, rule)) = action.rule_name.as_ref() {
seen_rules.insert((group.clone(), rule.clone()));
}
}

return Err(WorkspaceError::RuleError(
RuleError::ConflictingRuleFixesError {
rules: seen_rules.into_iter().collect(),
},
));
}
}
}
None => {
Expand Down
24 changes: 24 additions & 0 deletions crates/biome_service/src/file_handlers/graphql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::settings::{
FormatSettings, LanguageListSettings, LanguageSettings, OverrideSettings, ServiceLanguage,
Settings, check_feature_activity, check_override_feature_activity,
};
use crate::utils::growth_guard::GrowthGuard;
use crate::workspace::{
CodeAction, FixAction, FixFileMode, FixFileResult, GetSyntaxTreeResult, PullActionsResult,
};
Expand All @@ -35,6 +36,7 @@ use biome_parser::AnyParse;
use biome_rowan::{AstNode, NodeCache, TokenAtOffset};
use camino::Utf8Path;
use std::borrow::Cow;
use std::collections::HashSet;
use tracing::{debug_span, error, info, trace_span};

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -544,6 +546,7 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceEr
let mut actions = Vec::new();
let mut skipped_suggested_fixes = 0;
let mut errors: u16 = 0;
let mut growth_guard = GrowthGuard::new(tree.syntax().text_range_with_trivia().len().into());

loop {
let (action, _) = analyze(&tree, filter, &analyzer_options, |signal| {
Expand Down Expand Up @@ -612,6 +615,27 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceEr
.map(|(group, rule)| (Cow::Borrowed(group), Cow::Borrowed(rule))),
range,
});

// Check for runaway edit growth
let curr_len: u32 = tree.syntax().text_range_with_trivia().len().into();
if !growth_guard.check(curr_len) {
// In order to provide a useful diagnostic, we want to flag the rules that caused the conflict.
// We can do this by inspecting the last few fixes that were applied.
// We limit it to the last 10 fixes. If there is a chain of conflicting fixes longer than that, something is **really** fucked up.

let mut seen_rules = HashSet::new();
for action in actions.iter().rev().take(10) {
if let Some((group, rule)) = action.rule_name.as_ref() {
seen_rules.insert((group.clone(), rule.clone()));
}
}

return Err(WorkspaceError::RuleError(
RuleError::ConflictingRuleFixesError {
rules: seen_rules.into_iter().collect(),
},
));
}
}
}
None => {
Expand Down
25 changes: 25 additions & 0 deletions crates/biome_service/src/file_handlers/javascript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::file_handlers::{FixAllParams, is_diagnostic_error};
use crate::settings::{
OverrideSettings, Settings, check_feature_activity, check_override_feature_activity,
};
use crate::utils::growth_guard::GrowthGuard;
use crate::workspace::DocumentFileSource;
use crate::{
WorkspaceError,
Expand Down Expand Up @@ -56,6 +57,7 @@ use biome_rowan::{AstNode, BatchMutationExt, Direction, NodeCache, WalkEvent};
use camino::Utf8Path;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::HashSet;
use std::fmt::Debug;
use std::sync::Arc;
use tracing::{debug, debug_span, error, trace_span};
Expand Down Expand Up @@ -870,6 +872,8 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceEr
let mut actions = Vec::new();
let mut skipped_suggested_fixes = 0;
let mut errors: u16 = 0;
// For detecting runaway edit growth
let mut growth_guard = GrowthGuard::new(tree.syntax().text_range_with_trivia().len().into());

loop {
let services = JsAnalyzerServices::from((
Expand Down Expand Up @@ -954,6 +958,27 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceEr
.map(|(group, rule)| (Cow::Borrowed(group), Cow::Borrowed(rule))),
range,
});

// Check for runaway edit growth
let curr_len: u32 = tree.syntax().text_range_with_trivia().len().into();
if !growth_guard.check(curr_len) {
// In order to provide a useful diagnostic, we want to flag the rules that caused the conflict.
// We can do this by inspecting the last few fixes that were applied.
// We limit it to the last 10 fixes. If there is a chain of conflicting fixes longer than that, something is **really** fucked up.

let mut seen_rules = HashSet::new();
for action in actions.iter().rev().take(10) {
if let Some((group, rule)) = action.rule_name.as_ref() {
seen_rules.insert((group.clone(), rule.clone()));
}
}

return Err(WorkspaceError::RuleError(
RuleError::ConflictingRuleFixesError {
rules: seen_rules.into_iter().collect(),
},
));
}
}
}
None => {
Expand Down
24 changes: 24 additions & 0 deletions crates/biome_service/src/file_handlers/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::settings::{
FormatSettings, LanguageListSettings, LanguageSettings, OverrideSettings, ServiceLanguage,
Settings, check_feature_activity, check_override_feature_activity,
};
use crate::utils::growth_guard::GrowthGuard;
use crate::workspace::{
CodeAction, FixAction, FixFileMode, FixFileResult, GetSyntaxTreeResult, PullActionsResult,
};
Expand Down Expand Up @@ -42,6 +43,7 @@ use biome_rowan::{AstNode, NodeCache};
use biome_rowan::{TextRange, TextSize, TokenAtOffset};
use camino::Utf8Path;
use std::borrow::Cow;
use std::collections::HashSet;
use tracing::{debug_span, error, instrument};

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -644,6 +646,7 @@ fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceError> {
let mut actions = Vec::new();
let mut skipped_suggested_fixes = 0;
let mut errors: u16 = 0;
let mut growth_guard = GrowthGuard::new(tree.syntax().text_range_with_trivia().len().into());

loop {
let (action, _) = analyze(&tree, filter, &analyzer_options, file_source, |signal| {
Expand Down Expand Up @@ -712,6 +715,27 @@ fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceError> {
.map(|(group, rule)| (Cow::Borrowed(group), Cow::Borrowed(rule))),
range,
});

// Check for runaway edit growth
let curr_len: u32 = tree.syntax().text_range_with_trivia().len().into();
if !growth_guard.check(curr_len) {
// In order to provide a useful diagnostic, we want to flag the rules that caused the conflict.
// We can do this by inspecting the last few fixes that were applied.
// We limit it to the last 10 fixes. If there is a chain of conflicting fixes longer than that, something is **really** fucked up.

let mut seen_rules = HashSet::new();
for action in actions.iter().rev().take(10) {
if let Some((group, rule)) = action.rule_name.as_ref() {
seen_rules.insert((group.clone(), rule.clone()));
}
}

return Err(WorkspaceError::RuleError(
RuleError::ConflictingRuleFixesError {
rules: seen_rules.into_iter().collect(),
},
));
}
}
}
None => {
Expand Down
1 change: 1 addition & 0 deletions crates/biome_service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod workspace;
pub mod workspace_types;

mod scanner;
mod utils;

#[cfg(test)]
mod test_utils;
Expand Down
70 changes: 70 additions & 0 deletions crates/biome_service/src/utils/growth_guard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const RATIO_Q: u32 = 100; // fixed-point 2 decimals
const RATIO_GROWTH: u32 = 150; // 1.5x growth increase
const RATIO_ACCEL: u32 = 180; // 1.8x delta increase
const STREAK_LIMIT: u8 = 10;

/// A guard that ensures a value does not grow too quickly.
///
/// Used to prevent runaway growth of files when applying fixes.
pub(crate) struct GrowthGuard {
previous: u32,
previous_diff: u32,
/// multiplicative growth streak
growth_streak: u8,
/// delta acceleration streak
accel_streak: u8,
}

impl GrowthGuard {
pub fn new(initial: u32) -> Self {
Self {
previous: initial,
previous_diff: 0,
growth_streak: 0,
accel_streak: 0,
}
}

/// Check if the new value is allowed based on growth constraints.
///
/// Returns `true` if the new value is allowed, `false` otherwise.
pub fn check(&mut self, new: u32) -> bool {
if new < self.previous {
// Allow decreases
self.previous = new;
self.previous_diff = 0;
self.growth_streak = 0;
self.accel_streak = 0;
return true;
}

let diff = new.saturating_sub(self.previous);

// Check for multiplicative growth
if new.saturating_mul(RATIO_Q) >= self.previous.saturating_mul(RATIO_GROWTH) {
self.growth_streak = self.growth_streak.saturating_add(1);
} else {
self.growth_streak = 0;
}

// Check for delta acceleration
if diff.saturating_mul(RATIO_Q) >= self.previous_diff.saturating_mul(RATIO_ACCEL)
&& self.previous_diff > 0
{
self.accel_streak = self.accel_streak.saturating_add(1);
} else {
self.accel_streak = 0;
}

// Update state
self.previous = new;
self.previous_diff = diff;

// Enforce limits
if self.growth_streak >= STREAK_LIMIT || self.accel_streak >= STREAK_LIMIT {
return false;
}

true
}
}
1 change: 1 addition & 0 deletions crates/biome_service/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod growth_guard;