diff --git a/.changeset/tiny-dodos-ask.md b/.changeset/tiny-dodos-ask.md new file mode 100644 index 000000000000..7ecd5711be97 --- /dev/null +++ b/.changeset/tiny-dodos-ask.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Fixed an issue where some info diagnostics weren't tracked by the final summary. diff --git a/crates/biome_cli/examples/text_reporter.rs b/crates/biome_cli/examples/text_reporter.rs deleted file mode 100644 index 96c5d9ad849a..000000000000 --- a/crates/biome_cli/examples/text_reporter.rs +++ /dev/null @@ -1,60 +0,0 @@ -use biome_cli::{ - DiagnosticsPayload, Execution, Reporter, ReporterVisitor, TraversalSummary, VcsTargeted, -}; -use camino::Utf8Path; - -/// This will be the visitor, which where we **write** the data -struct BufferVisitor(String); - -/// This is the reporter, which will be a type that will hold the information needed to the reporter -struct TextReport { - summary: TraversalSummary, -} - -impl Reporter for TextReport { - fn write(self, visitor: &mut dyn ReporterVisitor) -> std::io::Result<()> { - let execution = Execution::new_format(VcsTargeted { - staged: false, - changed: false, - }); - visitor.report_summary(&execution, self.summary, false)?; - Ok(()) - } -} - -impl ReporterVisitor for BufferVisitor { - fn report_summary( - &mut self, - _execution: &Execution, - summary: TraversalSummary, - _verbose: bool, - ) -> std::io::Result<()> { - self.0 - .push_str(&format!("Total is {}", summary.changed + summary.unchanged)); - Ok(()) - } - - fn report_diagnostics( - &mut self, - _execution: &Execution, - _payload: DiagnosticsPayload, - _verbose: bool, - _working_directory: Option<&Utf8Path>, - ) -> std::io::Result<()> { - todo!() - } -} - -pub fn main() { - // In a real scenario, the project key is obtained from the - let summary = TraversalSummary { - changed: 32, - unchanged: 28, - ..TraversalSummary::default() - }; - let mut visitor = BufferVisitor(String::new()); - let reporter = TextReport { summary }; - reporter.write(&mut visitor).unwrap(); - - assert_eq!(visitor.0.as_str(), "Total is 64") -} diff --git a/crates/biome_cli/src/cli_options.rs b/crates/biome_cli/src/cli_options.rs index 1ff5ddc6d1a9..fe012c4f4123 100644 --- a/crates/biome_cli/src/cli_options.rs +++ b/crates/biome_cli/src/cli_options.rs @@ -6,7 +6,7 @@ use std::fmt::{Display, Formatter}; use std::str::FromStr; /// Global options applied to all commands -#[derive(Debug, Clone, Bpaf)] +#[derive(Debug, Default, Clone, Bpaf)] pub struct CliOptions { /// Set the formatting mode for markup: "off" prints everything as plain text, "force" forces the formatting of markup using ANSI even if the console output is determined to be incompatible #[bpaf(long("colors"), argument("off|force"))] diff --git a/crates/biome_cli/src/commands/check.rs b/crates/biome_cli/src/commands/check.rs index 2b08604d210b..f02d71646617 100644 --- a/crates/biome_cli/src/commands/check.rs +++ b/crates/biome_cli/src/commands/check.rs @@ -1,19 +1,28 @@ -use super::{FixFileModeOptions, LoadEditorConfig, determine_fix_file_mode}; +use super::{FixFileModeOptions, determine_fix_file_mode, get_files_to_process_with_cli_options}; +use crate::CliDiagnostic; use crate::cli_options::CliOptions; -use crate::commands::{CommandRunner, get_files_to_process_with_cli_options}; -use crate::{CliDiagnostic, Execution, TraversalMode}; +use crate::runner::execution::{AnalyzerSelectors, Execution, VcsTargeted}; +use crate::runner::impls::commands::traversal::{LoadEditorConfig, TraversalCommand}; +use crate::runner::impls::executions::summary_verb::SummaryVerbExecution; +use crate::runner::impls::process_file::check::CheckProcessFile; use biome_configuration::analyzer::LinterEnabled; use biome_configuration::analyzer::assist::{AssistConfiguration, AssistEnabled}; use biome_configuration::css::CssParserConfiguration; use biome_configuration::formatter::{FormatWithErrorsEnabled, FormatterEnabled}; use biome_configuration::json::JsonParserConfiguration; use biome_configuration::{Configuration, FormatterConfiguration, LinterConfiguration}; -use biome_console::Console; +use biome_console::{Console, MarkupBuf}; use biome_deserialize::Merge; +use biome_diagnostics::{Category, category}; use biome_fs::FileSystem; +use biome_service::workspace::{ + FeatureKind, FeatureName, FeaturesBuilder, FeaturesSupported, FixFileMode, ScanKind, + SupportKind, +}; use biome_service::{Workspace, WorkspaceError}; use camino::Utf8PathBuf; use std::ffi::OsString; +use std::time::Duration; pub(crate) struct CheckCommandPayload { pub(crate) write: bool, @@ -34,6 +43,97 @@ pub(crate) struct CheckCommandPayload { pub(crate) css_parser: Option, } +struct CheckExecution { + /// The type of fixes that should be applied when analyzing a file. + /// + /// It's [None] if the `check` command is called without `--apply` or `--apply-suggested` + /// arguments. + fix_file_mode: Option, + /// An optional tuple. + /// 1. The virtual path to the file + /// 2. The content of the file + stdin_file_path: Option, + /// A flag to know vcs integrated options such as `--staged` or `--changed` are enabled + vcs_targeted: VcsTargeted, + + /// Whether assist diagnostics should be promoted to error, and fail the CLI + enforce_assist: bool, + + /// It skips parse errors + skip_parse_errors: bool, +} + +impl Execution for CheckExecution { + fn features(&self) -> FeatureName { + FeaturesBuilder::new() + .with_linter() + .with_formatter() + .with_assist() + .build() + } + + fn can_handle(&self, features: FeaturesSupported) -> bool { + features.supports_lint() || features.supports_assist() || features.supports_format() + } + + fn is_vcs_targeted(&self) -> bool { + self.vcs_targeted.changed || self.vcs_targeted.staged + } + + fn supports_kind(&self, file_features: &FeaturesSupported) -> Option { + file_features + .support_kind_if_not_enabled(FeatureKind::Lint) + .and(file_features.support_kind_if_not_enabled(FeatureKind::Format)) + .and(file_features.support_kind_if_not_enabled(FeatureKind::Assist)) + } + + fn get_stdin_file_path(&self) -> Option<&str> { + self.stdin_file_path.as_deref() + } + + fn is_check(&self) -> bool { + true + } + + fn as_diagnostic_category(&self) -> &'static Category { + category!("check") + } + + fn is_safe_fixes_enabled(&self) -> bool { + self.fix_file_mode + .is_some_and(|fix_mode| fix_mode == FixFileMode::SafeFixes) + } + + fn is_safe_and_unsafe_fixes_enabled(&self) -> bool { + self.fix_file_mode + .is_some_and(|fix_mode| fix_mode == FixFileMode::SafeAndUnsafeFixes) + } + + fn as_fix_file_mode(&self) -> Option { + self.fix_file_mode + } + + fn should_skip_parse_errors(&self) -> bool { + self.skip_parse_errors + } + + fn requires_write_access(&self) -> bool { + self.fix_file_mode.is_some() + } + + fn analyzer_selectors(&self) -> AnalyzerSelectors { + AnalyzerSelectors::default() + } + + fn should_enforce_assist(&self) -> bool { + self.enforce_assist + } + + fn summary_phrase(&self, files: usize, duration: &Duration) -> MarkupBuf { + SummaryVerbExecution.summary_verb("Checked", files, duration) + } +} + impl LoadEditorConfig for CheckCommandPayload { fn should_load_editor_config(&self, fs_configuration: &Configuration) -> bool { self.configuration @@ -43,8 +143,39 @@ impl LoadEditorConfig for CheckCommandPayload { } } -impl CommandRunner for CheckCommandPayload { - const COMMAND_NAME: &'static str = "check"; +impl TraversalCommand for CheckCommandPayload { + type ProcessFile = CheckProcessFile; + + fn command_name(&self) -> &'static str { + "check" + } + + fn minimal_scan_kind(&self) -> Option { + None + } + + fn get_execution( + &self, + cli_options: &CliOptions, + _console: &mut dyn Console, + _workspace: &dyn Workspace, + ) -> Result, CliDiagnostic> { + let fix_file_mode = determine_fix_file_mode(FixFileModeOptions { + write: self.write, + suppress: false, + suppression_reason: None, + fix: self.fix, + unsafe_: self.unsafe_, + })?; + + Ok(Box::new(CheckExecution { + fix_file_mode, + stdin_file_path: self.stdin_file_path.clone(), + vcs_targeted: (self.staged, self.changed).into(), + enforce_assist: self.enforce_assist, + skip_parse_errors: cli_options.skip_parse_errors, + })) + } fn merge_configuration( &mut self, @@ -132,36 +263,4 @@ impl CommandRunner for CheckCommandPayload { Ok(paths) } - - fn get_stdin_file_path(&self) -> Option<&str> { - self.stdin_file_path.as_deref() - } - - fn should_write(&self) -> bool { - self.write || self.fix - } - - fn get_execution( - &self, - cli_options: &CliOptions, - console: &mut dyn Console, - _workspace: &dyn Workspace, - ) -> Result { - let fix_file_mode = determine_fix_file_mode(FixFileModeOptions { - write: self.write, - suppress: false, - suppression_reason: None, - fix: self.fix, - unsafe_: self.unsafe_, - })?; - - Ok(Execution::new(TraversalMode::Check { - fix_file_mode, - stdin: self.get_stdin(console)?, - vcs_targeted: (self.staged, self.changed).into(), - enforce_assist: self.enforce_assist, - skip_parse_errors: cli_options.skip_parse_errors, - }) - .set_report(cli_options)) - } } diff --git a/crates/biome_cli/src/commands/ci.rs b/crates/biome_cli/src/commands/ci.rs index 755ea4254bb5..2705735811b9 100644 --- a/crates/biome_cli/src/commands/ci.rs +++ b/crates/biome_cli/src/commands/ci.rs @@ -1,7 +1,10 @@ +use crate::CliDiagnostic; use crate::changed::get_changed_files; use crate::cli_options::CliOptions; -use crate::commands::{CommandRunner, LoadEditorConfig}; -use crate::{CliDiagnostic, Execution}; +use crate::runner::execution::{AnalyzerSelectors, Execution, ExecutionEnvironment, VcsTargeted}; +use crate::runner::impls::commands::traversal::{LoadEditorConfig, TraversalCommand}; +use crate::runner::impls::executions::summary_verb::SummaryVerbExecution; +use crate::runner::impls::process_file::check::CheckProcessFile; use biome_configuration::analyzer::LinterEnabled; use biome_configuration::analyzer::assist::{AssistConfiguration, AssistEnabled}; use biome_configuration::css::CssParserConfiguration; @@ -10,12 +13,17 @@ use biome_configuration::json::JsonParserConfiguration; use biome_configuration::{ Configuration, CssConfiguration, FormatterConfiguration, JsonConfiguration, LinterConfiguration, }; -use biome_console::Console; +use biome_console::{Console, MarkupBuf}; use biome_deserialize::Merge; +use biome_diagnostics::{Category, category}; use biome_fs::FileSystem; +use biome_service::workspace::{ + FeatureKind, FeatureName, FeaturesBuilder, FeaturesSupported, ScanKind, SupportKind, +}; use biome_service::{Workspace, WorkspaceError}; use camino::Utf8PathBuf; use std::ffi::OsString; +use std::time::Duration; pub(crate) struct CiCommandPayload { pub(crate) formatter_enabled: Option, @@ -31,6 +39,74 @@ pub(crate) struct CiCommandPayload { pub(crate) css_parser: Option, } +struct CiExecution { + /// Whether the CI is running in a specific environment, e.g. GitHub, GitLab, etc. + _environment: Option, + /// A flag to know vcs integrated options such as `--staged` or `--changed` are enabled + vcs_targeted: VcsTargeted, + /// Whether assist diagnostics should be promoted to error, and fail the CLI + enforce_assist: bool, + /// It skips parse errors + skip_parse_errors: bool, +} + +impl Execution for CiExecution { + fn features(&self) -> FeatureName { + FeaturesBuilder::new() + .with_linter() + .with_formatter() + .with_assist() + .build() + } + + fn can_handle(&self, features: FeaturesSupported) -> bool { + features.supports_lint() || features.supports_assist() || features.supports_format() + } + + fn is_vcs_targeted(&self) -> bool { + self.vcs_targeted.changed || self.vcs_targeted.staged + } + + fn supports_kind(&self, file_features: &FeaturesSupported) -> Option { + file_features + .support_kind_if_not_enabled(FeatureKind::Lint) + .and(file_features.support_kind_if_not_enabled(FeatureKind::Format)) + .and(file_features.support_kind_if_not_enabled(FeatureKind::Assist)) + } + + fn get_stdin_file_path(&self) -> Option<&str> { + None + } + + fn is_ci(&self) -> bool { + true + } + + fn as_diagnostic_category(&self) -> &'static Category { + category!("ci") + } + + fn should_skip_parse_errors(&self) -> bool { + self.skip_parse_errors + } + + fn requires_write_access(&self) -> bool { + false + } + + fn analyzer_selectors(&self) -> AnalyzerSelectors { + AnalyzerSelectors::default() + } + + fn should_enforce_assist(&self) -> bool { + self.enforce_assist + } + + fn summary_phrase(&self, files: usize, duration: &Duration) -> MarkupBuf { + SummaryVerbExecution.summary_verb("Checked", files, duration) + } +} + impl LoadEditorConfig for CiCommandPayload { fn should_load_editor_config(&self, fs_configuration: &Configuration) -> bool { self.configuration @@ -40,8 +116,39 @@ impl LoadEditorConfig for CiCommandPayload { } } -impl CommandRunner for CiCommandPayload { - const COMMAND_NAME: &'static str = "ci"; +impl TraversalCommand for CiCommandPayload { + type ProcessFile = CheckProcessFile; + + fn command_name(&self) -> &'static str { + "ci" + } + + fn minimal_scan_kind(&self) -> Option { + None + } + + fn get_execution( + &self, + cli_options: &CliOptions, + _console: &mut dyn Console, + _workspace: &dyn Workspace, + ) -> Result, CliDiagnostic> { + // Ref: https://docs.github.com/actions/learn-github-actions/variables#default-environment-variables + let is_github = std::env::var("GITHUB_ACTIONS") + .ok() + .is_some_and(|value| value == "true"); + + Ok(Box::new(CiExecution { + _environment: if is_github { + Some(ExecutionEnvironment::GitHub) + } else { + None + }, + vcs_targeted: (false, self.changed).into(), + enforce_assist: self.enforce_assist, + skip_parse_errors: cli_options.skip_parse_errors, + })) + } fn merge_configuration( &mut self, @@ -127,28 +234,6 @@ impl CommandRunner for CiCommandPayload { } } - fn get_stdin_file_path(&self) -> Option<&str> { - None - } - - fn should_write(&self) -> bool { - false - } - - fn get_execution( - &self, - cli_options: &CliOptions, - _console: &mut dyn Console, - _workspace: &dyn Workspace, - ) -> Result { - Ok(Execution::new_ci( - (false, self.changed).into(), - self.enforce_assist, - cli_options.skip_parse_errors, - ) - .set_report(cli_options)) - } - fn check_incompatible_arguments(&self) -> Result<(), CliDiagnostic> { if self.formatter_enabled.is_some_and(|v| !v.value()) && self.linter_enabled.is_some_and(|v| !v.value()) diff --git a/crates/biome_cli/src/commands/format.rs b/crates/biome_cli/src/commands/format.rs index 175acdfc1368..a69cdd8531b9 100644 --- a/crates/biome_cli/src/commands/format.rs +++ b/crates/biome_cli/src/commands/format.rs @@ -1,6 +1,10 @@ +use crate::CliDiagnostic; use crate::cli_options::CliOptions; -use crate::commands::{CommandRunner, LoadEditorConfig, get_files_to_process_with_cli_options}; -use crate::{CliDiagnostic, Execution, TraversalMode}; +use crate::commands::get_files_to_process_with_cli_options; +use crate::runner::execution::{AnalyzerSelectors, Execution, VcsTargeted}; +use crate::runner::impls::commands::traversal::{LoadEditorConfig, TraversalCommand}; +use crate::runner::impls::executions::summary_verb::SummaryVerbExecution; +use crate::runner::impls::process_file::format::FormatProcessFile; use biome_configuration::css::{CssFormatterConfiguration, CssParserConfiguration}; use biome_configuration::graphql::GraphqlFormatterConfiguration; use biome_configuration::html::HtmlFormatterConfiguration; @@ -8,12 +12,17 @@ use biome_configuration::javascript::JsFormatterConfiguration; use biome_configuration::json::{JsonFormatterConfiguration, JsonParserConfiguration}; use biome_configuration::vcs::VcsConfiguration; use biome_configuration::{Configuration, FilesConfiguration, FormatterConfiguration}; -use biome_console::Console; +use biome_console::{Console, MarkupBuf}; use biome_deserialize::Merge; +use biome_diagnostics::{Category, category}; use biome_fs::FileSystem; +use biome_service::workspace::{ + FeatureKind, FeatureName, FeaturesBuilder, FeaturesSupported, ScanKind, SupportKind, +}; use biome_service::{Workspace, WorkspaceError}; use camino::Utf8PathBuf; use std::ffi::OsString; +use std::time::Duration; pub(crate) struct FormatCommandPayload { pub(crate) javascript_formatter: Option, @@ -35,6 +44,65 @@ pub(crate) struct FormatCommandPayload { pub(crate) css_parser: Option, } +struct FormatExecution { + stdin_file_path: Option, + write: bool, + skip_parse_errors: bool, + + /// A flag to know vcs integrated options such as `--staged` or `--changed` are enabled + vcs_targeted: VcsTargeted, +} + +impl Execution for FormatExecution { + fn features(&self) -> FeatureName { + FeaturesBuilder::new().with_formatter().build() + } + + fn can_handle(&self, features: FeaturesSupported) -> bool { + features.supports_format() + } + + fn is_vcs_targeted(&self) -> bool { + self.vcs_targeted.changed || self.vcs_targeted.staged + } + + fn supports_kind(&self, file_features: &FeaturesSupported) -> Option { + Some(file_features.support_kind_for(FeatureKind::Format)) + } + + fn get_stdin_file_path(&self) -> Option<&str> { + self.stdin_file_path.as_deref() + } + + fn as_diagnostic_category(&self) -> &'static Category { + category!("format") + } + + fn should_skip_parse_errors(&self) -> bool { + self.skip_parse_errors + } + + fn requires_write_access(&self) -> bool { + self.write + } + + fn analyzer_selectors(&self) -> AnalyzerSelectors { + AnalyzerSelectors::default() + } + + fn is_format(&self) -> bool { + true + } + + fn summary_phrase(&self, files: usize, duration: &Duration) -> MarkupBuf { + if self.requires_write_access() { + SummaryVerbExecution.summary_verb("Formatted", files, duration) + } else { + SummaryVerbExecution.summary_verb("Checked", files, duration) + } + } +} + impl LoadEditorConfig for FormatCommandPayload { fn should_load_editor_config(&self, fs_configuration: &Configuration) -> bool { self.formatter_configuration @@ -44,8 +112,33 @@ impl LoadEditorConfig for FormatCommandPayload { } } -impl CommandRunner for FormatCommandPayload { - const COMMAND_NAME: &'static str = "format"; +impl TraversalCommand for FormatCommandPayload { + type ProcessFile = FormatProcessFile; + + fn command_name(&self) -> &'static str { + "format" + } + + fn minimal_scan_kind(&self) -> Option { + Some(ScanKind::KnownFiles) + } + + fn get_execution( + &self, + cli_options: &CliOptions, + _console: &mut dyn Console, + _workspace: &dyn Workspace, + ) -> Result, CliDiagnostic> { + Ok(Box::new(FormatExecution { + stdin_file_path: self.stdin_file_path.clone(), + write: self.write || self.fix, + skip_parse_errors: cli_options.skip_parse_errors, + vcs_targeted: VcsTargeted { + staged: self.staged, + changed: self.changed, + }, + })) + } fn merge_configuration( &mut self, @@ -128,30 +221,6 @@ impl CommandRunner for FormatCommandPayload { configuration, )? .unwrap_or(self.paths.clone()); - Ok(paths) } - - fn get_stdin_file_path(&self) -> Option<&str> { - self.stdin_file_path.as_deref() - } - - fn should_write(&self) -> bool { - self.write || self.fix - } - - fn get_execution( - &self, - cli_options: &CliOptions, - console: &mut dyn Console, - _workspace: &dyn Workspace, - ) -> Result { - Ok(Execution::new(TraversalMode::Format { - skip_parse_errors: cli_options.skip_parse_errors, - write: self.should_write(), - stdin: self.get_stdin(console)?, - vcs_targeted: (self.staged, self.changed).into(), - }) - .set_report(cli_options)) - } } diff --git a/crates/biome_cli/src/commands/lint.rs b/crates/biome_cli/src/commands/lint.rs index 3aa9c1a10b42..52503236c76f 100644 --- a/crates/biome_cli/src/commands/lint.rs +++ b/crates/biome_cli/src/commands/lint.rs @@ -1,7 +1,11 @@ use super::{FixFileModeOptions, determine_fix_file_mode}; +use crate::CliDiagnostic; use crate::cli_options::CliOptions; -use crate::commands::{CommandRunner, get_files_to_process_with_cli_options}; -use crate::{CliDiagnostic, Execution, TraversalMode}; +use crate::commands::get_files_to_process_with_cli_options; +use crate::runner::execution::{AnalyzerSelectors, Execution, VcsTargeted}; +use crate::runner::impls::commands::traversal::TraversalCommand; +use crate::runner::impls::executions::summary_verb::SummaryVerbExecution; +use crate::runner::impls::process_file::lint_and_assist::LintAssistProcessFile; use biome_configuration::analyzer::AnalyzerSelector; use biome_configuration::css::{CssLinterConfiguration, CssParserConfiguration}; use biome_configuration::graphql::GraphqlLinterConfiguration; @@ -9,12 +13,19 @@ use biome_configuration::javascript::JsLinterConfiguration; use biome_configuration::json::{JsonLinterConfiguration, JsonParserConfiguration}; use biome_configuration::vcs::VcsConfiguration; use biome_configuration::{Configuration, FilesConfiguration, LinterConfiguration}; -use biome_console::Console; +use biome_console::{Console, MarkupBuf}; use biome_deserialize::Merge; +use biome_diagnostics::{Category, category}; use biome_fs::FileSystem; +use biome_service::configuration::ProjectScanComputer; +use biome_service::workspace::{ + FeatureKind, FeatureName, FeaturesBuilder, FeaturesSupported, FixFileMode, ScanKind, + SupportKind, +}; use biome_service::{Workspace, WorkspaceError}; use camino::Utf8PathBuf; use std::ffi::OsString; +use std::time::Duration; pub(crate) struct LintCommandPayload { pub(crate) write: bool, @@ -40,8 +51,135 @@ pub(crate) struct LintCommandPayload { pub(crate) css_parser: Option, } -impl CommandRunner for LintCommandPayload { - const COMMAND_NAME: &'static str = "lint"; +struct LintExecution { + /// The type of fixes that should be applied when analyzing a file. + /// + /// It's [None] if the `lint` command is called without `--apply` or `--apply-suggested` + /// arguments. + fix_file_mode: Option, + /// An optional tuple. + /// 1. The virtual path to the file + /// 2. The content of the file + stdin_file_path: Option, + /// Run only the given rule or group of rules. + /// If the severity level of a rule is `off`, + /// then the severity level of the rule is set to `error` if it is a recommended rule or `warn` otherwise. + only: Vec, + /// Skip the given rule or group of rules by setting the severity level of the rules to `off`. + /// This option takes precedence over `--only`. + skip: Vec, + /// A flag to know vcs integrated options such as `--staged` or `--changed` are enabled + vcs_targeted: VcsTargeted, + /// Suppress existing diagnostics with a `// biome-ignore` comment + suppress: bool, + /// Explanation for suppressing diagnostics with `--suppress` and `--reason` + suppression_reason: Option, + /// It skips parse errors + skip_parse_errors: bool, +} + +impl Execution for LintExecution { + fn features(&self) -> FeatureName { + FeaturesBuilder::new().with_linter().build() + } + + fn can_handle(&self, features: FeaturesSupported) -> bool { + features.supports_lint() + } + + fn supports_kind(&self, file_features: &FeaturesSupported) -> Option { + Some(file_features.support_kind_for(FeatureKind::Lint)) + } + + fn is_vcs_targeted(&self) -> bool { + self.vcs_targeted.changed || self.vcs_targeted.staged + } + + fn get_stdin_file_path(&self) -> Option<&str> { + self.stdin_file_path.as_deref() + } + + fn scan_kind_computer(&self, computer: ProjectScanComputer) -> ScanKind { + computer + .with_rule_selectors(self.skip.as_ref(), self.only.as_ref()) + .compute() + } + + fn as_diagnostic_category(&self) -> &'static Category { + category!("lint") + } + + fn as_fix_file_mode(&self) -> Option { + self.fix_file_mode + } + + fn should_skip_parse_errors(&self) -> bool { + self.skip_parse_errors + } + + fn suppress(&self) -> bool { + self.suppress + } + + fn suppression_reason(&self) -> Option<&str> { + self.suppression_reason.as_deref() + } + + fn requires_write_access(&self) -> bool { + self.fix_file_mode.is_some() + } + + fn analyzer_selectors(&self) -> AnalyzerSelectors { + AnalyzerSelectors { + only: self.only.clone(), + skip: self.skip.clone(), + } + } + + fn summary_phrase(&self, files: usize, duration: &Duration) -> MarkupBuf { + SummaryVerbExecution.summary_verb("Checked", files, duration) + } + + fn is_lint(&self) -> bool { + true + } +} + +impl TraversalCommand for LintCommandPayload { + type ProcessFile = LintAssistProcessFile; + + fn command_name(&self) -> &'static str { + "lint" + } + + fn minimal_scan_kind(&self) -> Option { + None + } + + fn get_execution( + &self, + cli_options: &CliOptions, + _console: &mut dyn Console, + _workspace: &dyn Workspace, + ) -> Result, CliDiagnostic> { + let fix_file_mode = determine_fix_file_mode(FixFileModeOptions { + write: self.write, + fix: self.fix, + unsafe_: self.unsafe_, + suppress: self.suppress, + suppression_reason: self.suppression_reason.clone(), + })?; + Ok(Box::new(LintExecution { + fix_file_mode, + stdin_file_path: self.stdin_file_path.clone(), + only: self.only.clone(), + skip: self.skip.clone(), + vcs_targeted: (self.staged, self.changed).into(), + suppress: self.suppress, + suppression_reason: self.suppression_reason.clone(), + skip_parse_errors: cli_options.skip_parse_errors, + })) + } fn merge_configuration( &mut self, @@ -121,38 +259,4 @@ impl CommandRunner for LintCommandPayload { Ok(paths) } - - fn get_stdin_file_path(&self) -> Option<&str> { - self.stdin_file_path.as_deref() - } - - fn should_write(&self) -> bool { - self.write || self.fix - } - - fn get_execution( - &self, - cli_options: &CliOptions, - console: &mut dyn Console, - _workspace: &dyn Workspace, - ) -> Result { - let fix_file_mode = determine_fix_file_mode(FixFileModeOptions { - write: self.write, - fix: self.fix, - unsafe_: self.unsafe_, - suppress: self.suppress, - suppression_reason: self.suppression_reason.clone(), - })?; - Ok(Execution::new(TraversalMode::Lint { - fix_file_mode, - stdin: self.get_stdin(console)?, - only: self.only.clone(), - skip: self.skip.clone(), - vcs_targeted: (self.staged, self.changed).into(), - suppress: self.suppress, - suppression_reason: self.suppression_reason.clone(), - skip_parse_errors: cli_options.skip_parse_errors, - }) - .set_report(cli_options)) - } } diff --git a/crates/biome_cli/src/commands/migrate.rs b/crates/biome_cli/src/commands/migrate.rs index 051a816af2db..a1414083ff88 100644 --- a/crates/biome_cli/src/commands/migrate.rs +++ b/crates/biome_cli/src/commands/migrate.rs @@ -1,16 +1,20 @@ -use super::{ - CommandRunner, FixFileModeOptions, MigrateSubCommand, check_fix_incompatible_arguments, -}; -use crate::CliDiagnostic; use crate::cli_options::CliOptions; +use crate::commands::{FixFileModeOptions, MigrateSubCommand, check_fix_incompatible_arguments}; use crate::diagnostics::MigrationDiagnostic; -use crate::execute::{Execution, TraversalMode}; +use crate::runner::ConfiguredWorkspace; +use crate::runner::execution::{AnalyzerSelectors, Execution}; +use crate::runner::impls::commands::custom_execution::CustomExecutionCmd; +use crate::{CliDiagnostic, CliSession}; use biome_configuration::Configuration; -use biome_console::{Console, ConsoleExt, markup}; +use biome_console::{Console, ConsoleExt, MarkupBuf, markup}; +use biome_diagnostics::{Category, category}; use biome_fs::FileSystem; +use biome_service::workspace::{ + FeatureName, FeaturesBuilder, FeaturesSupported, ScanKind, SupportKind, +}; use biome_service::{Workspace, WorkspaceError}; use camino::Utf8PathBuf; -use std::ffi::OsString; +use std::time::Duration; pub(crate) struct MigrateCommandPayload { pub(crate) write: bool, @@ -20,34 +24,66 @@ pub(crate) struct MigrateCommandPayload { pub(crate) configuration_directory_path: Option, } -impl CommandRunner for MigrateCommandPayload { - const COMMAND_NAME: &'static str = "migrate"; +pub(crate) struct MigrateExecution { + write: bool, +} - fn merge_configuration( - &mut self, - loaded_configuration: Configuration, - loaded_directory: Option, - loaded_file: Option, - _fs: &dyn FileSystem, - _console: &mut dyn Console, - ) -> Result { - self.configuration_file_path = loaded_file; - self.configuration_directory_path = loaded_directory; - Ok(loaded_configuration) +impl Execution for MigrateExecution { + fn features(&self) -> FeatureName { + FeaturesBuilder::new().build() } - fn get_files_to_process( - &self, - _fs: &dyn FileSystem, - _configuration: &Configuration, - ) -> Result, CliDiagnostic> { - Ok(vec![]) + fn can_handle(&self, _: FeaturesSupported) -> bool { + true + } + + fn is_vcs_targeted(&self) -> bool { + false + } + + fn supports_kind(&self, _: &FeaturesSupported) -> Option { + None } fn get_stdin_file_path(&self) -> Option<&str> { None } + fn as_diagnostic_category(&self) -> &'static Category { + category!("migrate") + } + + fn requires_write_access(&self) -> bool { + self.write + } + + fn analyzer_selectors(&self) -> AnalyzerSelectors { + AnalyzerSelectors::default() + } + fn summary_phrase(&self, _files: usize, duration: &Duration) -> MarkupBuf { + if self.requires_write_access() { + markup! { + "Migrated your configuration file in "{duration}"." + } + .to_owned() + } else { + markup! { + "Checked your configuration file in "{duration}"." + } + .to_owned() + } + } +} + +impl CustomExecutionCmd for MigrateCommandPayload { + fn command_name(&self) -> &'static str { + "migrate" + } + + fn minimal_scan_kind(&self) -> Option { + Some(ScanKind::KnownFiles) + } + fn should_write(&self) -> bool { self.write || self.fix } @@ -57,12 +93,10 @@ impl CommandRunner for MigrateCommandPayload { _cli_options: &CliOptions, console: &mut dyn Console, _workspace: &dyn Workspace, - ) -> Result { - if let Some(path) = self.configuration_file_path.clone() { - Ok(Execution::new(TraversalMode::Migrate { + ) -> Result, CliDiagnostic> { + if self.configuration_file_path.is_some() { + Ok(Box::new(MigrateExecution { write: self.should_write(), - configuration_file_path: path, - sub_command: self.sub_command.clone(), })) } else { console.log(markup! { @@ -87,4 +121,42 @@ impl CommandRunner for MigrateCommandPayload { fn should_validate_configuration_diagnostics(&self) -> bool { false } + + fn execute_without_crawling( + &mut self, + session: CliSession, + configured_workspace: ConfiguredWorkspace, + ) -> Result<(), CliDiagnostic> { + let ConfiguredWorkspace { + execution: _, + project_key, + paths: _, + configuration_files, + duration: _, + } = configured_workspace; + + let payload = crate::execute::migrate::MigratePayload { + session, + project_key, + write: self.should_write(), + // SAFETY: checked during get_execution + configuration_file_path: self.configuration_file_path.clone().unwrap(), + sub_command: self.sub_command.clone(), + nested_configuration_files: configuration_files, + }; + crate::execute::migrate::run(payload) + } + + fn merge_configuration( + &mut self, + loaded_configuration: Configuration, + loaded_directory: Option, + loaded_file: Option, + _fs: &dyn FileSystem, + _console: &mut dyn Console, + ) -> Result { + self.configuration_file_path = loaded_file; + self.configuration_directory_path = loaded_directory; + Ok(loaded_configuration) + } } diff --git a/crates/biome_cli/src/commands/mod.rs b/crates/biome_cli/src/commands/mod.rs index 6a613135121c..47dd70ce5f9b 100644 --- a/crates/biome_cli/src/commands/mod.rs +++ b/crates/biome_cli/src/commands/mod.rs @@ -1,13 +1,8 @@ use crate::changed::{get_changed_files, get_staged_files}; use crate::cli_options::{CliOptions, CliReporter, ColorsArg, cli_options}; -use crate::commands::scan_kind::derive_best_scan_kind; -use crate::execute::Stdin; use crate::logging::log_options; use crate::logging::{LogOptions, LoggingKind}; -use crate::{ - CliDiagnostic, CliSession, Execution, LoggingLevel, TraversalMode, VERSION, execute_mode, - setup_cli_subscriber, -}; +use crate::{CliDiagnostic, LoggingLevel, VERSION}; use biome_configuration::analyzer::assist::AssistEnabled; use biome_configuration::analyzer::{AnalyzerSelector, LinterEnabled}; use biome_configuration::css::{ @@ -37,25 +32,15 @@ use biome_configuration::{ vcs::vcs_configuration, }; use biome_console::{Console, ConsoleExt, markup}; -use biome_deserialize::Merge; use biome_diagnostics::{Diagnostic, PrintDiagnostic, Severity}; -use biome_fs::{BiomePath, FileSystem}; +use biome_fs::FileSystem; use biome_grit_patterns::GritTargetLanguage; -use biome_resolver::FsWithResolverProxy; -use biome_service::configuration::{ - LoadedConfiguration, ProjectScanComputer, load_configuration, load_editorconfig, -}; +use biome_service::configuration::LoadedConfiguration; use biome_service::documentation::Doc; -use biome_service::projects::ProjectKey; -use biome_service::workspace::{ - FixFileMode, OpenProjectParams, ScanKind, ScanProjectParams, UpdateSettingsParams, -}; -use biome_service::{WatcherOptions, Workspace, WorkspaceError, watcher_options}; +use biome_service::workspace::FixFileMode; +use biome_service::{WatcherOptions, watcher_options}; use bpaf::Bpaf; -use camino::{Utf8Path, Utf8PathBuf}; use std::ffi::OsString; -use std::time::Duration; -use tracing::info; pub(crate) mod check; pub(crate) mod ci; @@ -67,7 +52,6 @@ pub(crate) mod init; pub(crate) mod lint; pub(crate) mod migrate; pub(crate) mod rage; -mod scan_kind; pub(crate) mod search; pub(crate) mod version; @@ -878,365 +862,6 @@ fn check_fix_incompatible_arguments(options: FixFileModeOptions) -> Result<(), C Ok(()) } -/// Generic interface for executing commands. -/// -/// Consumers must implement the following methods: -/// -/// - [CommandRunner::merge_configuration] -/// - [CommandRunner::get_files_to_process] -/// - [CommandRunner::get_stdin_file_path] -/// - [CommandRunner::should_write] -/// - [CommandRunner::get_execution] -/// -/// Optional methods: -/// - [CommandRunner::check_incompatible_arguments] -pub(crate) trait CommandRunner: Sized { - const COMMAND_NAME: &'static str; - - /// The main command to use. - fn run( - &mut self, - session: CliSession, - log_options: &LogOptions, - cli_options: &CliOptions, - ) -> Result<(), CliDiagnostic> { - setup_cli_subscriber( - log_options.log_file.as_deref(), - log_options.log_level, - log_options.log_kind, - cli_options.colors.as_ref(), - ); - let console = &mut *session.app.console; - let workspace = &*session.app.workspace; - let fs = workspace.fs(); - self.check_incompatible_arguments()?; - let ConfiguredWorkspace { - execution, - paths, - duration, - configuration_files, - project_key, - } = self.configure_workspace(fs, console, workspace, cli_options)?; - execute_mode( - execution, - session, - cli_options, - paths, - duration, - configuration_files, - project_key, - ) - } - - /// This function prepares the workspace with the following: - /// - Loading the configuration file. - /// - Configure the VCS integration - /// - Computes the paths to traverse/handle. This changes based on the VCS arguments that were passed. - /// - Register a project folder using the working directory. - /// - Updates the settings that belong to the project registered - fn configure_workspace( - &mut self, - fs: &dyn FsWithResolverProxy, - console: &mut dyn Console, - workspace: &dyn Workspace, - cli_options: &CliOptions, - ) -> Result { - let working_dir = fs.working_directory().unwrap_or_default(); - // Load configuration - let configuration_path_hint = cli_options.as_configuration_path_hint(working_dir.as_path()); - let loaded_configuration = load_configuration(fs, configuration_path_hint)?; - if self.should_validate_configuration_diagnostics() { - validate_configuration_diagnostics( - &loaded_configuration, - console, - cli_options.verbose, - )?; - } - info!( - "Configuration file loaded: {:?}, diagnostics detected {}", - loaded_configuration.file_path, - loaded_configuration.diagnostics.len(), - ); - let LoadedConfiguration { - extended_configurations, - configuration, - diagnostics: _, - directory_path, - file_path, - } = loaded_configuration; - - // Merge the FS configuration with the CLI arguments - let configuration = self.merge_configuration( - configuration, - directory_path.clone(), - file_path, - fs, - console, - )?; - - let execution = self.get_execution(cli_options, console, workspace)?; - - let root_configuration_dir = directory_path - .clone() - .unwrap_or_else(|| working_dir.clone()); - // Using `--config-path`, users can point to a (root) config file that - // is not actually at the root of the project. So between the working - // directory and configuration directory, we use whichever one is higher - // up in the file system. - let project_dir = if root_configuration_dir.starts_with(&working_dir) { - &working_dir - } else { - &root_configuration_dir - }; - - let paths = self.get_files_to_process(fs, &configuration)?; - let paths = validated_paths_for_execution(paths, &execution, &working_dir)?; - - // Open the project - let open_project_result = workspace.open_project(OpenProjectParams { - path: BiomePath::new(project_dir), - open_uninitialized: true, - })?; - - let scan_kind_computer = - if let TraversalMode::Lint { only, skip, .. } = execution.traversal_mode() { - ProjectScanComputer::new(&configuration).with_rule_selectors(skip, only) - } else { - ProjectScanComputer::new(&configuration) - }; - let scan_kind = derive_best_scan_kind( - scan_kind_computer.compute(), - &execution, - &root_configuration_dir, - &working_dir, - &configuration, - ); - - // Update the settings of the project - let result = workspace.update_settings(UpdateSettingsParams { - project_key: open_project_result.project_key, - workspace_directory: Some(BiomePath::new(project_dir)), - configuration, - extended_configurations: extended_configurations - .into_iter() - .map(|(path, config)| (BiomePath::from(path), config)) - .collect(), - })?; - if self.should_validate_configuration_diagnostics() { - print_diagnostics_from_workspace_result( - result.diagnostics.as_slice(), - console, - cli_options.verbose, - )?; - } - - // Scan the project - let scan_kind = match (scan_kind, execution.traversal_mode()) { - (scan_kind, TraversalMode::Migrate { .. }) => scan_kind, - (ScanKind::KnownFiles, _) => { - let target_paths = paths - .iter() - .map(|path| BiomePath::new(working_dir.join(path))) - .collect(); - ScanKind::TargetedKnownFiles { - target_paths, - descend_from_targets: true, - } - } - (scan_kind, _) => scan_kind, - }; - let result = workspace.scan_project(ScanProjectParams { - project_key: open_project_result.project_key, - watch: cli_options.use_server, - force: false, // TODO: Maybe we'll want a CLI flag for this. - scan_kind, - verbose: cli_options.verbose, - })?; - - if self.should_validate_configuration_diagnostics() { - print_diagnostics_from_workspace_result( - result.diagnostics.as_slice(), - console, - cli_options.verbose, - )?; - } - - Ok(ConfiguredWorkspace { - execution, - paths, - duration: Some(result.duration), - configuration_files: result.configuration_files, - project_key: open_project_result.project_key, - }) - } - - /// Computes [Stdin] if the CLI has the necessary information. - /// - /// ## Errors - /// - If the user didn't provide anything via `stdin` but the option `--stdin-file-path` is passed. - fn get_stdin(&self, console: &mut dyn Console) -> Result, CliDiagnostic> { - let stdin = if let Some(stdin_file_path) = self.get_stdin_file_path() { - let input_code = console.read(); - if let Some(input_code) = input_code { - let path = Utf8PathBuf::from(stdin_file_path); - Some((path, input_code).into()) - } else { - // we provided the argument without a piped stdin, we bail - return Err(CliDiagnostic::missing_argument("stdin", Self::COMMAND_NAME)); - } - } else { - None - }; - - Ok(stdin) - } - - // #region Methods that consumers must implement - - /// Implements this method if you need to merge CLI arguments to the loaded configuration. - /// - /// The CLI arguments take precedence over the option configured in the configuration file. - fn merge_configuration( - &mut self, - loaded_configuration: Configuration, - loaded_directory: Option, - loaded_file: Option, - fs: &dyn FileSystem, - console: &mut dyn Console, - ) -> Result; - - /// It returns the paths that need to be handled/traversed. - fn get_files_to_process( - &self, - fs: &dyn FileSystem, - configuration: &Configuration, - ) -> Result, CliDiagnostic>; - - /// It returns the file path to use in `stdin` mode. - fn get_stdin_file_path(&self) -> Option<&str>; - - /// Whether the command should write the files. - fn should_write(&self) -> bool; - - /// Returns the [Execution] mode. - fn get_execution( - &self, - cli_options: &CliOptions, - console: &mut dyn Console, - workspace: &dyn Workspace, - ) -> Result; - - // Below, methods that consumers can implement - - /// Optional method that can be implemented to check if some CLI arguments aren't compatible. - /// - /// The method is called before loading the configuration from disk. - fn check_incompatible_arguments(&self) -> Result<(), CliDiagnostic> { - Ok(()) - } - - /// Checks whether the configuration has errors. - fn should_validate_configuration_diagnostics(&self) -> bool { - true - } - - // #endregion -} - -/// Validates `paths` so they can be safely passed to the given `execution`. -/// -/// - Converts paths from `OsString` to `String`. -/// - If the `execution` expects paths to be given, we may initialise them with -/// the current directory if they were empty otherwise. -fn validated_paths_for_execution( - paths: Vec, - execution: &Execution, - working_dir: &Utf8Path, -) -> Result, CliDiagnostic> { - let mut paths = paths - .into_iter() - .map(|path| path.into_string().map_err(WorkspaceError::non_utf8_path)) - .collect::, _>>()?; - - if paths.is_empty() { - match execution.traversal_mode() { - TraversalMode::Check { .. } - | TraversalMode::Lint { .. } - | TraversalMode::Format { .. } - | TraversalMode::CI { .. } - | TraversalMode::Search { .. } => { - if execution.is_vcs_targeted() { - // If `--staged` or `--changed` is specified, it's - // acceptable for them to be empty, so ignore it. - } else { - paths.push(working_dir.to_string()); - } - } - TraversalMode::Migrate { .. } => { - // Migrate doesn't do any traversal, so it doesn't care. - } - } - } - - Ok(paths) -} - -pub(crate) struct ConfiguredWorkspace { - /// Execution context - pub execution: Execution, - /// Paths to crawl - pub paths: Vec, - /// The duration of the scanning - pub duration: Option, - /// Configuration files found inside the project - pub configuration_files: Vec, - /// The unique identifier of the project - pub project_key: ProjectKey, -} - -pub trait LoadEditorConfig: CommandRunner { - /// Whether this command should load the `.editorconfig` file. - fn should_load_editor_config(&self, fs_configuration: &Configuration) -> bool; - - /// It loads the `.editorconfig` from the file system, parses it and deserialize it into a [Configuration] - fn load_editor_config( - &self, - configuration_path: Option, - fs_configuration: &Configuration, - fs: &dyn FileSystem, - ) -> Result, WorkspaceError> { - Ok(if self.should_load_editor_config(fs_configuration) { - let (editorconfig, _editorconfig_diagnostics) = { - let search_path = fs.working_directory().unwrap_or_default(); - - load_editorconfig(fs, search_path, configuration_path)? - }; - editorconfig - } else { - Default::default() - }) - } - - fn combine_configuration( - &self, - configuration_path: Option, - biome_configuration: Configuration, - fs: &dyn FileSystem, - ) -> Result { - Ok( - if let Some(mut fs_configuration) = - self.load_editor_config(configuration_path, &biome_configuration, fs)? - { - // If both `biome.json` and `.editorconfig` exist, formatter settings from the biome.json take precedence. - fs_configuration.merge_with(biome_configuration); - fs_configuration - } else { - biome_configuration - }, - ) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/biome_cli/src/commands/scan_kind.rs b/crates/biome_cli/src/commands/scan_kind.rs deleted file mode 100644 index 01df340a6b77..000000000000 --- a/crates/biome_cli/src/commands/scan_kind.rs +++ /dev/null @@ -1,145 +0,0 @@ -use crate::{Execution, TraversalMode}; -use biome_configuration::Configuration; -use biome_fs::BiomePath; -use biome_service::workspace::ScanKind; -use camino::Utf8Path; - -/// Returns a forced scan kind based on the given `execution`. -fn get_forced_scan_kind( - execution: &Execution, - root_configuration_dir: &Utf8Path, - working_dir: &Utf8Path, -) -> Option { - if let Some(stdin) = execution.as_stdin_file() { - let path = stdin.as_path(); - if path - .parent() - .is_some_and(|dir| dir == root_configuration_dir) - { - return Some(ScanKind::NoScanner); - } else { - return Some(ScanKind::TargetedKnownFiles { - target_paths: vec![BiomePath::new(working_dir.join(path))], - descend_from_targets: false, - }); - } - } - - // We want to keep the `match`, so if we add new traversal modes, - // the compiler will error, and we will need to handle the new variant - match execution.traversal_mode() { - TraversalMode::Format { .. } - | TraversalMode::Migrate { .. } - | TraversalMode::Search { .. } => Some(ScanKind::KnownFiles), - // These traversals might enable lint rules that require project rules, - // so we need to return `None` so we can use the `ScanKind` returned by the workspace - TraversalMode::Lint { .. } | TraversalMode::Check { .. } | TraversalMode::CI { .. } => None, - } -} - -/// Figures out the best (as in, most efficient) scan kind for the given execution. -/// -/// Rules: -/// - When processing from `stdin`, we return [ScanKind::NoScanner] if the stdin -/// file path is in the directory of the root configuration, and -/// [ScanKind::TargetedKnownFiles] otherwise. -/// - Returns [ScanKind::KnownFiles] for `biome format`, `biome migrate`, and -/// `biome search`, because we know there is no use for project analysis with -/// these commands. -/// - If the linter is disabled, we don't ever return [ScanKind::Project], because -/// we don't need to scan the project in that case. -/// - Otherwise, we return the requested scan kind. -pub(crate) fn derive_best_scan_kind( - requested_scan_kind: ScanKind, - execution: &Execution, - root_configuration_dir: &Utf8Path, - working_dir: &Utf8Path, - configuration: &Configuration, -) -> ScanKind { - get_forced_scan_kind(execution, root_configuration_dir, working_dir).unwrap_or({ - let required_minimum_scan_kind = - if configuration.is_root() || configuration.use_ignore_file() { - ScanKind::KnownFiles - } else { - ScanKind::NoScanner - }; - if requested_scan_kind == ScanKind::NoScanner - || (requested_scan_kind == ScanKind::Project && !configuration.is_linter_enabled()) - { - // If we're here, it means we're executing `check`, `lint` or `ci` - // and the linter is disabled or no projects rules have been enabled. - // We scan known files if the configuration is a root or if the VCS integration is enabled - required_minimum_scan_kind - } else { - requested_scan_kind - } - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{TraversalMode, VcsTargeted}; - use biome_configuration::LinterConfiguration; - use biome_configuration::analyzer::RuleSelector; - - #[test] - fn should_return_none_for_lint_command() { - let execution = Execution::new(TraversalMode::Lint { - fix_file_mode: None, - stdin: None, - only: vec![], - skip: vec![RuleSelector::Rule("correctness", "noPrivateImports").into()], - - vcs_targeted: VcsTargeted::default(), - suppress: false, - suppression_reason: None, - skip_parse_errors: false, - }); - - let root_dir = Utf8Path::new("/"); - assert_eq!(get_forced_scan_kind(&execution, root_dir, root_dir), None); - } - - #[test] - fn should_return_known_files_for_format_command() { - let execution = Execution::new(TraversalMode::Format { - skip_parse_errors: false, - write: true, - stdin: None, - vcs_targeted: VcsTargeted::default(), - }); - - let root_dir = Utf8Path::new("/"); - assert_eq!( - get_forced_scan_kind(&execution, root_dir, root_dir), - Some(ScanKind::KnownFiles) - ); - } - - #[test] - fn should_not_scan_project_if_linter_disabled() { - let execution = Execution::new(TraversalMode::Check { - fix_file_mode: None, - stdin: None, - - vcs_targeted: VcsTargeted::default(), - skip_parse_errors: false, - enforce_assist: true, - }); - - let config = Configuration { - linter: Some(LinterConfiguration { - enabled: Some(false.into()), - ..Default::default() - }), - ..Default::default() - }; - - let root_dir = Utf8Path::new("/"); - assert_ne!( - derive_best_scan_kind(ScanKind::Project, &execution, root_dir, root_dir, &config), - ScanKind::Project - ); - } -} diff --git a/crates/biome_cli/src/commands/search.rs b/crates/biome_cli/src/commands/search.rs index ec60de05f1ae..7d41efbec0d1 100644 --- a/crates/biome_cli/src/commands/search.rs +++ b/crates/biome_cli/src/commands/search.rs @@ -1,16 +1,28 @@ +use crate::CliDiagnostic; use crate::cli_options::CliOptions; -use crate::commands::CommandRunner; -use crate::{CliDiagnostic, Execution, TraversalMode}; +use crate::runner::crawler::CrawlerContext; +use crate::runner::diagnostics::{ResultExt, SearchDiagnostic}; +use crate::runner::execution::{AnalyzerSelectors, Execution}; +use crate::runner::impls::commands::traversal::TraversalCommand; +use crate::runner::impls::executions::summary_verb::SummaryVerbExecution; +use crate::runner::process_file::{ + FileStatus, Message, ProcessFile, ProcessStdinFilePayload, WorkspaceFile, +}; use biome_configuration::vcs::VcsConfiguration; use biome_configuration::{Configuration, FilesConfiguration}; -use biome_console::Console; +use biome_console::{Console, MarkupBuf}; use biome_deserialize::Merge; +use biome_diagnostics::{Category, DiagnosticExt, category}; use biome_fs::FileSystem; -use biome_grit_patterns::GritTargetLanguage; -use biome_service::workspace::ParsePatternParams; +use biome_grit_patterns::{GritTargetLanguage, JsTargetLanguage}; +use biome_service::workspace::{ + DocumentFileSource, DropPatternParams, FeatureKind, FeatureName, FeaturesBuilder, + FeaturesSupported, ParsePatternParams, PatternId, ScanKind, SupportKind, +}; use biome_service::{Workspace, WorkspaceError}; use camino::Utf8PathBuf; use std::ffi::OsString; +use std::time::Duration; pub(crate) struct SearchCommandPayload { pub(crate) files_configuration: Option, @@ -21,8 +33,179 @@ pub(crate) struct SearchCommandPayload { pub(crate) vcs_configuration: Option, } -impl CommandRunner for SearchCommandPayload { - const COMMAND_NAME: &'static str = "search"; +struct SearchExecution { + /// The GritQL pattern to search for. + /// + /// Note that the search command does not support rewrites. + pattern: PatternId, + + /// The language to query for. + /// + /// Grit queries are specific to the grammar of the language they + /// target, so we currently do not support writing queries that apply + /// to multiple languages at once. + /// + /// If none given, the default language is JavaScript. + language: Option, + + /// An optional tuple. + /// 1. The virtual path to the file + /// 2. The content of the file + stdin_file_path: Option, +} + +impl Execution for SearchExecution { + fn features(&self) -> FeatureName { + FeaturesBuilder::new().with_search().build() + } + + fn can_handle(&self, features: FeaturesSupported) -> bool { + features.supports_search() + } + + fn on_post_crawl(&self, workspace: &dyn Workspace) -> Result<(), WorkspaceError> { + workspace.drop_pattern(DropPatternParams { + pattern: self.pattern.clone(), + }) + } + + fn is_vcs_targeted(&self) -> bool { + false + } + + fn supports_kind(&self, file_features: &FeaturesSupported) -> Option { + Some(file_features.support_kind_for(FeatureKind::Search)) + } + + fn get_stdin_file_path(&self) -> Option<&str> { + self.stdin_file_path.as_deref() + } + + fn as_diagnostic_category(&self) -> &'static Category { + category!("search") + } + + fn is_search(&self) -> bool { + true + } + + fn requires_write_access(&self) -> bool { + false + } + + fn analyzer_selectors(&self) -> AnalyzerSelectors { + AnalyzerSelectors::default() + } + + fn search_language(&self) -> Option { + self.language.clone() + } + + fn search_pattern(&self) -> Option<&PatternId> { + Some(&self.pattern) + } + + fn summary_phrase(&self, files: usize, duration: &Duration) -> MarkupBuf { + SummaryVerbExecution.summary_verb("Searched", files, duration) + } +} + +pub(crate) struct SearchProcessFile; + +impl SearchProcessFile { + fn is_file_compatible_with_pattern( + file_source: &DocumentFileSource, + pattern_language: &GritTargetLanguage, + ) -> bool { + match pattern_language { + GritTargetLanguage::JsTargetLanguage(_) => { + matches!(file_source, DocumentFileSource::Js(_)) + } + GritTargetLanguage::CssTargetLanguage(_) => { + matches!(file_source, DocumentFileSource::Css(_)) + } + } + } +} + +impl ProcessFile for SearchProcessFile { + fn process_file( + ctx: &Ctx, + workspace_file: &mut WorkspaceFile, + _features_supported: &FeaturesSupported, + ) -> Result + where + Ctx: CrawlerContext, + { + let execution = ctx.execution(); + let file_source = DocumentFileSource::from_path(workspace_file.path.as_path(), false); + let pattern_language = execution + .search_language() + .unwrap_or(GritTargetLanguage::JsTargetLanguage(JsTargetLanguage)); + // SAFETY: search_pattern is implemented in this file + let pattern = execution.search_pattern().unwrap(); + + // Ignore files that don't match the pattern's target language + if !Self::is_file_compatible_with_pattern(&file_source, &pattern_language) { + return Ok(FileStatus::Ignored); + } + + let result = workspace_file + .guard() + .search_pattern(pattern) + .with_file_path_and_code(workspace_file.path.to_string(), category!("search"))?; + + let input = workspace_file.input()?; + let file_name = workspace_file.path.to_string(); + let matches_len = result.matches.len(); + + let search_results = Message::Diagnostics { + file_path: file_name, + content: input, + diagnostics: result + .matches + .into_iter() + .map(|mat| SearchDiagnostic.with_file_span(mat)) + .collect(), + skipped_diagnostics: 0, + }; + + Ok(FileStatus::SearchResult(matches_len, search_results)) + } + + fn process_std_in(_payload: ProcessStdinFilePayload) -> Result<(), CliDiagnostic> { + Ok(()) + } +} + +impl TraversalCommand for SearchCommandPayload { + type ProcessFile = SearchProcessFile; + + fn command_name(&self) -> &'static str { + "search" + } + + fn minimal_scan_kind(&self) -> Option { + Some(ScanKind::KnownFiles) + } + fn get_execution( + &self, + _cli_options: &CliOptions, + _console: &mut dyn Console, + workspace: &dyn Workspace, + ) -> Result, CliDiagnostic> { + let pattern = workspace + .parse_pattern(ParsePatternParams { + pattern: self.pattern.clone(), + default_language: self.language.clone().unwrap_or_default(), + })? + .pattern_id; + Ok(Box::new(SearchExecution { + stdin_file_path: self.stdin_file_path.clone(), + language: self.language.clone(), + pattern, + })) + } fn merge_configuration( &mut self, @@ -49,32 +232,4 @@ impl CommandRunner for SearchCommandPayload { ) -> Result, CliDiagnostic> { Ok(self.paths.clone()) } - - fn get_stdin_file_path(&self) -> Option<&str> { - self.stdin_file_path.as_deref() - } - - fn should_write(&self) -> bool { - false - } - - fn get_execution( - &self, - cli_options: &CliOptions, - console: &mut dyn Console, - workspace: &dyn Workspace, - ) -> Result { - let pattern = workspace - .parse_pattern(ParsePatternParams { - pattern: self.pattern.clone(), - default_language: self.language.clone().unwrap_or_default(), - })? - .pattern_id; - Ok(Execution::new(TraversalMode::Search { - pattern, - language: self.language.clone(), - stdin: self.get_stdin(console)?, - }) - .set_report(cli_options)) - } } diff --git a/crates/biome_cli/src/diagnostics.rs b/crates/biome_cli/src/diagnostics.rs index 4e8a7bb9a600..3547a58f0f61 100644 --- a/crates/biome_cli/src/diagnostics.rs +++ b/crates/biome_cli/src/diagnostics.rs @@ -375,6 +375,19 @@ impl CliDiagnostic { }) } + /// Emitted when errors were emitted while running `format` command + pub fn format_error(category: &'static Category) -> Self { + Self::CheckError(CheckError { + category, + message: MessageAndDescription::from( + markup! { + "Some ""errors"" were emitted while ""running formatter""." + } + .to_owned(), + ), + }) + } + /// Emitted when warnings were emitted while running `check` command pub fn check_warnings(category: &'static Category) -> Self { Self::CheckError(CheckError { diff --git a/crates/biome_cli/src/execute/migrate.rs b/crates/biome_cli/src/execute/migrate.rs index 166e1a280ca1..3ea49ef060e8 100644 --- a/crates/biome_cli/src/execute/migrate.rs +++ b/crates/biome_cli/src/execute/migrate.rs @@ -1,6 +1,6 @@ use crate::commands::MigrateSubCommand; use crate::diagnostics::MigrationDiagnostic; -use crate::execute::diagnostics::{ContentDiffAdvice, MigrateDiffDiagnostic}; +use crate::runner::diagnostics::{ContentDiffAdvice, MigrateDiffDiagnostic}; use crate::{CliDiagnostic, CliSession}; use biome_analyze::AnalysisFilter; use biome_configuration::Configuration; diff --git a/crates/biome_cli/src/execute/mod.rs b/crates/biome_cli/src/execute/mod.rs index dfeeabe31199..9a79c13533e3 100644 --- a/crates/biome_cli/src/execute/mod.rs +++ b/crates/biome_cli/src/execute/mod.rs @@ -1,760 +1 @@ -mod diagnostics; -mod migrate; -mod process_file; -mod std_in; -pub(crate) mod traverse; - -use crate::cli_options::{CliOptions, CliReporter}; -use crate::commands::MigrateSubCommand; -use crate::diagnostics::ReportDiagnostic; -use crate::execute::migrate::MigratePayload; -use crate::execute::traverse::{TraverseResult, traverse}; -use crate::reporter::checkstyle::CheckstyleReporter; -use crate::reporter::github::{GithubReporter, GithubReporterVisitor}; -use crate::reporter::gitlab::{GitLabReporter, GitLabReporterVisitor}; -use crate::reporter::json::{JsonReporter, JsonReporterVisitor}; -use crate::reporter::junit::{JunitReporter, JunitReporterVisitor}; -use crate::reporter::rdjson::{RdJsonReporter, RdJsonReporterVisitor}; -use crate::reporter::summary::{SummaryReporter, SummaryReporterVisitor}; -use crate::reporter::terminal::{ConsoleReporter, ConsoleReporterVisitor}; -use crate::{ - CliDiagnostic, CliSession, DiagnosticsPayload, Reporter, TEMPORARY_INTERNAL_REPORTER_FILE, -}; -use biome_configuration::analyzer::AnalyzerSelector; -use biome_console::{ConsoleExt, markup}; -use biome_diagnostics::{Category, category}; -use biome_diagnostics::{Resource, SerdeJsonError}; -use biome_fs::BiomePath; -use biome_grit_patterns::GritTargetLanguage; -use biome_service::projects::ProjectKey; -use biome_service::workspace::{ - CloseFileParams, FeatureName, FeaturesBuilder, FileContent, FixFileMode, FormatFileParams, - OpenFileParams, PatternId, ScanKind, -}; -use camino::{Utf8Path, Utf8PathBuf}; -use std::cmp::Ordering; -use std::fmt::{Display, Formatter}; -use std::time::Duration; -use tracing::{info, instrument}; - -/// Useful information during the traversal of files and virtual content -#[derive(Debug, Clone)] -pub struct Execution { - /// How the information should be collected and reported - report_mode: ReportMode, - - /// The modality of execution of the traversal - traversal_mode: TraversalMode, - - /// The maximum number of diagnostics that can be printed in console - max_diagnostics: u32, -} - -#[derive(Debug, Clone, Copy)] -pub enum ExecutionEnvironment { - GitHub, -} - -/// A type that holds the information to execute the CLI via `stdin -#[derive(Debug, Clone)] -pub struct Stdin( - /// The virtual path to the file - Utf8PathBuf, - /// The content of the file - String, -); - -impl Stdin { - pub fn as_path(&self) -> &Utf8Path { - self.0.as_path() - } - - fn as_content(&self) -> &str { - self.1.as_str() - } -} - -impl From<(Utf8PathBuf, String)> for Stdin { - fn from((path, content): (Utf8PathBuf, String)) -> Self { - Self(path, content) - } -} - -#[derive(Default, Debug, Clone)] -pub struct VcsTargeted { - pub staged: bool, - pub changed: bool, -} - -impl From<(bool, bool)> for VcsTargeted { - fn from((staged, changed): (bool, bool)) -> Self { - Self { staged, changed } - } -} - -#[derive(Debug, Clone)] -pub enum TraversalMode { - /// This mode is enabled when running the command `biome check` - Check { - /// The type of fixes that should be applied when analyzing a file. - /// - /// It's [None] if the `check` command is called without `--apply` or `--apply-suggested` - /// arguments. - fix_file_mode: Option, - /// An optional tuple. - /// 1. The virtual path to the file - /// 2. The content of the file - stdin: Option, - /// A flag to know vcs integrated options such as `--staged` or `--changed` are enabled - vcs_targeted: VcsTargeted, - - /// Whether assist diagnostics should be promoted to error, and fail the CLI - enforce_assist: bool, - - /// It skips parse errors - skip_parse_errors: bool, - }, - /// This mode is enabled when running the command `biome lint` - Lint { - /// The type of fixes that should be applied when analyzing a file. - /// - /// It's [None] if the `lint` command is called without `--apply` or `--apply-suggested` - /// arguments. - fix_file_mode: Option, - /// An optional tuple. - /// 1. The virtual path to the file - /// 2. The content of the file - stdin: Option, - /// Run only the given rule or group of rules. - /// If the severity level of a rule is `off`, - /// then the severity level of the rule is set to `error` if it is a recommended rule or `warn` otherwise. - only: Vec, - /// Skip the given rule or group of rules by setting the severity level of the rules to `off`. - /// This option takes precedence over `--only`. - skip: Vec, - /// A flag to know vcs integrated options such as `--staged` or `--changed` are enabled - vcs_targeted: VcsTargeted, - /// Suppress existing diagnostics with a `// biome-ignore` comment - suppress: bool, - /// Explanation for suppressing diagnostics with `--suppress` and `--reason` - suppression_reason: Option, - - /// It skips parse errors - skip_parse_errors: bool, - }, - /// This mode is enabled when running the command `biome ci` - CI { - /// Whether the CI is running in a specific environment, e.g. GitHub, GitLab, etc. - environment: Option, - /// A flag to know vcs integrated options such as `--staged` or `--changed` are enabled - vcs_targeted: VcsTargeted, - /// Whether assist diagnostics should be promoted to error, and fail the CLI - enforce_assist: bool, - /// It skips parse errors - skip_parse_errors: bool, - }, - /// This mode is enabled when running the command `biome format` - Format { - /// It skips parse errors - skip_parse_errors: bool, - /// It writes the new content on file - write: bool, - /// An optional tuple. - /// 1. The virtual path to the file - /// 2. The content of the file - stdin: Option, - /// A flag to know vcs integrated options such as `--staged` or `--changed` are enabled - vcs_targeted: VcsTargeted, - }, - /// This mode is enabled when running the command `biome migrate` - Migrate { - /// Write result to disk - write: bool, - /// The path to `biome.json` - configuration_file_path: Utf8PathBuf, - sub_command: Option, - }, - /// This mode is enabled when running the command `biome search` - Search { - /// The GritQL pattern to search for. - /// - /// Note that the search command does not support rewrites. - pattern: PatternId, - - /// The language to query for. - /// - /// Grit queries are specific to the grammar of the language they - /// target, so we currently do not support writing queries that apply - /// to multiple languages at once. - /// - /// If none given, the default language is JavaScript. - language: Option, - - /// An optional tuple. - /// 1. The virtual path to the file - /// 2. The content of the file - stdin: Option, - }, -} - -impl Display for TraversalMode { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::Check { .. } => write!(f, "check"), - Self::CI { .. } => write!(f, "ci"), - Self::Format { .. } => write!(f, "format"), - Self::Migrate { .. } => write!(f, "migrate"), - Self::Lint { .. } => write!(f, "lint"), - Self::Search { .. } => write!(f, "search"), - } - } -} - -impl TraversalMode { - /// It returns the best [ScanKind] variant based on the [TraversalMode] - pub fn to_scan_kind(&self) -> ScanKind { - match self { - Self::CI { .. } => ScanKind::Project, - Self::Format { stdin, .. } => { - if stdin.is_none() { - ScanKind::KnownFiles - } else { - ScanKind::NoScanner - } - } - Self::Check { stdin, .. } | Self::Lint { stdin, .. } | Self::Search { stdin, .. } => { - if stdin.is_none() { - ScanKind::Project - } else { - ScanKind::NoScanner - } - } - Self::Migrate { .. } => ScanKind::NoScanner, - } - } -} - -/// Tells to the execution of the traversal how the information should be reported -#[derive(Copy, Clone, Debug)] -pub enum ReportMode { - /// Reports information straight to the console, it's the default mode - Terminal { with_summary: bool }, - /// Reports information in JSON format - Json { pretty: bool }, - /// Reports information for GitHub - GitHub, - /// JUnit output - /// Ref: https://github.com/testmoapp/junitxml?tab=readme-ov-file#basic-junit-xml-structure - Junit, - /// Reports information in the [GitLab Code Quality](https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool) format. - GitLab, - /// Reports diagnostics in [Checkstyle XML format](https://checkstyle.org/). - Checkstyle, - /// Reports information in [reviewdog JSON format](https://deepwiki.com/reviewdog/reviewdog/3.2-reviewdog-diagnostic-format) - RdJson, -} - -impl Default for ReportMode { - fn default() -> Self { - Self::Terminal { - with_summary: false, - } - } -} - -impl From for ReportMode { - fn from(value: CliReporter) -> Self { - match value { - CliReporter::Default => Self::Terminal { - with_summary: false, - }, - CliReporter::Summary => Self::Terminal { with_summary: true }, - CliReporter::Json => Self::Json { pretty: false }, - CliReporter::JsonPretty => Self::Json { pretty: true }, - CliReporter::GitHub => Self::GitHub, - CliReporter::Junit => Self::Junit, - CliReporter::GitLab => Self::GitLab {}, - CliReporter::Checkstyle => Self::Checkstyle, - CliReporter::RdJson => Self::RdJson, - } - } -} - -impl Execution { - pub(crate) fn new(mode: TraversalMode) -> Self { - Self { - report_mode: ReportMode::default(), - traversal_mode: mode, - max_diagnostics: 20, - } - } - - pub(crate) fn new_ci( - vcs_targeted: VcsTargeted, - enforce_assist: bool, - skip_parse_errors: bool, - ) -> Self { - // Ref: https://docs.github.com/actions/learn-github-actions/variables#default-environment-variables - let is_github = std::env::var("GITHUB_ACTIONS") - .ok() - .is_some_and(|value| value == "true"); - - Self { - report_mode: ReportMode::default(), - traversal_mode: TraversalMode::CI { - environment: if is_github { - Some(ExecutionEnvironment::GitHub) - } else { - None - }, - vcs_targeted, - enforce_assist, - skip_parse_errors, - }, - max_diagnostics: 20, - } - } - - /// It sets the reporting mode by reading the [CliOptions] - pub(crate) fn set_report(mut self, cli_options: &CliOptions) -> Self { - self.report_mode = cli_options.reporter.clone().into(); - self - } - - pub(crate) fn traversal_mode(&self) -> &TraversalMode { - &self.traversal_mode - } - - pub(crate) fn get_max_diagnostics(&self) -> u32 { - self.max_diagnostics - } - - /// `true` only when running the traversal in [TraversalMode::Check] and `should_fix` is `true` - pub(crate) fn as_fix_file_mode(&self) -> Option<&FixFileMode> { - match &self.traversal_mode { - TraversalMode::Check { fix_file_mode, .. } - | TraversalMode::Lint { fix_file_mode, .. } => fix_file_mode.as_ref(), - TraversalMode::Format { .. } - | TraversalMode::CI { .. } - | TraversalMode::Migrate { .. } - | TraversalMode::Search { .. } => None, - } - } - - pub(crate) fn as_diagnostic_category(&self) -> &'static Category { - match self.traversal_mode { - TraversalMode::Check { .. } => category!("check"), - TraversalMode::Lint { .. } => category!("lint"), - TraversalMode::CI { .. } => category!("ci"), - TraversalMode::Format { .. } => category!("format"), - TraversalMode::Migrate { .. } => category!("migrate"), - TraversalMode::Search { .. } => category!("search"), - } - } - - pub(crate) const fn is_ci(&self) -> bool { - matches!(self.traversal_mode, TraversalMode::CI { .. }) - } - - pub(crate) const fn is_search(&self) -> bool { - matches!(self.traversal_mode, TraversalMode::Search { .. }) - } - - pub(crate) const fn is_check(&self) -> bool { - matches!(self.traversal_mode, TraversalMode::Check { .. }) - } - - pub(crate) const fn is_lint(&self) -> bool { - matches!(self.traversal_mode, TraversalMode::Lint { .. }) - } - - #[instrument(level = "debug", skip(self), fields(result))] - pub(crate) fn is_safe_fixes_enabled(&self) -> bool { - let result = match self.traversal_mode { - TraversalMode::Check { fix_file_mode, .. } => { - fix_file_mode == Some(FixFileMode::SafeFixes) - } - _ => false, - }; - tracing::Span::current().record("result", result); - result - } - - #[instrument(level = "debug", skip(self), fields(result))] - pub(crate) fn is_safe_and_unsafe_fixes_enabled(&self) -> bool { - let result = match self.traversal_mode { - TraversalMode::Check { fix_file_mode, .. } => { - fix_file_mode == Some(FixFileMode::SafeAndUnsafeFixes) - } - _ => false, - }; - - tracing::Span::current().record("result", result); - result - } - - pub(crate) const fn is_format(&self) -> bool { - matches!(self.traversal_mode, TraversalMode::Format { .. }) - } - - pub(crate) const fn is_format_write(&self) -> bool { - if let TraversalMode::Format { write, .. } = self.traversal_mode { - write - } else { - false - } - } - - /// Whether the traversal mode requires write access to files - pub(crate) const fn requires_write_access(&self) -> bool { - match self.traversal_mode { - TraversalMode::Check { fix_file_mode, .. } - | TraversalMode::Lint { fix_file_mode, .. } => fix_file_mode.is_some(), - TraversalMode::CI { .. } | TraversalMode::Search { .. } => false, - TraversalMode::Format { write, .. } | TraversalMode::Migrate { write, .. } => write, - } - } - - pub(crate) fn as_stdin_file(&self) -> Option<&Stdin> { - match &self.traversal_mode { - TraversalMode::Format { stdin, .. } - | TraversalMode::Lint { stdin, .. } - | TraversalMode::Check { stdin, .. } - | TraversalMode::Search { stdin, .. } => stdin.as_ref(), - TraversalMode::CI { .. } | TraversalMode::Migrate { .. } => None, - } - } - - pub(crate) fn is_vcs_targeted(&self) -> bool { - match &self.traversal_mode { - TraversalMode::Check { vcs_targeted, .. } - | TraversalMode::Lint { vcs_targeted, .. } - | TraversalMode::Format { vcs_targeted, .. } - | TraversalMode::CI { vcs_targeted, .. } => vcs_targeted.staged || vcs_targeted.changed, - TraversalMode::Migrate { .. } | TraversalMode::Search { .. } => false, - } - } - - /// Returns [true] if the user used the `--write`/`--fix` option - pub(crate) fn is_write(&self) -> bool { - match self.traversal_mode { - TraversalMode::Check { fix_file_mode, .. } => fix_file_mode.is_some(), - TraversalMode::Lint { fix_file_mode, .. } => fix_file_mode.is_some(), - TraversalMode::CI { .. } => false, - TraversalMode::Format { write, .. } => write, - TraversalMode::Migrate { write, .. } => write, - TraversalMode::Search { .. } => false, - } - } - - pub fn new_format(vcs_targeted: VcsTargeted) -> Self { - Self { - traversal_mode: TraversalMode::Format { - skip_parse_errors: false, - write: false, - stdin: None, - vcs_targeted, - }, - report_mode: ReportMode::default(), - max_diagnostics: 0, - } - } - - pub fn report_mode(&self) -> &ReportMode { - &self.report_mode - } - pub(crate) fn to_feature(&self) -> FeatureName { - match self.traversal_mode { - TraversalMode::Format { .. } => FeaturesBuilder::new().with_formatter().build(), - TraversalMode::Lint { .. } => FeaturesBuilder::new().with_linter().build(), - TraversalMode::Check { .. } | TraversalMode::CI { .. } => FeaturesBuilder::new() - .with_formatter() - .with_linter() - .with_assist() - .build(), - TraversalMode::Migrate { .. } => FeatureName::empty(), - TraversalMode::Search { .. } => FeaturesBuilder::new().with_search().build(), - } - } - - #[instrument(level = "debug", skip(self), fields(result))] - pub(crate) fn should_write(&self) -> bool { - let result = match self.traversal_mode { - TraversalMode::Format { write, .. } => write, - - _ => self.is_safe_fixes_enabled() || self.is_safe_and_unsafe_fixes_enabled(), - }; - tracing::Span::current().record("result", result); - result - } - - #[instrument(level = "debug", skip(self), fields(result))] - pub(crate) fn should_skip_parse_errors(&self) -> bool { - let result = match self.traversal_mode { - TraversalMode::Format { - skip_parse_errors, .. - } - | TraversalMode::Check { - skip_parse_errors, .. - } - | TraversalMode::Lint { - skip_parse_errors, .. - } - | TraversalMode::CI { - skip_parse_errors, .. - } => skip_parse_errors, - - _ => false, - }; - tracing::Span::current().record("result", result); - - result - } - - pub(crate) fn should_enforce_assist(&self) -> bool { - match self.traversal_mode { - TraversalMode::CI { enforce_assist, .. } => enforce_assist, - TraversalMode::Check { enforce_assist, .. } => enforce_assist, - _ => false, - } - } -} - -/// Based on the [mode](TraversalMode), the function might launch a traversal of the file system -/// or handles the stdin file. -pub fn execute_mode( - mut execution: Execution, - mut session: CliSession, - cli_options: &CliOptions, - paths: Vec, - scanner_duration: Option, - nested_configuration_files: Vec, - project_key: ProjectKey, -) -> Result<(), CliDiagnostic> { - // If a custom reporter was provided, let's lift the limit so users can see all of them - execution.max_diagnostics = if cli_options.reporter.is_default() { - cli_options.max_diagnostics.into() - } else { - info!( - "Removing the limit of --max-diagnostics, because of a reporter different from the default one: {}", - cli_options.reporter - ); - u32::MAX - }; - - // migrate command doesn't do any traversal. - if let TraversalMode::Migrate { - write, - configuration_file_path, - sub_command, - } = execution.traversal_mode - { - let payload = MigratePayload { - session, - project_key, - write, - configuration_file_path, - sub_command, - nested_configuration_files, - }; - return migrate::run(payload); - } - - // don't do any traversal if there's some content coming from stdin - if let Some(stdin) = execution.as_stdin_file() { - let biome_path = BiomePath::new(stdin.as_path()); - return std_in::run( - session, - project_key, - &execution, - biome_path, - stdin.as_content(), - cli_options, - ); - } - - let TraverseResult { - mut summary, - evaluated_paths, - mut diagnostics, - } = traverse( - &execution, - &mut session, - project_key, - cli_options, - paths.clone(), - )?; - diagnostics.sort_unstable_by(|a, b| match a.severity().cmp(&b.severity()) { - Ordering::Equal => { - let a = a.location(); - let b = b.location(); - match (a.resource, b.resource) { - (Some(Resource::File(a)), Some(Resource::File(b))) => a.cmp(b), - (Some(Resource::File(_)), None) => Ordering::Greater, - (None, Some(Resource::File(_))) => Ordering::Less, - _ => Ordering::Equal, - } - } - result => result, - }); - // We join the duration of the scanning with the duration of the traverse. - summary.scanner_duration = scanner_duration; - let console = session.app.console; - let workspace = &*session.app.workspace; - let fs = workspace.fs(); - let errors = summary.errors; - let skipped = summary.skipped; - let processed = summary.changed + summary.unchanged; - let should_exit_on_warnings = summary.warnings > 0 && cli_options.error_on_warnings; - let diagnostics_payload = DiagnosticsPayload { - diagnostic_level: cli_options.diagnostic_level, - diagnostics, - max_diagnostics: cli_options.max_diagnostics, - }; - - match execution.report_mode { - ReportMode::Terminal { with_summary } => { - if with_summary { - let reporter = SummaryReporter { - summary, - diagnostics_payload, - execution: execution.clone(), - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - evaluated_paths, - }; - reporter.write(&mut SummaryReporterVisitor(console))?; - } else { - let reporter = ConsoleReporter { - summary, - diagnostics_payload, - execution: execution.clone(), - evaluated_paths, - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - }; - reporter.write(&mut ConsoleReporterVisitor(console))?; - } - } - ReportMode::Json { pretty } => { - console.error(markup! { - "The ""--json"" option is ""unstable/experimental"" and its output might change between patches/minor releases." - }); - let reporter = JsonReporter { - summary, - diagnostics: diagnostics_payload, - execution: execution.clone(), - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - }; - let mut buffer = JsonReporterVisitor::new(summary); - reporter.write(&mut buffer)?; - if pretty { - let content = serde_json::to_string(&buffer).map_err(|error| { - CliDiagnostic::Report(ReportDiagnostic::Serialization(SerdeJsonError::from( - error, - ))) - })?; - let report_file = BiomePath::new(TEMPORARY_INTERNAL_REPORTER_FILE); - session.app.workspace.open_file(OpenFileParams { - project_key, - content: FileContent::from_client(content), - path: report_file.clone(), - document_file_source: None, - persist_node_cache: false, - inline_config: None, - })?; - let code = session.app.workspace.format_file(FormatFileParams { - project_key, - path: report_file.clone(), - inline_config: None, - })?; - console.log(markup! { - {code.as_code()} - }); - session.app.workspace.close_file(CloseFileParams { - project_key, - path: report_file, - })?; - } else { - console.log(markup! { - {buffer} - }); - } - } - ReportMode::GitHub => { - let reporter = GithubReporter { - diagnostics_payload, - execution: execution.clone(), - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - }; - reporter.write(&mut GithubReporterVisitor(console))?; - } - ReportMode::GitLab => { - let reporter = GitLabReporter { - diagnostics: diagnostics_payload, - execution: execution.clone(), - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - }; - reporter.write(&mut GitLabReporterVisitor::new( - console, - session.app.workspace.fs().working_directory(), - ))?; - } - ReportMode::Junit => { - let reporter = JunitReporter { - summary, - diagnostics_payload, - execution: execution.clone(), - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - }; - reporter.write(&mut JunitReporterVisitor::new(console))?; - } - ReportMode::Checkstyle => { - let reporter = CheckstyleReporter { - summary, - diagnostics_payload, - execution: execution.clone(), - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - }; - reporter - .write(&mut crate::reporter::checkstyle::CheckstyleReporterVisitor::new(console))?; - } - ReportMode::RdJson => { - let reporter = RdJsonReporter { - diagnostics_payload, - execution: execution.clone(), - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - }; - reporter.write(&mut RdJsonReporterVisitor(console))?; - } - } - - // Processing emitted error diagnostics, exit with a non-zero code - if processed.saturating_sub(skipped) == 0 && !cli_options.no_errors_on_unmatched { - Err(CliDiagnostic::no_files_processed( - execution.as_diagnostic_category(), - paths, - )) - } else if errors > 0 || should_exit_on_warnings { - let category = execution.as_diagnostic_category(); - if should_exit_on_warnings { - if execution.is_safe_fixes_enabled() { - Err(CliDiagnostic::apply_warnings(category)) - } else { - Err(CliDiagnostic::check_warnings(category)) - } - } else if execution.is_safe_fixes_enabled() { - Err(CliDiagnostic::apply_error(category)) - } else { - Err(CliDiagnostic::check_error(category)) - } - } else { - Ok(()) - } -} +pub(crate) mod migrate; diff --git a/crates/biome_cli/src/execute/process_file.rs b/crates/biome_cli/src/execute/process_file.rs deleted file mode 100644 index f7dec6b76fff..000000000000 --- a/crates/biome_cli/src/execute/process_file.rs +++ /dev/null @@ -1,219 +0,0 @@ -mod check; -mod format; -mod lint_and_assist; -mod search; -pub(crate) mod workspace_file; - -use crate::execute::TraversalMode; -use crate::execute::diagnostics::{ResultExt, UnhandledDiagnostic}; -use crate::execute::traverse::TraversalOptions; -use biome_analyze::RuleCategoriesBuilder; -use biome_diagnostics::{DiagnosticExt, DiagnosticTags, Error, category}; -use biome_fs::BiomePath; -use biome_service::workspace::{ - DocumentFileSource, FeatureKind, FileFeaturesResult, SupportKind, SupportsFeatureParams, -}; -use check::check_file; -use format::format; -use lint_and_assist::lint_and_assist; -use search::search; -use std::marker::PhantomData; -use std::ops::Deref; - -#[derive(Debug)] -pub(crate) enum FileStatus { - /// File changed and it was a success - Changed, - /// File unchanged, and it was a success - Unchanged, - /// While handling the file, something happened - Message(Message), - /// A match was found while searching a file - SearchResult(usize, Message), - /// File ignored, it should not be count as "handled" - Ignored, - /// Files that belong to other tools and shouldn't be touched - Protected(String), -} - -impl FileStatus { - pub const fn is_changed(&self) -> bool { - matches!(self, Self::Changed) - } -} - -/// Wrapper type for messages that can be printed during the traversal process -#[derive(Debug)] -pub(crate) enum Message { - SkippedFixes { - /// Suggested fixes skipped during the lint traversal - skipped_suggested_fixes: u32, - }, - Failure, - Error(Error), - Diagnostics { - file_path: String, - content: String, - diagnostics: Vec, - skipped_diagnostics: u32, - }, - Diff { - file_name: String, - old: String, - new: String, - diff_kind: DiffKind, - }, -} - -impl Message { - pub(crate) const fn is_failure(&self) -> bool { - matches!(self, Self::Failure) - } -} - -#[derive(Debug)] -pub(crate) enum DiffKind { - Format, -} - -impl From for Message -where - Error: From, - D: std::fmt::Debug, -{ - fn from(err: D) -> Self { - Self::Error(Error::from(err)) - } -} - -/// The return type for [process_file], with the following semantics: -/// - `Ok(Success)` means the operation was successful (the file is added to -/// the `processed` counter) -/// - `Ok(Message(_))` means the operation was successful but a message still -/// needs to be printed (eg. the diff when not in CI or write mode) -/// - `Ok(Ignored)` means the file was ignored (the file is not added to the -/// `processed` or `skipped` counters) -/// - `Err(_)` means the operation failed and the file should be added to the -/// `skipped` counter -pub(crate) type FileResult = Result; - -/// Data structure that allows to pass [TraversalOptions] to multiple consumers, bypassing the -/// compiler constraints set by the lifetimes of the [TraversalOptions] -pub(crate) struct SharedTraversalOptions<'ctx, 'app> { - inner: &'app TraversalOptions<'ctx, 'app>, - _p: PhantomData<&'app ()>, -} - -impl<'ctx, 'app> SharedTraversalOptions<'ctx, 'app> { - fn new(t: &'app TraversalOptions<'ctx, 'app>) -> Self { - Self { - _p: PhantomData, - inner: t, - } - } -} - -impl<'ctx, 'app> Deref for SharedTraversalOptions<'ctx, 'app> { - type Target = TraversalOptions<'ctx, 'app>; - - fn deref(&self) -> &Self::Target { - self.inner - } -} - -/// This function performs the actual processing: it reads the file from disk -/// and parse it; analyze and / or format it; then it either fails if error -/// diagnostics were emitted, or compare the formatted code with the original -/// content of the file and emit a diff or write the new content to the disk if -/// write mode is enabled -pub(crate) fn process_file(ctx: &TraversalOptions, biome_path: &BiomePath) -> FileResult { - let _ = tracing::trace_span!("process_file", path = ?biome_path).entered(); - let FileFeaturesResult { - features_supported: file_features, - } = ctx - .workspace - .file_features(SupportsFeatureParams { - project_key: ctx.project_key, - path: biome_path.clone(), - features: ctx.execution.to_feature(), - inline_config: None, - }) - .with_file_path_and_code_and_tags( - biome_path.to_string(), - category!("files/missingHandler"), - DiagnosticTags::VERBOSE, - )?; - - // first we stop if there are some files that don't have ALL features enabled, e.g. images, fonts, etc. - if file_features.is_ignored() || file_features.is_not_enabled() { - return Ok(FileStatus::Ignored); - } else if file_features.is_not_supported() || !DocumentFileSource::can_read(biome_path) { - return Err(Message::from( - UnhandledDiagnostic.with_file_path(biome_path.to_string()), - )); - } - - // then we pick the specific features for this file - let unsupported_reason = match ctx.execution.traversal_mode() { - TraversalMode::Check { .. } | TraversalMode::CI { .. } => file_features - .support_kind_if_not_enabled(FeatureKind::Lint) - .and(file_features.support_kind_if_not_enabled(FeatureKind::Format)) - .and(file_features.support_kind_if_not_enabled(FeatureKind::Assist)), - TraversalMode::Format { .. } => Some(file_features.support_kind_for(FeatureKind::Format)), - TraversalMode::Lint { .. } => Some(file_features.support_kind_for(FeatureKind::Lint)), - TraversalMode::Migrate { .. } => None, - TraversalMode::Search { .. } => Some(file_features.support_kind_for(FeatureKind::Search)), - }; - - if let Some(reason) = unsupported_reason { - match reason { - SupportKind::FileNotSupported => { - return Err(Message::from( - UnhandledDiagnostic.with_file_path(biome_path.to_string()), - )); - } - SupportKind::FeatureNotEnabled | SupportKind::Ignored => { - return Ok(FileStatus::Ignored); - } - SupportKind::Protected => { - return Ok(FileStatus::Protected(biome_path.to_string())); - } - SupportKind::Supported => {} - }; - } - - let shared_context = &SharedTraversalOptions::new(ctx); - - match ctx.execution.traversal_mode { - TraversalMode::Lint { - ref suppression_reason, - suppress, - .. - } => { - let categories = RuleCategoriesBuilder::default().with_lint().with_syntax(); - // the unsupported case should be handled already at this point - lint_and_assist( - shared_context, - biome_path.clone(), - suppress, - suppression_reason.as_deref(), - categories.build(), - &file_features, - ) - } - TraversalMode::Format { .. } => { - // the unsupported case should be handled already at this point - format(shared_context, biome_path.clone(), &file_features) - } - TraversalMode::Check { .. } | TraversalMode::CI { .. } => { - check_file(shared_context, biome_path.clone(), &file_features) - } - TraversalMode::Migrate { .. } => { - unreachable!("The migration should not be called for this file") - } - TraversalMode::Search { ref pattern, .. } => { - // the unsupported case should be handled already at this point - search(shared_context, biome_path.clone(), pattern) - } - } -} diff --git a/crates/biome_cli/src/execute/process_file/check.rs b/crates/biome_cli/src/execute/process_file/check.rs deleted file mode 100644 index ff69d8162bb2..000000000000 --- a/crates/biome_cli/src/execute/process_file/check.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::execute::process_file::format::format_with_guard; -use crate::execute::process_file::lint_and_assist::analyze_with_guard; -use crate::execute::process_file::workspace_file::WorkspaceFile; -use crate::execute::process_file::{FileResult, FileStatus, Message, SharedTraversalOptions}; -use biome_analyze::RuleCategoriesBuilder; -use biome_diagnostics::DiagnosticExt; -use biome_fs::{BiomePath, TraversalContext}; -use biome_service::diagnostics::FileTooLarge; -use biome_service::workspace::FeaturesSupported; - -pub(crate) fn check_file<'ctx>( - ctx: &'ctx SharedTraversalOptions<'ctx, '_>, - path: BiomePath, - file_features: &FeaturesSupported, -) -> FileResult { - let mut has_failures = false; - let mut workspace_file = WorkspaceFile::new(ctx, path)?; - let result = workspace_file.guard().check_file_size()?; - if result.is_too_large() { - ctx.push_diagnostic( - FileTooLarge::from(result) - .with_file_path(workspace_file.path.to_string()) - .with_category(ctx.execution.as_diagnostic_category()), - ); - return Ok(FileStatus::Ignored); - } - let _ = tracing::info_span!("Check ", path =? workspace_file.path).entered(); - - let mut categories = RuleCategoriesBuilder::default().with_syntax(); - if file_features.supports_lint() { - categories = categories.with_lint(); - } - if file_features.supports_assist() { - categories = categories.with_assist(); - } - - let analyzer_result = analyze_with_guard( - ctx, - &mut workspace_file, - false, - None, - categories.build(), - file_features, - ); - - let mut changed = false; - // To reduce duplication of the same error on format and lint_and_assist - let mut skipped_parse_error = false; - - match analyzer_result { - Ok(status) => { - if matches!(status, FileStatus::Ignored) && ctx.execution.should_skip_parse_errors() { - skipped_parse_error = true; - } - - if status.is_changed() { - changed = true - } - if let FileStatus::Message(msg) = status { - if msg.is_failure() { - has_failures = true; - } - ctx.push_message(msg); - } - } - Err(err) => { - ctx.push_message(err); - has_failures = true; - } - } - - if file_features.supports_format() { - if ctx.execution.should_skip_parse_errors() && skipped_parse_error { - // Parse errors are already skipped during the analyze phase, so no need to do it here. - } else { - let format_result = format_with_guard(ctx, &mut workspace_file, file_features); - match format_result { - Ok(status) => { - if status.is_changed() { - changed = true - } - if let FileStatus::Message(msg) = status { - if msg.is_failure() { - has_failures = true; - } - ctx.push_message(msg); - } - } - Err(err) => { - ctx.push_message(err); - has_failures = true; - } - } - } - } - - if has_failures { - Ok(FileStatus::Message(Message::Failure)) - } else if changed { - Ok(FileStatus::Changed) - } else { - Ok(FileStatus::Unchanged) - } -} diff --git a/crates/biome_cli/src/execute/process_file/format.rs b/crates/biome_cli/src/execute/process_file/format.rs deleted file mode 100644 index c24f5aaf9207..000000000000 --- a/crates/biome_cli/src/execute/process_file/format.rs +++ /dev/null @@ -1,136 +0,0 @@ -use crate::execute::diagnostics::{ResultExt, SkippedDiagnostic}; -use crate::execute::process_file::workspace_file::WorkspaceFile; -use crate::execute::process_file::{ - DiffKind, FileResult, FileStatus, Message, SharedTraversalOptions, -}; -use biome_analyze::RuleCategoriesBuilder; -use biome_diagnostics::{Diagnostic, DiagnosticExt, Error, Severity, category}; -use biome_fs::{BiomePath, TraversalContext}; -use biome_service::diagnostics::FileTooLarge; -use biome_service::file_handlers::astro::AstroFileHandler; -use biome_service::file_handlers::svelte::SvelteFileHandler; -use biome_service::file_handlers::vue::VueFileHandler; -use biome_service::workspace::FeaturesSupported; -use tracing::{debug, instrument}; - -#[instrument(name = "cli_format", level = "debug", skip(ctx, path))] -pub(crate) fn format<'ctx>( - ctx: &'ctx SharedTraversalOptions<'ctx, '_>, - path: BiomePath, - features_supported: &FeaturesSupported, -) -> FileResult { - let mut workspace_file = WorkspaceFile::new(ctx, path)?; - let result = workspace_file.guard().check_file_size()?; - if result.is_too_large() { - ctx.push_diagnostic( - FileTooLarge::from(result) - .with_file_path(workspace_file.path.to_string()) - .with_category(category!("format")), - ); - Ok(FileStatus::Ignored) - } else { - format_with_guard(ctx, &mut workspace_file, features_supported) - } -} - -#[instrument(level = "debug", skip(ctx, workspace_file))] -pub(crate) fn format_with_guard<'ctx>( - ctx: &'ctx SharedTraversalOptions<'ctx, '_>, - workspace_file: &mut WorkspaceFile, - features_supported: &FeaturesSupported, -) -> FileResult { - let diagnostics_result = workspace_file - .guard() - .pull_diagnostics( - RuleCategoriesBuilder::default().with_syntax().build(), - Vec::new(), - Vec::new(), - false, // NOTE: probably to revisit - ) - .with_file_path_and_code(workspace_file.path.to_string(), category!("format"))?; - - let input = workspace_file.input()?; - let should_write = ctx.execution.should_write(); - let skip_parse_errors = ctx.execution.should_skip_parse_errors(); - - tracing::Span::current().record("should_write", tracing::field::display(&should_write)); - tracing::Span::current().record( - "skip_parse_errors", - tracing::field::display(&skip_parse_errors), - ); - - if diagnostics_result.errors > 0 && skip_parse_errors { - ctx.push_message(Message::from( - SkippedDiagnostic.with_file_path(workspace_file.path.to_string()), - )); - return Ok(FileStatus::Ignored); - } - - ctx.push_message(Message::Diagnostics { - file_path: workspace_file.path.to_string(), - content: input.clone(), - diagnostics: diagnostics_result - .diagnostics - .into_iter() - // Formatting is usually blocked by errors, so we want to print only diagnostics that - // Have error severity - .filter_map(|diagnostic| { - if diagnostic.severity() >= Severity::Error { - Some(Error::from(diagnostic)) - } else { - None - } - }) - .collect(), - skipped_diagnostics: diagnostics_result.skipped_diagnostics as u32, - }); - - let printed = workspace_file - .guard() - .format_file() - .with_file_path_and_code(workspace_file.path.to_string(), category!("format"))?; - - let mut output = printed.into_code(); - - if !features_supported.supports_full_html_support() { - match workspace_file.as_extension() { - Some("astro") => { - if output.is_empty() { - return Ok(FileStatus::Unchanged); - } - output = AstroFileHandler::output(input.as_str(), output.as_str()); - } - Some("vue") => { - if output.is_empty() { - return Ok(FileStatus::Unchanged); - } - output = VueFileHandler::output(input.as_str(), output.as_str()); - } - - Some("svelte") => { - if output.is_empty() { - return Ok(FileStatus::Unchanged); - } - output = SvelteFileHandler::output(input.as_str(), output.as_str()); - } - _ => {} - } - } - - debug!("Format output is different from input: {}", output != input); - if output != input { - if should_write { - workspace_file.update_file(output)?; - Ok(FileStatus::Changed) - } else { - Ok(FileStatus::Message(Message::Diff { - file_name: workspace_file.path.to_string(), - old: input, - new: output, - diff_kind: DiffKind::Format, - })) - } - } else { - Ok(FileStatus::Unchanged) - } -} diff --git a/crates/biome_cli/src/execute/process_file/lint_and_assist.rs b/crates/biome_cli/src/execute/process_file/lint_and_assist.rs deleted file mode 100644 index 21aacf62134e..000000000000 --- a/crates/biome_cli/src/execute/process_file/lint_and_assist.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::TraversalMode; -use crate::execute::diagnostics::{ResultExt, SkippedDiagnostic}; -use crate::execute::process_file::workspace_file::WorkspaceFile; -use crate::execute::process_file::{FileResult, FileStatus, Message, SharedTraversalOptions}; -use biome_analyze::RuleCategories; -use biome_diagnostics::{Diagnostic, DiagnosticExt, Error, Severity, category}; -use biome_fs::{BiomePath, TraversalContext}; -use biome_rowan::TextSize; -use biome_service::diagnostics::FileTooLarge; -use biome_service::file_handlers::astro::AstroFileHandler; -use biome_service::file_handlers::svelte::SvelteFileHandler; -use biome_service::file_handlers::vue::VueFileHandler; -use biome_service::workspace::FeaturesSupported; -use tracing::{info, instrument}; - -/// Lints a single file and returns a [FileResult] -#[instrument(level = "debug", name = "cli_lint", skip_all)] -pub(crate) fn lint_and_assist<'ctx>( - ctx: &'ctx SharedTraversalOptions<'ctx, '_>, - path: BiomePath, - suppress: bool, - suppression_reason: Option<&str>, - categories: RuleCategories, - features_supported: &FeaturesSupported, -) -> FileResult { - let mut workspace_file = WorkspaceFile::new(ctx, path)?; - let result = workspace_file.guard().check_file_size()?; - if result.is_too_large() { - ctx.push_diagnostic( - FileTooLarge::from(result) - .with_file_path(workspace_file.path.to_string()) - .with_category(category!("lint")), - ); - Ok(FileStatus::Ignored) - } else { - analyze_with_guard( - ctx, - &mut workspace_file, - suppress, - suppression_reason, - categories, - features_supported, - ) - } -} - -#[instrument(level = "debug", name = "cli_lint_guard", skip_all)] - -pub(crate) fn analyze_with_guard<'ctx>( - ctx: &'ctx SharedTraversalOptions<'ctx, '_>, - workspace_file: &mut WorkspaceFile, - suppress: bool, - suppression_reason: Option<&str>, - categories: RuleCategories, - features_supported: &FeaturesSupported, -) -> FileResult { - let mut input = workspace_file.input()?; - let mut changed = false; - let (only, skip) = - if let TraversalMode::Lint { only, skip, .. } = ctx.execution.traversal_mode() { - (only.clone(), skip.clone()) - } else { - (Vec::new(), Vec::new()) - }; - if let Some(fix_mode) = ctx.execution.as_fix_file_mode() { - let suppression_explanation = if suppress && suppression_reason.is_none() { - "ignored using `--suppress`" - } else { - suppression_reason.unwrap_or("") - }; - - let fix_result = workspace_file - .guard() - .fix_file( - *fix_mode, - false, - categories, - only.clone(), - skip.clone(), - Some(suppression_explanation.to_string()), - ) - .with_file_path_and_code( - workspace_file.path.to_string(), - ctx.execution.as_diagnostic_category(), - )?; - - info!( - "Fix file summary result. Errors {}, skipped fixes {}, actions {}", - fix_result.errors, - fix_result.skipped_suggested_fixes, - fix_result.actions.len() - ); - - ctx.push_message(Message::SkippedFixes { - skipped_suggested_fixes: fix_result.skipped_suggested_fixes, - }); - - let mut output = fix_result.code; - - if !features_supported.supports_full_html_support() { - match workspace_file.as_extension() { - Some("astro") => { - output = AstroFileHandler::output(input.as_str(), output.as_str()); - } - Some("vue") => { - output = VueFileHandler::output(input.as_str(), output.as_str()); - } - Some("svelte") => { - output = SvelteFileHandler::output(input.as_str(), output.as_str()); - } - _ => {} - } - } - if output != input { - changed = true; - workspace_file.update_file(output)?; - input = workspace_file.input()?; - } - } - - let pull_diagnostics_result = workspace_file - .guard() - .pull_diagnostics(categories, only, skip, true) - .with_file_path_and_code( - workspace_file.path.to_string(), - ctx.execution.as_diagnostic_category(), - )?; - - let skip_parse_errors = ctx.execution.should_skip_parse_errors(); - if pull_diagnostics_result.errors > 0 && skip_parse_errors { - ctx.push_message(Message::from( - SkippedDiagnostic.with_file_path(workspace_file.path.to_string()), - )); - return Ok(FileStatus::Ignored); - } - - let no_diagnostics = pull_diagnostics_result.diagnostics.is_empty() - && pull_diagnostics_result.skipped_diagnostics == 0; - - if !no_diagnostics { - let offset = if features_supported.supports_full_html_support() { - None - } else { - match workspace_file.as_extension() { - Some("vue") => VueFileHandler::start(input.as_str()), - Some("astro") => AstroFileHandler::start(input.as_str()), - Some("svelte") => SvelteFileHandler::start(input.as_str()), - _ => None, - } - }; - - ctx.push_message(Message::Diagnostics { - file_path: workspace_file.path.to_string(), - content: input, - diagnostics: pull_diagnostics_result - .diagnostics - .into_iter() - .map(|d| { - if let Some(offset) = offset { - d.with_offset(TextSize::from(offset)) - } else { - d - } - }) - .map(|diagnostic| { - let category = diagnostic.category(); - if let Some(category) = category - && category.name().starts_with("assist/") - && ctx.execution.should_enforce_assist() - { - return diagnostic.with_severity(Severity::Error); - } - Error::from(diagnostic) - }) - .collect(), - skipped_diagnostics: pull_diagnostics_result.skipped_diagnostics as u32, - }); - } - - if changed { - Ok(FileStatus::Changed) - } else { - Ok(FileStatus::Unchanged) - } -} diff --git a/crates/biome_cli/src/execute/process_file/search.rs b/crates/biome_cli/src/execute/process_file/search.rs deleted file mode 100644 index 65122f3c7749..000000000000 --- a/crates/biome_cli/src/execute/process_file/search.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::execute::TraversalMode; -use crate::execute::diagnostics::{ResultExt, SearchDiagnostic}; -use crate::execute::process_file::workspace_file::WorkspaceFile; -use crate::execute::process_file::{FileResult, FileStatus, Message, SharedTraversalOptions}; -use biome_diagnostics::{DiagnosticExt, category}; -use biome_fs::{BiomePath, TraversalContext}; -use biome_grit_patterns::{GritTargetLanguage, JsTargetLanguage}; -use biome_service::diagnostics::FileTooLarge; -use biome_service::file_handlers::DocumentFileSource; -use biome_service::workspace::PatternId; - -pub(crate) fn search<'ctx>( - ctx: &'ctx SharedTraversalOptions<'ctx, '_>, - path: BiomePath, - pattern: &PatternId, -) -> FileResult { - let mut workspace_file = WorkspaceFile::new(ctx, path)?; - let result = workspace_file.guard().check_file_size()?; - if result.is_too_large() { - ctx.push_diagnostic( - FileTooLarge::from(result) - .with_file_path(workspace_file.path.to_string()) - .with_category(category!("search")), - ); - Ok(FileStatus::Ignored) - } else { - search_with_guard(ctx, &mut workspace_file, pattern) - } -} - -pub(crate) fn search_with_guard<'ctx>( - _ctx: &'ctx SharedTraversalOptions<'ctx, '_>, - workspace_file: &mut WorkspaceFile, - pattern: &PatternId, -) -> FileResult { - let _ = tracing::info_span!("Search ", path =? workspace_file.path).entered(); - - let file_source = DocumentFileSource::from_path(workspace_file.path.as_path(), false); - let pattern_language = match &_ctx.execution.traversal_mode { - TraversalMode::Search { - language: Some(pattern_language), - .. - } => pattern_language, - TraversalMode::Search { language: None, .. } => { - // Default to JavaScript when no language is specified - &GritTargetLanguage::JsTargetLanguage(JsTargetLanguage) - } - _ => return Ok(FileStatus::Ignored), // unreachable - }; - - // Ignore files that don't match the pattern's target language - if !is_file_compatible_with_pattern(&file_source, pattern_language) { - return Ok(FileStatus::Ignored); - } - - let result = workspace_file - .guard() - .search_pattern(pattern) - .with_file_path_and_code(workspace_file.path.to_string(), category!("search"))?; - - let input = workspace_file.input()?; - let file_name = workspace_file.path.to_string(); - let matches_len = result.matches.len(); - - let search_results = Message::Diagnostics { - file_path: file_name, - content: input, - diagnostics: result - .matches - .into_iter() - .map(|mat| SearchDiagnostic.with_file_span(mat)) - .collect(), - skipped_diagnostics: 0, - }; - - Ok(FileStatus::SearchResult(matches_len, search_results)) -} - -fn is_file_compatible_with_pattern( - file_source: &DocumentFileSource, - pattern_language: &GritTargetLanguage, -) -> bool { - match pattern_language { - GritTargetLanguage::JsTargetLanguage(_) => matches!(file_source, DocumentFileSource::Js(_)), - GritTargetLanguage::CssTargetLanguage(_) => { - matches!(file_source, DocumentFileSource::Css(_)) - } - } -} diff --git a/crates/biome_cli/src/execute/process_file/workspace_file.rs b/crates/biome_cli/src/execute/process_file/workspace_file.rs deleted file mode 100644 index 8e37f871e7b2..000000000000 --- a/crates/biome_cli/src/execute/process_file/workspace_file.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::execute::diagnostics::{ResultExt, ResultIoExt}; -use crate::execute::process_file::SharedTraversalOptions; -use biome_diagnostics::{Error, category}; -use biome_fs::{BiomePath, File, OpenOptions}; -use biome_service::workspace::{FileContent, FileGuard, OpenFileParams}; -use biome_service::{Workspace, WorkspaceError}; - -/// Small wrapper that holds information and operations around the current processed file -pub(crate) struct WorkspaceFile<'ctx, 'app> { - guard: FileGuard<'app, dyn Workspace + 'ctx>, - file: Box, - pub(crate) path: BiomePath, -} - -impl<'ctx, 'app> WorkspaceFile<'ctx, 'app> { - /// It attempts to read the file from disk, creating a [FileGuard] and - /// saving these information internally - pub(crate) fn new( - ctx: &SharedTraversalOptions<'ctx, 'app>, - path: BiomePath, - ) -> Result { - let open_options = OpenOptions::default() - .read(true) - .write(ctx.execution.requires_write_access()); - let mut file = ctx - .fs - .open_with_options(path.as_path(), open_options) - .with_file_path(path.to_string())?; - - let mut input = String::new(); - file.read_to_string(&mut input) - .with_file_path(path.to_string())?; - - let guard = FileGuard::open( - ctx.workspace, - OpenFileParams { - project_key: ctx.project_key, - document_file_source: None, - path: path.clone(), - content: FileContent::from_client(&input), - persist_node_cache: false, - inline_config: None, - }, - ) - .with_file_path_and_code(path.to_string(), category!("internalError/fs"))?; - - Ok(Self { file, guard, path }) - } - - pub(crate) fn guard(&self) -> &FileGuard<'app, dyn Workspace + 'ctx> { - &self.guard - } - - pub(crate) fn input(&self) -> Result { - self.guard().get_file_content() - } - - pub(crate) fn as_extension(&self) -> Option<&str> { - self.path.extension() - } - - /// It updates the workspace file with `new_content` - pub(crate) fn update_file(&mut self, new_content: impl Into) -> Result<(), Error> { - let new_content = new_content.into(); - - self.file - .set_content(new_content.as_bytes()) - .with_file_path(self.path.to_string())?; - self.guard - .change_file(self.file.file_version(), new_content)?; - Ok(()) - } -} diff --git a/crates/biome_cli/src/execute/std_in.rs b/crates/biome_cli/src/execute/std_in.rs deleted file mode 100644 index a087758a2cbb..000000000000 --- a/crates/biome_cli/src/execute/std_in.rs +++ /dev/null @@ -1,266 +0,0 @@ -//! In here, there are the operations that run via standard input -//! -use crate::cli_options::CliOptions; -use crate::diagnostics::StdinDiagnostic; -use crate::execute::Execution; -use crate::{CliDiagnostic, CliSession, TraversalMode}; -use biome_analyze::RuleCategoriesBuilder; -use biome_console::{ConsoleExt, markup}; -use biome_diagnostics::Diagnostic; -use biome_diagnostics::PrintDiagnostic; -use biome_fs::BiomePath; -use biome_service::WorkspaceError; -use biome_service::file_handlers::{AstroFileHandler, SvelteFileHandler, VueFileHandler}; -use biome_service::projects::ProjectKey; -use biome_service::workspace::{ - ChangeFileParams, CloseFileParams, DropPatternParams, FeaturesBuilder, FileContent, - FileFeaturesResult, FixFileParams, FormatFileParams, OpenFileParams, SupportsFeatureParams, -}; -use std::borrow::Cow; - -pub(crate) fn run<'a>( - session: CliSession, - project_key: ProjectKey, - mode: &'a Execution, - biome_path: BiomePath, - content: &'a str, - cli_options: &CliOptions, -) -> Result<(), CliDiagnostic> { - let workspace = &*session.app.workspace; - let console = &mut *session.app.console; - let mut version = 0; - - if biome_path.extension().is_none() { - console.error(markup! { - {PrintDiagnostic::simple(&CliDiagnostic::from(StdinDiagnostic::new_no_extension()))} - }); - console.append(markup! {{content}}); - return Ok(()); - } - - if mode.is_format() { - let FileFeaturesResult { - features_supported: file_features, - } = workspace.file_features(SupportsFeatureParams { - project_key, - path: biome_path.clone(), - features: FeaturesBuilder::new().with_formatter().build(), - inline_config: None, - })?; - - if file_features.is_ignored() { - console.append(markup! {{content}}); - return Ok(()); - } - - if file_features.is_protected() { - let protected_diagnostic = WorkspaceError::protected_file(biome_path.to_string()); - if protected_diagnostic.tags().is_verbose() { - if cli_options.verbose { - console.error(markup! {{PrintDiagnostic::verbose(&protected_diagnostic)}}) - } - } else { - console.error(markup! {{PrintDiagnostic::simple(&protected_diagnostic)}}) - } - console.append(markup! {{content}}); - return Ok(()); - }; - if file_features.supports_format() { - workspace.open_file(OpenFileParams { - project_key, - path: biome_path.clone(), - content: FileContent::from_client(content), - document_file_source: None, - persist_node_cache: false, - inline_config: None, - })?; - let printed = workspace.format_file(FormatFileParams { - project_key, - path: biome_path.clone(), - inline_config: None, - })?; - - let code = printed.into_code(); - let output = if !file_features.supports_full_html_support() { - match biome_path.extension() { - Some("astro") => AstroFileHandler::output(content, code.as_str()), - Some("vue") => VueFileHandler::output(content, code.as_str()), - Some("svelte") => SvelteFileHandler::output(content, code.as_str()), - _ => code, - } - } else { - code - }; - console.append(markup! { - {output} - }); - workspace.close_file(CloseFileParams { - project_key, - path: biome_path.clone(), - })?; - } else { - console.append(markup! { - {content} - }); - console.error(markup! { - "The content was not formatted because the formatter is currently disabled." - }); - return Err(StdinDiagnostic::new_not_formatted().into()); - } - } else if mode.is_check() || mode.is_lint() { - let mut new_content = Cow::Borrowed(content); - - workspace.open_file(OpenFileParams { - project_key, - path: biome_path.clone(), - content: FileContent::from_client(content), - document_file_source: None, - persist_node_cache: false, - inline_config: None, - })?; - - // apply fix file of the linter - let FileFeaturesResult { - features_supported: file_features, - } = workspace.file_features(SupportsFeatureParams { - project_key, - path: biome_path.clone(), - features: FeaturesBuilder::new() - .with_linter() - .with_assist() - .with_formatter() - .build(), - inline_config: None, - })?; - - if file_features.is_ignored() { - console.append(markup! {{content}}); - return Ok(()); - } - - if file_features.is_protected() { - let protected_diagnostic = WorkspaceError::protected_file(biome_path.to_string()); - if protected_diagnostic.tags().is_verbose() { - if cli_options.verbose { - console.error(markup! {{PrintDiagnostic::verbose(&protected_diagnostic)}}) - } - } else { - console.error(markup! {{PrintDiagnostic::simple(&protected_diagnostic)}}) - } - console.append(markup! {{content}}); - - return Ok(()); - }; - - let (only, skip) = if let TraversalMode::Lint { only, skip, .. } = mode.traversal_mode() { - (only.clone(), skip.clone()) - } else { - (Vec::new(), Vec::new()) - }; - - if let Some(fix_file_mode) = mode.as_fix_file_mode() - && (file_features.supports_lint() || file_features.supports_assist()) - { - let mut rule_categories = RuleCategoriesBuilder::default().with_syntax(); - - if file_features.supports_lint() { - rule_categories = rule_categories.with_lint(); - } - - if file_features.supports_assist() { - rule_categories = rule_categories.with_assist(); - } - - let fix_file_result = workspace.fix_file(FixFileParams { - project_key, - fix_file_mode: *fix_file_mode, - path: biome_path.clone(), - should_format: mode.is_check() && file_features.supports_format(), - only: only.clone(), - skip: skip.clone(), - suppression_reason: None, - enabled_rules: vec![], - rule_categories: rule_categories.build(), - inline_config: None, - })?; - let code = fix_file_result.code; - let output = if !file_features.supports_full_html_support() { - match biome_path.extension() { - Some("astro") => AstroFileHandler::output(&new_content, code.as_str()), - Some("vue") => VueFileHandler::output(&new_content, code.as_str()), - Some("svelte") => SvelteFileHandler::output(&new_content, code.as_str()), - _ => code, - } - } else { - code - }; - if output != new_content { - version += 1; - workspace.change_file(ChangeFileParams { - project_key, - content: output.clone(), - path: biome_path.clone(), - version, - inline_config: None, - })?; - new_content = Cow::Owned(output); - } - } - - if file_features.supports_format() && mode.is_check() { - let printed = workspace.format_file(FormatFileParams { - project_key, - path: biome_path.clone(), - inline_config: None, - })?; - let code = printed.into_code(); - let output = if !file_features.supports_full_html_support() { - match biome_path.extension() { - Some("astro") => AstroFileHandler::output(&new_content, code.as_str()), - Some("vue") => VueFileHandler::output(&new_content, code.as_str()), - Some("svelte") => SvelteFileHandler::output(&new_content, code.as_str()), - _ => code, - } - } else { - code - }; - if (mode.is_safe_fixes_enabled() || mode.is_safe_and_unsafe_fixes_enabled()) - && output != new_content - { - new_content = Cow::Owned(output); - } - } - - match new_content { - Cow::Borrowed(original_content) => { - console.append(markup! { - {original_content} - }); - - if !mode.is_write() { - return Err(StdinDiagnostic::new_not_formatted().into()); - } - } - Cow::Owned(ref new_content) => { - console.append(markup! { - {new_content} - }); - } - } - workspace.close_file(CloseFileParams { - project_key, - path: biome_path.clone(), - })?; - } else if let TraversalMode::Search { pattern, .. } = mode.traversal_mode() { - // Make sure patterns are always cleaned up at the end of execution. - let _ = session.app.workspace.drop_pattern(DropPatternParams { - pattern: pattern.clone(), - }); - - console.append(markup! {{content}}); - } else { - console.append(markup! {{content}}); - } - - Ok(()) -} diff --git a/crates/biome_cli/src/execute/traverse.rs b/crates/biome_cli/src/execute/traverse.rs deleted file mode 100644 index 96cc142913f2..000000000000 --- a/crates/biome_cli/src/execute/traverse.rs +++ /dev/null @@ -1,674 +0,0 @@ -use super::process_file::{DiffKind, FileStatus, Message, process_file}; -use super::{Execution, TraversalMode}; -use crate::cli_options::CliOptions; -use crate::execute::diagnostics::{ - CIFormatDiffDiagnostic, ContentDiffAdvice, FormatDiffDiagnostic, PanicDiagnostic, -}; -use crate::reporter::TraversalSummary; -use crate::{CliDiagnostic, CliSession}; -use biome_diagnostics::DiagnosticTags; -use biome_diagnostics::{DiagnosticExt, Error, Resource, Severity, category}; -use biome_fs::{BiomePath, FileSystem, PathInterner}; -use biome_fs::{TraversalContext, TraversalScope}; -use biome_service::projects::ProjectKey; -use biome_service::workspace::{ - DocumentFileSource, DropPatternParams, FileFeaturesResult, IgnoreKind, PathIsIgnoredParams, -}; -use biome_service::{Workspace, WorkspaceError, extension_error, workspace::SupportsFeatureParams}; -use camino::{Utf8Path, Utf8PathBuf}; -use crossbeam::channel::{Receiver, Sender, unbounded}; -use rustc_hash::FxHashSet; -use std::collections::BTreeSet; -use std::sync::RwLock; -use std::sync::atomic::AtomicU32; -use std::{ - panic::catch_unwind, - sync::atomic::{AtomicUsize, Ordering}, - thread, - time::{Duration, Instant}, -}; -use tracing::instrument; - -pub(crate) struct TraverseResult { - pub(crate) summary: TraversalSummary, - pub(crate) evaluated_paths: BTreeSet, - pub(crate) diagnostics: Vec, -} - -pub(crate) fn traverse( - execution: &Execution, - session: &mut CliSession, - project_key: ProjectKey, - cli_options: &CliOptions, - inputs: Vec, -) -> Result { - let (interner, recv_files) = PathInterner::new(); - let (sender, receiver) = unbounded(); - - let changed = AtomicUsize::new(0); - let unchanged = AtomicUsize::new(0); - let matches = AtomicUsize::new(0); - let skipped = AtomicUsize::new(0); - - let workspace = &*session.app.workspace; - let fs = workspace.fs(); - - let max_diagnostics = execution.get_max_diagnostics(); - - let working_directory = fs.working_directory(); - let printer = DiagnosticsPrinter::new(execution, working_directory.as_deref()) - .with_verbose(cli_options.verbose) - .with_diagnostic_level(cli_options.diagnostic_level) - .with_max_diagnostics(max_diagnostics); - - let (duration, evaluated_paths, diagnostics) = thread::scope(|s| { - let handler = thread::Builder::new() - .name(String::from("biome::console")) - .spawn_scoped(s, || printer.run(receiver, recv_files)) - .expect("failed to spawn console thread"); - - // The traversal context is scoped to ensure all the channels it - // contains are properly closed once the traversal finishes - let (elapsed, evaluated_paths) = traverse_inputs( - fs, - inputs, - &TraversalOptions { - fs, - workspace, - project_key, - execution, - interner, - matches: &matches, - changed: &changed, - unchanged: &unchanged, - skipped: &skipped, - messages: sender, - evaluated_paths: RwLock::default(), - }, - ); - // wait for the main thread to finish - let diagnostics = handler.join().unwrap(); - - (elapsed, evaluated_paths, diagnostics) - }); - - // Make sure patterns are always cleaned up at the end of traversal. - if let TraversalMode::Search { pattern, .. } = execution.traversal_mode() { - let _ = session.app.workspace.drop_pattern(DropPatternParams { - pattern: pattern.clone(), - }); - } - - let errors = printer.errors(); - let warnings = printer.warnings(); - let infos = printer.infos(); - let changed = changed.load(Ordering::Relaxed); - let unchanged = unchanged.load(Ordering::Relaxed); - let matches = matches.load(Ordering::Relaxed); - let skipped = skipped.load(Ordering::Relaxed); - let suggested_fixes_skipped = printer.skipped_fixes(); - let diagnostics_not_printed = printer.not_printed_diagnostics(); - Ok(TraverseResult { - summary: TraversalSummary { - changed, - unchanged, - duration, - scanner_duration: None, - errors, - matches, - warnings, - infos, - skipped, - suggested_fixes_skipped, - diagnostics_not_printed, - }, - evaluated_paths, - diagnostics, - }) -} - -/// Initiate the filesystem traversal tasks with the provided input paths and -/// run it to completion, returning the duration of the process and the evaluated paths -fn traverse_inputs( - fs: &dyn FileSystem, - inputs: Vec, - ctx: &TraversalOptions, -) -> (Duration, BTreeSet) { - let start = Instant::now(); - fs.traversal(Box::new(move |scope: &dyn TraversalScope| { - for input in inputs { - scope.evaluate(ctx, Utf8PathBuf::from(input)); - } - })); - - let paths = ctx.evaluated_paths(); - fs.traversal(Box::new(|scope: &dyn TraversalScope| { - for path in paths { - scope.handle(ctx, path.to_path_buf()); - } - })); - - (start.elapsed(), ctx.evaluated_paths()) -} - -// struct DiagnosticsReporter<'ctx> {} - -struct DiagnosticsPrinter<'ctx> { - /// Execution of the traversal - execution: &'ctx Execution, - /// The maximum number of diagnostics the console thread is allowed to print - max_diagnostics: u32, - /// The approximate number of diagnostics the console will print before - /// folding the rest into the "skipped diagnostics" counter - remaining_diagnostics: AtomicU32, - /// Mutable reference to a boolean flag tracking whether the console thread - /// printed any error-level message - errors: AtomicU32, - /// Mutable reference to a boolean flag tracking whether the console thread - /// printed any warnings-level message - warnings: AtomicU32, - /// Mutable reference to a boolean flag tracking whether the console thread - /// printed any info-level message - infos: AtomicU32, - /// Whether the console thread should print diagnostics in verbose mode - verbose: bool, - /// The diagnostic level the console thread should print - diagnostic_level: Severity, - - not_printed_diagnostics: AtomicU32, - printed_diagnostics: AtomicU32, - total_skipped_suggested_fixes: AtomicU32, - - /// The current working directory, borrowed from [FileSystem] - working_directory: Option<&'ctx Utf8Path>, -} - -impl<'ctx> DiagnosticsPrinter<'ctx> { - fn new(execution: &'ctx Execution, working_directory: Option<&'ctx Utf8Path>) -> Self { - Self { - errors: AtomicU32::new(0), - warnings: AtomicU32::new(0), - infos: AtomicU32::new(0), - remaining_diagnostics: AtomicU32::new(0), - execution, - diagnostic_level: Severity::Hint, - verbose: false, - max_diagnostics: 20, - not_printed_diagnostics: AtomicU32::new(0), - printed_diagnostics: AtomicU32::new(0), - total_skipped_suggested_fixes: AtomicU32::new(0), - working_directory, - } - } - - fn with_verbose(mut self, verbose: bool) -> Self { - self.verbose = verbose; - self - } - - fn with_max_diagnostics(mut self, value: u32) -> Self { - self.max_diagnostics = value; - self - } - - fn with_diagnostic_level(mut self, value: Severity) -> Self { - self.diagnostic_level = value; - self - } - - fn errors(&self) -> u32 { - self.errors.load(Ordering::Relaxed) - } - - fn warnings(&self) -> u32 { - self.warnings.load(Ordering::Relaxed) - } - - fn infos(&self) -> u32 { - self.infos.load(Ordering::Relaxed) - } - - fn not_printed_diagnostics(&self) -> u32 { - self.not_printed_diagnostics.load(Ordering::Relaxed) - } - - fn skipped_fixes(&self) -> u32 { - self.total_skipped_suggested_fixes.load(Ordering::Relaxed) - } - - /// Checks if the diagnostic we received from the thread should be considered or not. Logic: - /// - it should not be considered if its severity level is lower than the one provided via CLI; - /// - it should not be considered if it's a verbose diagnostic and the CLI **didn't** request a `--verbose` option. - fn should_skip_diagnostic(&self, severity: Severity, diagnostic_tags: DiagnosticTags) -> bool { - if severity < self.diagnostic_level { - return true; - } - - if diagnostic_tags.is_verbose() && !self.verbose { - return true; - } - - false - } - - /// Count the diagnostic, and then returns a boolean that tells if it should be printed - fn should_print(&self) -> bool { - let printed_diagnostics = self.printed_diagnostics.load(Ordering::Relaxed); - let should_print = printed_diagnostics < self.max_diagnostics; - if should_print { - self.printed_diagnostics.fetch_add(1, Ordering::Relaxed); - self.remaining_diagnostics.store( - self.max_diagnostics.saturating_sub(printed_diagnostics), - Ordering::Relaxed, - ); - } else { - self.not_printed_diagnostics.fetch_add(1, Ordering::Relaxed); - } - - should_print - } - - fn run(&self, receiver: Receiver, interner: Receiver) -> Vec { - let mut paths: FxHashSet = FxHashSet::default(); - - let mut diagnostics_to_print = vec![]; - - while let Ok(msg) = receiver.recv() { - match msg { - Message::SkippedFixes { - skipped_suggested_fixes, - } => { - self.total_skipped_suggested_fixes - .fetch_add(skipped_suggested_fixes, Ordering::Relaxed); - } - - Message::Failure => { - self.errors.fetch_add(1, Ordering::Relaxed); - } - - Message::Error(mut err) => { - let location = err.location(); - if self.should_skip_diagnostic(err.severity(), err.tags()) { - continue; - } - if err.severity() == Severity::Warning { - self.warnings.fetch_add(1, Ordering::Relaxed); - } - // if err.severity() == Severity::Information { - // self.infos.fetch_add(1, Ordering::Relaxed); - // } - if let Some(Resource::File(file_path)) = location.resource.as_ref() { - // Retrieves the file name from the file ID cache, if it's a miss - // flush entries from the interner channel until it's found - let file_name = match paths.get(*file_path) { - Some(path) => Some(path), - None => loop { - match interner.recv() { - Ok(path) => { - paths.insert(path.to_string()); - if path.as_str() == *file_path { - break paths.get(&path.to_string()); - } - } - // In case the channel disconnected without sending - // the path we need, print the error without a file - // name (normally this should never happen) - Err(_) => break None, - } - }, - }; - - if let Some(path) = file_name { - let path = self.to_relative_file_path(path); - err = err.with_file_path(path.as_str()); - } - } - - let should_print = self.should_print(); - - if should_print { - diagnostics_to_print.push(err); - } - } - - Message::Diagnostics { - file_path, - content, - diagnostics, - skipped_diagnostics, - } => { - // we transform the file string into a path object so we can correctly strip - // the working directory without having leading slash in the file name - let file_path = self.to_relative_file_path(&file_path); - self.not_printed_diagnostics - .fetch_add(skipped_diagnostics, Ordering::Relaxed); - for diag in diagnostics { - let severity = diag.severity(); - if self.should_skip_diagnostic(severity, diag.tags()) { - continue; - } - if severity == Severity::Error { - self.errors.fetch_add(1, Ordering::Relaxed); - } - if severity == Severity::Warning { - self.warnings.fetch_add(1, Ordering::Relaxed); - } - if severity == Severity::Information { - self.infos.fetch_add(1, Ordering::Relaxed); - } - - let should_print = self.should_print(); - - let diag = diag - .with_file_path(file_path.as_str()) - .with_file_source_code(&content); - if should_print || self.execution.is_ci() { - diagnostics_to_print.push(diag) - } - } - } - Message::Diff { - file_name, - old, - new, - diff_kind, - } => { - let file_path = self.to_relative_file_path(&file_name); - // A diff is an error in CI mode and in format check mode - let is_error = self.execution.is_ci() || !self.execution.is_format_write(); - if is_error { - self.errors.fetch_add(1, Ordering::Relaxed); - } - - let severity: Severity = if is_error { - Severity::Error - } else { - // we set lowest - Severity::Hint - }; - - if self.should_skip_diagnostic(severity, DiagnosticTags::empty()) { - continue; - } - - let should_print = self.should_print(); - - if should_print { - match diff_kind { - DiffKind::Format => { - let diag = if self.execution.is_ci() { - CIFormatDiffDiagnostic { - diff: ContentDiffAdvice { - old: old.clone(), - new: new.clone(), - }, - } - .with_severity(severity) - .with_file_source_code(old.clone()) - .with_file_path(file_path.clone()) - } else { - FormatDiffDiagnostic { - diff: ContentDiffAdvice { - old: old.clone(), - new: new.clone(), - }, - } - .with_severity(severity) - .with_file_source_code(old.clone()) - .with_file_path(file_path.clone()) - }; - if should_print || self.execution.is_ci() { - diagnostics_to_print.push(diag); - } - } - } - } - } - } - } - diagnostics_to_print - } - - fn to_relative_file_path(&self, path: &str) -> String { - let file_path = Utf8Path::new(&path); - self.working_directory - .as_ref() - .and_then(|wd| file_path.strip_prefix(wd.as_str()).ok()) - .map(|path| path.to_string()) - .unwrap_or(file_path.to_string()) - } -} - -/// Context object shared between directory traversal tasks -pub(crate) struct TraversalOptions<'ctx, 'app> { - /// Shared instance of [FileSystem] - pub(crate) fs: &'app dyn FileSystem, - /// Instance of [Workspace] used by this instance of the CLI - pub(crate) workspace: &'ctx dyn Workspace, - /// Key of the project in which we're traversing. - pub(crate) project_key: ProjectKey, - /// Determines how the files should be processed - pub(crate) execution: &'ctx Execution, - /// File paths interner cache used by the filesystem traversal - interner: PathInterner, - /// Shared atomic counter storing the number of changed files - changed: &'ctx AtomicUsize, - /// Shared atomic counter storing the number of unchanged files - unchanged: &'ctx AtomicUsize, - /// Shared atomic counter storing the number of unchanged files - matches: &'ctx AtomicUsize, - /// Shared atomic counter storing the number of skipped files - skipped: &'ctx AtomicUsize, - /// Channel sending messages to the display thread - pub(crate) messages: Sender, - /// List of paths that should be processed - pub(crate) evaluated_paths: RwLock>, -} - -impl TraversalOptions<'_, '_> { - pub(crate) fn increment_changed(&self, path: &BiomePath) { - self.changed.fetch_add(1, Ordering::Relaxed); - self.evaluated_paths - .write() - .unwrap() - .replace(path.to_written()); - } - pub(crate) fn increment_unchanged(&self) { - self.unchanged.fetch_add(1, Ordering::Relaxed); - } - - pub(crate) fn increment_matches(&self, num_matches: usize) { - self.matches.fetch_add(num_matches, Ordering::Relaxed); - } - - /// Send a message to the display thread - pub(crate) fn push_message(&self, msg: impl Into) { - self.messages.send(msg.into()).ok(); - } - - pub(crate) fn miss_handler_err(&self, err: WorkspaceError, biome_path: &BiomePath) { - let file_path = self - .fs - .working_directory() - .as_ref() - .and_then(|wd| { - biome_path - .strip_prefix(wd) - .ok() - .map(|path| path.to_string()) - }) - .unwrap_or(biome_path.to_string()); - self.push_diagnostic( - err.with_category(category!("files/missingHandler")) - .with_file_path(file_path) - .with_tags(DiagnosticTags::VERBOSE), - ); - } - - /// Sends a diagnostic regarding the use of a protected file that can't be handled by Biome - pub(crate) fn protected_file(&self, biome_path: &BiomePath) { - self.push_diagnostic(WorkspaceError::protected_file(biome_path.to_string()).into()) - } -} - -/// Path entries that we want to ignore during the OS traversal. -pub const TRAVERSAL_IGNORE_ENTRIES: &[&[u8]] = &[ - b".git", - b".hg", - b".svn", - b".yarn", - b".DS_Store", - b"node_modules", -]; - -impl TraversalContext for TraversalOptions<'_, '_> { - fn interner(&self) -> &PathInterner { - &self.interner - } - - fn evaluated_paths(&self) -> BTreeSet { - self.evaluated_paths.read().unwrap().clone() - } - - fn push_diagnostic(&self, error: Error) { - self.push_message(error); - } - - #[instrument(level = "debug", skip(self, biome_path))] - fn can_handle(&self, biome_path: &BiomePath) -> bool { - if biome_path - .file_name() - .is_some_and(|file_name| TRAVERSAL_IGNORE_ENTRIES.contains(&file_name.as_bytes())) - { - return false; - } - - let path = biome_path.as_path(); - if self.fs.path_is_dir(path) || self.fs.path_is_symlink(path) { - // handle: - // - directories - // - symlinks - // - unresolved symlinks - // e.g `symlink/subdir` where symlink points to a directory that includes `subdir`. - // Note that `symlink/subdir` is not an existing file. - let can_handle = !self - .workspace - .is_path_ignored(PathIsIgnoredParams { - project_key: self.project_key, - path: biome_path.clone(), - features: self.execution.to_feature(), - ignore_kind: IgnoreKind::Ancestors, - }) - .unwrap_or_else(|err| { - self.push_diagnostic(err.into()); - false - }); - - return can_handle; - } - - // bail on fifo and socket files - if !self.fs.path_is_file(path) { - return false; - } - - let file_features = self.workspace.file_features(SupportsFeatureParams { - project_key: self.project_key, - path: biome_path.clone(), - features: self.execution.to_feature(), - inline_config: None, - }); - - let can_read = DocumentFileSource::can_read(biome_path); - - let file_features = match file_features { - Ok(FileFeaturesResult { - features_supported: file_features, - }) => { - if file_features.is_protected() { - self.protected_file(biome_path); - return false; - } - - if file_features.is_not_supported() && !file_features.is_ignored() && !can_read { - // we should throw a diagnostic if we can't handle a file that isn't ignored - self.miss_handler_err(extension_error(biome_path), biome_path); - return false; - } - file_features - } - Err(err) => { - self.miss_handler_err(err, biome_path); - return false; - } - }; - - match self.execution.traversal_mode() { - TraversalMode::Check { .. } | TraversalMode::CI { .. } => { - file_features.supports_lint() - || file_features.supports_format() - || file_features.supports_assist() - } - TraversalMode::Format { .. } => file_features.supports_format(), - TraversalMode::Lint { .. } => file_features.supports_lint(), - // Imagine if Biome can't handle its own configuration file... - TraversalMode::Migrate { .. } => true, - TraversalMode::Search { .. } => file_features.supports_search(), - } - } - - fn handle_path(&self, path: BiomePath) { - handle_file(self, &path) - } - - fn store_path(&self, path: BiomePath) { - self.evaluated_paths - .write() - .unwrap() - .insert(BiomePath::new(path.as_path())); - } -} - -/// This function wraps the [process_file] function implementing the traversal -/// in a [catch_unwind] block and emit diagnostics in case of error (either the -/// traversal function returns Err or panics) -fn handle_file(ctx: &TraversalOptions, path: &BiomePath) { - match catch_unwind(move || process_file(ctx, path)) { - Ok(Ok(FileStatus::Changed)) => { - ctx.increment_changed(path); - } - Ok(Ok(FileStatus::Unchanged)) => { - ctx.increment_unchanged(); - } - Ok(Ok(FileStatus::SearchResult(num_matches, msg))) => { - ctx.increment_unchanged(); - ctx.increment_matches(num_matches); - ctx.push_message(msg); - } - Ok(Ok(FileStatus::Message(msg))) => { - ctx.increment_unchanged(); - ctx.push_message(msg); - } - Ok(Ok(FileStatus::Protected(file_path))) => { - ctx.increment_unchanged(); - ctx.push_diagnostic(WorkspaceError::protected_file(file_path).into()); - } - Ok(Ok(FileStatus::Ignored)) => {} - Ok(Err(err)) => { - ctx.increment_unchanged(); - ctx.skipped.fetch_add(1, Ordering::Relaxed); - ctx.push_message(err); - } - Err(err) => { - let message = match err.downcast::() { - Ok(msg) => format!("processing panicked: {msg}"), - Err(err) => match err.downcast::<&'static str>() { - Ok(msg) => format!("processing panicked: {msg}"), - Err(_) => String::from("processing panicked"), - }, - }; - - ctx.push_message(PanicDiagnostic { message }.with_file_path(path.to_string())); - } - } -} diff --git a/crates/biome_cli/src/lib.rs b/crates/biome_cli/src/lib.rs index 84b1c3b42399..2442c80744b4 100644 --- a/crates/biome_cli/src/lib.rs +++ b/crates/biome_cli/src/lib.rs @@ -20,23 +20,24 @@ mod diagnostics; mod execute; mod logging; mod panic; -mod reporter; +pub(crate) mod reporter; +pub(crate) mod runner; mod service; -use crate::cli_options::{CliOptions, ColorsArg}; -use crate::commands::CommandRunner; +use crate::cli_options::ColorsArg; use crate::commands::check::CheckCommandPayload; use crate::commands::ci::CiCommandPayload; use crate::commands::format::FormatCommandPayload; use crate::commands::lint::LintCommandPayload; use crate::commands::migrate::MigrateCommandPayload; pub use crate::commands::{BiomeCommand, biome_command}; -use crate::logging::LogOptions; pub use crate::logging::{LoggingLevel, setup_cli_subscriber}; +use crate::runner::impls::commands::custom_execution::CustomExecutionCmdImpl; +use crate::runner::impls::commands::traversal::TraversalCommandImpl; +use crate::runner::run::run_command; pub use diagnostics::CliDiagnostic; -pub use execute::{Execution, TraversalMode, VcsTargeted, execute_mode}; pub use panic::setup_panic_handler; -pub use reporter::{DiagnosticsPayload, Reporter, ReporterVisitor, TraversalSummary}; +pub use reporter::{DiagnosticsPayload, TraversalSummary}; pub use service::{SocketTransport, open_transport}; pub(crate) const VERSION: &str = match option_env!("BIOME_VERSION") { @@ -100,7 +101,7 @@ impl<'app> CliSession<'app> { self, &log_options, &cli_options, - CheckCommandPayload { + TraversalCommandImpl(CheckCommandPayload { write, fix, unsafe_, @@ -117,7 +118,7 @@ impl<'app> CliSession<'app> { format_with_errors, json_parser, css_parser, - }, + }), ), BiomeCommand::Lint { write, @@ -147,7 +148,7 @@ impl<'app> CliSession<'app> { self, &log_options, &cli_options, - LintCommandPayload { + TraversalCommandImpl(LintCommandPayload { write, suppress, suppression_reason, @@ -169,7 +170,7 @@ impl<'app> CliSession<'app> { graphql_linter, css_parser, json_parser, - }, + }), ), BiomeCommand::Ci { linter_enabled, @@ -190,7 +191,7 @@ impl<'app> CliSession<'app> { self, &log_options, &cli_options, - CiCommandPayload { + TraversalCommandImpl(CiCommandPayload { linter_enabled, formatter_enabled, assist_enabled, @@ -202,7 +203,7 @@ impl<'app> CliSession<'app> { format_with_errors, css_parser, json_parser, - }, + }), ), BiomeCommand::Format { javascript_formatter, @@ -228,7 +229,7 @@ impl<'app> CliSession<'app> { self, &log_options, &cli_options, - FormatCommandPayload { + TraversalCommandImpl(FormatCommandPayload { javascript_formatter, formatter_configuration, stdin_file_path, @@ -246,7 +247,7 @@ impl<'app> CliSession<'app> { since, css_parser, json_parser, - }, + }), ), BiomeCommand::Explain { doc } => commands::explain::explain(self, doc), BiomeCommand::Init(emit_jsonc) => commands::init::init(self, emit_jsonc), @@ -265,13 +266,13 @@ impl<'app> CliSession<'app> { self, &log_options, &cli_options, - MigrateCommandPayload { + CustomExecutionCmdImpl(MigrateCommandPayload { write, fix, sub_command, configuration_directory_path: None, configuration_file_path: None, - }, + }), ), BiomeCommand::Search { cli_options, @@ -286,14 +287,14 @@ impl<'app> CliSession<'app> { self, &log_options, &cli_options, - SearchCommandPayload { + TraversalCommandImpl(SearchCommandPayload { files_configuration, paths, pattern, language, stdin_file_path, vcs_configuration, - }, + }), ), BiomeCommand::RunServer { stop_on_disconnect, @@ -318,13 +319,3 @@ pub fn to_color_mode(color: Option<&ColorsArg>) -> ColorMode { None => ColorMode::Auto, } } - -pub(crate) fn run_command( - session: CliSession, - log_options: &LogOptions, - cli_options: &CliOptions, - mut command: impl CommandRunner, -) -> Result<(), CliDiagnostic> { - let command = &mut command; - command.run(session, log_options, cli_options) -} diff --git a/crates/biome_cli/src/reporter/checkstyle.rs b/crates/biome_cli/src/reporter/checkstyle.rs index 87c23a5a548a..94941bf6bcc4 100644 --- a/crates/biome_cli/src/reporter/checkstyle.rs +++ b/crates/biome_cli/src/reporter/checkstyle.rs @@ -1,4 +1,6 @@ -use crate::{DiagnosticsPayload, Execution, Reporter, ReporterVisitor, TraversalSummary}; +use crate::reporter::{Reporter, ReporterVisitor}; +use crate::runner::execution::Execution; +use crate::{DiagnosticsPayload, TraversalSummary}; use biome_console::{Console, ConsoleExt, markup}; use biome_diagnostics::display::SourceFile; use biome_diagnostics::{Error, PrintDescription, Resource, Severity}; @@ -6,19 +8,19 @@ use camino::{Utf8Path, Utf8PathBuf}; use std::collections::BTreeMap; use std::io::{self, Write}; -pub struct CheckstyleReporter { +pub struct CheckstyleReporter<'a> { pub summary: TraversalSummary, pub diagnostics_payload: DiagnosticsPayload, - pub execution: Execution, + pub execution: &'a dyn Execution, pub verbose: bool, pub(crate) working_directory: Option, } -impl Reporter for CheckstyleReporter { +impl Reporter for CheckstyleReporter<'_> { fn write(self, visitor: &mut dyn ReporterVisitor) -> io::Result<()> { - visitor.report_summary(&self.execution, self.summary, self.verbose)?; + visitor.report_summary(self.execution, self.summary, self.verbose)?; visitor.report_diagnostics( - &self.execution, + self.execution, self.diagnostics_payload, self.verbose, self.working_directory.as_deref(), @@ -40,7 +42,7 @@ impl<'a> CheckstyleReporterVisitor<'a> { impl<'a> ReporterVisitor for CheckstyleReporterVisitor<'a> { fn report_summary( &mut self, - _execution: &Execution, + _execution: &dyn Execution, _summary: TraversalSummary, _verbose: bool, ) -> io::Result<()> { @@ -49,7 +51,7 @@ impl<'a> ReporterVisitor for CheckstyleReporterVisitor<'a> { fn report_diagnostics( &mut self, - _execution: &Execution, + _execution: &dyn Execution, payload: DiagnosticsPayload, verbose: bool, _working_directory: Option<&Utf8Path>, diff --git a/crates/biome_cli/src/reporter/github.rs b/crates/biome_cli/src/reporter/github.rs index 9d52b820717c..3bad67750569 100644 --- a/crates/biome_cli/src/reporter/github.rs +++ b/crates/biome_cli/src/reporter/github.rs @@ -1,20 +1,22 @@ -use crate::{DiagnosticsPayload, Execution, Reporter, ReporterVisitor, TraversalSummary}; +use crate::reporter::{Reporter, ReporterVisitor}; +use crate::runner::execution::Execution; +use crate::{DiagnosticsPayload, TraversalSummary}; use biome_console::{Console, ConsoleExt, markup}; use biome_diagnostics::PrintGitHubDiagnostic; use camino::{Utf8Path, Utf8PathBuf}; use std::io; -pub(crate) struct GithubReporter { +pub(crate) struct GithubReporter<'a> { pub(crate) diagnostics_payload: DiagnosticsPayload, - pub(crate) execution: Execution, + pub(crate) execution: &'a dyn Execution, pub(crate) verbose: bool, pub(crate) working_directory: Option, } -impl Reporter for GithubReporter { +impl Reporter for GithubReporter<'_> { fn write(self, visitor: &mut dyn ReporterVisitor) -> io::Result<()> { visitor.report_diagnostics( - &self.execution, + self.execution, self.diagnostics_payload, self.verbose, self.working_directory.as_deref(), @@ -27,7 +29,7 @@ pub(crate) struct GithubReporterVisitor<'a>(pub(crate) &'a mut dyn Console); impl ReporterVisitor for GithubReporterVisitor<'_> { fn report_summary( &mut self, - _execution: &Execution, + _execution: &dyn Execution, _summary: TraversalSummary, _verbose: bool, ) -> io::Result<()> { @@ -36,7 +38,7 @@ impl ReporterVisitor for GithubReporterVisitor<'_> { fn report_diagnostics( &mut self, - _execution: &Execution, + _execution: &dyn Execution, diagnostics_payload: DiagnosticsPayload, verbose: bool, _working_directory: Option<&Utf8Path>, diff --git a/crates/biome_cli/src/reporter/gitlab.rs b/crates/biome_cli/src/reporter/gitlab.rs index f93fd89cf50e..2ff9c5bf08f2 100644 --- a/crates/biome_cli/src/reporter/gitlab.rs +++ b/crates/biome_cli/src/reporter/gitlab.rs @@ -1,4 +1,6 @@ -use crate::{DiagnosticsPayload, Execution, Reporter, ReporterVisitor, TraversalSummary}; +use crate::reporter::{Reporter, ReporterVisitor}; +use crate::runner::execution::Execution; +use crate::{DiagnosticsPayload, TraversalSummary}; use biome_console::fmt::{Display, Formatter}; use biome_console::{Console, ConsoleExt, markup}; use biome_diagnostics::display::SourceFile; @@ -14,17 +16,17 @@ use std::{ path::Path, }; -pub struct GitLabReporter { - pub(crate) execution: Execution, +pub struct GitLabReporter<'a> { + pub(crate) execution: &'a dyn Execution, pub(crate) diagnostics: DiagnosticsPayload, pub(crate) verbose: bool, pub(crate) working_directory: Option, } -impl Reporter for GitLabReporter { +impl Reporter for GitLabReporter<'_> { fn write(self, visitor: &mut dyn ReporterVisitor) -> std::io::Result<()> { visitor.report_diagnostics( - &self.execution, + self.execution, self.diagnostics, self.verbose, self.working_directory.as_deref(), @@ -69,7 +71,7 @@ impl<'a> GitLabReporterVisitor<'a> { impl ReporterVisitor for GitLabReporterVisitor<'_> { fn report_summary( &mut self, - _: &Execution, + _: &dyn Execution, _: TraversalSummary, _verbose: bool, ) -> std::io::Result<()> { @@ -78,7 +80,7 @@ impl ReporterVisitor for GitLabReporterVisitor<'_> { fn report_diagnostics( &mut self, - _execution: &Execution, + _execution: &dyn Execution, payload: DiagnosticsPayload, verbose: bool, _working_directory: Option<&Utf8Path>, diff --git a/crates/biome_cli/src/reporter/json.rs b/crates/biome_cli/src/reporter/json.rs index a7a608d608c5..7492ac1293d5 100644 --- a/crates/biome_cli/src/reporter/json.rs +++ b/crates/biome_cli/src/reporter/json.rs @@ -1,4 +1,6 @@ -use crate::{DiagnosticsPayload, Execution, Reporter, ReporterVisitor, TraversalSummary}; +use crate::reporter::{Reporter, ReporterVisitor}; +use crate::runner::execution::Execution; +use crate::{DiagnosticsPayload, TraversalSummary}; use biome_console::fmt::Formatter; use camino::{Utf8Path, Utf8PathBuf}; use serde::Serialize; @@ -28,19 +30,19 @@ impl biome_console::fmt::Display for JsonReporterVisitor { } } -pub struct JsonReporter { - pub execution: Execution, +pub struct JsonReporter<'a> { + pub execution: &'a dyn Execution, pub diagnostics: DiagnosticsPayload, pub summary: TraversalSummary, pub verbose: bool, pub working_directory: Option, } -impl Reporter for JsonReporter { +impl Reporter for JsonReporter<'_> { fn write(self, visitor: &mut dyn ReporterVisitor) -> std::io::Result<()> { - visitor.report_summary(&self.execution, self.summary, self.verbose)?; + visitor.report_summary(self.execution, self.summary, self.verbose)?; visitor.report_diagnostics( - &self.execution, + self.execution, self.diagnostics, self.verbose, self.working_directory.as_deref(), @@ -53,19 +55,19 @@ impl Reporter for JsonReporter { impl ReporterVisitor for JsonReporterVisitor { fn report_summary( &mut self, - execution: &Execution, + execution: &dyn Execution, summary: TraversalSummary, _verbose: bool, ) -> std::io::Result<()> { self.summary = summary; - self.command = format!("{}", execution.traversal_mode()); + self.command = execution.as_diagnostic_category().name().to_string(); Ok(()) } fn report_diagnostics( &mut self, - _execution: &Execution, + _execution: &dyn Execution, payload: DiagnosticsPayload, verbose: bool, _working_directory: Option<&Utf8Path>, diff --git a/crates/biome_cli/src/reporter/junit.rs b/crates/biome_cli/src/reporter/junit.rs index bf42ea8988b7..50ed5209c2dc 100644 --- a/crates/biome_cli/src/reporter/junit.rs +++ b/crates/biome_cli/src/reporter/junit.rs @@ -1,4 +1,6 @@ -use crate::{DiagnosticsPayload, Execution, Reporter, ReporterVisitor, TraversalSummary}; +use crate::reporter::{Reporter, ReporterVisitor}; +use crate::runner::execution::Execution; +use crate::{DiagnosticsPayload, TraversalSummary}; use biome_console::{Console, ConsoleExt, markup}; use biome_diagnostics::display::SourceFile; use biome_diagnostics::{Error, Resource}; @@ -7,19 +9,19 @@ use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite}; use std::fmt::{Display, Formatter}; use std::io; -pub(crate) struct JunitReporter { +pub(crate) struct JunitReporter<'a> { pub(crate) diagnostics_payload: DiagnosticsPayload, - pub(crate) execution: Execution, + pub(crate) execution: &'a dyn Execution, pub(crate) summary: TraversalSummary, pub(crate) verbose: bool, pub(crate) working_directory: Option, } -impl Reporter for JunitReporter { +impl Reporter for JunitReporter<'_> { fn write(self, visitor: &mut dyn ReporterVisitor) -> io::Result<()> { - visitor.report_summary(&self.execution, self.summary, self.verbose)?; + visitor.report_summary(self.execution, self.summary, self.verbose)?; visitor.report_diagnostics( - &self.execution, + self.execution, self.diagnostics_payload, self.verbose, self.working_directory.as_deref(), @@ -50,7 +52,7 @@ impl<'a> JunitReporterVisitor<'a> { impl ReporterVisitor for JunitReporterVisitor<'_> { fn report_summary( &mut self, - _execution: &Execution, + _execution: &dyn Execution, summary: TraversalSummary, _verbose: bool, ) -> io::Result<()> { @@ -62,7 +64,7 @@ impl ReporterVisitor for JunitReporterVisitor<'_> { fn report_diagnostics( &mut self, - _execution: &Execution, + _execution: &dyn Execution, payload: DiagnosticsPayload, verbose: bool, _working_directory: Option<&Utf8Path>, diff --git a/crates/biome_cli/src/reporter/mod.rs b/crates/biome_cli/src/reporter/mod.rs index fc78cb510ae5..7d0e63a9b040 100644 --- a/crates/biome_cli/src/reporter/mod.rs +++ b/crates/biome_cli/src/reporter/mod.rs @@ -8,7 +8,7 @@ pub(crate) mod summary; pub(crate) mod terminal; use crate::cli_options::MaxDiagnostics; -use crate::execute::Execution; +use crate::runner::execution::Execution; use biome_diagnostics::advice::ListAdvice; use biome_diagnostics::{Diagnostic, Error, Severity}; use biome_fs::BiomePath; @@ -46,17 +46,17 @@ pub struct TraversalSummary { } /// When using this trait, the type that implements this trait is the one that holds the read-only information to pass around -pub trait Reporter: Sized { +pub(crate) trait Reporter: Sized { /// Writes the summary using the underling visitor fn write(self, visitor: &mut dyn ReporterVisitor) -> io::Result<()>; } /// When using this trait, the type that implements this trait is the one that will **write** the data, ideally inside a buffer -pub trait ReporterVisitor { +pub(crate) trait ReporterVisitor { /// Writes the summary in the underling writer fn report_summary( &mut self, - _execution: &Execution, + _execution: &dyn Execution, _summary: TraversalSummary, _verbose: bool, ) -> io::Result<()>; @@ -73,7 +73,7 @@ pub trait ReporterVisitor { /// Writes a diagnostics fn report_diagnostics( &mut self, - _execution: &Execution, + _execution: &dyn Execution, _payload: DiagnosticsPayload, _verbose: bool, _working_directory: Option<&Utf8Path>, diff --git a/crates/biome_cli/src/reporter/rdjson.rs b/crates/biome_cli/src/reporter/rdjson.rs index 0ec9f288d381..4fb1107b01bf 100644 --- a/crates/biome_cli/src/reporter/rdjson.rs +++ b/crates/biome_cli/src/reporter/rdjson.rs @@ -1,4 +1,6 @@ -use crate::{DiagnosticsPayload, Execution, Reporter, ReporterVisitor, TraversalSummary}; +use crate::reporter::{Reporter, ReporterVisitor}; +use crate::runner::execution::Execution; +use crate::{DiagnosticsPayload, TraversalSummary}; use biome_console::fmt::{Display, Formatter}; use biome_console::{Console, ConsoleExt, MarkupBuf, markup}; use biome_diagnostics::display::{SourceFile, markup_to_string}; @@ -6,17 +8,17 @@ use biome_diagnostics::{Error, Location, LogCategory, PrintDescription, Visit}; use camino::{Utf8Path, Utf8PathBuf}; use serde::Serialize; -pub(crate) struct RdJsonReporter { +pub(crate) struct RdJsonReporter<'a> { pub(crate) diagnostics_payload: DiagnosticsPayload, - pub(crate) execution: Execution, + pub(crate) execution: &'a dyn Execution, pub(crate) verbose: bool, pub(crate) working_directory: Option, } -impl Reporter for RdJsonReporter { +impl Reporter for RdJsonReporter<'_> { fn write(self, visitor: &mut dyn ReporterVisitor) -> std::io::Result<()> { visitor.report_diagnostics( - &self.execution, + self.execution, self.diagnostics_payload, self.verbose, self.working_directory.as_deref(), @@ -30,7 +32,7 @@ pub(crate) struct RdJsonReporterVisitor<'a>(pub(crate) &'a mut dyn Console); impl ReporterVisitor for RdJsonReporterVisitor<'_> { fn report_summary( &mut self, - _execution: &Execution, + _execution: &dyn Execution, _summary: TraversalSummary, _verbose: bool, ) -> std::io::Result<()> { @@ -39,7 +41,7 @@ impl ReporterVisitor for RdJsonReporterVisitor<'_> { fn report_diagnostics( &mut self, - _execution: &Execution, + _execution: &dyn Execution, payload: DiagnosticsPayload, verbose: bool, _working_directory: Option<&Utf8Path>, diff --git a/crates/biome_cli/src/reporter/summary.rs b/crates/biome_cli/src/reporter/summary.rs index 595c5423e69f..c06bcf28c03d 100644 --- a/crates/biome_cli/src/reporter/summary.rs +++ b/crates/biome_cli/src/reporter/summary.rs @@ -1,6 +1,7 @@ use crate::reporter::terminal::ConsoleTraversalSummary; -use crate::reporter::{EvaluatedPathsDiagnostic, FixedPathsDiagnostic}; -use crate::{DiagnosticsPayload, Execution, Reporter, ReporterVisitor, TraversalSummary}; +use crate::reporter::{EvaluatedPathsDiagnostic, FixedPathsDiagnostic, Reporter, ReporterVisitor}; +use crate::runner::execution::Execution; +use crate::{DiagnosticsPayload, TraversalSummary}; use biome_console::fmt::{Display, Formatter}; use biome_console::{Console, ConsoleExt, MarkupBuf, markup}; use biome_diagnostics::advice::ListAdvice; @@ -15,19 +16,19 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Debug; use std::io; -pub(crate) struct SummaryReporter { +pub(crate) struct SummaryReporter<'a> { pub(crate) summary: TraversalSummary, pub(crate) diagnostics_payload: DiagnosticsPayload, - pub(crate) execution: Execution, + pub(crate) execution: &'a dyn Execution, pub(crate) evaluated_paths: BTreeSet, pub(crate) working_directory: Option, pub(crate) verbose: bool, } -impl Reporter for SummaryReporter { +impl Reporter for SummaryReporter<'_> { fn write(self, visitor: &mut dyn ReporterVisitor) -> io::Result<()> { visitor.report_diagnostics( - &self.execution, + self.execution, self.diagnostics_payload, self.verbose, self.working_directory.as_deref(), @@ -36,7 +37,7 @@ impl Reporter for SummaryReporter { visitor .report_handled_paths(self.evaluated_paths, self.working_directory.as_deref())?; } - visitor.report_summary(&self.execution, self.summary, self.verbose)?; + visitor.report_summary(self.execution, self.summary, self.verbose)?; Ok(()) } } @@ -46,7 +47,7 @@ pub(crate) struct SummaryReporterVisitor<'a>(pub(crate) &'a mut dyn Console); impl ReporterVisitor for SummaryReporterVisitor<'_> { fn report_summary( &mut self, - execution: &Execution, + execution: &dyn Execution, summary: TraversalSummary, verbose: bool, ) -> io::Result<()> { @@ -65,7 +66,59 @@ impl ReporterVisitor for SummaryReporterVisitor<'_> { } self.0.log(markup! { - {ConsoleTraversalSummary(execution.traversal_mode(), &summary, verbose)} + {ConsoleTraversalSummary(execution, &summary, verbose)} + }); + + Ok(()) + } + + fn report_handled_paths( + &mut self, + evaluated_paths: BTreeSet, + working_directory: Option<&Utf8Path>, + ) -> io::Result<()> { + let evaluated_paths_diagnostic = EvaluatedPathsDiagnostic { + advice: ListAdvice { + list: evaluated_paths + .iter() + .map(|p| { + working_directory + .as_ref() + .and_then(|wd| { + p.strip_prefix(wd.as_str()) + .map(|path| path.to_string()) + .ok() + }) + .unwrap_or(p.to_string()) + }) + .collect(), + }, + }; + + let fixed_paths_diagnostic = FixedPathsDiagnostic { + advice: ListAdvice { + list: evaluated_paths + .iter() + .filter(|p| p.was_written()) + .map(|p| { + working_directory + .as_ref() + .and_then(|wd| { + p.strip_prefix(wd.as_str()) + .map(|path| path.to_string()) + .ok() + }) + .unwrap_or(p.to_string()) + }) + .collect(), + }, + }; + + self.0.log(markup! { + {PrintDiagnostic::verbose(&evaluated_paths_diagnostic)} + }); + self.0.log(markup! { + {PrintDiagnostic::verbose(&fixed_paths_diagnostic)} }); Ok(()) @@ -73,7 +126,7 @@ impl ReporterVisitor for SummaryReporterVisitor<'_> { fn report_diagnostics( &mut self, - execution: &Execution, + execution: &dyn Execution, diagnostics_payload: DiagnosticsPayload, verbose: bool, working_directory: Option<&Utf8Path>, @@ -97,9 +150,8 @@ impl ReporterVisitor for SummaryReporterVisitor<'_> { if diagnostic.severity() >= diagnostics_payload.diagnostic_level { if diagnostic.tags().is_verbose() { if verbose { - if (execution.is_check() || execution.is_lint()) - && let Some(category) = category - && category.name().starts_with("lint/") + if let Some(category) = category + && should_report_lint_diagnostic(category) { files_to_diagnostics.insert_rule_for_file( category.name(), @@ -120,16 +172,12 @@ impl ReporterVisitor for SummaryReporterVisitor<'_> { if (execution.is_check() || execution.is_lint() || execution.is_ci()) && let Some(category) = category - && (category.name().starts_with("lint/") - || category.name().starts_with("suppressions/") - || category.name().starts_with("assist/") - || category.name().starts_with("plugin")) + && should_report_lint_diagnostic(category) { files_to_diagnostics.insert_rule_for_file(category.name(), severity, location); } - if (execution.is_check() || execution.is_format() || execution.is_ci()) - && let Some(category) = category + if let Some(category) = category && category.name() == "format" { files_to_diagnostics.insert_format(location); @@ -141,58 +189,13 @@ impl ReporterVisitor for SummaryReporterVisitor<'_> { Ok(()) } +} - fn report_handled_paths( - &mut self, - evaluated_paths: BTreeSet, - working_directory: Option<&Utf8Path>, - ) -> io::Result<()> { - let evaluated_paths_diagnostic = EvaluatedPathsDiagnostic { - advice: ListAdvice { - list: evaluated_paths - .iter() - .map(|p| { - working_directory - .as_ref() - .and_then(|wd| { - p.strip_prefix(wd.as_str()) - .map(|path| path.to_string()) - .ok() - }) - .unwrap_or(p.to_string()) - }) - .collect(), - }, - }; - - let fixed_paths_diagnostic = FixedPathsDiagnostic { - advice: ListAdvice { - list: evaluated_paths - .iter() - .filter(|p| p.was_written()) - .map(|p| { - working_directory - .as_ref() - .and_then(|wd| { - p.strip_prefix(wd.as_str()) - .map(|path| path.to_string()) - .ok() - }) - .unwrap_or(p.to_string()) - }) - .collect(), - }, - }; - - self.0.log(markup! { - {PrintDiagnostic::verbose(&evaluated_paths_diagnostic)} - }); - self.0.log(markup! { - {PrintDiagnostic::verbose(&fixed_paths_diagnostic)} - }); - - Ok(()) - } +fn should_report_lint_diagnostic(category: &Category) -> bool { + category.name().starts_with("lint/") + || category.name().starts_with("suppressions/") + || category.name().starts_with("assist/") + || category.name().starts_with("plugin") } #[derive(Debug, Default)] diff --git a/crates/biome_cli/src/reporter/terminal.rs b/crates/biome_cli/src/reporter/terminal.rs index dce07f2047fb..3f4398e146bd 100644 --- a/crates/biome_cli/src/reporter/terminal.rs +++ b/crates/biome_cli/src/reporter/terminal.rs @@ -1,9 +1,8 @@ -use crate::Reporter; -use crate::execute::{Execution, TraversalMode}; use crate::reporter::{ - DiagnosticsPayload, EvaluatedPathsDiagnostic, FixedPathsDiagnostic, ReporterVisitor, + DiagnosticsPayload, EvaluatedPathsDiagnostic, FixedPathsDiagnostic, Reporter, ReporterVisitor, TraversalSummary, }; +use crate::runner::execution::Execution; use biome_console::fmt::Formatter; use biome_console::{Console, ConsoleExt, fmt, markup}; use biome_diagnostics::PrintDiagnostic; @@ -14,19 +13,19 @@ use std::collections::BTreeSet; use std::io; use std::time::Duration; -pub(crate) struct ConsoleReporter { +pub(crate) struct ConsoleReporter<'a> { pub(crate) summary: TraversalSummary, pub(crate) diagnostics_payload: DiagnosticsPayload, - pub(crate) execution: Execution, + pub(crate) execution: &'a dyn Execution, pub(crate) evaluated_paths: BTreeSet, pub(crate) working_directory: Option, pub(crate) verbose: bool, } -impl Reporter for ConsoleReporter { +impl<'a> Reporter for ConsoleReporter<'a> { fn write(self, visitor: &mut dyn ReporterVisitor) -> io::Result<()> { visitor.report_diagnostics( - &self.execution, + self.execution, self.diagnostics_payload, self.verbose, self.working_directory.as_deref(), @@ -35,7 +34,7 @@ impl Reporter for ConsoleReporter { visitor .report_handled_paths(self.evaluated_paths, self.working_directory.as_deref())?; } - visitor.report_summary(&self.execution, self.summary, self.verbose)?; + visitor.report_summary(self.execution, self.summary, self.verbose)?; Ok(()) } } @@ -45,7 +44,7 @@ pub(crate) struct ConsoleReporterVisitor<'a>(pub(crate) &'a mut dyn Console); impl ReporterVisitor for ConsoleReporterVisitor<'_> { fn report_summary( &mut self, - execution: &Execution, + execution: &dyn Execution, summary: TraversalSummary, verbose: bool, ) -> io::Result<()> { @@ -64,7 +63,7 @@ impl ReporterVisitor for ConsoleReporterVisitor<'_> { } self.0.log(markup! { - {ConsoleTraversalSummary(execution.traversal_mode(), &summary, verbose)} + {ConsoleTraversalSummary(execution, &summary, verbose)} }); Ok(()) @@ -124,7 +123,7 @@ impl ReporterVisitor for ConsoleReporterVisitor<'_> { fn report_diagnostics( &mut self, - execution: &Execution, + execution: &dyn Execution, diagnostics_payload: DiagnosticsPayload, verbose: bool, _working_directory: Option<&Utf8Path>, @@ -163,14 +162,17 @@ impl fmt::Display for Files { } } -struct SummaryDetail<'a>(pub(crate) &'a TraversalMode, usize); +struct SummaryDetail<'a>(pub(crate) &'a dyn Execution, usize); impl fmt::Display for SummaryDetail<'_> { fn fmt(&self, fmt: &mut Formatter) -> io::Result<()> { - let Self(mode, files) = self; - if let TraversalMode::Search { .. } = mode { + let Self(execution, files) = self; + + if execution.is_search() { return Ok(()); } + // For now, we'll assume all executions except search can have fixes + // This can be refined later when we have more specific execution types if *files > 0 { fmt.write_markup(markup! { @@ -194,57 +196,27 @@ impl fmt::Display for ScanSummary<'_> { } } -struct SummaryTotal<'a>(&'a TraversalMode, usize, &'a Duration); +struct SummaryTotal<'a>(&'a dyn Execution, usize, &'a Duration); impl fmt::Display for SummaryTotal<'_> { fn fmt(&self, fmt: &mut Formatter) -> io::Result<()> { - let Self(mode, files, duration) = self; - let files = Files(*files); - match mode { - TraversalMode::Check { .. } | TraversalMode::Lint { .. } | TraversalMode::CI { .. } => { - fmt.write_markup(markup! { - "Checked "{files}" in "{duration}"." - }) - } - TraversalMode::Format { write, .. } => { - if *write { - fmt.write_markup(markup! { - "Formatted "{files}" in "{duration}"." - }) - } else { - fmt.write_markup(markup! { - "Checked "{files}" in "{duration}"." - }) - } - } - - TraversalMode::Migrate { write, .. } => { - if *write { - fmt.write_markup(markup! { - "Migrated your configuration file in "{duration}"." - }) - } else { - fmt.write_markup(markup! { - "Checked your configuration file in "{duration}"." - }) - } - } + let Self(execution, files, duration) = self; + let summary_phrase = execution.summary_phrase(*files, duration); - TraversalMode::Search { .. } => fmt.write_markup(markup! { - "Searched "{files}" in "{duration}"." - }), - } + fmt.write_markup(markup! { + {summary_phrase} + }) } } pub(crate) struct ConsoleTraversalSummary<'a>( - pub(crate) &'a TraversalMode, + pub(crate) &'a dyn Execution, pub(crate) &'a TraversalSummary, pub(crate) bool, ); impl fmt::Display for ConsoleTraversalSummary<'_> { fn fmt(&self, fmt: &mut Formatter) -> io::Result<()> { - let Self(mode, summary, verbose) = *self; + let Self(execution, summary, verbose) = *self; let mut duration = summary.duration; if !verbose { if let Some(scanner_duration) = summary.scanner_duration { @@ -255,12 +227,14 @@ impl fmt::Display for ConsoleTraversalSummary<'_> { fmt.write_markup(markup!({scanned}))?; fmt.write_str("\n")?; } - let total = SummaryTotal(mode, summary.changed + summary.unchanged, &duration); - let detail = SummaryDetail(mode, summary.changed); + let total = SummaryTotal(execution, summary.changed + summary.unchanged, &duration); + let detail = SummaryDetail(execution, summary.changed); fmt.write_markup(markup!({total}{detail}))?; // The search emits info diagnostics, so we use if control-flow to print a different message - if let TraversalMode::Search { .. } = mode { + // For now, we'll assume this is a search command if there are matches + // This can be refined later when we have more specific execution types + if summary.matches > 0 { if summary.matches == 1 { fmt.write_markup(markup!(" ""Found "{summary.matches}" match."))? } else { diff --git a/crates/biome_cli/src/reports/mod.rs b/crates/biome_cli/src/reports/mod.rs deleted file mode 100644 index 35c2ff161400..000000000000 --- a/crates/biome_cli/src/reports/mod.rs +++ /dev/null @@ -1,92 +0,0 @@ -pub mod formatter; - -use crate::reports::formatter::{FormatterReportFileDetail, FormatterReportSummary}; -use biome_diagnostics::{Category, Severity}; -use biome_service::WorkspaceError; -use formatter::FormatterReport; -use rustc_hash::FxHashMap; -use serde::Serialize; - -#[derive(Debug, Default, Serialize)] -pub struct Report { - /// Information related to the formatter - formatter: FormatterReport, - - /// Diagnostics tracked during a generic traversal - /// - /// The key is the path of the file where the diagnostics occurred - diagnostics: FxHashMap, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub enum ReportErrorKind { - Diagnostic(ReportDiagnostic), - Diff(ReportDiff), -} - -/// Information computed from a [diagnostic][biome_diagnostics::Diagnostic] -#[derive(Debug, Serialize)] -pub struct ReportDiagnostic { - /// Severity of the [diagnostic][biome_diagnostics::Diagnostic] - pub severity: Severity, - /// The code of the [diagnostic][biome_diagnostics::Diagnostic] - pub code: Option<&'static Category>, - /// The title of the [diagnostic][biome_diagnostics::Diagnostic] - pub title: String, -} - -/// Information computed from a diff result -#[derive(Debug, Serialize)] -pub struct ReportDiff { - /// The severity fo the diff - pub severity: Severity, - /// How was the code before the command - pub before: String, - /// How is the code after the command - pub after: String, -} - -impl Default for ReportDiagnostic { - fn default() -> Self { - Self { - severity: Severity::Error, - code: None, - title: String::new(), - } - } -} - -#[derive(Debug)] -pub enum ReportKind { - Formatter(String, FormatterReportFileDetail), - Error(String, ReportErrorKind), -} - -impl Report { - /// Creates or updates a stat - pub fn push_detail_report(&mut self, stat: ReportKind) { - match stat { - ReportKind::Formatter(path, stat) => { - self.formatter.insert_file_content(path, stat); - } - ReportKind::Error(path, error) => { - self.diagnostics.insert(path, error); - } - } - } - - /// It tracks a generic diagnostic - pub fn push_error(&mut self, path: String, err: ReportErrorKind) { - self.diagnostics.insert(path, err); - } - - pub fn set_formatter_summary(&mut self, summary: FormatterReportSummary) { - self.formatter.set_summary(summary); - } - - pub fn as_serialized_reports(&self) -> Result { - serde_json::to_string(&self) - .map_err(|err| WorkspaceError::report_not_serializable(err.to_string())) - } -} diff --git a/crates/biome_cli/src/runner/collector.rs b/crates/biome_cli/src/runner/collector.rs new file mode 100644 index 000000000000..7fbc2fe69fad --- /dev/null +++ b/crates/biome_cli/src/runner/collector.rs @@ -0,0 +1,65 @@ +use crate::runner::execution::Execution; +use crate::runner::process_file::Message; +use biome_diagnostics::{DiagnosticTags, Severity}; +use camino::Utf8PathBuf; +use crossbeam::channel::Receiver; +use std::time::Duration; + +pub(crate) trait Collector: Send + Sync { + type Result: Send + Sync; + + fn should_collect(&self) -> bool; + + fn diagnostic_level(&self) -> Severity; + + fn verbose(&self) -> bool; + + /// Checks if the diagnostic we received from the thread should be considered or not. Logic: + /// - it should not be considered if its severity level is lower than the one provided via CLI; + /// - it should not be considered if it's a verbose diagnostic and the CLI **didn't** request a `--verbose` option. + fn should_skip_diagnostic(&self, severity: Severity, diagnostic_tags: DiagnosticTags) -> bool { + if severity < self.diagnostic_level() { + return true; + } + + if diagnostic_tags.is_verbose() && !self.verbose() { + return true; + } + + false + } + fn run( + &self, + _receiver: Receiver, + _interner: Receiver, + _execution: &dyn Execution, + ); + + fn result(self, _duration: Duration) -> Self::Result; +} + +impl Collector for () { + type Result = (); + + fn should_collect(&self) -> bool { + false + } + + fn diagnostic_level(&self) -> Severity { + Severity::Hint + } + + fn verbose(&self) -> bool { + false + } + + fn run( + &self, + _receiver: Receiver, + _interner: Receiver, + _execution: &dyn Execution, + ) { + } + + fn result(self, _duration: Duration) -> Self::Result {} +} diff --git a/crates/biome_cli/src/runner/crawler.rs b/crates/biome_cli/src/runner/crawler.rs new file mode 100644 index 000000000000..ffa7fd50e212 --- /dev/null +++ b/crates/biome_cli/src/runner/crawler.rs @@ -0,0 +1,250 @@ +use crate::CliDiagnostic; +use crate::runner::collector::Collector; +use crate::runner::execution::Execution; +use crate::runner::handler::Handler; +use crate::runner::process_file::{Message, MessageStat, ProcessFile}; +use biome_diagnostics::Error; +use biome_fs::{BiomePath, FileSystem, PathInterner, TraversalContext, TraversalScope}; +use biome_service::Workspace; +use biome_service::projects::ProjectKey; +use camino::Utf8PathBuf; +use crossbeam::channel::{Sender, unbounded}; +use std::collections::BTreeSet; +use std::marker::PhantomData; +use std::sync::RwLock; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::thread; +use std::time::{Duration, Instant}; +use tracing::instrument; + +pub trait Crawler { + type Handler: Handler; + type ProcessFile: ProcessFile; + type Collector: Collector; + + fn output( + collector_result: ::Result, + evaluated_paths: BTreeSet, + duration: Duration, + ) -> Output; + + fn crawl( + execution: &dyn Execution, + workspace: &dyn Workspace, + fs: &dyn FileSystem, + project_key: ProjectKey, + inputs: Vec, + collector: Self::Collector, + ) -> Result { + let (interner, recv_files) = PathInterner::new(); + let (sender, receiver) = unbounded(); + + let (duration, evaluated_paths) = thread::scope(|s| { + let handler = thread::Builder::new() + .name(String::from("biome::console")) + .spawn_scoped(s, || collector.run(receiver, recv_files, execution)) + .expect("failed to spawn console thread"); + + // The traversal context is scoped to ensure all the channels it + // contains are properly closed once the traversal finishes + let (elapsed, evaluated_paths) = Self::crawl_inputs( + fs, + inputs, + // Don't move it. If ctx is declared outside of this function, it doesn't + // go out of scope, causing a deadlock because the main thread waits for + // ctx to be dropped + &CrawlerOptions::new(fs, workspace, project_key, interner, sender, execution), + ); + // wait for the main thread to finish + handler.join().unwrap(); + + (elapsed, evaluated_paths) + }); + + execution.on_post_crawl(workspace)?; + let result = collector.result(duration); + Ok(Self::output(result, evaluated_paths, duration)) + } + + /// Initiate the filesystem traversal tasks with the provided input paths and + /// run it to completion, returning the duration of the process and the evaluated paths + fn crawl_inputs( + fs: &dyn FileSystem, + inputs: Vec, + ctx: &CrawlerOptions, + ) -> (Duration, BTreeSet) { + let start = Instant::now(); + fs.traversal(Box::new(move |scope: &dyn TraversalScope| { + for input in inputs { + scope.evaluate(ctx, Utf8PathBuf::from(input)); + } + })); + + let paths = ctx.evaluated_paths(); + fs.traversal(Box::new(|scope: &dyn TraversalScope| { + for path in paths { + scope.handle(ctx, path.to_path_buf()); + } + })); + + (start.elapsed(), ctx.evaluated_paths()) + } +} + +pub trait CrawlerContext: TraversalContext { + fn increment_changed(&self, path: &BiomePath); + fn increment_unchanged(&self); + fn increment_matches(&self, num_matches: usize); + fn increment_skipped(&self); + /// Send a message to the display thread + fn push_message(&self, msg: Message); + fn fs(&self) -> &dyn FileSystem; + fn workspace(&self) -> &dyn Workspace; + fn project_key(&self) -> ProjectKey; + fn execution(&self) -> &dyn Execution; +} + +/// Context object shared between directory traversal tasks +pub(crate) struct CrawlerOptions<'ctx, 'app, H, P> { + /// Shared instance of [FileSystem] + pub(crate) fs: &'app dyn FileSystem, + /// Instance of [Workspace] used by this instance of the CLI + pub(crate) workspace: &'ctx dyn Workspace, + /// Key of the project in which we're traversing. + pub(crate) project_key: ProjectKey, + /// File paths interner cache used by the filesystem traversal + interner: PathInterner, + /// Shared atomic counter storing the number of changed files + changed: AtomicUsize, + /// Shared atomic counter storing the number of unchanged files + unchanged: AtomicUsize, + /// Shared atomic counter storing the number of unchanged files + matches: AtomicUsize, + /// Shared atomic counter storing the number of skipped files + skipped: AtomicUsize, + /// Channel sending messages to the display thread + pub(crate) messages: Sender, + /// List of paths that should be processed + pub(crate) evaluated_paths: RwLock>, + + execution: &'app dyn Execution, + + handler: H, + + _p: PhantomData

, +} + +impl<'ctx, 'app, H, P> CrawlerContext for CrawlerOptions<'ctx, 'app, H, P> +where + H: Handler, + P: ProcessFile, +{ + fn increment_changed(&self, path: &BiomePath) { + self.changed.fetch_add(1, Ordering::Relaxed); + self.evaluated_paths + .write() + .unwrap() + .replace(path.to_written()); + self.push_message(Message::Stats(MessageStat::Changed)); + } + fn increment_unchanged(&self) { + self.push_message(Message::Stats(MessageStat::Unchanged)); + self.unchanged.fetch_add(1, Ordering::Relaxed); + } + + fn increment_matches(&self, num_matches: usize) { + self.push_message(Message::Stats(MessageStat::Matches)); + self.matches.fetch_add(num_matches, Ordering::Relaxed); + } + + fn increment_skipped(&self) { + self.push_message(Message::Stats(MessageStat::Skipped)); + self.skipped.fetch_add(1, Ordering::Relaxed); + } + + /// Send a message to the display thread + fn push_message(&self, msg: Message) { + self.messages.send(msg).ok(); + } + + fn fs(&self) -> &dyn FileSystem { + self.fs + } + + fn workspace(&self) -> &dyn Workspace { + self.workspace + } + + fn project_key(&self) -> ProjectKey { + self.project_key + } + + fn execution(&self) -> &dyn Execution { + self.execution + } +} + +impl<'ctx, 'app, I, P> CrawlerOptions<'ctx, 'app, I, P> +where + I: Handler, + P: ProcessFile, +{ + pub(crate) fn new( + fs: &'app dyn FileSystem, + workspace: &'ctx dyn Workspace, + project_key: ProjectKey, + interner: PathInterner, + sender: Sender, + execution: &'app dyn Execution, + ) -> Self { + Self { + fs, + workspace, + project_key, + interner, + messages: sender, + evaluated_paths: RwLock::default(), + handler: I::default(), + changed: AtomicUsize::new(0), + unchanged: AtomicUsize::new(0), + matches: AtomicUsize::new(0), + skipped: AtomicUsize::new(0), + execution, + _p: PhantomData::

, + } + } +} + +impl TraversalContext for CrawlerOptions<'_, '_, I, P> +where + I: Handler, + P: ProcessFile, +{ + fn interner(&self) -> &PathInterner { + &self.interner + } + + fn evaluated_paths(&self) -> BTreeSet { + self.evaluated_paths.read().unwrap().clone() + } + + fn push_diagnostic(&self, error: Error) { + self.push_message(error.into()); + } + + #[instrument(level = "debug", skip(self, biome_path))] + fn can_handle(&self, biome_path: &BiomePath) -> bool { + self.handler.can_handle(biome_path, self) + } + + fn handle_path(&self, path: BiomePath) { + self.handler.handle_path::(&path, self) + } + + fn store_path(&self, path: BiomePath) { + self.evaluated_paths + .write() + .unwrap() + .insert(BiomePath::new(path.as_path())); + } +} diff --git a/crates/biome_cli/src/execute/diagnostics.rs b/crates/biome_cli/src/runner/diagnostics.rs similarity index 100% rename from crates/biome_cli/src/execute/diagnostics.rs rename to crates/biome_cli/src/runner/diagnostics.rs diff --git a/crates/biome_cli/src/runner/execution.rs b/crates/biome_cli/src/runner/execution.rs new file mode 100644 index 000000000000..238e6ac9b8c4 --- /dev/null +++ b/crates/biome_cli/src/runner/execution.rs @@ -0,0 +1,206 @@ +use crate::cli_options::CliOptions; +use biome_configuration::analyzer::AnalyzerSelector; +use biome_console::MarkupBuf; +use biome_diagnostics::Category; +use biome_fs::BiomePath; +use biome_grit_patterns::GritTargetLanguage; +use biome_service::configuration::ProjectScanComputer; +use biome_service::workspace::{ + FeatureName, FeaturesSupported, FixFileMode, PatternId, ScanKind, SupportKind, +}; +use biome_service::{Workspace, WorkspaceError}; +use camino::{Utf8Path, Utf8PathBuf}; +use std::time::Duration; +use tracing::info; + +pub(crate) trait Execution: Send + Sync + std::panic::RefUnwindSafe { + /// The features that this command requires to be enabled. + fn features(&self) -> FeatureName; + + /// Whether this command can handle the incoming file given its features. + fn can_handle(&self, features: FeaturesSupported) -> bool; + + /// Hook used after the crawling, and before processing the final output. + fn on_post_crawl(&self, _workspace: &dyn Workspace) -> Result<(), WorkspaceError> { + Ok(()) + } + + fn get_max_diagnostics(&self, cli_options: &CliOptions) -> u32 { + if cli_options.reporter.is_default() { + cli_options.max_diagnostics.into() + } else { + info!( + "Removing the limit of --max-diagnostics, because of a reporter different from the default one: {}", + cli_options.reporter + ); + u32::MAX + } + } + + /// Whether this command should be aware of the VCS integration + fn is_vcs_targeted(&self) -> bool; + + /// Used by [crate::runner::ProcessFile::execute] to determine which kind of support kind the file has + fn supports_kind(&self, file_features: &FeaturesSupported) -> Option; + + /// It should returns the value of `--stdin-file-path` + fn get_stdin_file_path(&self) -> Option<&str>; + + /// Derives the [ScanKind] for this execution + fn scan_kind_computer(&self, computer: ProjectScanComputer) -> ScanKind { + computer.compute() + } + + fn compute_scan_kind( + &self, + target_known_paths: &[String], + working_dir: &Utf8Path, + scan_kind: ScanKind, + ) -> ScanKind { + match scan_kind { + ScanKind::KnownFiles => { + let target_paths = target_known_paths + .iter() + .map(|path| BiomePath::new(working_dir.join(path))) + .collect(); + ScanKind::TargetedKnownFiles { + target_paths, + descend_from_targets: true, + } + } + _ => scan_kind, + } + } + + /// If the execution is running in CI mode, it will return true. + /// Otherwise, it will return false. + /// + /// At the moment, CI is equal to `biome ci` + fn is_ci(&self) -> bool { + false + } + + /// `biome check` command + fn is_check(&self) -> bool { + false + } + + /// `biome lint` command + fn is_lint(&self) -> bool { + false + } + + /// The [Category] that should be used when running the command. + fn as_diagnostic_category(&self) -> &'static Category; + + /// Whether the execution should apply safe fixes + fn is_safe_fixes_enabled(&self) -> bool { + false + } + + /// Whether the execution should apply safe and unsafe fixes + fn is_safe_and_unsafe_fixes_enabled(&self) -> bool { + false + } + + /// `biome search` command + fn is_search(&self) -> bool { + false + } + + /// The kind of fix mode to apply + fn as_fix_file_mode(&self) -> Option { + None + } + + /// The value of `--skip-parse-errors` + fn should_skip_parse_errors(&self) -> bool { + false + } + + /// The value of `--suppress` + fn suppress(&self) -> bool { + false + } + + /// The value of `--suppression-reason` + fn suppression_reason(&self) -> Option<&str> { + None + } + + /// Whether this command needs to write on the file system + fn requires_write_access(&self) -> bool; + + /// Values of `--only` and `--skip` + fn analyzer_selectors(&self) -> AnalyzerSelectors; + + /// Whether assists should be enforced + fn should_enforce_assist(&self) -> bool { + false + } + + /// `biome format` command + fn is_format(&self) -> bool { + false + } + + /// The search target language + fn search_language(&self) -> Option { + None + } + /// The search pattern to search for. + fn search_pattern(&self) -> Option<&PatternId> { + None + } + + /// Used when printing summary + fn summary_phrase(&self, files: usize, duration: &Duration) -> MarkupBuf; +} + +#[derive(Debug, Default, Clone)] +pub(crate) struct AnalyzerSelectors { + pub(crate) only: Vec, + pub(crate) skip: Vec, +} + +/// A type that holds the information to execute the CLI via `stdin +#[derive(Debug, Clone)] +pub struct Stdin( + /// The virtual path to the file + Utf8PathBuf, + /// The content of the file + String, +); + +impl Stdin { + pub(crate) fn as_path(&self) -> &Utf8Path { + self.0.as_path() + } + + pub(crate) fn as_content(&self) -> &str { + self.1.as_str() + } +} + +impl From<(Utf8PathBuf, String)> for Stdin { + fn from((path, content): (Utf8PathBuf, String)) -> Self { + Self(path, content) + } +} + +#[derive(Default, Debug, Clone)] +pub struct VcsTargeted { + pub staged: bool, + pub changed: bool, +} + +impl From<(bool, bool)> for VcsTargeted { + fn from((staged, changed): (bool, bool)) -> Self { + Self { staged, changed } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum ExecutionEnvironment { + GitHub, +} diff --git a/crates/biome_cli/src/runner/finalizer.rs b/crates/biome_cli/src/runner/finalizer.rs new file mode 100644 index 000000000000..fe074a88ff67 --- /dev/null +++ b/crates/biome_cli/src/runner/finalizer.rs @@ -0,0 +1,40 @@ +use crate::CliDiagnostic; +use crate::cli_options::CliOptions; +use crate::runner::execution::Execution; +use biome_console::Console; +use biome_fs::FileSystem; +use biome_service::Workspace; +use biome_service::projects::ProjectKey; +use std::time::Duration; + +pub trait Finalizer { + /// The type accepted by the trait. That's usually the output of the crawler. + type Input; + + /// Optional hook to run before finalization. Useful for commands that need + /// to work with the Workspace before finally finalise the command. + fn before_finalize( + _project_key: ProjectKey, + _fs: &dyn FileSystem, + _workspace: &dyn Workspace, + _crawler_output: &mut Self::Input, + ) -> Result<(), CliDiagnostic> { + Ok(()) + } + + /// Finalize the command. This is where the command needs to work the final input. + /// This step can be used to print diagnostics to console. + fn finalize(payload: FinalizePayload<'_, Self::Input>) -> Result<(), CliDiagnostic>; +} + +pub(crate) struct FinalizePayload<'a, I> { + pub(crate) project_key: ProjectKey, + pub(crate) fs: &'a dyn FileSystem, + pub(crate) workspace: &'a dyn Workspace, + pub(crate) scan_duration: Option, + pub(crate) console: &'a mut dyn Console, + pub(crate) cli_options: &'a CliOptions, + pub(crate) crawler_output: I, + pub(crate) execution: &'a dyn Execution, + pub(crate) paths: Vec, +} diff --git a/crates/biome_cli/src/runner/handler.rs b/crates/biome_cli/src/runner/handler.rs new file mode 100644 index 000000000000..fa15dafc3c93 --- /dev/null +++ b/crates/biome_cli/src/runner/handler.rs @@ -0,0 +1,182 @@ +use crate::runner::crawler::CrawlerContext; +use crate::runner::diagnostics::PanicDiagnostic; +use crate::runner::process_file::{FileStatus, ProcessFile}; +use biome_diagnostics::{DiagnosticExt, DiagnosticTags, category}; +use biome_fs::{BiomePath, FileSystem, TraversalContext}; +use biome_service::file_handlers::DocumentFileSource; +use biome_service::workspace::{ + FileFeaturesResult, IgnoreKind, PathIsIgnoredParams, SupportsFeatureParams, +}; +use biome_service::{WorkspaceError, extension_error}; +use std::fmt::Debug; +use std::panic::catch_unwind; + +/// Path entries that we want to ignore during the OS traversal. +pub const TRAVERSAL_IGNORE_ENTRIES: &[&[u8]] = &[ + b".git", + b".hg", + b".svn", + b".yarn", + b".DS_Store", + b"node_modules", +]; + +pub trait Handler: Default + Send + Sync + Debug + std::panic::RefUnwindSafe { + fn can_handle(&self, biome_path: &BiomePath, ctx: &Ctx) -> bool + where + Ctx: CrawlerContext, + { + if biome_path + .file_name() + .is_some_and(|file_name| TRAVERSAL_IGNORE_ENTRIES.contains(&file_name.as_bytes())) + { + return false; + } + let fs = ctx.fs(); + let workspace = ctx.workspace(); + let execution = ctx.execution(); + let path = biome_path.as_path(); + let project_key = ctx.project_key(); + if fs.path_is_dir(path) || fs.path_is_symlink(path) { + // handle: + // - directories + // - symlinks + // - unresolved symlinks + // e.g `symlink/subdir` where symlink points to a directory that includes `subdir`. + // Note that `symlink/subdir` is not an existing file. + let can_handle = !workspace + .is_path_ignored(PathIsIgnoredParams { + project_key, + path: biome_path.clone(), + features: execution.features(), + ignore_kind: IgnoreKind::Ancestors, + }) + .unwrap_or_else(|err| { + ctx.push_diagnostic(err.into()); + false + }); + + return can_handle; + } + + // bail on fifo and socket files + if !fs.path_is_file(path) { + return false; + } + + let file_features = workspace.file_features(SupportsFeatureParams { + project_key, + path: biome_path.clone(), + features: execution.features(), + inline_config: None, + }); + + let can_read = DocumentFileSource::can_read(biome_path); + + let file_features = match file_features { + Ok(FileFeaturesResult { + features_supported: file_features, + }) => { + if file_features.is_protected() { + ctx.push_diagnostic( + WorkspaceError::protected_file(biome_path.to_string()).into(), + ); + return false; + } + + if file_features.is_not_supported() && !file_features.is_ignored() && !can_read { + // we should throw a diagnostic if we can't handle a file that isn't ignored + miss_handler_err(ctx, fs, extension_error(biome_path), biome_path); + return false; + } + file_features + } + Err(err) => { + miss_handler_err(ctx, fs, err, biome_path); + return false; + } + }; + + execution.can_handle(file_features) + } + + /// This function wraps the [process_file] function implementing the traversal + /// in a [catch_unwind] block and emit diagnostics in case of error (either the + /// traversal function returns Err or panics) + fn handle_path(&self, biome_path: &BiomePath, ctx: &Ctx) + where + Ctx: CrawlerContext + std::panic::RefUnwindSafe, + P: ProcessFile + std::panic::RefUnwindSafe, + { + // ProcessFile::process_file is generic over Ctx: TraversalContext + // We pass &Ctx which should also implement TraversalContext + + match catch_unwind(move || P::execute(ctx, biome_path)) { + Ok(Ok(FileStatus::Changed)) => { + ctx.increment_changed(biome_path); + } + Ok(Ok(FileStatus::Unchanged)) => { + ctx.increment_unchanged(); + } + Ok(Ok(FileStatus::SearchResult(num_matches, msg))) => { + ctx.increment_unchanged(); + ctx.increment_matches(num_matches); + ctx.push_message(msg); + } + Ok(Ok(FileStatus::Message(msg))) => { + ctx.increment_unchanged(); + ctx.push_message(msg); + } + Ok(Ok(FileStatus::Protected(file_path))) => { + ctx.increment_unchanged(); + ctx.push_diagnostic(WorkspaceError::protected_file(file_path).into()); + } + Ok(Ok(FileStatus::Ignored)) => {} + Ok(Err(err)) => { + ctx.increment_unchanged(); + ctx.increment_skipped(); + ctx.push_message(err); + } + Err(err) => { + let message = match err.downcast::() { + Ok(msg) => format!("processing panicked: {msg}"), + Err(err) => match err.downcast::<&'static str>() { + Ok(msg) => format!("processing panicked: {msg}"), + Err(_) => String::from("processing panicked"), + }, + }; + + ctx.push_message( + PanicDiagnostic { message } + .with_file_path(biome_path.to_string()) + .into(), + ); + } + } + } +} + +impl Handler for () {} + +pub(crate) fn miss_handler_err( + ctx: &dyn TraversalContext, + fs: &dyn FileSystem, + err: WorkspaceError, + biome_path: &BiomePath, +) { + let file_path = fs + .working_directory() + .as_ref() + .and_then(|wd| { + biome_path + .strip_prefix(wd) + .ok() + .map(|path| path.to_string()) + }) + .unwrap_or(biome_path.to_string()); + ctx.push_diagnostic( + err.with_category(category!("files/missingHandler")) + .with_file_path(file_path) + .with_tags(DiagnosticTags::VERBOSE), + ); +} diff --git a/crates/biome_cli/src/runner/impls/collectors/default.rs b/crates/biome_cli/src/runner/impls/collectors/default.rs new file mode 100644 index 000000000000..5f92dcc1a584 --- /dev/null +++ b/crates/biome_cli/src/runner/impls/collectors/default.rs @@ -0,0 +1,375 @@ +use crate::runner::collector::Collector; +use crate::runner::diagnostics::{CIFormatDiffDiagnostic, ContentDiffAdvice, FormatDiffDiagnostic}; +use crate::runner::execution::Execution; +use crate::runner::process_file::{DiffKind, Message, MessageStat}; +use biome_diagnostics::{DiagnosticExt, DiagnosticTags, Error, Resource, Severity}; +use camino::{Utf8Path, Utf8PathBuf}; +use crossbeam::channel::Receiver; +use rustc_hash::FxHashSet; +use std::sync::RwLock; +use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; +use std::time::Duration; + +pub(crate) struct DefaultCollector { + /// The maximum number of diagnostics the console thread is allowed to print + max_diagnostics: u32, + /// The approximate number of diagnostics the console will print before + /// folding the rest into the "skipped diagnostics" counter + remaining_diagnostics: AtomicU32, + /// Mutable reference to a boolean flag tracking whether the console thread + /// printed any error-level message + errors: AtomicU32, + /// Mutable reference to a boolean flag tracking whether the console thread + /// printed any warnings-level message + warnings: AtomicU32, + /// Mutable reference to a boolean flag tracking whether the console thread + /// printed any info-level message + infos: AtomicU32, + /// Whether the console thread should print diagnostics in verbose mode + verbose: bool, + /// The diagnostic level the console thread should print + diagnostic_level: Severity, + + /// Diagnostics that aren't collected + not_printed_diagnostics: AtomicU32, + + /// Diagnostics that should be printed + printed_diagnostics: AtomicU32, + + /// Suggested fixes that were skipped + total_skipped_suggested_fixes: AtomicU32, + + /// The current working directory, borrowed from [FileSystem] + working_directory: Option, + + diagnostics_to_print: RwLock>, + + changed: AtomicUsize, + unchanged: AtomicUsize, + matches: AtomicUsize, + skipped: AtomicUsize, +} + +impl DefaultCollector { + pub(crate) fn new(working_directory: Option<&Utf8Path>) -> Self { + Self { + errors: AtomicU32::new(0), + warnings: AtomicU32::new(0), + infos: AtomicU32::new(0), + remaining_diagnostics: AtomicU32::new(0), + diagnostic_level: Severity::Hint, + verbose: false, + max_diagnostics: 20, + not_printed_diagnostics: AtomicU32::new(0), + printed_diagnostics: AtomicU32::new(0), + total_skipped_suggested_fixes: AtomicU32::new(0), + working_directory: working_directory.map(|wd| wd.to_path_buf()), + diagnostics_to_print: RwLock::default(), + changed: AtomicUsize::new(0), + unchanged: AtomicUsize::new(0), + matches: AtomicUsize::new(0), + skipped: AtomicUsize::new(0), + } + } + + fn push_diagnostic(&self, diagnostic: Error) { + let mut diagnostics_to_print = self.diagnostics_to_print.write().unwrap(); + diagnostics_to_print.push(diagnostic); + } + + pub(crate) fn with_verbose(mut self, verbose: bool) -> Self { + self.verbose = verbose; + self + } + + pub(crate) fn with_max_diagnostics(mut self, value: u32) -> Self { + self.max_diagnostics = value; + self + } + + pub(crate) fn with_diagnostic_level(mut self, value: Severity) -> Self { + self.diagnostic_level = value; + self + } + + fn errors(&self) -> u32 { + self.errors.load(Ordering::Relaxed) + } + + fn warnings(&self) -> u32 { + self.warnings.load(Ordering::Relaxed) + } + + fn infos(&self) -> u32 { + self.infos.load(Ordering::Relaxed) + } + + fn not_printed_diagnostics(&self) -> u32 { + self.not_printed_diagnostics.load(Ordering::Relaxed) + } + + fn skipped_fixes(&self) -> u32 { + self.total_skipped_suggested_fixes.load(Ordering::Relaxed) + } + + fn to_relative_file_path(&self, path: &str) -> String { + let file_path = Utf8Path::new(&path); + self.working_directory + .as_ref() + .and_then(|wd| file_path.strip_prefix(wd.as_str()).ok()) + .map(|path| path.to_string()) + .unwrap_or(file_path.to_string()) + } +} + +impl Collector for DefaultCollector { + type Result = CollectorSummary; + + fn should_collect(&self) -> bool { + let printed_diagnostics = self.printed_diagnostics.load(Ordering::Relaxed); + let should_print = printed_diagnostics < self.max_diagnostics; + if should_print { + self.printed_diagnostics.fetch_add(1, Ordering::Relaxed); + self.remaining_diagnostics.store( + self.max_diagnostics.saturating_sub(printed_diagnostics), + Ordering::Relaxed, + ); + } else { + self.not_printed_diagnostics.fetch_add(1, Ordering::Relaxed); + } + + should_print + } + + fn diagnostic_level(&self) -> Severity { + self.diagnostic_level + } + + fn verbose(&self) -> bool { + self.verbose + } + + fn run( + &self, + receiver: Receiver, + interner: Receiver, + execution: &dyn Execution, + ) { + let mut paths: FxHashSet = FxHashSet::default(); + + while let Ok(msg) = receiver.recv() { + match msg { + Message::SkippedFixes { + skipped_suggested_fixes, + } => { + self.total_skipped_suggested_fixes + .fetch_add(skipped_suggested_fixes, Ordering::Relaxed); + } + + Message::Failure => { + self.errors.fetch_add(1, Ordering::Relaxed); + } + + Message::Error(mut err) => { + let location = err.location(); + if self.should_skip_diagnostic(err.severity(), err.tags()) { + continue; + } + if err.severity() == Severity::Warning { + self.warnings.fetch_add(1, Ordering::Relaxed); + } + if err.severity() == Severity::Information { + self.infos.fetch_add(1, Ordering::Relaxed); + } + if let Some(Resource::File(file_path)) = location.resource.as_ref() { + // Retrieves the file name from the file ID cache, if it's a miss + // flush entries from the interner channel until it's found + let file_name = match paths.get(*file_path) { + Some(path) => Some(path), + None => loop { + match interner.recv() { + Ok(path) => { + paths.insert(path.to_string()); + if path.as_str() == *file_path { + break paths.get(&path.to_string()); + } + } + // In case the channel disconnected without sending + // the path we need, print the error without a file + // name (normally this should never happen) + Err(_) => break None, + } + }, + }; + + if let Some(path) = file_name { + let path = self.to_relative_file_path(path); + err = err.with_file_path(path.as_str()); + } + } + + let should_collect = self.should_collect(); + + if should_collect { + self.push_diagnostic(err) + } + } + + Message::Diagnostics { + file_path, + content, + diagnostics, + skipped_diagnostics, + } => { + // we transform the file string into a path object so we can correctly strip + // the working directory without having leading slash in the file name + let file_path = self.to_relative_file_path(&file_path); + self.not_printed_diagnostics + .fetch_add(skipped_diagnostics, Ordering::Relaxed); + for diag in diagnostics { + let severity = diag.severity(); + if self.should_skip_diagnostic(severity, diag.tags()) { + continue; + } + if severity == Severity::Error { + self.errors.fetch_add(1, Ordering::Relaxed); + } + if severity == Severity::Warning { + self.warnings.fetch_add(1, Ordering::Relaxed); + } + if severity == Severity::Information { + self.infos.fetch_add(1, Ordering::Relaxed); + } + + let should_collect = self.should_collect(); + + let diag = diag + .with_file_path(file_path.as_str()) + .with_file_source_code(&content); + if should_collect || execution.is_ci() { + self.push_diagnostic(diag); + } + } + } + Message::Diff { + file_name, + old, + new, + diff_kind, + } => { + let file_path = self.to_relative_file_path(&file_name); + // A diff is an error in CI mode and in format/check command in non-write mode + let is_error = execution.is_ci() + || (execution.is_format() + || execution.is_check() && !execution.requires_write_access()); + if is_error { + self.errors.fetch_add(1, Ordering::Relaxed); + } + + let severity: Severity = if is_error { + Severity::Error + } else { + // we set lowest + Severity::Hint + }; + + if self.should_skip_diagnostic(severity, DiagnosticTags::empty()) { + continue; + } + let should_collect = self.should_collect(); + + if should_collect { + match diff_kind { + DiffKind::Format => { + let diag = if execution.is_ci() { + CIFormatDiffDiagnostic { + diff: ContentDiffAdvice { + old: old.clone(), + new: new.clone(), + }, + } + .with_severity(severity) + .with_file_source_code(old.clone()) + .with_file_path(file_path.clone()) + } else { + FormatDiffDiagnostic { + diff: ContentDiffAdvice { + old: old.clone(), + new: new.clone(), + }, + } + .with_severity(severity) + .with_file_source_code(old.clone()) + .with_file_path(file_path.clone()) + }; + if should_collect || execution.is_ci() { + self.push_diagnostic(diag); + } + } + } + } + } + Message::Stats(stats) => match stats { + MessageStat::Changed => { + self.changed.fetch_add(1, Ordering::Relaxed); + } + MessageStat::Unchanged => { + self.unchanged.fetch_add(1, Ordering::Relaxed); + } + MessageStat::Matches => { + self.matches.fetch_add(1, Ordering::Relaxed); + } + MessageStat::Skipped => { + self.skipped.fetch_add(1, Ordering::Relaxed); + } + }, + } + } + } + + fn result(self, duration: Duration) -> Self::Result { + let errors = self.errors(); + let warnings = self.warnings(); + let infos = self.infos(); + let suggested_fixes_skipped = self.skipped_fixes(); + let diagnostics_not_printed = self.not_printed_diagnostics(); + let changed = self.changed.load(Ordering::Relaxed); + let unchanged = self.unchanged.load(Ordering::Relaxed); + let matches = self.matches.load(Ordering::Relaxed); + let skipped = self.skipped.load(Ordering::Relaxed); + // last + let diagnostics_to_print = self.diagnostics_to_print.into_inner().unwrap(); + + CollectorSummary { + duration, + scanner_duration: None, + errors, + warnings, + infos, + suggested_fixes_skipped, + diagnostics_not_printed, + diagnostics: diagnostics_to_print, + changed, + skipped, + matches, + unchanged, + } + } +} + +pub(crate) struct CollectorSummary { + // We skip it during testing because the time isn't predictable + pub duration: Duration, + // We skip it during testing because the time isn't predictable + pub scanner_duration: Option, + pub errors: u32, + pub warnings: u32, + pub infos: u32, + pub suggested_fixes_skipped: u32, + pub diagnostics_not_printed: u32, + pub diagnostics: Vec, + pub changed: usize, + pub unchanged: usize, + pub matches: usize, + pub skipped: usize, +} diff --git a/crates/biome_cli/src/runner/impls/collectors/mod.rs b/crates/biome_cli/src/runner/impls/collectors/mod.rs new file mode 100644 index 000000000000..4ac00fe18338 --- /dev/null +++ b/crates/biome_cli/src/runner/impls/collectors/mod.rs @@ -0,0 +1 @@ +pub(crate) mod default; diff --git a/crates/biome_cli/src/runner/impls/commands/custom_execution.rs b/crates/biome_cli/src/runner/impls/commands/custom_execution.rs new file mode 100644 index 000000000000..13d9b4a3f102 --- /dev/null +++ b/crates/biome_cli/src/runner/impls/commands/custom_execution.rs @@ -0,0 +1,155 @@ +use crate::cli_options::CliOptions; +use crate::runner::execution::Execution; +use crate::runner::{CommandRunner, ConfiguredWorkspace}; +use crate::{CliDiagnostic, CliSession}; +use biome_configuration::Configuration; +use biome_console::Console; +use biome_fs::FileSystem; +use biome_service::workspace::ScanKind; +use biome_service::{Workspace, WorkspaceError}; +use camino::Utf8PathBuf; +use std::ffi::OsString; +use std::ops::{Deref, DerefMut}; + +/// A command that doesn't require crawling but requires a custom execution +pub(crate) struct CustomExecutionCmdImpl(pub C) +where + C: CustomExecutionCmd; + +impl Deref for CustomExecutionCmdImpl +where + C: CustomExecutionCmd, +{ + type Target = C; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for CustomExecutionCmdImpl +where + C: CustomExecutionCmd, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub(crate) trait CustomExecutionCmd { + /// Alias of [CommandRunner::command_name] + fn command_name(&self) -> &'static str; + + /// Alias of [CommandRunner::minimal_scan_kind] + fn minimal_scan_kind(&self) -> Option; + + /// Alias of [crate::runner::CommandRunner::should_write] + fn should_write(&self) -> bool; + + /// Alias of [CommandRunner::get_execution] + fn get_execution( + &self, + cli_options: &CliOptions, + console: &mut dyn Console, + workspace: &dyn Workspace, + ) -> Result, CliDiagnostic>; + + /// Alias for [CommandRunner::check_incompatible_arguments] + fn check_incompatible_arguments(&self) -> Result<(), CliDiagnostic>; + + /// Alias for [CommandRunner::should_validate_configuration_diagnostics] + fn should_validate_configuration_diagnostics(&self) -> bool; + + /// Alias for [CommandRunner::execute_without_crawling] + fn execute_without_crawling( + &mut self, + session: CliSession, + configured_workspace: ConfiguredWorkspace, + ) -> Result<(), CliDiagnostic>; + + /// Alias for [CommandRunner::merge_configuration] + fn merge_configuration( + &mut self, + _: Configuration, + _: Option, + _: Option, + _: &dyn FileSystem, + _: &mut dyn Console, + ) -> Result; +} + +impl CommandRunner for CustomExecutionCmdImpl +where + SC: CustomExecutionCmd, +{ + type CrawlerOutput = (); + type Collector = (); + type Crawler = (); + type Finalizer = (); + type Handler = (); + type ProcessFile = (); + + fn command_name(&self) -> &'static str { + self.deref().command_name() + } + + fn requires_crawling(&self) -> bool { + false + } + + fn minimal_scan_kind(&self) -> Option { + self.deref().minimal_scan_kind() + } + + fn collector(&self, _: &dyn FileSystem, _: &dyn Execution, _: &CliOptions) -> Self::Collector {} + + fn merge_configuration( + &mut self, + loaded_configuration: Configuration, + loaded_directory: Option, + loaded_file: Option, + fs: &dyn FileSystem, + console: &mut dyn Console, + ) -> Result { + self.deref_mut().merge_configuration( + loaded_configuration, + loaded_directory, + loaded_file, + fs, + console, + ) + } + + fn get_files_to_process( + &self, + _: &dyn FileSystem, + _: &Configuration, + ) -> Result, CliDiagnostic> { + Ok(vec![]) + } + + fn get_execution( + &self, + cli_options: &CliOptions, + console: &mut dyn Console, + workspace: &dyn Workspace, + ) -> Result, CliDiagnostic> { + self.deref().get_execution(cli_options, console, workspace) + } + + fn check_incompatible_arguments(&self) -> Result<(), CliDiagnostic> { + self.deref().check_incompatible_arguments() + } + + fn should_validate_configuration_diagnostics(&self) -> bool { + self.deref().should_validate_configuration_diagnostics() + } + + fn execute_without_crawling( + &mut self, + _session: CliSession, + _configured_workspace: ConfiguredWorkspace, + ) -> Result<(), CliDiagnostic> { + self.deref_mut() + .execute_without_crawling(_session, _configured_workspace) + } +} diff --git a/crates/biome_cli/src/runner/impls/commands/mod.rs b/crates/biome_cli/src/runner/impls/commands/mod.rs new file mode 100644 index 000000000000..1d15f5daba6a --- /dev/null +++ b/crates/biome_cli/src/runner/impls/commands/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod custom_execution; +pub(crate) mod traversal; diff --git a/crates/biome_cli/src/runner/impls/commands/traversal.rs b/crates/biome_cli/src/runner/impls/commands/traversal.rs new file mode 100644 index 000000000000..8db999a9b298 --- /dev/null +++ b/crates/biome_cli/src/runner/impls/commands/traversal.rs @@ -0,0 +1,214 @@ +use crate::cli_options::CliOptions; +use crate::runner::CommandRunner; +use crate::runner::execution::Execution; +use crate::runner::impls::collectors::default::DefaultCollector; +use crate::runner::impls::crawlers::default::DefaultCrawler; +use crate::runner::impls::finalizers::default::DefaultFinalizer; +use crate::runner::impls::handlers::default::DefaultHandler; +use crate::runner::process_file::ProcessFile; +use crate::{CliDiagnostic, TraversalSummary}; +use biome_configuration::Configuration; +use biome_console::Console; +use biome_deserialize::Merge; +use biome_diagnostics::Error; +use biome_fs::{BiomePath, FileSystem}; +use biome_service::configuration::load_editorconfig; +use biome_service::workspace::ScanKind; +use biome_service::{Workspace, WorkspaceError}; +use camino::Utf8PathBuf; +use std::collections::BTreeSet; +use std::ffi::OsString; +use std::ops::{Deref, DerefMut}; + +pub trait LoadEditorConfig: TraversalCommand { + /// Whether this command should load the `.editorconfig` file. + fn should_load_editor_config(&self, fs_configuration: &Configuration) -> bool; + + /// It loads the `.editorconfig` from the file system, parses it and deserialize it into a [Configuration] + fn load_editor_config( + &self, + configuration_path: Option, + fs_configuration: &Configuration, + fs: &dyn FileSystem, + ) -> Result, WorkspaceError> { + Ok(if self.should_load_editor_config(fs_configuration) { + let (editorconfig, _editorconfig_diagnostics) = { + let search_path = fs.working_directory().unwrap_or_default(); + + load_editorconfig(fs, search_path, configuration_path)? + }; + editorconfig + } else { + Default::default() + }) + } + + fn combine_configuration( + &self, + configuration_path: Option, + biome_configuration: Configuration, + fs: &dyn FileSystem, + ) -> Result { + Ok( + if let Some(mut fs_configuration) = + self.load_editor_config(configuration_path, &biome_configuration, fs)? + { + // If both `biome.json` and `.editorconfig` exist, formatter settings from the biome.json take precedence. + fs_configuration.merge_with(biome_configuration); + fs_configuration + } else { + biome_configuration + }, + ) + } +} +/// A trait that returns a [TraverseResult] from a traversal command. +pub(crate) struct TraversalCommandImpl(pub C) +where + P: ProcessFile, + C: TraversalCommand; + +impl Deref for TraversalCommandImpl +where + P: ProcessFile, + C: TraversalCommand, +{ + type Target = C; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for TraversalCommandImpl +where + P: ProcessFile, + C: TraversalCommand, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub(crate) trait TraversalCommand { + type ProcessFile: ProcessFile; + + /// Alias of [CommandRunner::command_name] + fn command_name(&self) -> &'static str; + + /// Alias of [CommandRunner::minimal_scan_kind] + fn minimal_scan_kind(&self) -> Option; + + /// Alias of [CommandRunner::get_execution] + fn get_execution( + &self, + cli_options: &CliOptions, + console: &mut dyn Console, + workspace: &dyn Workspace, + ) -> Result, CliDiagnostic>; + + /// Alias of [CommandRunner::merge_configuration] + fn merge_configuration( + &mut self, + loaded_configuration: Configuration, + loaded_directory: Option, + loaded_file: Option, + fs: &dyn FileSystem, + console: &mut dyn Console, + ) -> Result; + + /// Alias of [CommanderRunner::get_files_to_process] + fn get_files_to_process( + &self, + fs: &dyn FileSystem, + configuration: &Configuration, + ) -> Result, CliDiagnostic>; + + /// Alias of [CommandRunner::check_incompatible_arguments] + fn check_incompatible_arguments(&self) -> Result<(), CliDiagnostic> { + Ok(()) + } +} + +impl CommandRunner for TraversalCommandImpl +where + P: ProcessFile, + C: TraversalCommand, +{ + type CrawlerOutput = TraverseResult; + type Collector = DefaultCollector; + type Crawler = DefaultCrawler; + type Finalizer = DefaultFinalizer; + type Handler = DefaultHandler; + type ProcessFile = P; + + /// The name of the command that will appear in the diagnostics + fn command_name(&self) -> &'static str { + self.deref().command_name() + } + + fn requires_crawling(&self) -> bool { + true + } + + /// The [ScanKind] to use for this command + fn minimal_scan_kind(&self) -> Option { + self.deref().minimal_scan_kind() + } + + fn collector( + &self, + fs: &dyn FileSystem, + execution: &dyn Execution, + cli_options: &CliOptions, + ) -> Self::Collector { + DefaultCollector::new(fs.working_directory().as_deref()) + .with_verbose(cli_options.verbose) + .with_diagnostic_level(cli_options.diagnostic_level) + .with_max_diagnostics(execution.get_max_diagnostics(cli_options)) + } + + fn merge_configuration( + &mut self, + loaded_configuration: Configuration, + loaded_directory: Option, + loaded_file: Option, + fs: &dyn FileSystem, + console: &mut dyn Console, + ) -> Result { + self.deref_mut().merge_configuration( + loaded_configuration, + loaded_directory, + loaded_file, + fs, + console, + ) + } + + fn get_files_to_process( + &self, + fs: &dyn FileSystem, + configuration: &Configuration, + ) -> Result, CliDiagnostic> { + self.deref().get_files_to_process(fs, configuration) + } + + fn get_execution( + &self, + cli_options: &CliOptions, + console: &mut dyn Console, + workspace: &dyn Workspace, + ) -> Result, CliDiagnostic> { + self.deref().get_execution(cli_options, console, workspace) + } + + fn check_incompatible_arguments(&self) -> Result<(), CliDiagnostic> { + self.deref().check_incompatible_arguments() + } +} + +pub(crate) struct TraverseResult { + pub(crate) summary: TraversalSummary, + pub(crate) evaluated_paths: BTreeSet, + pub(crate) diagnostics: Vec, +} diff --git a/crates/biome_cli/src/runner/impls/crawlers/default.rs b/crates/biome_cli/src/runner/impls/crawlers/default.rs new file mode 100644 index 000000000000..7f2c8df96668 --- /dev/null +++ b/crates/biome_cli/src/runner/impls/crawlers/default.rs @@ -0,0 +1,68 @@ +use crate::TraversalSummary; +use crate::runner::collector::Collector; +use crate::runner::crawler::Crawler; +use crate::runner::impls::collectors::default::{CollectorSummary, DefaultCollector}; +use crate::runner::impls::commands::traversal::TraverseResult; +use crate::runner::impls::handlers::default::DefaultHandler; +use crate::runner::process_file::ProcessFile; +use biome_fs::BiomePath; +use std::collections::BTreeSet; +use std::time::Duration; + +pub(crate) struct DefaultCrawler

(P); + +impl

Crawler for DefaultCrawler

+where + P: ProcessFile, +{ + type Handler = DefaultHandler; + type ProcessFile = P; + type Collector = DefaultCollector; + + fn output( + collector_result: CollectorSummary, + evaluated_paths: BTreeSet, + _duration: Duration, + ) -> TraverseResult { + let CollectorSummary { + duration, + scanner_duration, + errors, + warnings, + infos, + suggested_fixes_skipped, + diagnostics_not_printed, + diagnostics, + changed, + unchanged, + matches, + skipped, + } = collector_result; + + TraverseResult { + summary: TraversalSummary { + changed, + unchanged, + matches, + duration, + scanner_duration, + errors, + warnings, + infos, + skipped, + suggested_fixes_skipped, + diagnostics_not_printed, + }, + diagnostics, + evaluated_paths, + } + } +} + +impl Crawler<()> for () { + type Handler = (); + type ProcessFile = (); + type Collector = (); + + fn output(_: ::Result, _: BTreeSet, _: Duration) {} +} diff --git a/crates/biome_cli/src/runner/impls/crawlers/mod.rs b/crates/biome_cli/src/runner/impls/crawlers/mod.rs new file mode 100644 index 000000000000..4ac00fe18338 --- /dev/null +++ b/crates/biome_cli/src/runner/impls/crawlers/mod.rs @@ -0,0 +1 @@ +pub(crate) mod default; diff --git a/crates/biome_cli/src/runner/impls/executions/mod.rs b/crates/biome_cli/src/runner/impls/executions/mod.rs new file mode 100644 index 000000000000..21ce34e4a858 --- /dev/null +++ b/crates/biome_cli/src/runner/impls/executions/mod.rs @@ -0,0 +1 @@ +pub(crate) mod summary_verb; diff --git a/crates/biome_cli/src/runner/impls/executions/summary_verb.rs b/crates/biome_cli/src/runner/impls/executions/summary_verb.rs new file mode 100644 index 000000000000..3373d2ec3af9 --- /dev/null +++ b/crates/biome_cli/src/runner/impls/executions/summary_verb.rs @@ -0,0 +1,31 @@ +use biome_console::fmt::Formatter; +use biome_console::{MarkupBuf, fmt, markup}; +use std::io; +use std::time::Duration; + +pub(crate) struct SummaryVerbExecution; + +impl SummaryVerbExecution { + /// Prints " in " + pub(crate) fn summary_verb(&self, verb: &str, files: usize, duration: &Duration) -> MarkupBuf { + let files = Files(files); + + markup!( + {verb}" "{files} " in " {duration}"." + ) + .to_owned() + } +} + +struct Files(usize); + +impl fmt::Display for Files { + fn fmt(&self, fmt: &mut Formatter) -> io::Result<()> { + fmt.write_markup(markup!({self.0} " "))?; + if self.0 == 1 { + fmt.write_str("file") + } else { + fmt.write_str("files") + } + } +} diff --git a/crates/biome_cli/src/runner/impls/finalizers/default.rs b/crates/biome_cli/src/runner/impls/finalizers/default.rs new file mode 100644 index 000000000000..6bb50cfe686c --- /dev/null +++ b/crates/biome_cli/src/runner/impls/finalizers/default.rs @@ -0,0 +1,274 @@ +use crate::cli_options::CliReporter; +use crate::diagnostics::ReportDiagnostic; +use crate::reporter::Reporter; +use crate::reporter::checkstyle::CheckstyleReporter; +use crate::reporter::github::{GithubReporter, GithubReporterVisitor}; +use crate::reporter::gitlab::{GitLabReporter, GitLabReporterVisitor}; +use crate::reporter::json::{JsonReporter, JsonReporterVisitor}; +use crate::reporter::junit::{JunitReporter, JunitReporterVisitor}; +use crate::reporter::rdjson::{RdJsonReporter, RdJsonReporterVisitor}; +use crate::reporter::summary::{SummaryReporter, SummaryReporterVisitor}; +use crate::reporter::terminal::{ConsoleReporter, ConsoleReporterVisitor}; +use crate::runner::finalizer::{FinalizePayload, Finalizer}; +use crate::runner::impls::commands::traversal::TraverseResult; +use crate::{CliDiagnostic, DiagnosticsPayload, TEMPORARY_INTERNAL_REPORTER_FILE}; +use biome_console::{ConsoleExt, markup}; +use biome_diagnostics::{Resource, SerdeJsonError}; +use biome_fs::BiomePath; +use biome_service::workspace::{CloseFileParams, FileContent, FormatFileParams, OpenFileParams}; +use std::cmp::Ordering; + +pub(crate) struct DefaultFinalizer; + +impl Finalizer for DefaultFinalizer { + type Input = TraverseResult; + + fn finalize(payload: FinalizePayload<'_, Self::Input>) -> Result<(), CliDiagnostic> { + let FinalizePayload { + project_key, + fs, + workspace, + scan_duration, + console, + cli_options, + crawler_output: result, + execution, + paths, + } = payload; + + let TraverseResult { + mut summary, + evaluated_paths, + mut diagnostics, + } = result; + + diagnostics.sort_unstable_by(|a, b| match a.severity().cmp(&b.severity()) { + Ordering::Equal => { + let a = a.location(); + let b = b.location(); + match (a.resource, b.resource) { + (Some(Resource::File(a)), Some(Resource::File(b))) => a.cmp(b), + (Some(Resource::File(_)), None) => Ordering::Greater, + (None, Some(Resource::File(_))) => Ordering::Less, + _ => Ordering::Equal, + } + } + result => result, + }); + + // We join the duration of the scanning with the duration of the traverse. + summary.scanner_duration = scan_duration; + let errors = summary.errors; + let skipped = summary.skipped; + let processed = summary.changed + summary.unchanged; + let should_exit_on_warnings = summary.warnings > 0 && cli_options.error_on_warnings; + let diagnostics_payload = DiagnosticsPayload { + diagnostic_level: cli_options.diagnostic_level, + diagnostics, + max_diagnostics: cli_options.max_diagnostics, + }; + + let reporter_mode = ReportMode::from(&cli_options.reporter); + + match reporter_mode { + ReportMode::Terminal { with_summary } => { + if with_summary { + let reporter = SummaryReporter { + summary, + diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + evaluated_paths, + }; + reporter.write(&mut SummaryReporterVisitor(console))?; + } else { + let reporter = ConsoleReporter { + summary, + diagnostics_payload, + execution, + evaluated_paths, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + }; + reporter.write(&mut ConsoleReporterVisitor(console))?; + } + } + ReportMode::Json { pretty } => { + console.error(markup! { + "The ""--json"" option is ""unstable/experimental"" and its output might change between patches/minor releases." + }); + let reporter = JsonReporter { + summary, + diagnostics: diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + }; + let mut buffer = JsonReporterVisitor::new(summary); + reporter.write(&mut buffer)?; + if pretty { + let content = serde_json::to_string(&buffer).map_err(|error| { + CliDiagnostic::Report(ReportDiagnostic::Serialization( + SerdeJsonError::from(error), + )) + })?; + let report_file = BiomePath::new(TEMPORARY_INTERNAL_REPORTER_FILE); + workspace.open_file(OpenFileParams { + project_key, + content: FileContent::from_client(content), + path: report_file.clone(), + document_file_source: None, + persist_node_cache: false, + inline_config: None, + })?; + let code = workspace.format_file(FormatFileParams { + project_key, + path: report_file.clone(), + inline_config: None, + })?; + console.log(markup! { + {code.as_code()} + }); + workspace.close_file(CloseFileParams { + project_key, + path: report_file, + })?; + } else { + console.log(markup! { + {buffer} + }); + } + } + ReportMode::GitHub => { + let reporter = GithubReporter { + diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + }; + reporter.write(&mut GithubReporterVisitor(console))?; + } + ReportMode::GitLab => { + let reporter = GitLabReporter { + diagnostics: diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + }; + reporter.write(&mut GitLabReporterVisitor::new( + console, + workspace.fs().working_directory(), + ))?; + } + ReportMode::Junit => { + let reporter = JunitReporter { + summary, + diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + }; + reporter.write(&mut JunitReporterVisitor::new(console))?; + } + ReportMode::Checkstyle => { + let reporter = CheckstyleReporter { + summary, + diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + }; + reporter.write( + &mut crate::reporter::checkstyle::CheckstyleReporterVisitor::new(console), + )?; + } + ReportMode::RdJson => { + let reporter = RdJsonReporter { + diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + }; + reporter.write(&mut RdJsonReporterVisitor(console))?; + } + } + + // Processing emitted error diagnostics, exit with a non-zero code + if processed.saturating_sub(skipped) == 0 && !cli_options.no_errors_on_unmatched { + Err(CliDiagnostic::no_files_processed( + execution.as_diagnostic_category(), + paths, + )) + } else if errors > 0 || should_exit_on_warnings { + let category = execution.as_diagnostic_category(); + if should_exit_on_warnings { + if execution.is_safe_fixes_enabled() { + Err(CliDiagnostic::apply_warnings(category)) + } else { + Err(CliDiagnostic::check_warnings(category)) + } + } else if execution.is_safe_fixes_enabled() { + Err(CliDiagnostic::apply_error(category)) + } else { + Err(CliDiagnostic::check_error(category)) + } + } else { + Ok(()) + } + } +} + +impl Finalizer for () { + type Input = (); + + fn finalize(_: FinalizePayload<'_, Self::Input>) -> Result<(), CliDiagnostic> { + Ok(()) + } +} + +/// Tells to the execution of the traversal how the information should be reported +#[derive(Copy, Clone, Debug)] +pub enum ReportMode { + /// Reports information straight to the console, it's the default mode + Terminal { with_summary: bool }, + /// Reports information in JSON format + Json { pretty: bool }, + /// Reports information for GitHub + GitHub, + /// JUnit output + /// Ref: https://github.com/testmoapp/junitxml?tab=readme-ov-file#basic-junit-xml-structure + Junit, + /// Reports information in the [GitLab Code Quality](https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool) format. + GitLab, + /// Reports diagnostics in [Checkstyle XML format](https://checkstyle.org/). + Checkstyle, + /// Reports information in [reviewdog JSON format](https://deepwiki.com/reviewdog/reviewdog/3.2-reviewdog-diagnostic-format) + RdJson, +} + +impl Default for ReportMode { + fn default() -> Self { + Self::Terminal { + with_summary: false, + } + } +} + +impl From<&CliReporter> for ReportMode { + fn from(value: &CliReporter) -> Self { + match value { + CliReporter::Default => Self::Terminal { + with_summary: false, + }, + CliReporter::Summary => Self::Terminal { with_summary: true }, + CliReporter::Json => Self::Json { pretty: false }, + CliReporter::JsonPretty => Self::Json { pretty: true }, + CliReporter::GitHub => Self::GitHub, + CliReporter::Junit => Self::Junit, + CliReporter::GitLab => Self::GitLab {}, + CliReporter::Checkstyle => Self::Checkstyle, + CliReporter::RdJson => Self::RdJson, + } + } +} diff --git a/crates/biome_cli/src/runner/impls/finalizers/mod.rs b/crates/biome_cli/src/runner/impls/finalizers/mod.rs new file mode 100644 index 000000000000..4ac00fe18338 --- /dev/null +++ b/crates/biome_cli/src/runner/impls/finalizers/mod.rs @@ -0,0 +1 @@ +pub(crate) mod default; diff --git a/crates/biome_cli/src/runner/impls/handlers/default.rs b/crates/biome_cli/src/runner/impls/handlers/default.rs new file mode 100644 index 000000000000..5e4660530da4 --- /dev/null +++ b/crates/biome_cli/src/runner/impls/handlers/default.rs @@ -0,0 +1,6 @@ +use crate::runner::handler::Handler; + +#[derive(Debug, Default)] +pub(crate) struct DefaultHandler; + +impl Handler for DefaultHandler {} diff --git a/crates/biome_cli/src/runner/impls/handlers/mod.rs b/crates/biome_cli/src/runner/impls/handlers/mod.rs new file mode 100644 index 000000000000..4ac00fe18338 --- /dev/null +++ b/crates/biome_cli/src/runner/impls/handlers/mod.rs @@ -0,0 +1 @@ +pub(crate) mod default; diff --git a/crates/biome_cli/src/runner/impls/mod.rs b/crates/biome_cli/src/runner/impls/mod.rs new file mode 100644 index 000000000000..d7dac6be6d18 --- /dev/null +++ b/crates/biome_cli/src/runner/impls/mod.rs @@ -0,0 +1,7 @@ +pub(crate) mod collectors; +pub(crate) mod commands; +pub(crate) mod crawlers; +pub(crate) mod executions; +pub(crate) mod finalizers; +pub(crate) mod handlers; +pub(crate) mod process_file; diff --git a/crates/biome_cli/src/runner/impls/process_file/check.rs b/crates/biome_cli/src/runner/impls/process_file/check.rs new file mode 100644 index 000000000000..9bbb751552ee --- /dev/null +++ b/crates/biome_cli/src/runner/impls/process_file/check.rs @@ -0,0 +1,91 @@ +use crate::CliDiagnostic; +use crate::runner::crawler::CrawlerContext; +use crate::runner::impls::process_file::format::FormatProcessFile; +use crate::runner::impls::process_file::lint_and_assist::LintAssistProcessFile; +use crate::runner::process_file::{ + FileStatus, Message, ProcessFile, ProcessStdinFilePayload, WorkspaceFile, +}; +use biome_service::workspace::FeaturesSupported; + +pub(crate) struct CheckProcessFile; + +impl ProcessFile for CheckProcessFile { + fn process_file( + ctx: &Ctx, + workspace_file: &mut WorkspaceFile, + features_supported: &FeaturesSupported, + ) -> Result + where + Ctx: CrawlerContext, + { + let execution = ctx.execution(); + let mut has_failures = false; + let analyzer_result = + LintAssistProcessFile::process_file(ctx, workspace_file, features_supported); + + let mut changed = false; + // To reduce duplication of the same error on format and lint_and_assist + let mut skipped_parse_error = false; + + match analyzer_result { + Ok(status) => { + if matches!(status, FileStatus::Ignored) && execution.should_skip_parse_errors() { + skipped_parse_error = true; + } + + if status.is_changed() { + changed = true + } + if let FileStatus::Message(msg) = status { + if msg.is_failure() { + has_failures = true; + } + ctx.push_message(msg); + } + } + Err(err) => { + ctx.push_message(err); + has_failures = true; + } + } + + if features_supported.supports_format() { + if execution.should_skip_parse_errors() && skipped_parse_error { + // Parse errors are already skipped during the analyze phase, so no need to do it here. + } else { + let format_result = + FormatProcessFile::process_file(ctx, workspace_file, features_supported); + + match format_result { + Ok(status) => { + if status.is_changed() { + changed = true + } + if let FileStatus::Message(msg) = status { + if msg.is_failure() { + has_failures = true; + } + ctx.push_message(msg); + } + } + Err(err) => { + ctx.push_message(err); + has_failures = true; + } + } + } + } + + if has_failures { + Ok(FileStatus::Message(Message::Failure)) + } else if changed { + Ok(FileStatus::Changed) + } else { + Ok(FileStatus::Unchanged) + } + } + + fn process_std_in(payload: ProcessStdinFilePayload) -> Result<(), CliDiagnostic> { + LintAssistProcessFile::process_std_in(payload) + } +} diff --git a/crates/biome_cli/src/runner/impls/process_file/format.rs b/crates/biome_cli/src/runner/impls/process_file/format.rs new file mode 100644 index 000000000000..9978b540801e --- /dev/null +++ b/crates/biome_cli/src/runner/impls/process_file/format.rs @@ -0,0 +1,205 @@ +use crate::CliDiagnostic; +use crate::diagnostics::StdinDiagnostic; +use crate::runner::crawler::CrawlerContext; +use crate::runner::diagnostics::{ResultExt, SkippedDiagnostic}; +use crate::runner::process_file::{ + DiffKind, FileStatus, Message, ProcessFile, ProcessStdinFilePayload, WorkspaceFile, +}; +use biome_analyze::RuleCategoriesBuilder; +use biome_console::{ConsoleExt, markup}; +use biome_diagnostics::{Diagnostic, DiagnosticExt, Error, PrintDiagnostic, Severity, category}; +use biome_service::WorkspaceError; +use biome_service::file_handlers::{AstroFileHandler, SvelteFileHandler, VueFileHandler}; +use biome_service::workspace::{ + CloseFileParams, FeaturesBuilder, FeaturesSupported, FileContent, FileFeaturesResult, + FormatFileParams, OpenFileParams, SupportsFeatureParams, +}; +use tracing::debug; + +pub(crate) struct FormatProcessFile; + +impl ProcessFile for FormatProcessFile { + fn process_file( + ctx: &Ctx, + workspace_file: &mut WorkspaceFile, + features_supported: &FeaturesSupported, + ) -> Result + where + Ctx: CrawlerContext, + { + // Open the file and create a workspace guard + let execution = ctx.execution(); + let guard = workspace_file.guard(); + + // Get file features + let diagnostics_result = guard + .pull_diagnostics( + RuleCategoriesBuilder::default().with_syntax().build(), + Vec::new(), + Vec::new(), + false, // NOTE: probably to revisit + ) + .with_file_path_and_code(workspace_file.path.to_string(), category!("format"))?; + + let input = workspace_file.input()?; + let should_write = execution.requires_write_access(); + let skip_parse_errors = execution.should_skip_parse_errors(); + + if diagnostics_result.errors > 0 && skip_parse_errors { + ctx.push_message(Message::from( + SkippedDiagnostic.with_file_path(workspace_file.path.to_string()), + )); + return Ok(FileStatus::Ignored); + } + + ctx.push_message(Message::Diagnostics { + file_path: workspace_file.path.to_string(), + content: input.clone(), + diagnostics: diagnostics_result + .diagnostics + .into_iter() + // Formatting is usually blocked by errors, so we want to print only diagnostics that + // Have error severity + .filter_map(|diagnostic| { + if diagnostic.severity() >= Severity::Error { + Some(Error::from(diagnostic)) + } else { + None + } + }) + .collect(), + skipped_diagnostics: diagnostics_result.skipped_diagnostics as u32, + }); + + let printed = workspace_file + .guard() + .format_file() + .with_file_path_and_code(workspace_file.path.to_string(), category!("format"))?; + + let mut output = printed.into_code(); + + if !features_supported.supports_full_html_support() { + match workspace_file.as_extension() { + Some("astro") => { + if output.is_empty() { + return Ok(FileStatus::Unchanged); + } + output = AstroFileHandler::output(input.as_str(), output.as_str()); + } + Some("vue") => { + if output.is_empty() { + return Ok(FileStatus::Unchanged); + } + output = VueFileHandler::output(input.as_str(), output.as_str()); + } + + Some("svelte") => { + if output.is_empty() { + return Ok(FileStatus::Unchanged); + } + output = SvelteFileHandler::output(input.as_str(), output.as_str()); + } + _ => {} + } + } + + debug!("Format output is different from input: {}", output != input); + if output != input { + if should_write { + workspace_file.update_file(output)?; + Ok(FileStatus::Changed) + } else { + Ok(FileStatus::Message(Message::Diff { + file_name: workspace_file.path.to_string(), + old: input, + new: output, + diff_kind: DiffKind::Format, + })) + } + } else { + Ok(FileStatus::Unchanged) + } + } + + fn process_std_in(payload: ProcessStdinFilePayload) -> Result<(), CliDiagnostic> { + let ProcessStdinFilePayload { + workspace, + content, + project_key, + biome_path, + console, + cli_options, + execution: _, + } = payload; + let FileFeaturesResult { + features_supported: file_features, + } = workspace.file_features(SupportsFeatureParams { + project_key, + path: biome_path.clone(), + features: FeaturesBuilder::new().with_formatter().build(), + inline_config: None, + })?; + + if file_features.is_ignored() { + console.append(markup! {{content}}); + return Ok(()); + } + + if file_features.is_protected() { + let protected_diagnostic = WorkspaceError::protected_file(biome_path.to_string()); + if protected_diagnostic.tags().is_verbose() { + if cli_options.verbose { + console.error(markup! {{PrintDiagnostic::verbose(&protected_diagnostic)}}) + } + } else { + console.error(markup! {{PrintDiagnostic::simple(&protected_diagnostic)}}) + } + console.append(markup! {{content}}); + return Ok(()); + }; + if file_features.supports_format() { + workspace.open_file(OpenFileParams { + project_key, + path: biome_path.clone(), + content: FileContent::from_client(content), + document_file_source: None, + persist_node_cache: false, + inline_config: None, + })?; + let printed = workspace.format_file(FormatFileParams { + project_key, + path: biome_path.clone(), + inline_config: None, + })?; + + let code = printed.into_code(); + let output = if !file_features.supports_full_html_support() { + match biome_path.extension() { + Some("astro") => AstroFileHandler::output(content, code.as_str()), + Some("vue") => VueFileHandler::output(content, code.as_str()), + Some("svelte") => SvelteFileHandler::output(content, code.as_str()), + _ => code, + } + } else { + code + }; + console.append(markup! { + {output} + }); + workspace + .close_file(CloseFileParams { + project_key, + path: biome_path.clone(), + }) + .map_err(|err| err.into()) + } else { + console.append(markup! { + {content} + }); + console.error(markup! { + "The content was not formatted because the formatter is currently disabled." + }); + Err(StdinDiagnostic::new_not_formatted().into()) + } + } +} diff --git a/crates/biome_cli/src/runner/impls/process_file/lint_and_assist.rs b/crates/biome_cli/src/runner/impls/process_file/lint_and_assist.rs new file mode 100644 index 000000000000..3ade7f70d450 --- /dev/null +++ b/crates/biome_cli/src/runner/impls/process_file/lint_and_assist.rs @@ -0,0 +1,322 @@ +use crate::CliDiagnostic; +use crate::diagnostics::StdinDiagnostic; +use crate::runner::crawler::CrawlerContext; +use crate::runner::diagnostics::{ResultExt, SkippedDiagnostic}; +use crate::runner::execution::AnalyzerSelectors; +use crate::runner::process_file::{ + FileStatus, Message, ProcessFile, ProcessStdinFilePayload, WorkspaceFile, +}; +use biome_analyze::RuleCategoriesBuilder; +use biome_console::{ConsoleExt, markup}; +use biome_css_syntax::TextSize; +use biome_diagnostics::{Diagnostic, DiagnosticExt, Error, PrintDiagnostic, Severity}; +use biome_service::WorkspaceError; +use biome_service::file_handlers::{AstroFileHandler, SvelteFileHandler, VueFileHandler}; +use biome_service::workspace::{ + ChangeFileParams, CloseFileParams, FeaturesBuilder, FeaturesSupported, FileContent, + FileFeaturesResult, FixFileParams, FormatFileParams, OpenFileParams, SupportsFeatureParams, +}; +use std::borrow::Cow; +use tracing::info; + +pub struct LintAssistProcessFile; + +impl ProcessFile for LintAssistProcessFile { + fn process_file( + ctx: &Ctx, + workspace_file: &mut WorkspaceFile, + features_supported: &FeaturesSupported, + ) -> Result + where + Ctx: CrawlerContext, + { + let execution = ctx.execution(); + let mut categories = RuleCategoriesBuilder::default().with_syntax(); + if features_supported.supports_lint() { + categories = categories.with_lint(); + } + if features_supported.supports_assist() && (execution.is_check() || execution.is_ci()) { + categories = categories.with_assist(); + } + let categories = categories.build(); + let mut input = workspace_file.input()?; + let mut changed = false; + let AnalyzerSelectors { only, skip } = execution.analyzer_selectors(); + if let Some(fix_mode) = execution.as_fix_file_mode() { + let suppression_explanation = + if execution.suppress() && execution.suppression_reason().is_none() { + "ignored using `--suppress`" + } else { + execution.suppression_reason().unwrap_or("") + }; + + let fix_result = workspace_file + .guard() + .fix_file( + fix_mode, + false, + categories, + only.clone(), + skip.clone(), + Some(suppression_explanation.to_string()), + ) + .with_file_path_and_code( + workspace_file.path.to_string(), + execution.as_diagnostic_category(), + )?; + + info!( + "Fix file summary result. Errors {}, skipped fixes {}, actions {}", + fix_result.errors, + fix_result.skipped_suggested_fixes, + fix_result.actions.len() + ); + + ctx.push_message(Message::SkippedFixes { + skipped_suggested_fixes: fix_result.skipped_suggested_fixes, + }); + + let mut output = fix_result.code; + + if !features_supported.supports_full_html_support() { + match workspace_file.as_extension() { + Some("astro") => { + output = AstroFileHandler::output(input.as_str(), output.as_str()); + } + Some("vue") => { + output = VueFileHandler::output(input.as_str(), output.as_str()); + } + Some("svelte") => { + output = SvelteFileHandler::output(input.as_str(), output.as_str()); + } + _ => {} + } + } + if output != input { + changed = true; + workspace_file.update_file(output)?; + input = workspace_file.input()?; + } + } + + let pull_diagnostics_result = workspace_file + .guard() + .pull_diagnostics(categories, only, skip, true) + .with_file_path_and_code( + workspace_file.path.to_string(), + execution.as_diagnostic_category(), + )?; + + let skip_parse_errors = execution.should_skip_parse_errors(); + if pull_diagnostics_result.errors > 0 && skip_parse_errors { + ctx.push_message(Message::from( + SkippedDiagnostic.with_file_path(workspace_file.path.to_string()), + )); + return Ok(FileStatus::Ignored); + } + + let no_diagnostics = pull_diagnostics_result.diagnostics.is_empty() + && pull_diagnostics_result.skipped_diagnostics == 0; + + if !no_diagnostics { + let offset = if features_supported.supports_full_html_support() { + None + } else { + match workspace_file.as_extension() { + Some("vue") => VueFileHandler::start(input.as_str()), + Some("astro") => AstroFileHandler::start(input.as_str()), + Some("svelte") => SvelteFileHandler::start(input.as_str()), + _ => None, + } + }; + + ctx.push_message(Message::Diagnostics { + file_path: workspace_file.path.to_string(), + content: input, + diagnostics: pull_diagnostics_result + .diagnostics + .into_iter() + .map(|d| { + if let Some(offset) = offset { + d.with_offset(TextSize::from(offset)) + } else { + d + } + }) + .map(|diagnostic| { + let category = diagnostic.category(); + if let Some(category) = category + && category.name().starts_with("assist/") + && execution.should_enforce_assist() + { + return diagnostic.with_severity(Severity::Error); + } + Error::from(diagnostic) + }) + .collect(), + skipped_diagnostics: pull_diagnostics_result.skipped_diagnostics as u32, + }); + } + + if changed { + Ok(FileStatus::Changed) + } else { + Ok(FileStatus::Unchanged) + } + } + + fn process_std_in(payload: ProcessStdinFilePayload) -> Result<(), CliDiagnostic> { + let ProcessStdinFilePayload { + workspace, + content, + project_key, + biome_path, + console, + cli_options, + execution, + } = payload; + + let mut new_content = Cow::Borrowed(content); + let mut version = 0; + + workspace.open_file(OpenFileParams { + project_key, + path: biome_path.clone(), + content: FileContent::from_client(content), + document_file_source: None, + persist_node_cache: false, + inline_config: None, + })?; + + // apply fix file of the linter + let FileFeaturesResult { + features_supported: file_features, + } = workspace.file_features(SupportsFeatureParams { + project_key, + path: biome_path.clone(), + features: FeaturesBuilder::new() + .with_linter() + .with_assist() + .with_formatter() + .build(), + inline_config: None, + })?; + + if file_features.is_ignored() { + console.append(markup! {{content}}); + return Ok(()); + } + + if file_features.is_protected() { + let protected_diagnostic = WorkspaceError::protected_file(biome_path.to_string()); + if protected_diagnostic.tags().is_verbose() { + if cli_options.verbose { + console.error(markup! {{PrintDiagnostic::verbose(&protected_diagnostic)}}) + } + } else { + console.error(markup! {{PrintDiagnostic::simple(&protected_diagnostic)}}) + } + console.append(markup! {{content}}); + + return Ok(()); + }; + + let AnalyzerSelectors { only, skip } = execution.analyzer_selectors(); + + if let Some(fix_file_mode) = execution.as_fix_file_mode() + && (file_features.supports_lint() || file_features.supports_assist()) + { + let mut rule_categories = RuleCategoriesBuilder::default().with_syntax(); + + if file_features.supports_lint() { + rule_categories = rule_categories.with_lint(); + } + + if file_features.supports_assist() { + rule_categories = rule_categories.with_assist(); + } + + let fix_file_result = workspace.fix_file(FixFileParams { + project_key, + fix_file_mode, + path: biome_path.clone(), + should_format: false, + only: only.clone(), + skip: skip.clone(), + suppression_reason: None, + enabled_rules: vec![], + rule_categories: rule_categories.build(), + inline_config: None, + })?; + let code = fix_file_result.code; + let output = if !file_features.supports_full_html_support() { + match biome_path.extension() { + Some("astro") => AstroFileHandler::output(&new_content, code.as_str()), + Some("vue") => VueFileHandler::output(&new_content, code.as_str()), + Some("svelte") => SvelteFileHandler::output(&new_content, code.as_str()), + _ => code, + } + } else { + code + }; + if output != new_content { + version += 1; + workspace.change_file(ChangeFileParams { + project_key, + content: output.clone(), + path: biome_path.clone(), + version, + inline_config: None, + })?; + new_content = Cow::Owned(output); + } + } + + if file_features.supports_format() && execution.is_check() { + let printed = workspace.format_file(FormatFileParams { + project_key, + path: biome_path.clone(), + inline_config: None, + })?; + let code = printed.into_code(); + let output = if !file_features.supports_full_html_support() { + match biome_path.extension() { + Some("astro") => AstroFileHandler::output(&new_content, code.as_str()), + Some("vue") => VueFileHandler::output(&new_content, code.as_str()), + Some("svelte") => SvelteFileHandler::output(&new_content, code.as_str()), + _ => code, + } + } else { + code + }; + if (execution.is_safe_fixes_enabled() || execution.is_safe_and_unsafe_fixes_enabled()) + && output != new_content + { + new_content = Cow::Owned(output); + } + } + + match new_content { + Cow::Borrowed(original_content) => { + console.append(markup! { + {original_content} + }); + + if !execution.requires_write_access() { + return Err(StdinDiagnostic::new_not_formatted().into()); + } + } + Cow::Owned(ref new_content) => { + console.append(markup! { + {new_content} + }); + } + } + workspace + .close_file(CloseFileParams { + project_key, + path: biome_path.clone(), + }) + .map_err(|e| e.into()) + } +} diff --git a/crates/biome_cli/src/runner/impls/process_file/mod.rs b/crates/biome_cli/src/runner/impls/process_file/mod.rs new file mode 100644 index 000000000000..e16bba1f0ef6 --- /dev/null +++ b/crates/biome_cli/src/runner/impls/process_file/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod check; +pub(crate) mod format; +pub(crate) mod lint_and_assist; diff --git a/crates/biome_cli/src/runner/mod.rs b/crates/biome_cli/src/runner/mod.rs new file mode 100644 index 000000000000..616e403fc20e --- /dev/null +++ b/crates/biome_cli/src/runner/mod.rs @@ -0,0 +1,563 @@ +//! # Command Runner Architecture +//! +//! This module provides a trait-based infrastructure for implementing CLI commands +//! that operate on files in a Biome project. It's designed to be flexible enough to +//! support both standard file-traversing commands (like `format`, `lint`, `check`) +//! and special-case commands that don't traverse files (like `migrate`). +//! +//! ## Architecture Overview +//! +//! ```text +//! CommandRunner::run() +//! │ +//! ├─→ configure_workspace() ────→ ConfiguredWorkspace +//! │ ├─ project_key +//! │ ├─ configuration_files +//! │ ├─ execution context +//! │ └─ paths to process +//! │ +//! ├─→ [if requires_crawling() = false] +//! │ └─→ execute_without_crawling() ──→ Custom logic (e.g., migrate) +//! │ +//! └─→ [if requires_crawling() = true] +//! │ +//! ├─→ Crawler::crawl() +//! │ │ +//! │ ├─→ spawns Collector thread ──→ Collects diagnostics/messages +//! │ │ +//! │ └─→ traverses files via Handler +//! │ │ +//! │ ├─→ Handler::can_handle() ──→ Filter files +//! │ │ +//! │ └─→ Handler::handle_path() +//! │ │ +//! │ └─→ ProcessFile::process_file() +//! │ └─→ Calls workspace APIs +//! │ └─→ Returns FileStatus +//! │ +//! └─→ Finalizer::finalize() ──→ Print results, exit codes +//! ``` +//! +//! ## Core Traits +//! +//! ### [`CommandRunner`] +//! The main trait that orchestrates command execution. Commands typically don't implement +//! this directly, but instead use one of the helper traits below that provide automatic +//! `CommandRunner` implementations. +//! +//! ### [`impls::commands::traversal::TraversalCommand`] +//! High-level trait for commands that traverse files. Commands implement this trait +//! and wrap their implementation in [`impls::commands::traversal::TraversalCommandImpl`] +//! to automatically get a `CommandRunner` implementation with sensible defaults. +//! Used for commands like `format`, `lint`, and `check`. +//! +//! ### [`impls::commands::custom_execution::CustomExecutionCmd`] +//! High-level trait for commands that don't traverse files but need custom execution. +//! Commands implement this trait and wrap their implementation in +//! [`impls::commands::custom_execution::CustomExecutionCmdImpl`] to automatically get a +//! `CommandRunner` implementation that bypasses file crawling. Used for commands like `migrate`. +//! +//! ### [`Crawler`] +//! Orchestrates the file traversal process. Spawns a collector thread, walks the +//! file system, and produces structured output. +//! +//! ### [`Handler`] +//! Defines per-file processing logic. Filters which files to process and dispatches +//! to the appropriate file processor. Provides sensible defaults for both filtering +//! and processing. +//! +//! ### [`ProcessFile`] +//! Defines the actual file processing logic. This is where command-specific workspace +//! API calls happen (formatting, linting, searching, etc.). +//! +//! ### [`Collector`] +//! Runs in a separate thread to collect diagnostics and messages during traversal. +//! Filters and prints diagnostics based on severity and verbosity. +//! +//! ### [`Finalizer`] +//! Prints final results and determines exit code. Can format output in different +//! styles (terminal, JSON, GitHub Actions, etc.). +//! +//! ### [`Execution`] +//! Defines what features a command needs from the workspace and checks if files +//! support those features. +//! +//! ## Design Principles +//! +//! - **Separation of Concerns**: Each trait handles a distinct aspect of command execution +//! - **Composability**: Share implementations across commands where it makes sense +//! - **Type Safety**: Associated types ensure components fit together correctly +//! - **Sensible Defaults**: Most traits provide default implementations that work for common cases +//! - **Flexibility**: Override only what you need for command-specific behavior +//! - **Wrapper Pattern**: High-level traits use wrapper types to provide automatic implementations +//! +//! ## Implementing Commands +//! +//! ### For file-traversing commands (format, lint, check): +//! 1. Implement [`impls::commands::traversal::TraversalCommand`] for your command type +//! 2. Wrap it in [`impls::commands::traversal::TraversalCommandImpl`] +//! 3. Call `run()` on the wrapper to execute the command +//! +//! ### For custom execution commands (migrate): +//! 1. Implement [`impls::commands::custom_execution::CustomExecutionCmd`] for your command type +//! 2. Wrap it in [`impls::commands::custom_execution::CustomExecutionCmdImpl`] +//! 3. Call `run()` on the wrapper to execute the command +//! +//! ## Module Organization +//! +//! - [`collector`]: Diagnostic collection during traversal +//! - [`crawler`]: File system traversal orchestration +//! - [`execution`]: Feature requirements and capability checks +//! - [`finalizer`]: Results presentation and reporting +//! - [`handler`]: Per-file filtering and processing dispatch +//! - [`process_file`]: File processing trait and status types +//! - [`scan_kind`]: Utilities for determining scan strategy +//! - [`impls`]: Concrete implementations and helper traits +//! - [`impls::commands`]: High-level command traits with automatic `CommandRunner` implementations +//! - [`impls::collectors`]: Collector implementations +//! - [`impls::crawlers`]: Crawler implementations +//! - [`impls::finalizers`]: Finalizer implementations +//! - [`impls::handlers`]: Handler implementations +//! - [`impls::process_file`]: ProcessFile implementations + +pub(crate) mod collector; +pub(crate) mod crawler; +pub(crate) mod diagnostics; +pub(crate) mod execution; +pub(crate) mod finalizer; +pub(crate) mod handler; +pub(crate) mod impls; +pub(crate) mod process_file; +pub(crate) mod run; +pub(crate) mod scan_kind; + +use crate::cli_options::CliOptions; +use crate::commands::{ + print_diagnostics_from_workspace_result, validate_configuration_diagnostics, +}; +use crate::diagnostics::StdinDiagnostic; +use crate::logging::LogOptions; +use crate::runner::collector::Collector; +use crate::runner::crawler::Crawler; +use crate::runner::execution::{Execution, Stdin}; +use crate::runner::finalizer::{FinalizePayload, Finalizer}; +use crate::runner::handler::Handler; +use crate::runner::process_file::{ProcessFile, ProcessStdinFilePayload}; +use crate::runner::scan_kind::derive_best_scan_kind; +use crate::{CliDiagnostic, CliSession, setup_cli_subscriber}; +use biome_configuration::Configuration; +use biome_console::{Console, ConsoleExt, markup}; +use biome_diagnostics::PrintDiagnostic; +use biome_fs::{BiomePath, FileSystem}; +use biome_resolver::FsWithResolverProxy; +use biome_service::configuration::{LoadedConfiguration, ProjectScanComputer, load_configuration}; +use biome_service::projects::ProjectKey; +use biome_service::workspace::{ + OpenProjectParams, ScanKind, ScanProjectParams, UpdateSettingsParams, +}; +use biome_service::{Workspace, WorkspaceError}; +use camino::{Utf8Path, Utf8PathBuf}; +use std::ffi::OsString; +use std::time::Duration; +use tracing::info; + +/// Generic interface for executing commands. +/// +/// Consumers must implement the following methods: +/// +/// - [CommandRunner::merge_configuration] +/// - [CommandRunner::get_files_to_process] +/// - [CommandRunner::get_stdin_file_path] +/// - [CommandRunner::should_write] +/// - [CommandRunner::get_execution] +/// +/// Optional methods: +/// - [CommandRunner::check_incompatible_arguments] +/// - [CommandRunner::execute_without_crawling] (only if requires_crawling() = false) +pub(crate) trait CommandRunner { + type CrawlerOutput; + type Collector: Collector; + type Crawler: Crawler< + Self::CrawlerOutput, + Handler = Self::Handler, + ProcessFile = Self::ProcessFile, + Collector = Self::Collector, + >; + type Finalizer: Finalizer; + type Handler: Handler; + type ProcessFile: ProcessFile; + + /// The name of the command that will appear in the diagnostics + fn command_name(&self) -> &'static str; + + /// Whether this command requires file crawling. + /// + /// If `false`, the command must implement [CommandRunner::execute_without_crawling] + /// and will bypass the standard crawler/collector/finalizer flow. + /// + /// This is useful for commands like `migrate` that operate on configuration files + /// directly rather than traversing source files. + fn requires_crawling(&self) -> bool; + + /// The [ScanKind] that could be used for this command. Some commands shouldn't implement this + /// because it should be derived from the configuration. + fn minimal_scan_kind(&self) -> Option; + + /// Generates the collector to use for this command. + fn collector( + &self, + fs: &dyn FileSystem, + execution: &dyn Execution, + cli_options: &CliOptions, + ) -> Self::Collector; + + fn validated_paths_for_execution( + &self, + paths: Vec, + working_dir: &Utf8Path, + execution: &dyn Execution, + ) -> Result, CliDiagnostic> { + let mut paths = paths + .into_iter() + .map(|path| path.into_string().map_err(WorkspaceError::non_utf8_path)) + .collect::, _>>()?; + + if paths.is_empty() { + if execution.is_vcs_targeted() { + // If `--staged` or `--changed` is specified, it's + // acceptable for them to be empty, so ignore it. + } else { + paths.push(working_dir.to_string()); + } + } + + Ok(paths) + } + + fn setup_logging(&self, log_options: &LogOptions, cli_options: &CliOptions) { + setup_cli_subscriber( + log_options.log_file.as_deref(), + log_options.log_level, + log_options.log_kind, + cli_options.colors.as_ref(), + ); + } + + /// The main command to use. + fn run( + &mut self, + session: CliSession, + log_options: &LogOptions, + cli_options: &CliOptions, + ) -> Result<(), CliDiagnostic> { + self.setup_logging(log_options, cli_options); + self.check_incompatible_arguments()?; + + let console = &mut *session.app.console; + let workspace = &*session.app.workspace; + let fs = workspace.fs(); + + let configured_workspace = self.configure_workspace(fs, console, workspace, cli_options)?; + + // Commands that don't require crawling can implement custom execution logic + if !self.requires_crawling() { + return self.execute_without_crawling(session, configured_workspace); + } + + let ConfiguredWorkspace { + execution, + paths, + duration, + configuration_files: _, + project_key, + } = configured_workspace; + + if let Some(stdin) = self.get_stdin(console, execution.as_ref())? { + let biome_path = BiomePath::new(stdin.as_path()); + if biome_path.extension().is_none() { + console.error(markup! { + {PrintDiagnostic::simple(&CliDiagnostic::from(StdinDiagnostic::new_no_extension()))} + }); + console.append(markup! {{stdin.as_content()}}); + return Ok(()); + } + + return Self::ProcessFile::process_std_in(ProcessStdinFilePayload { + biome_path: &biome_path, + project_key, + workspace, + execution: execution.as_ref(), + content: stdin.as_content(), + cli_options, + console, + }); + } + + let collector = self.collector(fs, execution.as_ref(), cli_options); + let mut output: Self::CrawlerOutput = Self::Crawler::crawl( + execution.as_ref(), + workspace, + fs, + project_key, + paths.clone(), + collector, + )?; + + Self::Finalizer::before_finalize(project_key, fs, workspace, &mut output)?; + + Self::Finalizer::finalize(FinalizePayload { + cli_options, + project_key, + execution: execution.as_ref(), + fs, + console, + workspace, + scan_duration: duration, + crawler_output: output, + paths, + }) + } + + /// This function prepares the workspace with the following: + /// - Loading the configuration file. + /// - Configure the VCS integration + /// - Computes the paths to traverse/handle. This changes based on the VCS arguments that were passed. + /// - Register a project folder using the working directory. + /// - Updates the settings that belong to the project registered + fn configure_workspace( + &mut self, + fs: &dyn FsWithResolverProxy, + console: &mut dyn Console, + workspace: &dyn Workspace, + cli_options: &CliOptions, + ) -> Result { + let working_dir = fs.working_directory().unwrap_or_default(); + // Load configuration + let configuration_path_hint = cli_options.as_configuration_path_hint(working_dir.as_path()); + let loaded_configuration = load_configuration(fs, configuration_path_hint)?; + if self.should_validate_configuration_diagnostics() { + validate_configuration_diagnostics( + &loaded_configuration, + console, + cli_options.verbose, + )?; + } + info!( + "Configuration file loaded: {:?}, diagnostics detected {}", + loaded_configuration.file_path, + loaded_configuration.diagnostics.len(), + ); + let LoadedConfiguration { + extended_configurations, + configuration, + diagnostics: _, + directory_path, + file_path, + } = loaded_configuration; + + // Merge the FS configuration with the CLI arguments + let configuration = self.merge_configuration( + configuration, + directory_path.clone(), + file_path, + fs, + console, + )?; + + let execution = self.get_execution(cli_options, console, workspace)?; + + let root_configuration_dir = directory_path + .clone() + .unwrap_or_else(|| working_dir.clone()); + // Using `--config-path`, users can point to a (root) config file that + // is not actually at the root of the project. So between the working + // directory and configuration directory, we use whichever one is higher + // up in the file system. + let project_dir = if root_configuration_dir.starts_with(&working_dir) { + &working_dir + } else { + &root_configuration_dir + }; + + let paths = self.get_files_to_process(fs, &configuration)?; + let paths = self.validated_paths_for_execution(paths, &working_dir, execution.as_ref())?; + + // Open the project + let open_project_result = workspace.open_project(OpenProjectParams { + path: BiomePath::new(project_dir), + open_uninitialized: true, + })?; + + let stdin = self.get_stdin(console, execution.as_ref())?; + let computed_scan_kind = + execution.scan_kind_computer(ProjectScanComputer::new(&configuration)); + + let scan_kind = derive_best_scan_kind( + computed_scan_kind, + stdin.as_ref(), + &root_configuration_dir, + &working_dir, + &configuration, + self.minimal_scan_kind(), + ); + + // Update the settings of the project + let result = workspace.update_settings(UpdateSettingsParams { + project_key: open_project_result.project_key, + workspace_directory: Some(BiomePath::new(project_dir)), + configuration, + extended_configurations: extended_configurations + .into_iter() + .map(|(path, config)| (BiomePath::from(path), config)) + .collect(), + })?; + if self.should_validate_configuration_diagnostics() { + print_diagnostics_from_workspace_result( + result.diagnostics.as_slice(), + console, + cli_options.verbose, + )?; + } + + // Scan the project + let scan_kind = + execution.compute_scan_kind(paths.as_slice(), working_dir.as_path(), scan_kind); + + let result = workspace.scan_project(ScanProjectParams { + project_key: open_project_result.project_key, + watch: cli_options.use_server, + force: false, // TODO: Maybe we'll want a CLI flag for this. + scan_kind, + verbose: cli_options.verbose, + })?; + + if self.should_validate_configuration_diagnostics() { + print_diagnostics_from_workspace_result( + result.diagnostics.as_slice(), + console, + cli_options.verbose, + )?; + } + + Ok(ConfiguredWorkspace { + execution, + paths, + duration: Some(result.duration), + configuration_files: result.configuration_files, + project_key: open_project_result.project_key, + }) + } + + /// Computes [Stdin] if the CLI has the necessary information. + /// + /// ## Errors + /// - If the user didn't provide anything via `stdin` but the option `--stdin-file-path` is passed. + fn get_stdin( + &self, + console: &mut dyn Console, + execution: &dyn Execution, + ) -> Result, CliDiagnostic> { + let stdin = if let Some(stdin_file_path) = execution.get_stdin_file_path() { + let input_code = console.read(); + if let Some(input_code) = input_code { + let path = Utf8PathBuf::from(stdin_file_path); + Some((path, input_code).into()) + } else { + // we provided the argument without a piped stdin, we bail + return Err(CliDiagnostic::missing_argument( + "stdin", + self.command_name(), + )); + } + } else { + None + }; + + Ok(stdin) + } + + // #region Methods that consumers must implement + + /// Implements this method if you need to merge CLI arguments to the loaded configuration. + /// + /// The CLI arguments take precedence over the option configured in the configuration file. + fn merge_configuration( + &mut self, + loaded_configuration: Configuration, + loaded_directory: Option, + loaded_file: Option, + fs: &dyn FileSystem, + console: &mut dyn Console, + ) -> Result; + + /// It returns the paths that need to be handled/traversed. + fn get_files_to_process( + &self, + fs: &dyn FileSystem, + configuration: &Configuration, + ) -> Result, CliDiagnostic>; + + /// Returns the [Execution] mode. + fn get_execution( + &self, + cli_options: &CliOptions, + console: &mut dyn Console, + workspace: &dyn Workspace, + ) -> Result, CliDiagnostic>; + + // Below, methods that consumers can implement + + /// Optional method that can be implemented to check if some CLI arguments aren't compatible. + /// + /// The method is called before loading the configuration from disk. + fn check_incompatible_arguments(&self) -> Result<(), CliDiagnostic> { + Ok(()) + } + + /// Checks whether the configuration has errors. + fn should_validate_configuration_diagnostics(&self) -> bool { + true + } + + /// Custom execution for commands that don't require file crawling. + /// + /// This method is only called when [CommandRunner::requires_crawling] returns `false`. + /// Commands like `migrate` can implement this to bypass the standard + /// crawler/collector/finalizer flow and execute their custom logic directly. + /// + /// The [ConfiguredWorkspace] provides access to: + /// - `project_key`: The project identifier + /// - `configuration_files`: Nested configuration files discovered during project scan + /// - `execution`: The execution context + /// - `paths`: The validated paths (may be empty for non-crawling commands) + /// - `duration`: The duration of the project scan + /// + /// # Default implementation + /// + /// Panics if called, as commands with `requires_crawling() = true` should never reach this code path. + fn execute_without_crawling( + &mut self, + _session: CliSession, + _configured_workspace: ConfiguredWorkspace, + ) -> Result<(), CliDiagnostic> { + panic!( + "{} command has requires_crawling() = false but did not implement execute_without_crawling()", + self.command_name() + ) + } + + // #endregion +} + +pub(crate) struct ConfiguredWorkspace { + /// Execution context + pub execution: Box, + /// Paths to crawl + pub paths: Vec, + /// The duration of the scanning + pub duration: Option, + /// Configuration files found inside the project + pub configuration_files: Vec, + /// The unique identifier of the project + pub project_key: ProjectKey, +} diff --git a/crates/biome_cli/src/runner/process_file.rs b/crates/biome_cli/src/runner/process_file.rs new file mode 100644 index 000000000000..4a219df66e85 --- /dev/null +++ b/crates/biome_cli/src/runner/process_file.rs @@ -0,0 +1,294 @@ +use crate::CliDiagnostic; +use crate::cli_options::CliOptions; +use crate::runner::crawler::CrawlerContext; +use crate::runner::diagnostics::{ResultExt, ResultIoExt, UnhandledDiagnostic}; +use crate::runner::execution::Execution; +use biome_console::Console; +use biome_diagnostics::{DiagnosticExt, DiagnosticTags, Error, category}; +use biome_fs::{BiomePath, File, OpenOptions}; +use biome_service::diagnostics::FileTooLarge; +use biome_service::file_handlers::DocumentFileSource; +use biome_service::projects::ProjectKey; +use biome_service::workspace::{ + FeaturesSupported, FileFeaturesResult, SupportKind, SupportsFeatureParams, +}; +use biome_service::workspace::{FileContent, FileGuard, OpenFileParams}; +use biome_service::{Workspace, WorkspaceError}; + +#[derive(Debug)] +pub(crate) enum FileStatus { + /// File changed and it was a success + Changed, + /// File unchanged, and it was a success + Unchanged, + /// While handling the file, something happened + Message(Message), + /// A match was found while searching a file + SearchResult(usize, Message), + /// File ignored, it should not be count as "handled" + Ignored, + /// Files that belong to other tools and shouldn't be touched + Protected(String), +} + +impl FileStatus { + pub const fn is_changed(&self) -> bool { + matches!(self, Self::Changed) + } +} + +#[derive(Debug)] +pub(crate) enum MessageStat { + Changed, + Unchanged, + Matches, + Skipped, +} + +/// Wrapper type for messages that can be printed during the traversal process +#[derive(Debug)] +pub(crate) enum Message { + Stats(MessageStat), + SkippedFixes { + /// Suggested fixes skipped during the lint traversal + skipped_suggested_fixes: u32, + }, + Failure, + Error(Error), + Diagnostics { + file_path: String, + content: String, + diagnostics: Vec, + skipped_diagnostics: u32, + }, + // DiagnosticsWithActions { + // file_path: String, + // content: String, + // diagnostics_with_actions: Vec<(Diagnostic, Vec)>, + // skipped_diagnostics: u32, + // }, + Diff { + file_name: String, + old: String, + new: String, + diff_kind: DiffKind, + }, +} + +impl Message { + pub(crate) const fn is_failure(&self) -> bool { + matches!(self, Self::Failure) + } +} + +#[derive(Debug)] +pub(crate) enum DiffKind { + Format, +} + +impl From for Message +where + Error: From, + D: std::fmt::Debug, +{ + fn from(err: D) -> Self { + Self::Error(Error::from(err)) + } +} + +/// The return type for [crate::execute::process_file::process_file], with the following semantics: +/// - `Ok(Success)` means the operation was successful (the file is added to +/// the `processed` counter) +/// - `Ok(Message(_))` means the operation was successful but a message still +/// needs to be printed (eg. the diff when not in CI or write mode) +/// - `Ok(Ignored)` means the file was ignored (the file is not added to the +/// `processed` or `skipped` counters) +/// - `Err(_)` means the operation failed and the file should be added to the +/// `skipped` counter +pub(crate) type FileResult = Result; + +pub(crate) struct ProcessStdinFilePayload<'a> { + pub(crate) biome_path: &'a BiomePath, + pub(crate) content: &'a str, + pub(crate) project_key: ProjectKey, + pub(crate) workspace: &'a dyn Workspace, + pub(crate) console: &'a mut dyn Console, + pub(crate) cli_options: &'a CliOptions, + pub(crate) execution: &'a dyn Execution, +} + +pub(crate) trait ProcessFile: Send + Sync + std::panic::RefUnwindSafe { + fn process_file( + ctx: &Ctx, + workspace_file: &mut WorkspaceFile, + features_supported: &FeaturesSupported, + ) -> Result + where + Ctx: CrawlerContext; + + fn process_std_in(payload: ProcessStdinFilePayload) -> Result<(), CliDiagnostic>; + + fn execute(ctx: &Ctx, biome_path: &BiomePath) -> FileResult + where + Ctx: CrawlerContext, + { + let FileFeaturesResult { + features_supported: file_features, + } = ctx + .workspace() + .file_features(SupportsFeatureParams { + project_key: ctx.project_key(), + path: biome_path.clone(), + features: ctx.execution().features(), + inline_config: None, + }) + .with_file_path_and_code_and_tags( + biome_path.to_string(), + category!("files/missingHandler"), + DiagnosticTags::VERBOSE, + )?; + + // first we stop if there are some files that don't have ALL features enabled, e.g. images, fonts, etc. + if file_features.is_ignored() || file_features.is_not_enabled() { + return Ok(FileStatus::Ignored); + } else if file_features.is_not_supported() || !DocumentFileSource::can_read(biome_path) { + return Err(Message::from( + UnhandledDiagnostic.with_file_path(biome_path.to_string()), + )); + } + + // then we pick the specific features for this file + let unsupported_reason = ctx.execution().supports_kind(&file_features); + + // TODO move logic + // { + // TraversalMode::Check { .. } | TraversalMode::CI { .. } => file_features + // .support_kind_if_not_enabled(FeatureKind::Lint) + // .and(file_features.support_kind_if_not_enabled(FeatureKind::Format)) + // .and(file_features.support_kind_if_not_enabled(FeatureKind::Assist)), + // TraversalMode::Format { .. } => { + // Some(file_features.support_kind_for(FeatureKind::Format)) + // } + // TraversalMode::Lint { .. } => Some(file_features.support_kind_for(FeatureKind::Lint)), + // TraversalMode::Migrate { .. } => None, + // TraversalMode::Search { .. } => { + // Some(file_features.support_kind_for(FeatureKind::Search)) + // } + // }; + + if let Some(reason) = unsupported_reason { + match reason { + SupportKind::FileNotSupported => { + return Err(Message::from( + UnhandledDiagnostic.with_file_path(biome_path.to_string()), + )); + } + SupportKind::FeatureNotEnabled | SupportKind::Ignored => { + return Ok(FileStatus::Ignored); + } + SupportKind::Protected => { + return Ok(FileStatus::Protected(biome_path.to_string())); + } + SupportKind::Supported => {} + }; + } + + let mut workspace_file = WorkspaceFile::new(ctx, biome_path.clone())?; + let result = workspace_file.guard().check_file_size()?; + if result.is_too_large() { + ctx.push_diagnostic( + FileTooLarge::from(result) + .with_file_path(workspace_file.path.to_string()) + .with_category(ctx.execution().as_diagnostic_category()), + ); + return Ok(FileStatus::Ignored); + } + + Self::process_file(ctx, &mut workspace_file, &file_features) + } +} + +impl ProcessFile for () { + fn process_file( + _: &Ctx, + _: &mut WorkspaceFile, + _: &FeaturesSupported, + ) -> Result + where + Ctx: CrawlerContext, + { + Ok(FileStatus::Unchanged) + } + + fn process_std_in(_payload: ProcessStdinFilePayload) -> Result<(), CliDiagnostic> { + Ok(()) + } +} + +/// Small wrapper that holds information and operations around the current processed file +pub(crate) struct WorkspaceFile<'ctx, 'app> { + guard: FileGuard<'app, dyn Workspace + 'ctx>, + file: Box, + pub(crate) path: BiomePath, +} + +impl<'ctx, 'app> WorkspaceFile<'ctx, 'app> { + /// It attempts to read the file from disk, creating a [FileGuard] and + /// saving these information internally + pub(crate) fn new(ctx: &'ctx Ctx, path: BiomePath) -> Result + where + Ctx: CrawlerContext, + { + let open_options = OpenOptions::default() + .read(true) + .write(ctx.execution().requires_write_access()); + + let mut file = ctx + .fs() + .open_with_options(path.as_path(), open_options) + .with_file_path(path.to_string())?; + + let mut input = String::new(); + file.read_to_string(&mut input) + .with_file_path(path.to_string())?; + + let guard = FileGuard::open( + ctx.workspace(), + OpenFileParams { + project_key: ctx.project_key(), + document_file_source: None, + path: path.clone(), + content: FileContent::from_client(&input), + persist_node_cache: false, + inline_config: None, + }, + ) + .with_file_path_and_code(path.to_string(), category!("internalError/fs"))?; + + Ok(Self { file, guard, path }) + } + + pub(crate) fn guard(&self) -> &FileGuard<'app, dyn Workspace + 'ctx> { + &self.guard + } + + pub(crate) fn input(&self) -> Result { + self.guard().get_file_content() + } + + pub(crate) fn as_extension(&self) -> Option<&str> { + self.path.extension() + } + + /// It updates the workspace file with `new_content` + pub(crate) fn update_file(&mut self, new_content: impl Into) -> Result<(), Error> { + let new_content = new_content.into(); + + self.file + .set_content(new_content.as_bytes()) + .with_file_path(self.path.to_string())?; + self.guard + .change_file(self.file.file_version(), new_content)?; + Ok(()) + } +} diff --git a/crates/biome_cli/src/runner/run.rs b/crates/biome_cli/src/runner/run.rs new file mode 100644 index 000000000000..4d015ddc5cb9 --- /dev/null +++ b/crates/biome_cli/src/runner/run.rs @@ -0,0 +1,14 @@ +use crate::cli_options::CliOptions; +use crate::logging::LogOptions; +use crate::runner::CommandRunner; +use crate::{CliDiagnostic, CliSession}; + +pub(crate) fn run_command( + session: CliSession, + log_options: &LogOptions, + cli_options: &CliOptions, + mut command: impl CommandRunner, +) -> Result<(), CliDiagnostic> { + let command = &mut command; + command.run(session, log_options, cli_options) +} diff --git a/crates/biome_cli/src/runner/scan_kind.rs b/crates/biome_cli/src/runner/scan_kind.rs new file mode 100644 index 000000000000..9cd457b45c07 --- /dev/null +++ b/crates/biome_cli/src/runner/scan_kind.rs @@ -0,0 +1,76 @@ +use crate::runner::execution::Stdin; +use biome_configuration::Configuration; +use biome_fs::BiomePath; +use biome_service::workspace::ScanKind; +use camino::Utf8Path; + +/// Returns a forced scan kind based on the given `execution`. +fn get_forced_scan_kind( + stdin: Option<&Stdin>, + root_configuration_dir: &Utf8Path, + working_dir: &Utf8Path, + maybe_scan_kind: Option, +) -> Option { + if let Some(stdin) = stdin { + let path = stdin.as_path(); + if path + .parent() + .is_some_and(|dir| dir == root_configuration_dir) + { + return Some(ScanKind::NoScanner); + } else { + return Some(ScanKind::TargetedKnownFiles { + target_paths: vec![BiomePath::new(working_dir.join(path))], + descend_from_targets: false, + }); + } + } + + maybe_scan_kind +} + +/// Figures out the best (as in, most efficient) scan kind for the given execution. +/// +/// Rules: +/// - When processing from `stdin`, we return [ScanKind::NoScanner] if the stdin +/// file path is in the directory of the root configuration, and +/// [ScanKind::TargetedKnownFiles] otherwise. +/// - Returns [ScanKind::KnownFiles] for `biome format`, `biome migrate`, and +/// `biome search`, because we know there is no use for project analysis with +/// these commands. +/// - If the linter is disabled, we don't ever return [ScanKind::Project], because +/// we don't need to scan the project in that case. +/// - Otherwise, we return the requested scan kind. +pub(crate) fn derive_best_scan_kind( + requested_scan_kind: ScanKind, + stdin: Option<&Stdin>, + root_configuration_dir: &Utf8Path, + working_dir: &Utf8Path, + configuration: &Configuration, + command_scan_kind: Option, +) -> ScanKind { + get_forced_scan_kind( + stdin, + root_configuration_dir, + working_dir, + command_scan_kind, + ) + .unwrap_or({ + let required_minimum_scan_kind = + if configuration.is_root() || configuration.use_ignore_file() { + ScanKind::KnownFiles + } else { + ScanKind::NoScanner + }; + if requested_scan_kind == ScanKind::NoScanner + || (requested_scan_kind == ScanKind::Project && !configuration.is_linter_enabled()) + { + // If we're here, it means we're executing `check`, `lint` or `ci` + // and the linter is disabled or no projects rules have been enabled. + // We scan known files if the configuration is a root or if the VCS integration is enabled + required_minimum_scan_kind + } else { + requested_scan_kind + } + }) +} diff --git a/crates/biome_cli/tests/snapshots/main_cases_diagnostics/max_diagnostics_verbose.snap b/crates/biome_cli/tests/snapshots/main_cases_diagnostics/max_diagnostics_verbose.snap index a1bdda2d917c..8a209c40d76c 100644 --- a/crates/biome_cli/tests/snapshots/main_cases_diagnostics/max_diagnostics_verbose.snap +++ b/crates/biome_cli/tests/snapshots/main_cases_diagnostics/max_diagnostics_verbose.snap @@ -153,4 +153,5 @@ src/file.js format ━━━━━━━━━━━━━━━━━━━━ Scanned project folder in