mirror of https://github.com/astral-sh/ruff
Merge f5d4d2d119 into b0bc990cbf
This commit is contained in:
commit
38e1a49916
|
|
@ -246,12 +246,40 @@ pub struct CheckCommand {
|
|||
|
||||
/// Output serialization format for violations.
|
||||
/// The default serialization format is "full".
|
||||
///
|
||||
/// Note: For multiple output formats, use --output-file with format:target syntax instead.
|
||||
/// Example: --output-file gitlab:gl-code-quality.json --output-file full:stdout
|
||||
#[arg(long, value_enum, env = "RUFF_OUTPUT_FORMAT")]
|
||||
pub output_format: Option<OutputFormat>,
|
||||
|
||||
/// Specify file to write the linter output to (default: stdout).
|
||||
#[arg(short, long, env = "RUFF_OUTPUT_FILE")]
|
||||
pub output_file: Option<PathBuf>,
|
||||
/// Specify output destination(s) using format:target syntax.
|
||||
/// Can be specified multiple times for multiple outputs.
|
||||
/// Also supports comma-separated values: --output-file gitlab:file.json,full:stdout
|
||||
///
|
||||
/// Examples:
|
||||
/// --output-file gitlab:gl-code-quality.json
|
||||
/// --output-file full:stdout
|
||||
/// --output-file concise:stderr
|
||||
/// --output-file gitlab:gl-code-quality.json --output-file full:stdout
|
||||
/// --output-file gitlab:file.json,full:stdout
|
||||
///
|
||||
/// Environment variable `RUFF_OUTPUT_FILE` also supports comma-separated values:
|
||||
/// RUFF_OUTPUT_FILE="gitlab:gl-code-quality.json,full:stdout"
|
||||
///
|
||||
/// Targets can be: stdout, stderr, or a file path.
|
||||
/// Formats: full, concise, grouped, json, json-lines, junit, github, gitlab, pylint, rdjson, azure, sarif
|
||||
///
|
||||
/// Legacy: If used with --output-format (without format: prefix), writes that format to the specified file.
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
action = clap::ArgAction::Append,
|
||||
value_parser = OutputTargetPairParser,
|
||||
env = "RUFF_OUTPUT_FILE",
|
||||
value_delimiter = ',',
|
||||
value_name = "FORMAT:TARGET"
|
||||
)]
|
||||
pub output_file: Vec<OutputTargetPairOrPath>,
|
||||
/// The minimum Python version that should be supported.
|
||||
#[arg(long, value_enum)]
|
||||
pub target_version: Option<PythonVersion>,
|
||||
|
|
@ -746,6 +774,7 @@ impl CheckCommand {
|
|||
ignore_noqa: self.ignore_noqa,
|
||||
no_cache: self.no_cache,
|
||||
output_file: self.output_file,
|
||||
output_format: self.output_format,
|
||||
show_files: self.show_files,
|
||||
show_settings: self.show_settings,
|
||||
statistics: self.statistics,
|
||||
|
|
@ -898,6 +927,136 @@ impl InvalidConfigFlagReason {
|
|||
}
|
||||
}
|
||||
|
||||
/// Represents a destination for output (stdout, stderr, or a file path).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum OutputTarget {
|
||||
Stdout,
|
||||
Stderr,
|
||||
File(PathBuf),
|
||||
}
|
||||
|
||||
/// Represents a format:target pair for output (e.g., "gitlab:gl-code-quality.json" or "full:stdout").
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct OutputTargetPair {
|
||||
pub format: OutputFormat,
|
||||
pub target: OutputTarget,
|
||||
}
|
||||
|
||||
/// Parser for output target pairs in the format "FORMAT:target".
|
||||
/// Examples:
|
||||
/// - "gitlab:gl-code-quality.json" -> `OutputFormat::Gitlab`, `OutputTarget::File("gl-code-quality.json`")
|
||||
/// - "full:stdout" -> `OutputFormat::Full`, `OutputTarget::Stdout`
|
||||
/// - "concise:stderr" -> `OutputFormat::Concise`, `OutputTarget::Stderr`
|
||||
#[derive(Clone)]
|
||||
pub struct OutputTargetPairParser;
|
||||
|
||||
/// Represents either a format:target pair or a plain path (for backward compatibility).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum OutputTargetPairOrPath {
|
||||
Pair(OutputTargetPair),
|
||||
Path(PathBuf),
|
||||
}
|
||||
|
||||
impl TypedValueParser for OutputTargetPairParser {
|
||||
type Value = OutputTargetPairOrPath;
|
||||
|
||||
fn parse_ref(
|
||||
&self,
|
||||
cmd: &clap::Command,
|
||||
arg: Option<&clap::Arg>,
|
||||
value: &std::ffi::OsStr,
|
||||
) -> Result<Self::Value, clap::Error> {
|
||||
let value = value
|
||||
.to_str()
|
||||
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
|
||||
|
||||
// If it contains a colon, parse as format:target
|
||||
if let Some((format_str, target_str)) = value.split_once(':') {
|
||||
// Parse the format using clap's ValueEnum parsing
|
||||
// We need to handle both kebab-case (from clap) and the display format
|
||||
let format = match format_str {
|
||||
"full" => OutputFormat::Full,
|
||||
"concise" => OutputFormat::Concise,
|
||||
"grouped" => OutputFormat::Grouped,
|
||||
"json" => OutputFormat::Json,
|
||||
"json-lines" | "json_lines" => OutputFormat::JsonLines,
|
||||
"junit" => OutputFormat::Junit,
|
||||
"github" => OutputFormat::Github,
|
||||
"gitlab" => OutputFormat::Gitlab,
|
||||
"pylint" => OutputFormat::Pylint,
|
||||
"rdjson" => OutputFormat::Rdjson,
|
||||
"azure" => OutputFormat::Azure,
|
||||
"sarif" => OutputFormat::Sarif,
|
||||
_ => {
|
||||
let mut error =
|
||||
clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
|
||||
if let Some(arg) = arg {
|
||||
error.insert(
|
||||
clap::error::ContextKind::InvalidArg,
|
||||
clap::error::ContextValue::String(arg.to_string()),
|
||||
);
|
||||
}
|
||||
error.insert(
|
||||
clap::error::ContextKind::InvalidValue,
|
||||
clap::error::ContextValue::String(format!(
|
||||
"Invalid output format '{}'. Valid formats: {}",
|
||||
format_str,
|
||||
[
|
||||
"full",
|
||||
"concise",
|
||||
"grouped",
|
||||
"json",
|
||||
"json-lines",
|
||||
"junit",
|
||||
"github",
|
||||
"gitlab",
|
||||
"pylint",
|
||||
"rdjson",
|
||||
"azure",
|
||||
"sarif"
|
||||
]
|
||||
.join(", ")
|
||||
)),
|
||||
);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
let target = match target_str {
|
||||
"stdout" => OutputTarget::Stdout,
|
||||
"stderr" => OutputTarget::Stderr,
|
||||
path => {
|
||||
// Expand environment variables and tildes
|
||||
let expanded_path = shellexpand::full(path)
|
||||
.map(|expanded| PathBuf::from(expanded.as_ref()))
|
||||
.unwrap_or_else(|_| PathBuf::from(path));
|
||||
OutputTarget::File(expanded_path)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(OutputTargetPairOrPath::Pair(OutputTargetPair {
|
||||
format,
|
||||
target,
|
||||
}))
|
||||
} else {
|
||||
// No colon: treat as a plain path (for backward compatibility with --output-format)
|
||||
// Expand environment variables and tildes
|
||||
let expanded_path = shellexpand::full(value)
|
||||
.map(|expanded| PathBuf::from(expanded.as_ref()))
|
||||
.unwrap_or_else(|_| PathBuf::from(value));
|
||||
Ok(OutputTargetPairOrPath::Path(expanded_path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueParserFactory for OutputTargetPairOrPath {
|
||||
type Parser = OutputTargetPairParser;
|
||||
|
||||
fn value_parser() -> Self::Parser {
|
||||
OutputTargetPairParser
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumeration to represent a single `--config` argument
|
||||
/// passed via the CLI.
|
||||
///
|
||||
|
|
@ -1080,7 +1239,8 @@ pub struct CheckArguments {
|
|||
pub files: Vec<PathBuf>,
|
||||
pub ignore_noqa: bool,
|
||||
pub no_cache: bool,
|
||||
pub output_file: Option<PathBuf>,
|
||||
pub output_file: Vec<OutputTargetPairOrPath>,
|
||||
pub output_format: Option<OutputFormat>,
|
||||
pub show_files: bool,
|
||||
pub show_settings: bool,
|
||||
pub statistics: bool,
|
||||
|
|
@ -1088,6 +1248,56 @@ pub struct CheckArguments {
|
|||
pub watch: bool,
|
||||
}
|
||||
|
||||
impl CheckArguments {
|
||||
/// Resolve output targets, handling backward compatibility with old --output-format and --output-file flags.
|
||||
/// Returns a list of format:target pairs.
|
||||
pub fn resolve_output_targets(&self) -> anyhow::Result<Vec<OutputTargetPair>> {
|
||||
let mut targets = Vec::new();
|
||||
|
||||
// Process output_file entries
|
||||
for entry in &self.output_file {
|
||||
match entry {
|
||||
OutputTargetPairOrPath::Pair(pair) => {
|
||||
targets.push(pair.clone());
|
||||
}
|
||||
OutputTargetPairOrPath::Path(path) => {
|
||||
// Backward compatibility: if --output-format is set, use it with this path
|
||||
if let Some(format) = self.output_format {
|
||||
targets.push(OutputTargetPair {
|
||||
format,
|
||||
target: OutputTarget::File(path.clone()),
|
||||
});
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Cannot use plain file path '{}' without --output-format. Use format:target syntax instead (e.g., 'full:{}').",
|
||||
path.display(),
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no output_file entries but --output-format is specified, default to stdout
|
||||
if targets.is_empty() {
|
||||
if let Some(format) = self.output_format {
|
||||
targets.push(OutputTargetPair {
|
||||
format,
|
||||
target: OutputTarget::Stdout,
|
||||
});
|
||||
} else {
|
||||
// Default: full format to stdout
|
||||
targets.push(OutputTargetPair {
|
||||
format: OutputFormat::Full,
|
||||
target: OutputTarget::Stdout,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(targets)
|
||||
}
|
||||
}
|
||||
|
||||
/// CLI settings that are distinct from configuration (commands, lists of files,
|
||||
/// etc.).
|
||||
#[expect(clippy::struct_excessive_bools)]
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ use ruff_linter::{fs, warn_user, warn_user_once};
|
|||
use ruff_workspace::Settings;
|
||||
|
||||
use crate::args::{
|
||||
AnalyzeCommand, AnalyzeGraphCommand, Args, CheckCommand, Command, FormatCommand,
|
||||
AnalyzeCommand, AnalyzeGraphCommand, Args, CheckCommand, Command, FormatCommand, OutputTarget,
|
||||
};
|
||||
use crate::printer::{Flags as PrinterFlags, Printer};
|
||||
|
||||
|
|
@ -240,23 +240,45 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
|||
// files are present, or files are injected from outside of the hierarchy.
|
||||
let pyproject_config = resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?;
|
||||
|
||||
let mut writer: Box<dyn Write> = match cli.output_file {
|
||||
Some(path) if !cli.watch => {
|
||||
colored::control::set_override(false);
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let file = File::create(path)?;
|
||||
Box::new(BufWriter::new(file))
|
||||
}
|
||||
_ => Box::new(BufWriter::new(io::stdout())),
|
||||
// Resolve output targets (handles backward compatibility)
|
||||
let output_targets = cli.resolve_output_targets()?;
|
||||
|
||||
// Helper function to create a default writer for show_settings/show_files
|
||||
let create_default_writer = || -> Box<dyn Write> {
|
||||
output_targets
|
||||
.iter()
|
||||
.find(|t| t.format.is_human_readable())
|
||||
.or(output_targets.first())
|
||||
.map(|t| -> Box<dyn Write> {
|
||||
match &t.target {
|
||||
OutputTarget::Stdout => Box::new(BufWriter::new(io::stdout())),
|
||||
OutputTarget::Stderr => Box::new(BufWriter::new(io::stderr())),
|
||||
OutputTarget::File(path) => {
|
||||
if !cli.watch {
|
||||
colored::control::set_override(false);
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
if let Ok(file) = File::create(path) {
|
||||
Box::new(BufWriter::new(file))
|
||||
} else {
|
||||
Box::new(BufWriter::new(io::stdout()))
|
||||
}
|
||||
} else {
|
||||
Box::new(BufWriter::new(io::stdout()))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| Box::new(BufWriter::new(io::stdout())))
|
||||
};
|
||||
let stderr_writer = Box::new(BufWriter::new(io::stderr()));
|
||||
|
||||
let is_stdin = is_stdin(&cli.files, cli.stdin_filename.as_deref());
|
||||
let files = resolve_default_files(cli.files, is_stdin);
|
||||
|
||||
if cli.show_settings {
|
||||
// For show_settings, use stdout (these are informational commands)
|
||||
let mut writer = BufWriter::new(io::stdout());
|
||||
commands::show_settings::show_settings(
|
||||
&files,
|
||||
&pyproject_config,
|
||||
|
|
@ -266,6 +288,8 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
|||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
if cli.show_files {
|
||||
// For show_files, use stdout (these are informational commands)
|
||||
let mut writer = BufWriter::new(io::stdout());
|
||||
commands::show_files::show_files(
|
||||
&files,
|
||||
&pyproject_config,
|
||||
|
|
@ -281,7 +305,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
|||
fix,
|
||||
fix_only,
|
||||
unsafe_fixes,
|
||||
output_format,
|
||||
output_format: _,
|
||||
show_fixes,
|
||||
..
|
||||
} = pyproject_config.settings;
|
||||
|
|
@ -343,14 +367,6 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
|||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
let printer = Printer::new(
|
||||
output_format,
|
||||
config_arguments.log_level,
|
||||
fix_mode,
|
||||
unsafe_fixes,
|
||||
printer_flags,
|
||||
);
|
||||
|
||||
// the settings should already be combined with the CLI overrides at this point
|
||||
// TODO(jane): let's make this `PreviewMode`
|
||||
// TODO: this should reference the global preview mode once https://github.com/astral-sh/ruff/issues/8232
|
||||
|
|
@ -358,7 +374,11 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
|||
let preview = pyproject_config.settings.linter.preview.is_enabled();
|
||||
|
||||
if cli.watch {
|
||||
if output_format != OutputFormat::default() {
|
||||
if !output_targets.is_empty()
|
||||
&& output_targets
|
||||
.iter()
|
||||
.any(|t| t.format != OutputFormat::default())
|
||||
{
|
||||
warn_user!(
|
||||
"`--output-format {}` is always used in watch mode.",
|
||||
OutputFormat::default()
|
||||
|
|
@ -377,7 +397,15 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
|||
|
||||
// Perform an initial run instantly.
|
||||
Printer::clear_screen()?;
|
||||
printer.write_to_user("Starting linter in watch mode...\n");
|
||||
// Create a printer for watch mode (always uses default format)
|
||||
let watch_printer = Printer::new(
|
||||
OutputFormat::default(),
|
||||
config_arguments.log_level,
|
||||
fix_mode,
|
||||
unsafe_fixes,
|
||||
printer_flags,
|
||||
);
|
||||
watch_printer.write_to_user("Starting linter in watch mode...\n");
|
||||
|
||||
let diagnostics = commands::check::check(
|
||||
&files,
|
||||
|
|
@ -388,7 +416,8 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
|||
fix_mode,
|
||||
unsafe_fixes,
|
||||
)?;
|
||||
printer.write_continuously(&mut writer, &diagnostics, preview)?;
|
||||
let mut watch_writer = create_default_writer();
|
||||
watch_printer.write_continuously(&mut *watch_writer, &diagnostics, preview)?;
|
||||
|
||||
// In watch mode, we may need to re-resolve the configuration.
|
||||
// TODO(charlie): Re-compute other derivative values, like the `printer`.
|
||||
|
|
@ -406,7 +435,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
|||
resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?;
|
||||
}
|
||||
Printer::clear_screen()?;
|
||||
printer.write_to_user("File change detected...\n");
|
||||
watch_printer.write_to_user("File change detected...\n");
|
||||
|
||||
let diagnostics = commands::check::check(
|
||||
&files,
|
||||
|
|
@ -417,7 +446,8 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
|||
fix_mode,
|
||||
unsafe_fixes,
|
||||
)?;
|
||||
printer.write_continuously(&mut writer, &diagnostics, preview)?;
|
||||
let mut watch_writer = create_default_writer();
|
||||
watch_printer.write_continuously(&mut *watch_writer, &diagnostics, preview)?;
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
|
|
@ -444,18 +474,79 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
|||
)?
|
||||
};
|
||||
|
||||
// Always try to print violations (though the printer itself may suppress output)
|
||||
// If we're writing fixes via stdin, the transformed source code goes to the writer
|
||||
// so send the summary to stderr instead
|
||||
let mut summary_writer = if is_stdin && matches!(fix_mode, FixMode::Apply | FixMode::Diff) {
|
||||
stderr_writer
|
||||
} else {
|
||||
writer
|
||||
};
|
||||
if cli.statistics {
|
||||
printer.write_statistics(&diagnostics, &mut summary_writer)?;
|
||||
} else {
|
||||
printer.write_once(&diagnostics, &mut summary_writer, preview)?;
|
||||
// Write to all output targets
|
||||
// Special case: If we're writing fixes via stdin, the transformed source code goes to stdout,
|
||||
// so we need to handle summary separately for stdout targets
|
||||
let is_stdin_with_fixes = is_stdin && matches!(fix_mode, FixMode::Apply | FixMode::Diff);
|
||||
|
||||
for target_pair in &output_targets {
|
||||
// For stdin with fixes, if this is a stdout target, skip it (source code already written)
|
||||
// and we'll handle the summary separately
|
||||
if is_stdin_with_fixes && matches!(target_pair.target, OutputTarget::Stdout) {
|
||||
// Skip writing diagnostics to stdout when fixes are applied via stdin
|
||||
// The source code transformation is already written to stdout
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut writer: Box<dyn Write> = match &target_pair.target {
|
||||
OutputTarget::Stdout => Box::new(BufWriter::new(io::stdout())),
|
||||
OutputTarget::Stderr => Box::new(BufWriter::new(io::stderr())),
|
||||
OutputTarget::File(path) => {
|
||||
colored::control::set_override(false);
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let file = File::create(path)?;
|
||||
Box::new(BufWriter::new(file))
|
||||
}
|
||||
};
|
||||
|
||||
let target_printer = Printer::new(
|
||||
target_pair.format,
|
||||
config_arguments.log_level,
|
||||
fix_mode,
|
||||
unsafe_fixes,
|
||||
printer_flags,
|
||||
);
|
||||
|
||||
if cli.statistics {
|
||||
target_printer.write_statistics(&diagnostics, &mut *writer)?;
|
||||
} else {
|
||||
target_printer.write_once(&diagnostics, &mut *writer, preview)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for stdin with fixes: write summary to stderr
|
||||
if is_stdin_with_fixes {
|
||||
// Check if we have a stderr target, otherwise create a temporary one for the summary
|
||||
let has_stderr_target = output_targets
|
||||
.iter()
|
||||
.any(|t| matches!(t.target, OutputTarget::Stderr));
|
||||
if !has_stderr_target {
|
||||
// Write summary to stderr (old behavior)
|
||||
let mut stderr_writer = BufWriter::new(io::stderr());
|
||||
// Find the format to use for the summary (prefer human-readable, or first format)
|
||||
let summary_format = output_targets
|
||||
.iter()
|
||||
.find(|t| t.format.is_human_readable())
|
||||
.or(output_targets.first())
|
||||
.map(|t| t.format)
|
||||
.unwrap_or(OutputFormat::Full);
|
||||
|
||||
let summary_printer = Printer::new(
|
||||
summary_format,
|
||||
config_arguments.log_level,
|
||||
fix_mode,
|
||||
unsafe_fixes,
|
||||
printer_flags,
|
||||
);
|
||||
|
||||
if cli.statistics {
|
||||
summary_printer.write_statistics(&diagnostics, &mut stderr_writer)?;
|
||||
} else {
|
||||
summary_printer.write_once(&diagnostics, &mut stderr_writer, preview)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !cli.exit_zero {
|
||||
|
|
|
|||
|
|
@ -3876,3 +3876,161 @@ fn supported_file_extensions_preview_enabled() -> Result<()> {
|
|||
");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_output_formats() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
test.write_file("test.py", "import os # F401\n")?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args([
|
||||
"check",
|
||||
"--no-cache",
|
||||
"--output-file",
|
||||
"gitlab:gl-code-quality.json",
|
||||
"--output-file",
|
||||
"full:stdout",
|
||||
"--select",
|
||||
"F401",
|
||||
"test.py",
|
||||
])
|
||||
.output()?;
|
||||
|
||||
// Check that stdout contains the full format output
|
||||
let stdout = str::from_utf8(&output.stdout)?;
|
||||
// Full format shows diagnostics differently than concise format
|
||||
assert!(
|
||||
stdout.contains("F401") && stdout.contains("test.py"),
|
||||
"Expected full format output with F401 and test.py. Got: {stdout}"
|
||||
);
|
||||
assert!(stdout.contains("Found") || stdout.contains("All checks passed"));
|
||||
|
||||
// Check that the GitLab format file was created
|
||||
let gitlab_content = fs::read_to_string(test.root().join("gl-code-quality.json"))?;
|
||||
assert!(gitlab_content.contains("check_name"));
|
||||
assert!(gitlab_content.contains("F401"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_output_formats_env_var_comma_separated() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
test.write_file("test.py", "import os # F401\n")?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.env(
|
||||
"RUFF_OUTPUT_FILE",
|
||||
"gitlab:gl-code-quality.json,full:stdout",
|
||||
)
|
||||
.args(["check", "--no-cache", "--select", "F401", "test.py"])
|
||||
.output()?;
|
||||
|
||||
// Check that stdout contains the full format output
|
||||
let stdout = str::from_utf8(&output.stdout)?;
|
||||
assert!(
|
||||
stdout.contains("F401") && stdout.contains("test.py"),
|
||||
"Expected full format output with F401 and test.py. Got: {stdout}"
|
||||
);
|
||||
|
||||
// Check that the GitLab format file was created
|
||||
let gitlab_content = fs::read_to_string(test.root().join("gl-code-quality.json"))?;
|
||||
assert!(gitlab_content.contains("check_name"));
|
||||
assert!(gitlab_content.contains("F401"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_output_formats_cli_comma_separated() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
test.write_file("test.py", "import os # F401\n")?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args([
|
||||
"check",
|
||||
"--no-cache",
|
||||
"--output-file",
|
||||
"gitlab:gl-code-quality.json,full:stdout",
|
||||
"--select",
|
||||
"F401",
|
||||
"test.py",
|
||||
])
|
||||
.output()?;
|
||||
|
||||
// Check that stdout contains the full format output
|
||||
let stdout = str::from_utf8(&output.stdout)?;
|
||||
assert!(
|
||||
stdout.contains("F401") && stdout.contains("test.py"),
|
||||
"Expected full format output with F401 and test.py. Got: {stdout}"
|
||||
);
|
||||
|
||||
// Check that the GitLab format file was created
|
||||
let gitlab_content = fs::read_to_string(test.root().join("gl-code-quality.json"))?;
|
||||
assert!(gitlab_content.contains("check_name"));
|
||||
assert!(gitlab_content.contains("F401"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_output_formats_stdout_stderr() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
test.write_file("test.py", "import os # F401\n")?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args([
|
||||
"check",
|
||||
"--no-cache",
|
||||
"--output-file",
|
||||
"concise:stdout",
|
||||
"--output-file",
|
||||
"json:stderr",
|
||||
"--select",
|
||||
"F401",
|
||||
"test.py",
|
||||
])
|
||||
.output()?;
|
||||
|
||||
// Check that stdout contains concise format
|
||||
let stdout = str::from_utf8(&output.stdout)?;
|
||||
assert!(stdout.contains("test.py:1:8: F401"));
|
||||
|
||||
// Check that stderr contains JSON format
|
||||
let stderr = str::from_utf8(&output.stderr)?;
|
||||
assert!(stderr.contains("\"code\""));
|
||||
assert!(stderr.contains("F401"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backward_compatibility_output_format_file() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
test.write_file("test.py", "import os # F401\n")?;
|
||||
|
||||
test.command()
|
||||
.args([
|
||||
"check",
|
||||
"--no-cache",
|
||||
"--output-format",
|
||||
"gitlab",
|
||||
"--output-file",
|
||||
"gl-code-quality.json",
|
||||
"--select",
|
||||
"F401",
|
||||
"test.py",
|
||||
])
|
||||
.output()?;
|
||||
|
||||
// Check that the GitLab format file was created
|
||||
let gitlab_content = fs::read_to_string(test.root().join("gl-code-quality.json"))?;
|
||||
assert!(gitlab_content.contains("check_name"));
|
||||
assert!(gitlab_content.contains("F401"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -605,9 +605,11 @@ Options:
|
|||
format is "full" [env: RUFF_OUTPUT_FORMAT=] [possible values:
|
||||
concise, full, json, json-lines, junit, grouped, github, gitlab,
|
||||
pylint, rdjson, azure, sarif]
|
||||
-o, --output-file <OUTPUT_FILE>
|
||||
Specify file to write the linter output to (default: stdout) [env:
|
||||
RUFF_OUTPUT_FILE=]
|
||||
-o, --output-file <FORMAT:TARGET>
|
||||
Specify output destination(s) using format:target syntax. Can be
|
||||
specified multiple times for multiple outputs. Also supports
|
||||
comma-separated values: --output-file gitlab:file.json,full:stdout
|
||||
[env: RUFF_OUTPUT_FILE=]
|
||||
--target-version <TARGET_VERSION>
|
||||
The minimum Python version that should be supported [possible values:
|
||||
py37, py38, py39, py310, py311, py312, py313, py314]
|
||||
|
|
|
|||
Loading…
Reference in New Issue