From 08ad08f7914be42bddbf5c80dc2aeed15a639605 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 30 Jun 2023 14:55:47 +0100 Subject: [PATCH] feat(rome_cli): command `rome lint` (#4629) --- crates/rome_cli/src/commands/lint.rs | 94 + crates/rome_cli/src/commands/mod.rs | 24 + crates/rome_cli/src/execute/lint_file.rs | 85 + crates/rome_cli/src/execute/mod.rs | 64 +- crates/rome_cli/src/execute/process_file.rs | 22 +- crates/rome_cli/src/execute/std_in.rs | 8 +- crates/rome_cli/src/execute/traverse.rs | 5 +- crates/rome_cli/src/lib.rs | 19 + crates/rome_cli/tests/commands/lint.rs | 2215 +++++++++++++++++ crates/rome_cli/tests/commands/mod.rs | 1 + .../main_commands_lint/all_rules.snap | 53 + .../apply_bogus_argument.snap | 39 + .../main_commands_lint/apply_noop.snap | 17 + .../main_commands_lint/apply_ok.snap | 17 + .../main_commands_lint/apply_suggested.snap | 19 + .../apply_suggested_error.snap | 25 + .../apply_unsafe_no_assign_in_expression.snap | 17 + .../apply_unsafe_with_error.snap | 56 + .../main_commands_lint/check_help.snap | 71 + .../main_commands_lint/check_json_files.snap | 60 + .../check_stdin_apply_successfully.snap | 17 + ...check_stdin_apply_unsafe_successfully.snap | 17 + .../config_recommended_group.snap | 58 + .../deprecated_suppression_comment.snap | 36 + ...esnt_error_if_no_files_were_processed.snap | 11 + .../downgrade_severity.snap | 47 + .../main_commands_lint/file_too_large.snap | 34 + .../file_too_large_cli_limit.snap | 41 + .../file_too_large_config_limit.snap | 51 + .../files_max_size_parse_error.snap | 26 + .../fs_error_dereferenced_symlink.snap | 32 + .../fs_error_infinite_symlink_expansion.snap | 42 + .../fs_error_read_only.snap | 36 + .../main_commands_lint/fs_error_unknown.snap | 32 + .../fs_files_ignore_symlink.snap | 11 + .../ignore_configured_globals.snap | 27 + .../ignore_vcs_ignored_file.snap | 76 + .../ignore_vcs_ignored_file_via_cli.snap | 64 + .../ignore_vcs_os_independent_parse.snap | 55 + .../ignores_unknown_file.snap | 24 + .../main_commands_lint/lint_error.snap | 59 + .../main_commands_lint/max_diagnostics.snap | 167 ++ .../max_diagnostics_default.snap | 307 +++ .../maximum_diagnostics.snap | 498 ++++ ..._if_files_are_listed_in_ignore_option.snap | 48 + .../no_lint_if_linter_is_disabled.snap | 38 + ..._if_linter_is_disabled_when_run_apply.snap | 38 + .../no_lint_when_file_is_ignored.snap | 39 + .../no_supported_file_found.snap | 22 + .../main_commands_lint/nursery_unstable.snap | 46 + .../main_commands_lint/parse_error.snap | 47 + .../main_commands_lint/print_verbose.snap | 59 + .../should_apply_correct_file_source.snap | 32 + .../should_disable_a_rule.snap | 33 + .../should_disable_a_rule_group.snap | 32 + ...disable_recommended_rules_for_a_group.snap | 73 + ...ould_not_enable_all_recommended_rules.snap | 48 + .../should_not_enable_nursery_rules.snap | 43 + .../suppression_syntax_error.snap | 41 + .../top_level_all_down_level_not_all.snap | 137 + .../top_level_not_all_down_level_all.snap | 111 + .../main_commands_lint/unsupported_file.snap | 29 + .../main_commands_lint/upgrade_severity.snap | 59 + crates/rome_lsp/src/handlers/analysis.rs | 8 + crates/rome_service/src/workspace.rs | 8 +- crates/rome_service/src/workspace/server.rs | 3 +- npm/backend-jsonrpc/src/workspace.ts | 1 + 67 files changed, 5644 insertions(+), 30 deletions(-) create mode 100644 crates/rome_cli/src/commands/lint.rs create mode 100644 crates/rome_cli/src/execute/lint_file.rs create mode 100644 crates/rome_cli/tests/commands/lint.rs create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/all_rules.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/apply_bogus_argument.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/apply_noop.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/apply_ok.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/apply_suggested.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/apply_suggested_error.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/apply_unsafe_no_assign_in_expression.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/apply_unsafe_with_error.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/check_help.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/check_json_files.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/check_stdin_apply_successfully.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/check_stdin_apply_unsafe_successfully.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/config_recommended_group.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/deprecated_suppression_comment.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/doesnt_error_if_no_files_were_processed.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/downgrade_severity.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/file_too_large.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/file_too_large_cli_limit.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/file_too_large_config_limit.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/files_max_size_parse_error.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/fs_error_dereferenced_symlink.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/fs_error_infinite_symlink_expansion.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/fs_error_read_only.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/fs_error_unknown.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/fs_files_ignore_symlink.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/ignore_configured_globals.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/ignore_vcs_ignored_file.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/ignore_vcs_ignored_file_via_cli.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/ignore_vcs_os_independent_parse.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/ignores_unknown_file.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/lint_error.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/max_diagnostics.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/max_diagnostics_default.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/maximum_diagnostics.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/no_lint_if_files_are_listed_in_ignore_option.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/no_lint_if_linter_is_disabled.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/no_lint_if_linter_is_disabled_when_run_apply.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/no_lint_when_file_is_ignored.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/no_supported_file_found.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/nursery_unstable.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/parse_error.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/print_verbose.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/should_apply_correct_file_source.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/should_disable_a_rule.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/should_disable_a_rule_group.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/should_not_disable_recommended_rules_for_a_group.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/should_not_enable_all_recommended_rules.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/should_not_enable_nursery_rules.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/suppression_syntax_error.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/top_level_all_down_level_not_all.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/top_level_not_all_down_level_all.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/unsupported_file.snap create mode 100644 crates/rome_cli/tests/snapshots/main_commands_lint/upgrade_severity.snap diff --git a/crates/rome_cli/src/commands/lint.rs b/crates/rome_cli/src/commands/lint.rs new file mode 100644 index 00000000000..cc690606ad8 --- /dev/null +++ b/crates/rome_cli/src/commands/lint.rs @@ -0,0 +1,94 @@ +use crate::cli_options::CliOptions; +use crate::configuration::{load_configuration, LoadedConfiguration}; +use crate::vcs::store_path_to_ignore_from_vcs; +use crate::{execute_mode, CliDiagnostic, CliSession, Execution, TraversalMode}; +use rome_service::workspace::{FixFileMode, UpdateSettingsParams}; +use rome_service::{Configuration, MergeWith}; +use std::ffi::OsString; +use std::path::PathBuf; + +pub(crate) struct LintCommandPayload { + pub(crate) apply: bool, + pub(crate) apply_unsafe: bool, + pub(crate) cli_options: CliOptions, + pub(crate) configuration: Option, + pub(crate) paths: Vec, + pub(crate) stdin_file_path: Option, +} + +/// Handler for the "lint" command of the Rome CLI +pub(crate) fn lint( + mut session: CliSession, + payload: LintCommandPayload, +) -> Result<(), CliDiagnostic> { + let LintCommandPayload { + apply, + apply_unsafe, + cli_options, + configuration, + paths, + stdin_file_path, + } = payload; + + let fix_file_mode = if apply && apply_unsafe { + return Err(CliDiagnostic::incompatible_arguments( + "--apply", + "--apply-unsafe", + )); + } else if !apply && !apply_unsafe { + None + } else if apply && !apply_unsafe { + Some(FixFileMode::SafeFixes) + } else { + Some(FixFileMode::SafeAndUnsafeFixes) + }; + + let LoadedConfiguration { + configuration: mut fs_configuration, + directory_path: configuration_path, + .. + } = load_configuration(&mut session, &cli_options)? + .or_diagnostic(session.app.console, cli_options.verbose)?; + + fs_configuration.merge_with(configuration); + + // check if support of git ignore files is enabled + let vcs_base_path = configuration_path.or(session.app.fs.working_directory()); + store_path_to_ignore_from_vcs( + &mut session, + &mut fs_configuration, + vcs_base_path, + &cli_options, + )?; + + let stdin = if let Some(stdin_file_path) = stdin_file_path { + let console = &mut session.app.console; + let input_code = console.read(); + if let Some(input_code) = input_code { + let path = PathBuf::from(stdin_file_path); + Some((path, input_code)) + } else { + // we provided the argument without a piped stdin, we bail + return Err(CliDiagnostic::missing_argument("stdin", "lint")); + } + } else { + None + }; + + session + .app + .workspace + .update_settings(UpdateSettingsParams { + configuration: fs_configuration, + })?; + + execute_mode( + Execution::new(TraversalMode::Lint { + fix_file_mode, + stdin, + }), + session, + &cli_options, + paths, + ) +} diff --git a/crates/rome_cli/src/commands/mod.rs b/crates/rome_cli/src/commands/mod.rs index 2330679447e..4bd9269285f 100644 --- a/crates/rome_cli/src/commands/mod.rs +++ b/crates/rome_cli/src/commands/mod.rs @@ -14,6 +14,7 @@ pub(crate) mod ci; pub(crate) mod daemon; pub(crate) mod format; pub(crate) mod init; +pub(crate) mod lint; pub(crate) mod migrate; pub(crate) mod rage; pub(crate) mod version; @@ -75,6 +76,26 @@ pub enum RomeCommand { #[bpaf(positional("PATH"), many)] paths: Vec, }, + /// Run various checks on a set of files. + #[bpaf(command)] + Lint { + /// Apply safe fixes, formatting + #[bpaf(long("apply"), switch)] + apply: bool, + /// Apply safe fixes and unsafe fixes, formatting and import sorting + #[bpaf(long("apply-unsafe"), switch)] + apply_unsafe: bool, + #[bpaf(external, hide_usage, optional)] + configuration: Option, + #[bpaf(external, hide_usage)] + cli_options: CliOptions, + /// A file name with its extension to pass when reading from standard in, e.g. echo 'let a;' | rome lint --stdin-file-path=file.js" + #[bpaf(long("stdin-file-path"), argument("PATH"), hide_usage)] + stdin_file_path: Option, + /// Single file, single path or list of paths + #[bpaf(positional("PATH"), many)] + paths: Vec, + }, /// Run the formatter on a set of files. #[bpaf(command)] Format { @@ -160,6 +181,7 @@ impl RomeCommand { RomeCommand::Start => None, RomeCommand::Stop => None, RomeCommand::Check { cli_options, .. } => cli_options.colors.as_ref(), + RomeCommand::Lint { cli_options, .. } => cli_options.colors.as_ref(), RomeCommand::Ci { cli_options, .. } => cli_options.colors.as_ref(), RomeCommand::Format { cli_options, .. } => cli_options.colors.as_ref(), RomeCommand::Init => None, @@ -177,6 +199,7 @@ impl RomeCommand { RomeCommand::Start => false, RomeCommand::Stop => false, RomeCommand::Check { cli_options, .. } => cli_options.use_server, + RomeCommand::Lint { cli_options, .. } => cli_options.use_server, RomeCommand::Ci { cli_options, .. } => cli_options.use_server, RomeCommand::Format { cli_options, .. } => cli_options.use_server, RomeCommand::Init => false, @@ -198,6 +221,7 @@ impl RomeCommand { RomeCommand::Start => false, RomeCommand::Stop => false, RomeCommand::Check { cli_options, .. } => cli_options.verbose, + RomeCommand::Lint { cli_options, .. } => cli_options.verbose, RomeCommand::Format { cli_options, .. } => cli_options.verbose, RomeCommand::Ci { cli_options, .. } => cli_options.verbose, RomeCommand::Init => false, diff --git a/crates/rome_cli/src/execute/lint_file.rs b/crates/rome_cli/src/execute/lint_file.rs new file mode 100644 index 00000000000..f82d028f7d5 --- /dev/null +++ b/crates/rome_cli/src/execute/lint_file.rs @@ -0,0 +1,85 @@ +use crate::execute::diagnostics::{ResultExt, ResultIoExt}; +use crate::execute::process_file::{FileResult, FileStatus, Message}; +use crate::execute::traverse::TraversalOptions; +use crate::CliDiagnostic; +use rome_diagnostics::{category, Error}; +use rome_fs::{OpenOptions, RomePath}; +use rome_service::file_handlers::Language; +use rome_service::workspace::{FileGuard, OpenFileParams, RuleCategories}; +use std::path::Path; +use std::sync::atomic::Ordering; + +pub(crate) struct LintFile<'ctx, 'app> { + pub(crate) ctx: &'app TraversalOptions<'ctx, 'app>, + pub(crate) path: &'app Path, +} + +/// Lints a single file and returns a [FileResult] +pub(crate) fn lint_file(payload: LintFile) -> FileResult { + let LintFile { ctx, path } = payload; + let rome_path = RomePath::new(path); + let mut errors = 0; + let open_options = OpenOptions::default() + .read(true) + .write(ctx.execution.requires_write_access()); + let mut file = ctx + .fs + .open_with_options(path, open_options) + .with_file_path(path.display().to_string())?; + + let mut input = String::new(); + file.read_to_string(&mut input) + .with_file_path(path.display().to_string())?; + + let file_guard = FileGuard::open( + ctx.workspace, + OpenFileParams { + path: rome_path, + version: 0, + content: input.clone(), + language_hint: Language::default(), + }, + ) + .with_file_path_and_code(path.display().to_string(), category!("internalError/fs"))?; + if let Some(fix_mode) = ctx.execution.as_fix_file_mode() { + let fixed = file_guard + .fix_file(*fix_mode, false) + .with_file_path_and_code(path.display().to_string(), category!("lint"))?; + + ctx.push_message(Message::SkippedFixes { + skipped_suggested_fixes: fixed.skipped_suggested_fixes, + }); + + if fixed.code != input { + file.set_content(fixed.code.as_bytes()) + .with_file_path(path.display().to_string())?; + file_guard.change_file(file.file_version(), fixed.code)?; + } + errors = fixed.errors; + } + + let max_diagnostics = ctx.remaining_diagnostics.load(Ordering::Relaxed); + let result = file_guard + .pull_diagnostics(RuleCategories::LINT, max_diagnostics.into()) + .with_file_path_and_code(path.display().to_string(), category!("lint"))?; + + let no_diagnostics = result.diagnostics.is_empty() && result.skipped_diagnostics == 0; + let result = if no_diagnostics || ctx.execution.is_format() { + FileStatus::Success + } else { + FileStatus::Message(Message::Diagnostics { + name: path.display().to_string(), + content: input.clone(), + diagnostics: result.diagnostics.into_iter().map(Error::from).collect(), + skipped_diagnostics: result.skipped_diagnostics, + }) + }; + ctx.increment_processed(); + if errors > 0 { + return Ok(FileStatus::Message(Message::ApplyError( + CliDiagnostic::file_apply_error(path.display().to_string()), + ))); + } else { + Ok(result) + } +} diff --git a/crates/rome_cli/src/execute/mod.rs b/crates/rome_cli/src/execute/mod.rs index daff479634b..737c9c5f204 100644 --- a/crates/rome_cli/src/execute/mod.rs +++ b/crates/rome_cli/src/execute/mod.rs @@ -1,4 +1,5 @@ mod diagnostics; +mod lint_file; mod migrate; mod process_file; mod std_in; @@ -11,6 +12,7 @@ use rome_diagnostics::MAXIMUM_DISPLAYABLE_DIAGNOSTICS; use rome_fs::RomePath; use rome_service::workspace::{FeatureName, FixFileMode}; use std::ffi::OsString; +use std::fmt::{Display, Formatter}; use std::path::PathBuf; /// Useful information during the traversal of files and virtual content @@ -48,6 +50,18 @@ pub(crate) enum TraversalMode { /// 2. The content of the file stdin: Option<(PathBuf, String)>, }, + /// This mode is enabled when running the command `rome lint` + Lint { + /// 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<(PathBuf, String)>, + }, /// This mode is enabled when running the command `rome ci` CI, /// This mode is enabled when running the command `rome format` @@ -68,6 +82,18 @@ pub(crate) enum TraversalMode { }, } +impl Display for TraversalMode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + TraversalMode::Check { .. } => write!(f, "check"), + TraversalMode::CI { .. } => write!(f, "ci"), + TraversalMode::Format { .. } => write!(f, "format"), + TraversalMode::Migrate { .. } => write!(f, "migrate"), + TraversalMode::Lint { .. } => write!(f, "lint"), + } + } +} + /// Tells to the execution of the traversal how the information should be reported #[derive(Copy, Clone, Default)] pub(crate) enum ReportMode { @@ -111,10 +137,12 @@ impl Execution { /// `true` only when running the traversal in [TraversalMode::Check] and `should_fix` is `true` pub(crate) fn as_fix_file_mode(&self) -> Option<&FixFileMode> { - if let TraversalMode::Check { fix_file_mode, .. } = &self.traversal_mode { - fix_file_mode.as_ref() - } else { - None + 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 { .. } => { + None + } } } @@ -126,6 +154,10 @@ impl Execution { matches!(self.traversal_mode, TraversalMode::Check { .. }) } + pub(crate) const fn is_lint(&self) -> bool { + matches!(self.traversal_mode, TraversalMode::Lint { .. }) + } + pub(crate) const fn is_check_apply(&self) -> bool { matches!( self.traversal_mode, @@ -153,7 +185,8 @@ impl Execution { /// 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, .. } => fix_file_mode.is_some(), + TraversalMode::Check { fix_file_mode, .. } + | TraversalMode::Lint { fix_file_mode, .. } => fix_file_mode.is_some(), TraversalMode::CI => false, TraversalMode::Format { write, .. } => write, TraversalMode::Migrate { write: dry_run, .. } => dry_run, @@ -162,19 +195,10 @@ impl Execution { pub(crate) fn as_stdin_file(&self) -> Option<&(PathBuf, String)> { match &self.traversal_mode { - TraversalMode::Format { stdin, .. } => stdin.as_ref(), - TraversalMode::Check { stdin, .. } => stdin.as_ref(), - _ => None, - } - } - - /// Returns the subcommand of the [traversal mode](TraversalMode) execution - pub(crate) fn traversal_mode_subcommand(&self) -> &'static str { - match self.traversal_mode { - TraversalMode::Check { .. } => "check", - TraversalMode::CI { .. } => "ci", - TraversalMode::Format { .. } => "format", - TraversalMode::Migrate { .. } => "migrate", + TraversalMode::Format { stdin, .. } + | TraversalMode::Lint { stdin, .. } + | TraversalMode::Check { stdin, .. } => stdin.as_ref(), + TraversalMode::CI { .. } | TraversalMode::Migrate { .. } => None, } } } @@ -197,10 +221,10 @@ pub(crate) fn execute_mode( max_diagnostics } else { - // The command `rome check` gives a default value of 20. + // The commands `rome check` and `rome lint` give a default value of 20. // In case of other commands that pass here, we limit to 50 to avoid to delay the terminal. match &mode.traversal_mode { - TraversalMode::Check { .. } => 20, + TraversalMode::Check { .. } | TraversalMode::Lint { .. } => 20, TraversalMode::CI | TraversalMode::Format { .. } | TraversalMode::Migrate { .. } => 50, } }; diff --git a/crates/rome_cli/src/execute/process_file.rs b/crates/rome_cli/src/execute/process_file.rs index 8ba23fcb3f1..051345382bb 100644 --- a/crates/rome_cli/src/execute/process_file.rs +++ b/crates/rome_cli/src/execute/process_file.rs @@ -1,4 +1,5 @@ use crate::execute::diagnostics::{ResultExt, ResultIoExt, SkippedDiagnostic, UnhandledDiagnostic}; +use crate::execute::lint_file::{lint_file, LintFile}; use crate::execute::traverse::TraversalOptions; use crate::execute::TraversalMode; use crate::{CliDiagnostic, FormatterReportFileDetail}; @@ -156,6 +157,7 @@ pub(crate) fn process_file(ctx: &TraversalOptions, path: &Path) -> FileResult { }), ), TraversalMode::Format { .. } => file_features.support_kind_for(&FeatureName::Format), + TraversalMode::Lint { .. } => file_features.support_kind_for(&FeatureName::Lint), TraversalMode::Migrate { .. } => None, }; @@ -173,6 +175,20 @@ pub(crate) fn process_file(ctx: &TraversalOptions, path: &Path) -> FileResult { }; } + // NOTE: this is a work in progress that will be refactored over time + // + // With time, we will create a separate file for each traversal mode. Reason to do so + // is to keep the business logics of each traversal separate. Doing so would allow us to + // lower the changes to break the business logic of other traversal. + // + // This would definitely repeat the code, but it's worth the effort in the long run. + if let TraversalMode::Lint { .. } = ctx.execution.traversal_mode { + // the unsupported case should be handled already at this point + if file_features.supports_for(&FeatureName::Lint) { + return lint_file(LintFile { ctx, path }); + } + } + let open_options = OpenOptions::default() .read(true) .write(ctx.execution.requires_write_access()); @@ -201,7 +217,7 @@ pub(crate) fn process_file(ctx: &TraversalOptions, path: &Path) -> FileResult { if let Some(fix_mode) = ctx.execution.as_fix_file_mode() { let fixed = file_guard - .fix_file(*fix_mode) + .fix_file(*fix_mode, file_features.supports_for(&FeatureName::Format)) .with_file_path_and_code(path.display().to_string(), category!("lint"))?; ctx.push_message(Message::SkippedFixes { @@ -330,7 +346,9 @@ pub(crate) fn process_file(ctx: &TraversalOptions, path: &Path) -> FileResult { let should_write = match ctx.execution.traversal_mode() { // In check mode do not run the formatter and return the result immediately, // but only if the argument `--apply` is not passed. - TraversalMode::Check { .. } => ctx.execution.as_fix_file_mode().is_some(), + TraversalMode::Check { .. } | TraversalMode::Lint { .. } => { + ctx.execution.as_fix_file_mode().is_some() + } TraversalMode::CI { .. } => false, TraversalMode::Format { write, .. } => *write, TraversalMode::Migrate { write: dry_run, .. } => *dry_run, diff --git a/crates/rome_cli/src/execute/std_in.rs b/crates/rome_cli/src/execute/std_in.rs index 93b76a8b291..94c2d99edf5 100644 --- a/crates/rome_cli/src/execute/std_in.rs +++ b/crates/rome_cli/src/execute/std_in.rs @@ -51,7 +51,7 @@ pub(crate) fn run<'a>( "The content was not formatted because the formatter is currently disabled." }) } - } else if mode.is_check() { + } else if mode.is_check() || mode.is_lint() { let mut diagnostics = Vec::new(); let mut new_content = Cow::Borrowed(content); @@ -75,6 +75,8 @@ pub(crate) fn run<'a>( let fix_file_result = workspace.fix_file(FixFileParams { fix_file_mode: *fix_file_mode, path: rome_path.clone(), + should_format: mode.is_check() + && file_features.supports_for(&FeatureName::Format), })?; if fix_file_result.code != new_content { version += 1; @@ -87,7 +89,7 @@ pub(crate) fn run<'a>( } } - if file_features.supports_for(&FeatureName::OrganizeImports) { + if file_features.supports_for(&FeatureName::OrganizeImports) && mode.is_check() { let result = workspace.organize_imports(OrganizeImportsParams { path: rome_path.clone(), })?; @@ -115,7 +117,7 @@ pub(crate) fn run<'a>( diagnostics.extend(result.diagnostics); } - if file_features.supports_for(&FeatureName::Format) { + if file_features.supports_for(&FeatureName::Format) && mode.is_check() { let printed = workspace.format_file(FormatFileParams { path: rome_path.clone(), })?; diff --git a/crates/rome_cli/src/execute/traverse.rs b/crates/rome_cli/src/execute/traverse.rs index ca163d70633..137056498ae 100644 --- a/crates/rome_cli/src/execute/traverse.rs +++ b/crates/rome_cli/src/execute/traverse.rs @@ -66,7 +66,7 @@ pub(crate) fn traverse( if inputs.is_empty() && execution.as_stdin_file().is_none() { return Err(CliDiagnostic::missing_argument( "", - execution.traversal_mode_subcommand(), + format!("{}", execution.traversal_mode), )); } @@ -130,7 +130,7 @@ pub(crate) fn traverse( if execution.should_report_to_terminal() { match execution.traversal_mode() { - TraversalMode::Check { .. } => { + TraversalMode::Check { .. } | TraversalMode::Lint { .. } => { if execution.as_fix_file_mode().is_some() { console.log(markup! { "Fixed "{count}" file(s) in "{duration} @@ -676,6 +676,7 @@ impl<'ctx, 'app> TraversalContext for TraversalOptions<'ctx, 'app> { || file_features.supports_for(&FeatureName::OrganizeImports) } TraversalMode::Format { .. } => file_features.supports_for(&FeatureName::Format), + TraversalMode::Lint { .. } => file_features.supports_for(&FeatureName::Lint), // Imagine if Rome can't handle its own configuration file... TraversalMode::Migrate { .. } => true, } diff --git a/crates/rome_cli/src/lib.rs b/crates/rome_cli/src/lib.rs index 831f732078c..6cfe890bf5f 100644 --- a/crates/rome_cli/src/lib.rs +++ b/crates/rome_cli/src/lib.rs @@ -27,6 +27,7 @@ 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; pub use crate::commands::{parse_command, RomeCommand}; pub use diagnostics::CliDiagnostic; pub(crate) use execute::{execute_mode, Execution, TraversalMode}; @@ -98,6 +99,24 @@ impl<'app> CliSession<'app> { formatter_enabled, }, ), + RomeCommand::Lint { + apply, + apply_unsafe, + cli_options, + configuration: rome_configuration, + paths, + stdin_file_path, + } => commands::lint::lint( + self, + LintCommandPayload { + apply_unsafe, + apply, + cli_options, + configuration: rome_configuration, + paths, + stdin_file_path, + }, + ), RomeCommand::Ci { linter_enabled, formatter_enabled, diff --git a/crates/rome_cli/tests/commands/lint.rs b/crates/rome_cli/tests/commands/lint.rs new file mode 100644 index 00000000000..1124c314ce1 --- /dev/null +++ b/crates/rome_cli/tests/commands/lint.rs @@ -0,0 +1,2215 @@ +use bpaf::Args; +use std::env::temp_dir; +use std::fs::{create_dir, create_dir_all, remove_dir_all, File}; +use std::io::Write; +#[cfg(target_family = "unix")] +use std::os::unix::fs::symlink; +#[cfg(target_os = "windows")] +use std::os::windows::fs::{symlink_dir, symlink_file}; +use std::path::{Path, PathBuf}; + +use crate::configs::{ + CONFIG_FILE_SIZE_LIMIT, CONFIG_IGNORE_SYMLINK, CONFIG_LINTER_AND_FILES_IGNORE, + CONFIG_LINTER_DISABLED, CONFIG_LINTER_DOWNGRADE_DIAGNOSTIC, CONFIG_LINTER_IGNORED_FILES, + CONFIG_LINTER_SUPPRESSED_GROUP, CONFIG_LINTER_SUPPRESSED_RULE, + CONFIG_LINTER_UPGRADE_DIAGNOSTIC, CONFIG_RECOMMENDED_GROUP, +}; +use crate::snap_test::{markup_to_string, SnapshotPayload}; +use crate::{assert_cli_snapshot, run_cli, FORMATTED, LINT_ERROR, PARSE_ERROR}; +use rome_console::{markup, BufferConsole, LogLevel, MarkupBuf}; +use rome_fs::{ErrorEntry, FileSystemExt, MemoryFileSystem, OsFileSystem}; +use rome_service::DynRef; + +const ERRORS: &str = r#" +for(;true;);for(;true;);for(;true;);for(;true;);for(;true;);for(;true;); +for(;true;);for(;true;);for(;true;);for(;true;);for(;true;);for(;true;); +for(;true;);for(;true;);for(;true;);for(;true;);for(;true;);for(;true;); +for(;true;);for(;true;);for(;true;);for(;true;);for(;true;);for(;true;); +for(;true;);for(;true;);for(;true;);for(;true;);for(;true;);for(;true;); +for(;true;);for(;true;);for(;true;);for(;true;);for(;true;);for(;true;); +for(;true;);for(;true;);for(;true;);for(;true;);for(;true;);for(;true;); +for(;true;);for(;true;);for(;true;);for(;true;);for(;true;);for(;true;); +"#; + +const NO_DEBUGGER: &str = "debugger;"; +const NEW_SYMBOL: &str = "new Symbol(\"\");"; + +const FIX_BEFORE: &str = "(1 >= -0)"; +const FIX_AFTER: &str = "(1 >= 0)"; + +const APPLY_SUGGESTED_BEFORE: &str = "let a = 4; +debugger; +console.log(a); +"; + +const APPLY_SUGGESTED_AFTER: &str = "const a = 4;\nconsole.log(a);\n"; + +const NO_DEBUGGER_BEFORE: &str = "debugger;\n"; +const NO_DEBUGGER_AFTER: &str = "debugger;\n"; + +const UPGRADE_SEVERITY_CODE: &str = r#"if(!cond) { exprA(); } else { exprB() }"#; + +const NURSERY_UNSTABLE: &str = r#"if(a = b) {}"#; + +#[test] +fn check_help() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), "--help"]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "check_help", + fs, + console, + result, + )); +} + +#[test] +fn ok() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("check.js"); + fs.insert(file_path.into(), FORMATTED.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); +} + +#[test] +fn ok_read_only() { + let mut fs = MemoryFileSystem::new_read_only(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("check.js"); + fs.insert(file_path.into(), FORMATTED.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); +} + +#[test] +fn parse_error() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("check.js"); + fs.insert(file_path.into(), PARSE_ERROR.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "parse_error", + fs, + console, + result, + )); +} + +#[test] +fn lint_error() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("check.js"); + fs.insert(file_path.into(), LINT_ERROR.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "lint_error", + fs, + console, + result, + )); +} + +#[test] +fn maximum_diagnostics() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + let file_path = Path::new("check.js"); + fs.insert(file_path.into(), ERRORS.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + let messages = &console.out_buffer; + + assert_eq!( + messages + .iter() + .filter(|m| m.level == LogLevel::Error) + .count(), + 20_usize + ); + + assert!(messages + .iter() + .filter(|m| m.level == LogLevel::Log) + .any(|m| { + let content = format!("{:?}", m.content); + content.contains("The number of diagnostics exceeds the number allowed by Rome") + && content.contains("Diagnostics not shown") + && content.contains("76") + })); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "maximum_diagnostics", + fs, + console, + result, + )); +} + +#[test] +fn apply_ok() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("fix.js"); + fs.insert(file_path.into(), FIX_BEFORE.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--apply"), + file_path.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + let mut buffer = String::new(); + fs.open(file_path) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + + assert_eq!(buffer, FIX_AFTER); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "apply_ok", + fs, + console, + result, + )); +} + +#[test] +fn apply_noop() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("fix.js"); + fs.insert(file_path.into(), FIX_AFTER.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--apply"), + file_path.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "apply_noop", + fs, + console, + result, + )); +} + +#[test] +fn apply_suggested_error() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("fix.js"); + fs.insert(file_path.into(), APPLY_SUGGESTED_BEFORE.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--apply-unsafe"), + ("--apply"), + file_path.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "apply_suggested_error", + fs, + console, + result, + )); +} + +#[test] +fn apply_suggested() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("fix.js"); + fs.insert(file_path.into(), APPLY_SUGGESTED_BEFORE.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--apply-unsafe"), + file_path.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + let mut buffer = String::new(); + fs.open(file_path) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + + assert_eq!(buffer, APPLY_SUGGESTED_AFTER); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "apply_suggested", + fs, + console, + result, + )); +} + +#[test] +fn apply_unsafe_with_error() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + // last line doesn't have code fix + let source = "let a = 4; +debugger; +console.log(a); +function f() { arguments; } +"; + + let expected = "const a = 4; +console.log(a); +function f() { arguments; } +"; + + let test1 = Path::new("test1.js"); + fs.insert(test1.into(), source.as_bytes()); + + let test2 = Path::new("test2.js"); + fs.insert(test2.into(), source.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--apply-unsafe"), + test1.as_os_str().to_str().unwrap(), + test2.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + let mut file = fs + .open(test1) + .expect("formatting target file was removed by the CLI"); + + let mut content = String::new(); + file.read_to_string(&mut content) + .expect("failed to read file from memory FS"); + + assert_eq!(content, expected); + drop(file); + + content.clear(); + + let mut file = fs + .open(test2) + .expect("formatting target file was removed by the CLI"); + + file.read_to_string(&mut content) + .expect("failed to read file from memory FS"); + + drop(file); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "apply_unsafe_with_error", + fs, + console, + result, + )); +} + +#[test] +fn no_lint_if_linter_is_disabled_when_run_apply() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("fix.js"); + fs.insert(file_path.into(), FIX_BEFORE.as_bytes()); + + let config_path = Path::new("rome.json"); + fs.insert(config_path.into(), CONFIG_LINTER_DISABLED.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--apply"), + file_path.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + let mut buffer = String::new(); + fs.open(file_path) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + + assert_eq!(buffer, FIX_BEFORE); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "no_lint_if_linter_is_disabled_when_run_apply", + fs, + console, + result, + )); +} + +#[test] +fn no_lint_if_linter_is_disabled() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("fix.js"); + fs.insert(file_path.into(), FIX_BEFORE.as_bytes()); + + let config_path = Path::new("rome.json"); + fs.insert(config_path.into(), CONFIG_LINTER_DISABLED.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + let mut buffer = String::new(); + fs.open(file_path) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + + assert_eq!(buffer, FIX_BEFORE); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "no_lint_if_linter_is_disabled", + fs, + console, + result, + )); +} + +#[test] +fn should_disable_a_rule() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("fix.js"); + fs.insert(file_path.into(), NO_DEBUGGER_BEFORE.as_bytes()); + + let config_path = Path::new("rome.json"); + fs.insert(config_path.into(), CONFIG_LINTER_SUPPRESSED_RULE.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--apply"), + file_path.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + let mut buffer = String::new(); + fs.open(file_path) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + + assert_eq!(buffer, NO_DEBUGGER_AFTER); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "should_disable_a_rule", + fs, + console, + result, + )); +} + +#[test] +fn should_disable_a_rule_group() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("fix.js"); + fs.insert(file_path.into(), FIX_BEFORE.as_bytes()); + + let config_path = Path::new("rome.json"); + fs.insert( + config_path.into(), + CONFIG_LINTER_SUPPRESSED_GROUP.as_bytes(), + ); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--apply"), + file_path.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + let mut buffer = String::new(); + fs.open(file_path) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + + assert_eq!(buffer, "(1 >= -0)"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "should_disable_a_rule_group", + fs, + console, + result, + )); +} + +#[test] +fn downgrade_severity() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + let file_path = Path::new("rome.json"); + fs.insert( + file_path.into(), + CONFIG_LINTER_DOWNGRADE_DIAGNOSTIC.as_bytes(), + ); + + let file_path = Path::new("file.js"); + fs.insert(file_path.into(), NO_DEBUGGER.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + println!("{console:?}"); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + let messages = &console.out_buffer; + + assert_eq!( + messages + .iter() + .filter(|m| m.level == LogLevel::Error) + .filter(|m| { + let content = format!("{:#?}", m.content); + content.contains("suspicious/noDebugger") + }) + .count(), + 1 + ); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "downgrade_severity", + fs, + console, + result, + )); +} + +#[test] +fn upgrade_severity() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + let file_path = Path::new("rome.json"); + fs.insert( + file_path.into(), + CONFIG_LINTER_UPGRADE_DIAGNOSTIC.as_bytes(), + ); + + let file_path = Path::new("file.js"); + fs.insert(file_path.into(), UPGRADE_SEVERITY_CODE.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + let messages = &console.out_buffer; + + let error_count = messages + .iter() + .filter(|m| m.level == LogLevel::Error) + .filter(|m| { + let content = format!("{:?}", m.content); + content.contains("style/noNegationElse") + }) + .count(); + + assert_eq!( + error_count, 1, + "expected 1 error-level message in console buffer, found {error_count:?}:\n{:?}", + console.out_buffer + ); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "upgrade_severity", + fs, + console, + result, + )); +} + +#[test] +fn no_lint_when_file_is_ignored() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("rome.json"); + fs.insert(file_path.into(), CONFIG_LINTER_IGNORED_FILES.as_bytes()); + + let file_path = Path::new("test.js"); + fs.insert(file_path.into(), FIX_BEFORE.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--apply"), + file_path.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + let mut buffer = String::new(); + fs.open(file_path) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + + assert_eq!(buffer, FIX_BEFORE); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "no_lint_when_file_is_ignored", + fs, + console, + result, + )); +} + +#[test] +fn no_lint_if_files_are_listed_in_ignore_option() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("rome.json"); + fs.insert(file_path.into(), CONFIG_LINTER_AND_FILES_IGNORE.as_bytes()); + + let file_path_test1 = Path::new("test1.js"); + fs.insert(file_path_test1.into(), FIX_BEFORE.as_bytes()); + + let file_path_test2 = Path::new("test2.js"); + fs.insert(file_path_test2.into(), FIX_BEFORE.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--apply"), + file_path_test1.as_os_str().to_str().unwrap(), + file_path_test2.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + let mut buffer = String::new(); + fs.open(file_path_test1) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + + assert_eq!(buffer, FIX_BEFORE); + + let mut buffer = String::new(); + fs.open(file_path_test2) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + + assert_eq!(buffer, FIX_BEFORE); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "no_lint_if_files_are_listed_in_ignore_option", + fs, + console, + result, + )); +} + +/// Creating a symbolic link will fail on Windows if the current process is +/// unprivileged. Since running tests as administrator is uncommon and +/// constraining, this error gets silently ignored if we're not running on CI +/// (the workflows are being being run with the correct permissions on CI) +#[cfg(target_os = "windows")] +macro_rules! check_windows_symlink { + ($result:expr) => { + match $result { + Ok(res) => res, + Err(err) if option_env!("CI") == Some("1") => panic!("failed to create symlink: {err}"), + Err(_) => return, + } + }; +} + +#[test] +fn fs_error_dereferenced_symlink() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let root_path = temp_dir().join("rome_test_broken_symlink"); + let subdir_path = root_path.join("prefix"); + + #[allow(unused_must_use)] + { + remove_dir_all(root_path.display().to_string().as_str()); + } + create_dir(root_path.display().to_string().as_str()).unwrap(); + create_dir(subdir_path).unwrap(); + + #[cfg(target_family = "unix")] + { + symlink(root_path.join("null"), root_path.join("broken_symlink")).unwrap(); + } + + #[cfg(target_os = "windows")] + { + check_windows_symlink!(symlink_file( + root_path.join("null"), + root_path.join("broken_symlink") + )); + } + + let result = run_cli( + DynRef::Owned(Box::new(OsFileSystem)), + &mut console, + Args::from(&[("lint"), root_path.display().to_string().as_str()]), + ); + + remove_dir_all(root_path).unwrap(); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "fs_error_dereferenced_symlink", + fs, + console, + result, + )); +} + +#[test] +fn fs_error_infinite_symlink_exapansion() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let root_path = temp_dir().join("rome_test_infinite_symlink_exapansion"); + let subdir1_path = root_path.join("prefix"); + let subdir2_path = root_path.join("foo").join("bar"); + + #[allow(unused_must_use)] + { + remove_dir_all(root_path.display().to_string().as_str()); + } + create_dir(root_path.display().to_string().as_str()).unwrap(); + create_dir(subdir1_path.clone()).unwrap(); + + create_dir_all(subdir2_path.clone()).unwrap(); + + #[cfg(target_family = "unix")] + { + symlink(subdir1_path.clone(), root_path.join("self_symlink1")).unwrap(); + symlink(subdir1_path, subdir2_path.join("self_symlink2")).unwrap(); + } + + #[cfg(target_os = "windows")] + { + check_windows_symlink!(symlink_dir( + subdir1_path.clone(), + root_path.join("self_symlink1") + )); + check_windows_symlink!(symlink_dir( + subdir1_path, + subdir2_path.join("self_symlink2") + )); + } + + let result = run_cli( + DynRef::Owned(Box::new(OsFileSystem)), + &mut console, + Args::from(&[("lint"), (root_path.display().to_string().as_str())]), + ); + + remove_dir_all(root_path).unwrap(); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "fs_error_infinite_symlink_expansion", + fs, + console, + result, + )); +} + +#[test] +fn fs_error_read_only() { + let mut fs = MemoryFileSystem::new_read_only(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("test.js"); + fs.insert(file_path.into(), *b"content"); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--apply"), + file_path.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + // Do not store the content of the file in the snapshot + fs.remove(file_path); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "fs_error_read_only", + fs, + console, + result, + )); +} + +#[test] +fn fs_error_unknown() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + fs.insert_error(PathBuf::from("prefix/ci.js"), ErrorEntry::UnknownFileType); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), ("prefix")]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "fs_error_unknown", + fs, + console, + result, + )); +} + +// Symbolic link ignore pattern test +// +// Verifies, that ignore patterns to symbolic links are allowed. +// +// ├── rome.json +// ├── hidden_nested +// │ └── test +// │ └── symlink_testcase1_2 -> hidden_testcase1 +// ├── hidden_testcase1 +// │ └── test +// │ └── test.js // ok +// ├── hidden_testcase2 +// │ ├── test1.ts // ignored +// │ ├── test2.ts // ignored +// │ └── test.js // ok +// └── src +// ├── symlink_testcase1_1 -> hidden_nested +// └── symlink_testcase2 -> hidden_testcase2 +#[test] +fn fs_files_ignore_symlink() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let root_path = temp_dir().join("rome_test_files_ignore_symlink"); + let src_path = root_path.join("src"); + + let testcase1_path = root_path.join("hidden_testcase1"); + let testcase1_sub_path = testcase1_path.join("test"); + let testcase2_path = root_path.join("hidden_testcase2"); + + let nested_path = root_path.join("hidden_nested"); + let nested_sub_path = nested_path.join("test"); + + #[allow(unused_must_use)] + { + remove_dir_all(root_path.display().to_string().as_str()); + } + create_dir(root_path.display().to_string().as_str()).unwrap(); + create_dir(src_path.clone()).unwrap(); + create_dir_all(testcase1_sub_path.clone()).unwrap(); + create_dir(testcase2_path.clone()).unwrap(); + create_dir_all(nested_sub_path.clone()).unwrap(); + + // src/symlink_testcase1_1 + let symlink_testcase1_1_path = src_path.join("symlink_testcase1_1"); + // hidden_nested/test/symlink_testcase1_2 + let symlink_testcase1_2_path = nested_sub_path.join("symlink_testcase1_2"); + // src/symlink_testcase2 + let symlink_testcase2_path = src_path.join("symlink_testcase2"); + + #[cfg(target_family = "unix")] + { + // src/test/symlink_testcase1_1 -> hidden_nested + symlink(nested_path, symlink_testcase1_1_path).unwrap(); + // hidden_nested/test/symlink_testcase1_2 -> hidden_testcase1 + symlink(testcase1_path, symlink_testcase1_2_path).unwrap(); + // src/symlink_testcase2 -> hidden_testcase2 + symlink(testcase2_path.clone(), symlink_testcase2_path).unwrap(); + } + + #[cfg(target_os = "windows")] + { + check_windows_symlink!(symlink_dir(nested_path.clone(), symlink_testcase1_1_path)); + check_windows_symlink!(symlink_dir( + testcase1_path.clone(), + symlink_testcase1_2_path + )); + check_windows_symlink!(symlink_dir(testcase2_path.clone(), symlink_testcase2_path)); + } + + let config_path = root_path.join("rome.json"); + let mut config_file = File::create(config_path).unwrap(); + config_file + .write_all(CONFIG_IGNORE_SYMLINK.as_bytes()) + .unwrap(); + + let files: [PathBuf; 4] = [ + testcase1_sub_path.join("test.js"), // ok + testcase2_path.join("test.js"), // ok + testcase2_path.join("test1.ts"), // ignored + testcase2_path.join("test2.ts"), // ignored + ]; + + for file_path in files { + let mut file = File::create(file_path).unwrap(); + file.write_all(APPLY_SUGGESTED_BEFORE.as_bytes()).unwrap(); + } + + let result = run_cli( + DynRef::Owned(Box::new(OsFileSystem)), + &mut console, + Args::from(&[ + ("lint"), + ("--config-path"), + (root_path.display().to_string().as_str()), + ("--apply-unsafe"), + (src_path.display().to_string().as_str()), + ]), + ); + + remove_dir_all(root_path).unwrap(); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "fs_files_ignore_symlink", + fs, + console, + result, + )); +} + +#[test] +fn file_too_large() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("check.js"); + fs.insert(file_path.into(), "statement();\n".repeat(80660).as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + // Do not store the content of the file in the snapshot + fs.remove(file_path); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "file_too_large", + fs, + console, + result, + )); +} + +#[test] +fn file_too_large_config_limit() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + fs.insert(PathBuf::from("rome.json"), CONFIG_FILE_SIZE_LIMIT); + + let file_path = Path::new("check.js"); + fs.insert(file_path.into(), "statement1();\nstatement2();"); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "file_too_large_config_limit", + fs, + console, + result, + )); +} + +#[test] +fn file_too_large_cli_limit() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("check.js"); + fs.insert(file_path.into(), "statement1();\nstatement2();"); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--files-max-size=16"), + file_path.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "file_too_large_cli_limit", + fs, + console, + result, + )); +} + +#[test] +fn files_max_size_parse_error() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("check.js"); + fs.insert(file_path.into(), "statement1();\nstatement2();"); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--files-max-size=-1"), + file_path.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "files_max_size_parse_error", + fs, + console, + result, + )); +} + +#[test] +fn max_diagnostics_default() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + // Creates 40 diagnostics. + for i in 0..40 { + let file_path = PathBuf::from(format!("src/file_{i}.js")); + fs.insert(file_path, LINT_ERROR.as_bytes()); + } + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), ("src")]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + let mut diagnostic_count = 0; + let mut filtered_messages = Vec::new(); + + for msg in console.out_buffer { + let MarkupBuf(nodes) = &msg.content; + let is_diagnostic = nodes.iter().any(|node| { + node.content.contains("useWhile") + || node.content.contains("useBlockStatements") + || node.content.contains("noConstantCondition") + }); + + if is_diagnostic { + diagnostic_count += 1; + } else { + filtered_messages.push(msg); + } + } + + console.out_buffer = filtered_messages; + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "max_diagnostics_default", + fs, + console, + result, + )); + assert_eq!(diagnostic_count, 20); +} + +#[test] +fn max_diagnostics() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + for i in 0..20 { + let file_path = PathBuf::from(format!("src/file_{i}.js")); + fs.insert(file_path, LINT_ERROR.as_bytes()); + } + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--max-diagnostics"), + ("10"), + Path::new("src").as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + let mut diagnostic_count = 0; + let mut filtered_messages = Vec::new(); + + for msg in console.out_buffer { + let MarkupBuf(nodes) = &msg.content; + let is_diagnostic = nodes.iter().any(|node| { + node.content.contains("useWhile") + || node.content.contains("useBlockStatements") + || node.content.contains("noConstantCondition") + }); + + if is_diagnostic { + diagnostic_count += 1; + } else { + filtered_messages.push(msg); + } + } + + console.out_buffer = filtered_messages; + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "max_diagnostics", + fs, + console, + result, + )); + + assert_eq!(diagnostic_count, 10); +} + +#[test] +fn no_supported_file_found() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), "."]), + ); + + eprintln!("{:?}", console.out_buffer); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "no_supported_file_found", + fs, + console, + result, + )); +} + +#[test] +fn deprecated_suppression_comment() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("file.js"); + fs.insert( + file_path.into(), + *b"// rome-ignore lint(suspicious/noDoubleEquals): test +a == b;", + ); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "deprecated_suppression_comment", + fs, + console, + result, + )); +} + +#[test] +fn print_verbose() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("check.js"); + fs.insert(file_path.into(), LINT_ERROR.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--verbose"), + file_path.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "print_verbose", + fs, + console, + result, + )); +} + +#[test] +fn unsupported_file() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("check.txt"); + fs.insert(file_path.into(), LINT_ERROR.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "unsupported_file", + fs, + console, + result, + )); +} + +#[test] +fn suppression_syntax_error() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("check.js"); + fs.insert(file_path.into(), *b"// rome-ignore(:\n"); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "suppression_syntax_error", + fs, + console, + result, + )); +} + +#[test] +fn config_recommended_group() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("rome.json"); + fs.insert(file_path.into(), CONFIG_RECOMMENDED_GROUP.as_bytes()); + + let file_path = Path::new("check.js"); + fs.insert(file_path.into(), NEW_SYMBOL.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + assert!(result.is_err(), "run_cli returned {result:?}"); + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "config_recommended_group", + fs, + console, + result, + )); +} + +#[test] +fn nursery_unstable() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("check.js"); + fs.insert(file_path.into(), NURSERY_UNSTABLE.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "nursery_unstable", + fs, + console, + result, + )); +} + +#[test] +fn all_rules() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let rome_json = r#"{ + "linter": { + "rules": { "all": true } + } + }"#; + + let file_path = Path::new("fix.js"); + fs.insert(file_path.into(), FIX_BEFORE.as_bytes()); + + let config_path = Path::new("rome.json"); + fs.insert(config_path.into(), rome_json.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "all_rules", + fs, + console, + result, + )); +} + +#[test] +fn top_level_all_down_level_not_all() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let rome_json = r#"{ + "linter": { + "rules": { + "all": true, + "style": { + "all": false + } + } + } + }"#; + + // style/noArguments + // style/noShoutyConstants + // style/useSingleVarDeclarator + let code = r#" + function f() {arguments;} + const FOO = "FOO"; + var x, y; + "#; + + let file_path = Path::new("fix.js"); + fs.insert(file_path.into(), code.as_bytes()); + + let config_path = Path::new("rome.json"); + fs.insert(config_path.into(), rome_json.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "top_level_all_down_level_not_all", + fs, + console, + result, + )); +} + +#[test] +fn top_level_not_all_down_level_all() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let rome_json = r#"{ + "linter": { + "rules": { + "all": false, + "style": { + "all": true + } + } + } + }"#; + + // style/noArguments + // style/noShoutyConstants + // style/useSingleVarDeclarator + let code = r#" + function f() {arguments;} + const FOO = "FOO"; + var x, y; + "#; + + let file_path = Path::new("fix.js"); + fs.insert(file_path.into(), code.as_bytes()); + + let config_path = Path::new("rome.json"); + fs.insert(config_path.into(), rome_json.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "top_level_not_all_down_level_all", + fs, + console, + result, + )); +} + +#[test] +fn ignore_configured_globals() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let rome_json = r#"{ + "javascript": { + "globals": ["foo", "bar"] + } + }"#; + + // style/useSingleVarDeclarator + let code = r#"foo.call(); bar.call();"#; + + let file_path = Path::new("fix.js"); + fs.insert(file_path.into(), code.as_bytes()); + + let config_path = Path::new("rome.json"); + fs.insert(config_path.into(), rome_json.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "ignore_configured_globals", + fs, + console, + result, + )); +} + +#[test] +fn ignore_vcs_ignored_file() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let rome_json = r#"{ + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + } + }"#; + + let git_ignore = r#" +file2.js +"#; + + let code2 = r#"foo.call(); bar.call();"#; + let code1 = r#"array.map(sentence => sentence.split(' ')).flat();"#; + + // ignored files + let file_path1 = Path::new("file1.js"); + fs.insert(file_path1.into(), code1.as_bytes()); + let file_path2 = Path::new("file2.js"); + fs.insert(file_path2.into(), code2.as_bytes()); + + // configuration + let config_path = Path::new("rome.json"); + fs.insert(config_path.into(), rome_json.as_bytes()); + + // git folder + let git_folder = Path::new(".git"); + fs.insert(git_folder.into(), "".as_bytes()); + + // git ignore file + let ignore_file = Path::new(".gitignore"); + fs.insert(ignore_file.into(), git_ignore.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + file_path1.as_os_str().to_str().unwrap(), + file_path2.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "ignore_vcs_ignored_file", + fs, + console, + result, + )); +} + +#[test] +fn ignore_vcs_os_independent_parse() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let rome_json = r#"{ + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + } + }"#; + + let git_ignore = "something.js\nfile2.js\r\nfile3.js"; + + let code3 = r#"console.log('rome is cool');"#; + let code2 = r#"foo.call(); bar.call();"#; + let code1 = r#"blah.call();"#; + + let file_path1 = Path::new("file1.js"); + fs.insert(file_path1.into(), code1.as_bytes()); + + // ignored files + let file_path2 = Path::new("file2.js"); + fs.insert(file_path2.into(), code2.as_bytes()); + let file_path3 = Path::new("file3.js"); + fs.insert(file_path3.into(), code3.as_bytes()); + + // configuration + let config_path = Path::new("rome.json"); + fs.insert(config_path.into(), rome_json.as_bytes()); + + // git folder + let git_folder = Path::new(".git"); + fs.insert(git_folder.into(), "".as_bytes()); + + // git ignore file + let ignore_file = Path::new(".gitignore"); + fs.insert(ignore_file.into(), git_ignore.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + file_path1.as_os_str().to_str().unwrap(), + file_path2.as_os_str().to_str().unwrap(), + file_path3.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "ignore_vcs_os_independent_parse", + fs, + console, + result, + )); +} + +#[test] +fn ignore_vcs_ignored_file_via_cli() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let git_ignore = r#" +file2.js +"#; + + let code2 = r#"foo.call(); bar.call();"#; + let code1 = r#"array.map(sentence => sentence.split(' ')).flat();"#; + + // ignored files + let file_path1 = Path::new("file1.js"); + fs.insert(file_path1.into(), code1.as_bytes()); + let file_path2 = Path::new("file2.js"); + fs.insert(file_path2.into(), code2.as_bytes()); + + // git folder + let git_folder = Path::new("./.git"); + fs.insert(git_folder.into(), "".as_bytes()); + + // git ignore file + let ignore_file = Path::new("./.gitignore"); + fs.insert(ignore_file.into(), git_ignore.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + ("--vcs-enabled=true"), + ("--vcs-client-kind=git"), + ("--vcs-use-ignore-file=true"), + ("--vcs-root=."), + file_path1.as_os_str().to_str().unwrap(), + file_path2.as_os_str().to_str().unwrap(), + ]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "ignore_vcs_ignored_file_via_cli", + fs, + console, + result, + )); +} + +#[test] +fn check_stdin_apply_successfully() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + console + .in_buffer + .push("function f() {return{}} class Foo { constructor() {} }".to_string()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), "--apply", ("--stdin-file-path"), ("mock.js")]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + let message = console + .out_buffer + .get(0) + .expect("Console should have written a message"); + + let content = markup_to_string(markup! { + {message.content} + }); + + assert_eq!(content, "function f() {return{}} class Foo { }"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "check_stdin_apply_successfully", + fs, + console, + result, + )); +} + +#[test] +fn check_stdin_apply_unsafe_successfully() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + console + .in_buffer + .push("function f() {return{}} class Foo { constructor() {} }".to_string()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + "--apply-unsafe", + ("--stdin-file-path"), + ("mock.js"), + ]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + let message = console + .out_buffer + .get(0) + .expect("Console should have written a message"); + + let content = markup_to_string(markup! { + {message.content} + }); + + assert_eq!(content, "function f() {return{}} class Foo { }"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "check_stdin_apply_unsafe_successfully", + fs, + console, + result, + )); +} + +#[test] +fn should_apply_correct_file_source() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("file.ts"); + fs.insert( + file_path.into(), + "type A = { a: string }; type B = Partial".as_bytes(), + ); + + let config_path = Path::new("rome.json"); + fs.insert( + config_path.into(), + r#"{ + "linter": { + "rules": { + "recommended": true, + "correctness": { + "noUndeclaredVariables": "error" + } + } + } + }"# + .as_bytes(), + ); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + let mut buffer = String::new(); + fs.open(file_path) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "should_apply_correct_file_source", + fs, + console, + result, + )); +} + +#[test] +fn apply_unsafe_no_assign_in_expression() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("fix.js"); + fs.insert( + file_path.into(), + "res.onAborted(() => (aborted = true));".as_bytes(), + ); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + file_path.as_os_str().to_str().unwrap(), + ("--apply-unsafe"), + ]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "apply_unsafe_no_assign_in_expression", + fs, + console, + result, + )); +} + +#[test] +fn should_not_enable_all_recommended_rules() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let configuration = r#" { + "$schema": "https://docs.rome.tools/schemas/12.1.0/schema.json", + "organizeImports": { + "enabled": false + }, + "linter": { + "enabled": true, + "rules": { + "recommended": false, + "a11y": {}, + "complexity": {}, + "correctness": {}, + "performance": {}, + "security": {}, + "style": {}, + "suspicious": {} + } + } + }"#; + + let configuration_path = Path::new("rome.json"); + fs.insert(configuration_path.into(), configuration.as_bytes()); + + let file_path = Path::new("fix.js"); + fs.insert( + file_path.into(), + r#" + LOOP: for (const x of xs) { + if (x > 0) { + break; + } + f(x); + } + "#, + ); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "should_not_enable_all_recommended_rules", + fs, + console, + result, + )); +} + +#[test] +fn should_not_disable_recommended_rules_for_a_group() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let configuration = r#" { + "$schema": "https://docs.rome.tools/schemas/12.1.0/schema.json", + "organizeImports": { + "enabled": false + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noUselessSwitchCase": "off" + } + } + } +}"#; + + let configuration_path = Path::new("rome.json"); + fs.insert(configuration_path.into(), configuration.as_bytes()); + + let file_path = Path::new("fix.js"); + fs.insert( + file_path.into(), + r#"const array = ["split", "the text", "into words"]; +// next line should error because of the recommended rule +array.map((sentence) => sentence.split(" ")).flat(); + "#, + ); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "should_not_disable_recommended_rules_for_a_group", + fs, + console, + result, + )); +} + +#[test] +fn should_not_enable_nursery_rules() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let configuration = r#" { + "$schema": "https://docs.rome.tools/schemas/12.1.0/schema.json", + "organizeImports": { + "enabled": false + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "nursery": { + "noAccumulatingSpread": "error" + } + } + } +}"#; + + let configuration_path = Path::new("rome.json"); + fs.insert(configuration_path.into(), configuration.as_bytes()); + + let file_path = Path::new("fix.ts"); + fs.insert( + file_path.into(), + r#"const bannedType: Boolean = true; + +if (true) { + const obj = {}; + obj["useLiteralKey"]; +} + "#, + ); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "should_not_enable_nursery_rules", + fs, + console, + result, + )); +} + +#[test] +fn apply_bogus_argument() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("fix.js"); + fs.insert( + file_path.into(), + "function _13_1_3_fun(arguments) { }".as_bytes(), + ); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + file_path.as_os_str().to_str().unwrap(), + ("--apply-unsafe"), + ]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "apply_bogus_argument", + fs, + console, + result, + )); +} + +#[test] +fn ignores_unknown_file() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path1 = Path::new("test.txt"); + fs.insert(file_path1.into(), *b"content"); + + let file_path2 = Path::new("test.js"); + fs.insert(file_path2.into(), *b"console.log('bar');\n"); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[ + ("lint"), + file_path1.as_os_str().to_str().unwrap(), + file_path2.as_os_str().to_str().unwrap(), + "--files-ignore-unknown=true", + ]), + ); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "ignores_unknown_file", + fs, + console, + result, + )); +} + +#[test] +fn check_json_files() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path1 = Path::new("test.json"); + fs.insert( + file_path1.into(), + r#"{ "foo": true, "foo": true }"#.as_bytes(), + ); + + let configuration = Path::new("rome.json"); + fs.insert( + configuration.into(), + r#"{ + "linter": { + "rules": { + "nursery": { + "noDuplicateJsonKeys": "error" + } + } + } + }"# + .as_bytes(), + ); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), file_path1.as_os_str().to_str().unwrap()]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "check_json_files", + fs, + console, + result, + )); +} + +#[test] +fn doesnt_error_if_no_files_were_processed() { + let mut console = BufferConsole::default(); + let mut fs = MemoryFileSystem::default(); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from(&[("lint"), "--no-errors-on-unmatched", ("file.js")]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "doesnt_error_if_no_files_were_processed", + fs, + console, + result, + )); +} diff --git a/crates/rome_cli/tests/commands/mod.rs b/crates/rome_cli/tests/commands/mod.rs index 142c8784348..3ab45656b4c 100644 --- a/crates/rome_cli/tests/commands/mod.rs +++ b/crates/rome_cli/tests/commands/mod.rs @@ -2,6 +2,7 @@ mod check; mod ci; mod format; mod init; +mod lint; mod lsp_proxy; mod migrate; mod rage; diff --git a/crates/rome_cli/tests/snapshots/main_commands_lint/all_rules.snap b/crates/rome_cli/tests/snapshots/main_commands_lint/all_rules.snap new file mode 100644 index 00000000000..952023a8561 --- /dev/null +++ b/crates/rome_cli/tests/snapshots/main_commands_lint/all_rules.snap @@ -0,0 +1,53 @@ +--- +source: crates/rome_cli/tests/snap_test.rs +expression: content +--- +## `rome.json` + +```json +{ + "linter": { + "rules": { "all": true } + } +} +``` + +## `fix.js` + +```js +(1 >= -0) +``` + +# Termination Message + +```block +internalError/io ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Some errors were emitted while running checks + + + +``` + +# Emitted Messages + +```block +fix.js:1:2 lint/suspicious/noCompareNegZero FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Do not use the >= operator to compare against -0. + + > 1 │ (1 >= -0) + │ ^^^^^^^ + + i Safe fix: Replace -0 with 0 + + 1 │ (1·>=·-0) + │ - + +``` + +```block +Checked 1 file(s) in +``` + +# Emitted Messages + +```block +Checked 1 file(s) in