Update CLI to respect fix applicability (#7769)

Rebase of https://github.com/astral-sh/ruff/pull/5119 authored by
@evanrittenhouse with additional refinements.

## Changes

- Adds `--unsafe-fixes` / `--no-unsafe-fixes` flags to `ruff check`
- Violations with unsafe fixes are not shown as fixable unless opted-in
- Fix applicability is respected now
    - `Applicability::Never` fixes are no longer applied
    - `Applicability::Sometimes` fixes require opt-in
    - `Applicability::Always` fixes are unchanged
- Hints for availability of `--unsafe-fixes` added to `ruff check`
output

## Examples

Check hints at hidden unsafe fixes
```
❯ ruff check example.py --no-cache --select F601,W292
example.py:1:14: F601 Dictionary key literal `'a'` repeated
example.py:2:15: W292 [*] No newline at end of file
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
```

We could add an indicator for which violations have hidden fixes in the
future.

Check treats unsafe fixes as applicable with opt-in
```
❯ ruff check example.py --no-cache --select F601,W292 --unsafe-fixes
example.py:1:14: F601 [*] Dictionary key literal `'a'` repeated
example.py:2:15: W292 [*] No newline at end of file
Found 2 errors.
[*] 2 fixable with the --fix option.
```

Also can be enabled in the config file

```
❯ cat ruff.toml
unsafe-fixes = true
```

And opted-out per invocation

```
❯ ruff check example.py --no-cache --select F601,W292 --no-unsafe-fixes
example.py:1:14: F601 Dictionary key literal `'a'` repeated
example.py:2:15: W292 [*] No newline at end of file
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
```

Diff does not include unsafe fixes
```
❯ ruff check example.py --no-cache --select F601,W292 --diff
--- example.py
+++ example.py
@@ -1,2 +1,2 @@
 x = {'a': 1, 'a': 1}
-print(('foo'))
+print(('foo'))
\ No newline at end of file

Would fix 1 error.
```

Unless there is opt-in
```
❯ ruff check example.py --no-cache --select F601,W292 --diff --unsafe-fixes
--- example.py
+++ example.py
@@ -1,2 +1,2 @@
-x = {'a': 1}
-print(('foo'))
+x = {'a': 1, 'a': 1}
+print(('foo'))
\ No newline at end of file

Would fix 2 errors.
```

https://github.com/astral-sh/ruff/pull/7790 will improve the diff
messages following this pull request

Similarly, `--fix` and `--fix-only` require the `--unsafe-fixes` flag to
apply unsafe fixes.

## Related

Replaces #5119
Closes https://github.com/astral-sh/ruff/issues/4185
Closes https://github.com/astral-sh/ruff/issues/7214
Closes https://github.com/astral-sh/ruff/issues/4845
Closes https://github.com/astral-sh/ruff/issues/3863
Addresses https://github.com/astral-sh/ruff/issues/6835
Addresses https://github.com/astral-sh/ruff/issues/7019
Needs follow-up https://github.com/astral-sh/ruff/issues/6962
Needs follow-up https://github.com/astral-sh/ruff/issues/4845
Needs follow-up https://github.com/astral-sh/ruff/issues/7436
Needs follow-up https://github.com/astral-sh/ruff/issues/7025
Needs follow-up https://github.com/astral-sh/ruff/issues/6434
Follow-up #7790 
Follow-up https://github.com/astral-sh/ruff/pull/7792

---------

Co-authored-by: Evan Rittenhouse <evanrittenhouse@gmail.com>
This commit is contained in:
Zanie Blue 2023-10-05 22:41:43 -05:00 committed by GitHub
parent e8d2cbc3f6
commit 22e18741bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 704 additions and 150 deletions

View file

@ -9,6 +9,7 @@ use ruff_linter::logging::LogLevel;
use ruff_linter::registry::Rule;
use ruff_linter::settings::types::{
FilePattern, PatternPrefixPair, PerFileIgnore, PreviewMode, PythonVersion, SerializationFormat,
UnsafeFixes,
};
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
use ruff_workspace::configuration::{Configuration, RuleSelection};
@ -76,12 +77,18 @@ pub enum Command {
pub struct CheckCommand {
/// List of files or directories to check.
pub files: Vec<PathBuf>,
/// Attempt to automatically fix lint violations.
/// Use `--no-fix` to disable.
/// Apply fixes to resolve lint violations.
/// Use `--no-fix` to disable or `--unsafe-fixes` to include unsafe fixes.
#[arg(long, overrides_with("no_fix"))]
fix: bool,
#[clap(long, overrides_with("fix"), hide = true)]
no_fix: bool,
/// Include fixes that may not retain the original intent of the code.
/// Use `--no-unsafe-fixes` to disable.
#[arg(long, overrides_with("no_unsafe_fixes"))]
unsafe_fixes: bool,
#[arg(long, overrides_with("unsafe_fixes"), hide = true)]
no_unsafe_fixes: bool,
/// Show violations with source code.
/// Use `--no-show-source` to disable.
#[arg(long, overrides_with("no_show_source"))]
@ -100,8 +107,8 @@ pub struct CheckCommand {
/// Run in watch mode by re-running whenever files change.
#[arg(short, long)]
pub watch: bool,
/// Fix any fixable lint violations, but don't report on leftover violations. Implies `--fix`.
/// Use `--no-fix-only` to disable.
/// Apply fixes to resolve lint violations, but don't report on leftover violations. Implies `--fix`.
/// Use `--no-fix-only` to disable or `--unsafe-fixes` to include unsafe fixes.
#[arg(long, overrides_with("no_fix_only"))]
fix_only: bool,
#[clap(long, overrides_with("fix_only"), hide = true)]
@ -497,6 +504,8 @@ impl CheckCommand {
cache_dir: self.cache_dir,
fix: resolve_bool_arg(self.fix, self.no_fix),
fix_only: resolve_bool_arg(self.fix_only, self.no_fix_only),
unsafe_fixes: resolve_bool_arg(self.unsafe_fixes, self.no_unsafe_fixes)
.map(UnsafeFixes::from),
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
output_format: self.output_format.or(self.format),
show_fixes: resolve_bool_arg(self.show_fixes, self.no_show_fixes),
@ -599,6 +608,7 @@ pub struct CliOverrides {
pub cache_dir: Option<PathBuf>,
pub fix: Option<bool>,
pub fix_only: Option<bool>,
pub unsafe_fixes: Option<UnsafeFixes>,
pub force_exclude: Option<bool>,
pub output_format: Option<SerializationFormat>,
pub show_fixes: Option<bool>,
@ -624,6 +634,9 @@ impl ConfigurationTransformer for CliOverrides {
if let Some(fix_only) = &self.fix_only {
config.fix_only = Some(*fix_only);
}
if self.unsafe_fixes.is_some() {
config.unsafe_fixes = self.unsafe_fixes;
}
config.lint.rule_selections.push(RuleSelection {
select: self.select.clone(),
ignore: self

View file

@ -338,6 +338,7 @@ pub(crate) fn init(path: &Path) -> Result<()> {
#[cfg(test)]
mod tests {
use filetime::{set_file_mtime, FileTime};
use ruff_linter::settings::types::UnsafeFixes;
use std::env::temp_dir;
use std::fs;
use std::io;
@ -410,6 +411,7 @@ mod tests {
Some(&cache),
flags::Noqa::Enabled,
flags::FixMode::Generate,
UnsafeFixes::Enabled,
)
.unwrap();
if diagnostics
@ -455,6 +457,7 @@ mod tests {
Some(&cache),
flags::Noqa::Enabled,
flags::FixMode::Generate,
UnsafeFixes::Enabled,
)
.unwrap();
}
@ -712,6 +715,7 @@ mod tests {
Some(cache),
flags::Noqa::Enabled,
flags::FixMode::Generate,
UnsafeFixes::Enabled,
)
}
}

View file

@ -11,6 +11,7 @@ use itertools::Itertools;
use log::{debug, error, warn};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
use ruff_linter::settings::types::UnsafeFixes;
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
@ -36,6 +37,7 @@ pub(crate) fn check(
cache: flags::Cache,
noqa: flags::Noqa,
fix_mode: flags::FixMode,
unsafe_fixes: UnsafeFixes,
) -> Result<Diagnostics> {
// Collect all the Python files to check.
let start = Instant::now();
@ -119,7 +121,16 @@ pub(crate) fn check(
}
});
lint_path(path, package, &settings.linter, cache, noqa, fix_mode).map_err(|e| {
lint_path(
path,
package,
&settings.linter,
cache,
noqa,
fix_mode,
unsafe_fixes,
)
.map_err(|e| {
(Some(path.to_owned()), {
let mut error = e.to_string();
for cause in e.chain() {
@ -199,9 +210,10 @@ fn lint_path(
cache: Option<&Cache>,
noqa: flags::Noqa,
fix_mode: flags::FixMode,
unsafe_fixes: UnsafeFixes,
) -> Result<Diagnostics> {
let result = catch_unwind(|| {
crate::diagnostics::lint_path(path, package, settings, cache, noqa, fix_mode)
crate::diagnostics::lint_path(path, package, settings, cache, noqa, fix_mode, unsafe_fixes)
});
match result {
@ -233,6 +245,8 @@ mod test {
use std::os::unix::fs::OpenOptionsExt;
use anyhow::Result;
use ruff_linter::settings::types::UnsafeFixes;
use rustc_hash::FxHashMap;
use tempfile::TempDir;
@ -285,6 +299,7 @@ mod test {
flags::Cache::Disabled,
flags::Noqa::Disabled,
flags::FixMode::Generate,
UnsafeFixes::Enabled,
)
.unwrap();
let mut output = Vec::new();

View file

@ -11,6 +11,7 @@ use anyhow::{Context, Result};
use colored::Colorize;
use filetime::FileTime;
use log::{debug, error, warn};
use ruff_linter::settings::types::UnsafeFixes;
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
@ -168,6 +169,7 @@ pub(crate) fn lint_path(
cache: Option<&Cache>,
noqa: flags::Noqa,
fix_mode: flags::FixMode,
unsafe_fixes: UnsafeFixes,
) -> Result<Diagnostics> {
// Check the cache.
// TODO(charlie): `fixer::Mode::Apply` and `fixer::Mode::Diff` both have
@ -244,8 +246,15 @@ pub(crate) fn lint_path(
result,
transformed,
fixed,
}) = lint_fix(path, package, noqa, settings, &source_kind, source_type)
{
}) = lint_fix(
path,
package,
noqa,
unsafe_fixes,
settings,
&source_kind,
source_type,
) {
if !fixed.is_empty() {
match fix_mode {
flags::FixMode::Apply => transformed.write(&mut File::create(path)?)?,
@ -355,6 +364,7 @@ pub(crate) fn lint_stdin(
path.unwrap_or_else(|| Path::new("-")),
package,
noqa,
settings.unsafe_fixes,
&settings.linter,
&source_kind,
source_type,

View file

@ -10,7 +10,7 @@ use log::warn;
use notify::{recommended_watcher, RecursiveMode, Watcher};
use ruff_linter::logging::{set_up_logging, LogLevel};
use ruff_linter::settings::flags;
use ruff_linter::settings::flags::FixMode;
use ruff_linter::settings::types::SerializationFormat;
use ruff_linter::{fs, warn_user, warn_user_once};
use ruff_workspace::Settings;
@ -228,6 +228,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
let Settings {
fix,
fix_only,
unsafe_fixes,
output_format,
show_fixes,
show_source,
@ -236,17 +237,20 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
// Fix rules are as follows:
// - By default, generate all fixes, but don't apply them to the filesystem.
// - If `--fix` or `--fix-only` is set, always apply fixes to the filesystem (or
// - If `--fix` or `--fix-only` is set, apply applicable fixes to the filesystem (or
// print them to stdout, if we're reading from stdin).
// - If `--diff` or `--fix-only` are set, don't print any violations (only
// fixes).
// - If `--diff` or `--fix-only` are set, don't print any violations (only applicable fixes)
// - By default, applicable fixes only include [`Applicablility::Automatic`], but if
// `--unsafe-fixes` is set, then [`Applicablility::Suggested`] fixes are included.
let fix_mode = if cli.diff {
flags::FixMode::Diff
FixMode::Diff
} else if fix || fix_only {
flags::FixMode::Apply
FixMode::Apply
} else {
flags::FixMode::Generate
FixMode::Generate
};
let cache = !cli.no_cache;
let noqa = !cli.ignore_noqa;
let mut printer_flags = PrinterFlags::empty();
@ -290,7 +294,13 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
return Ok(ExitStatus::Success);
}
let printer = Printer::new(output_format, log_level, fix_mode, printer_flags);
let printer = Printer::new(
output_format,
log_level,
fix_mode,
unsafe_fixes,
printer_flags,
);
if cli.watch {
if output_format != SerializationFormat::Text {
@ -318,6 +328,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
cache.into(),
noqa.into(),
fix_mode,
unsafe_fixes,
)?;
printer.write_continuously(&mut writer, &messages)?;
@ -350,6 +361,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
cache.into(),
noqa.into(),
fix_mode,
unsafe_fixes,
)?;
printer.write_continuously(&mut writer, &messages)?;
}
@ -376,13 +388,14 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
cache.into(),
noqa.into(),
fix_mode,
unsafe_fixes,
)?
};
// Always try to print violations (the printer itself may suppress output),
// unless we're writing fixes via stdin (in which case, the transformed
// source code goes to stdout).
if !(is_stdin && matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff)) {
if !(is_stdin && matches!(fix_mode, FixMode::Apply | FixMode::Diff)) {
if cli.statistics {
printer.write_statistics(&diagnostics, &mut writer)?;
} else {

View file

@ -19,8 +19,8 @@ use ruff_linter::message::{
};
use ruff_linter::notify_user;
use ruff_linter::registry::{AsRule, Rule};
use ruff_linter::settings::flags;
use ruff_linter::settings::types::SerializationFormat;
use ruff_linter::settings::flags::{self};
use ruff_linter::settings::types::{SerializationFormat, UnsafeFixes};
use crate::diagnostics::Diagnostics;
@ -73,6 +73,7 @@ pub(crate) struct Printer {
format: SerializationFormat,
log_level: LogLevel,
fix_mode: flags::FixMode,
unsafe_fixes: UnsafeFixes,
flags: Flags,
}
@ -81,12 +82,14 @@ impl Printer {
format: SerializationFormat,
log_level: LogLevel,
fix_mode: flags::FixMode,
unsafe_fixes: UnsafeFixes,
flags: Flags,
) -> Self {
Self {
format,
log_level,
fix_mode,
unsafe_fixes,
flags,
}
}
@ -118,19 +121,8 @@ impl Printer {
writeln!(writer, "Found {remaining} error{s}.")?;
}
if show_fix_status(self.fix_mode) {
let num_fixable = diagnostics
.messages
.iter()
.filter(|message| message.fix.is_some())
.count();
if num_fixable > 0 {
writeln!(
writer,
"[{}] {num_fixable} potentially fixable with the --fix option.",
"*".cyan(),
)?;
}
if let Some(fixables) = FixableSummary::try_from(diagnostics, self.unsafe_fixes) {
writeln!(writer, "{fixables}")?;
}
} else {
let fixed = diagnostics
@ -178,6 +170,7 @@ impl Printer {
}
let context = EmitterContext::new(&diagnostics.notebook_indexes);
let fixables = FixableSummary::try_from(diagnostics, self.unsafe_fixes);
match self.format {
SerializationFormat::Json => {
@ -191,9 +184,10 @@ impl Printer {
}
SerializationFormat::Text => {
TextEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode))
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_show_fix_diff(self.flags.intersects(Flags::SHOW_FIX_DIFF))
.with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
.with_unsafe_fixes(self.unsafe_fixes)
.emit(writer, &diagnostics.messages, &context)?;
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
@ -209,7 +203,8 @@ impl Printer {
SerializationFormat::Grouped => {
GroupedEmitter::default()
.with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
.with_show_fix_status(show_fix_status(self.fix_mode))
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_unsafe_fixes(self.unsafe_fixes)
.emit(writer, &diagnostics.messages, &context)?;
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
@ -359,6 +354,8 @@ impl Printer {
);
}
let fixables = FixableSummary::try_from(diagnostics, self.unsafe_fixes);
if !diagnostics.messages.is_empty() {
if self.log_level >= LogLevel::Default {
writeln!(writer)?;
@ -366,8 +363,9 @@ impl Printer {
let context = EmitterContext::new(&diagnostics.notebook_indexes);
TextEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode))
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
.with_unsafe_fixes(self.unsafe_fixes)
.emit(writer, &diagnostics.messages, &context)?;
}
writer.flush()?;
@ -390,13 +388,13 @@ fn num_digits(n: usize) -> usize {
}
/// Return `true` if the [`Printer`] should indicate that a rule is fixable.
const fn show_fix_status(fix_mode: flags::FixMode) -> bool {
fn show_fix_status(fix_mode: flags::FixMode, fixables: Option<&FixableSummary>) -> bool {
// If we're in application mode, avoid indicating that a rule is fixable.
// If the specific violation were truly fixable, it would've been fixed in
// this pass! (We're occasionally unable to determine whether a specific
// violation is fixable without trying to fix it, so if fix is not
// enabled, we may inadvertently indicate that a rule is fixable.)
!fix_mode.is_apply()
(!fix_mode.is_apply()) && fixables.is_some_and(FixableSummary::any_applicable_fixes)
}
fn print_fix_summary(writer: &mut dyn Write, fixed: &FxHashMap<String, FixTable>) -> Result<()> {
@ -439,3 +437,80 @@ fn print_fix_summary(writer: &mut dyn Write, fixed: &FxHashMap<String, FixTable>
}
Ok(())
}
/// Summarizes [applicable][ruff_diagnostics::Applicability] fixes.
#[derive(Debug)]
struct FixableSummary {
applicable: u32,
unapplicable: u32,
unsafe_fixes: UnsafeFixes,
}
impl FixableSummary {
fn try_from(diagnostics: &Diagnostics, unsafe_fixes: UnsafeFixes) -> Option<Self> {
let mut applicable = 0;
let mut unapplicable = 0;
for message in &diagnostics.messages {
if let Some(fix) = &message.fix {
if fix.applies(unsafe_fixes.required_applicability()) {
applicable += 1;
} else {
unapplicable += 1;
}
}
}
if applicable == 0 && unapplicable == 0 {
None
} else {
Some(Self {
applicable,
unapplicable,
unsafe_fixes,
})
}
}
fn any_applicable_fixes(&self) -> bool {
self.applicable > 0
}
}
impl Display for FixableSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let fix_prefix = format!("[{}]", "*".cyan());
if self.unsafe_fixes.is_enabled() {
write!(
f,
"{fix_prefix} {} fixable with the --fix option.",
self.applicable
)
} else {
if self.applicable > 0 && self.unapplicable > 0 {
let es = if self.unapplicable == 1 { "" } else { "es" };
write!(
f,
"{fix_prefix} {} fixable with the `--fix` option ({} hidden fix{es} can be enabled with the `--unsafe-fixes` option).",
self.applicable, self.unapplicable
)
} else if self.applicable > 0 {
// Only applicable fixes
write!(
f,
"{fix_prefix} {} fixable with the `--fix` option.",
self.applicable,
)
} else {
// Only unapplicable fixes
let es = if self.unapplicable == 1 { "" } else { "es" };
write!(
f,
"{} hidden fix{es} can be enabled with the `--unsafe-fixes` option.",
self.unapplicable
)
}
}
}
}

View file

@ -46,16 +46,16 @@ fn stdin_success() {
fn stdin_error() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.pass_stdin("import os\n"), @r#"
.pass_stdin("import os\n"), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 potentially fixable with the --fix option.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"#);
"###);
}
#[test]
@ -69,7 +69,7 @@ fn stdin_filename() {
----- stdout -----
F401.py:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 potentially fixable with the --fix option.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
@ -87,7 +87,7 @@ fn stdin_source_type_py() {
----- stdout -----
TCH.py:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 potentially fixable with the --fix option.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
@ -861,7 +861,7 @@ fn check_input_from_argfile() -> Result<()> {
----- stdout -----
/path/to/a.py:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 potentially fixable with the --fix option.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
@ -869,3 +869,239 @@ fn check_input_from_argfile() -> Result<()> {
Ok(())
}
#[test]
fn check_hints_hidden_unsafe_fixes() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format=text",
"--isolated",
"--select",
"F601,UP034",
"--no-cache",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
-:1:14: F601 Dictionary key literal `'a'` repeated
-:2:7: UP034 [*] Avoid extraneous parentheses
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
"###);
}
#[test]
fn check_hints_hidden_unsafe_fixes_with_no_safe_fixes() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["-", "--output-format", "text", "--no-cache", "--isolated", "--select", "F601"])
.pass_stdin("x = {'a': 1, 'a': 1}\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
-:1:14: F601 Dictionary key literal `'a'` repeated
Found 1 error.
1 hidden fix can be enabled with the `--unsafe-fixes` option.
----- stderr -----
"###);
}
#[test]
fn check_shows_unsafe_fixes_with_opt_in() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format=text",
"--isolated",
"--select",
"F601,UP034",
"--no-cache",
"--unsafe-fixes",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
-:1:14: F601 [*] Dictionary key literal `'a'` repeated
-:2:7: UP034 [*] Avoid extraneous parentheses
Found 2 errors.
[*] 2 fixable with the --fix option.
----- stderr -----
"###);
}
#[test]
fn fix_applies_safe_fixes_by_default() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"F601,UP034",
"--fix",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
x = {'a': 1, 'a': 1}
print('foo')
----- stderr -----
"###);
}
#[test]
fn fix_applies_unsafe_fixes_with_opt_in() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"F601,UP034",
"--fix",
"--unsafe-fixes",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: true
exit_code: 0
----- stdout -----
x = {'a': 1}
print('foo')
----- stderr -----
"###);
}
#[test]
fn fix_only_flag_applies_safe_fixes_by_default() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"F601,UP034",
"--fix-only",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: true
exit_code: 0
----- stdout -----
x = {'a': 1, 'a': 1}
print('foo')
----- stderr -----
"###);
}
#[test]
fn fix_only_flag_applies_unsafe_fixes_with_opt_in() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"F601,UP034",
"--fix-only",
"--unsafe-fixes",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: true
exit_code: 0
----- stdout -----
x = {'a': 1}
print('foo')
----- stderr -----
"###);
}
#[test]
fn diff_shows_safe_fixes_by_default() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"F601,UP034",
"--diff",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
@@ -1,2 +1,2 @@
x = {'a': 1, 'a': 1}
-print('foo')
+print(('foo'))
----- stderr -----
"###
);
}
#[test]
fn diff_shows_unsafe_fixes_with_opt_in() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args([
"-",
"--output-format",
"text",
"--isolated",
"--no-cache",
"--select",
"F601,UP034",
"--diff",
"--unsafe-fixes",
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
@@ -1,2 +1,2 @@
-x = {'a': 1}
-print('foo')
+x = {'a': 1, 'a': 1}
+print(('foo'))
----- stderr -----
"###
);
}

View file

@ -40,7 +40,7 @@ inline-quotes = "single"
-:1:5: B005 Using `.strip()` with multi-character strings is misleading
-:1:19: Q000 [*] Double quotes found but single quotes preferred
Found 3 errors.
[*] 2 potentially fixable with the --fix option.
[*] 2 fixable with the `--fix` option.
----- stderr -----
"###);
@ -75,7 +75,7 @@ inline-quotes = "single"
-:1:5: B005 Using `.strip()` with multi-character strings is misleading
-:1:19: Q000 [*] Double quotes found but single quotes preferred
Found 3 errors.
[*] 2 potentially fixable with the --fix option.
[*] 2 fixable with the `--fix` option.
----- stderr -----
"###);
@ -110,7 +110,7 @@ inline-quotes = "single"
-:1:5: B005 Using `.strip()` with multi-character strings is misleading
-:1:19: Q000 [*] Double quotes found but single quotes preferred
Found 3 errors.
[*] 2 potentially fixable with the --fix option.
[*] 2 fixable with the `--fix` option.
----- stderr -----
"###);
@ -149,7 +149,7 @@ inline-quotes = "single"
-:1:5: B005 Using `.strip()` with multi-character strings is misleading
-:1:19: Q000 [*] Double quotes found but single quotes preferred
Found 3 errors.
[*] 2 potentially fixable with the --fix option.
[*] 2 fixable with the `--fix` option.
----- stderr -----
"###);

View file

@ -6,21 +6,21 @@ use ruff_text_size::{Ranged, TextSize};
use crate::edit::Edit;
/// Indicates if a fix can be applied.
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum Applicability {
/// The fix is safe and can always be applied.
/// The fix is definitely what the user intended, or it maintains the exact meaning of the code.
Always,
/// The fix is unsafe and should only be manually applied by the user.
/// The fix is likely to be incorrect or the resulting code may have invalid syntax.
Never,
/// The fix is unsafe and should only be applied with user opt-in.
/// The fix may be what the user intended, but it is uncertain; the resulting code will have valid syntax.
Sometimes,
/// The fix is unsafe and should only be manually applied by the user.
/// The fix is likely to be incorrect or the resulting code may have invalid syntax.
Never,
/// The fix is safe and can always be applied.
/// The fix is definitely what the user intended, or it maintains the exact meaning of the code.
Always,
}
/// Indicates the level of isolation required to apply a fix.
@ -133,4 +133,9 @@ impl Fix {
self.isolation_level = isolation;
self
}
/// Return [`true`] if this [`Fix`] should be applied with at a given [`Applicability`].
pub fn applies(&self, applicability: Applicability) -> bool {
self.applicability >= applicability
}
}

View file

@ -9,6 +9,7 @@ use ruff_source_file::Locator;
use crate::linter::FixTable;
use crate::registry::{AsRule, Rule};
use crate::settings::types::UnsafeFixes;
pub(crate) mod codemods;
pub(crate) mod edits;
@ -23,11 +24,22 @@ pub(crate) struct FixResult {
pub(crate) source_map: SourceMap,
}
/// Auto-fix errors in a file, and write the fixed source code to disk.
pub(crate) fn fix_file(diagnostics: &[Diagnostic], locator: &Locator) -> Option<FixResult> {
/// Fix errors in a file, and write the fixed source code to disk.
pub(crate) fn fix_file(
diagnostics: &[Diagnostic],
locator: &Locator,
unsafe_fixes: UnsafeFixes,
) -> Option<FixResult> {
let required_applicability = unsafe_fixes.required_applicability();
let mut with_fixes = diagnostics
.iter()
.filter(|diag| diag.fix.is_some())
.filter(|diagnostic| {
diagnostic
.fix
.as_ref()
.map_or(false, |fix| fix.applies(required_applicability))
})
.peekable();
if with_fixes.peek().is_none() {

View file

@ -32,6 +32,7 @@ use crate::message::Message;
use crate::noqa::add_noqa;
use crate::registry::{AsRule, Rule};
use crate::rules::pycodestyle;
use crate::settings::types::UnsafeFixes;
use crate::settings::{flags, LinterSettings};
use crate::source_kind::SourceKind;
use crate::{directives, fs};
@ -415,10 +416,12 @@ fn diagnostics_to_messages(
/// Generate `Diagnostic`s from source code content, iteratively fixing
/// until stable.
#[allow(clippy::too_many_arguments)]
pub fn lint_fix<'a>(
path: &Path,
package: Option<&Path>,
noqa: flags::Noqa,
unsafe_fixes: UnsafeFixes,
settings: &LinterSettings,
source_kind: &'a SourceKind,
source_type: PySourceType,
@ -494,7 +497,7 @@ pub fn lint_fix<'a>(
code: fixed_contents,
fixes: applied,
source_map,
}) = fix_file(&result.data.0, &locator)
}) = fix_file(&result.data.0, &locator, unsafe_fixes)
{
if iterations < MAX_ITERATIONS {
// Count the number of fixed errors.

View file

@ -13,11 +13,13 @@ use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
use crate::message::{
group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation,
};
use crate::settings::types::UnsafeFixes;
#[derive(Default)]
pub struct GroupedEmitter {
show_fix_status: bool,
show_source: bool,
unsafe_fixes: UnsafeFixes,
}
impl GroupedEmitter {
@ -32,6 +34,12 @@ impl GroupedEmitter {
self.show_source = show_source;
self
}
#[must_use]
pub fn with_unsafe_fixes(mut self, unsafe_fixes: UnsafeFixes) -> Self {
self.unsafe_fixes = unsafe_fixes;
self
}
}
impl Emitter for GroupedEmitter {
@ -68,6 +76,7 @@ impl Emitter for GroupedEmitter {
notebook_index: context.notebook_index(message.filename()),
message,
show_fix_status: self.show_fix_status,
unsafe_fixes: self.unsafe_fixes,
show_source: self.show_source,
row_length,
column_length,
@ -89,6 +98,7 @@ impl Emitter for GroupedEmitter {
struct DisplayGroupedMessage<'a> {
message: MessageWithLocation<'a>,
show_fix_status: bool,
unsafe_fixes: UnsafeFixes,
show_source: bool,
row_length: NonZeroUsize,
column_length: NonZeroUsize,
@ -138,7 +148,8 @@ impl Display for DisplayGroupedMessage<'_> {
),
code_and_body = RuleCodeAndBody {
message,
show_fix_status: self.show_fix_status
show_fix_status: self.show_fix_status,
unsafe_fixes: self.unsafe_fixes
},
)?;
@ -196,6 +207,7 @@ mod tests {
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::GroupedEmitter;
use crate::settings::types::UnsafeFixes;
#[test]
fn default() {
@ -222,4 +234,15 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn fix_status_unsafe() {
let mut emitter = GroupedEmitter::default()
.with_show_fix_status(true)
.with_show_source(true)
.with_unsafe_fixes(UnsafeFixes::Enabled);
let content = capture_emitter_output(&mut emitter, &create_messages());
assert_snapshot!(content);
}
}

View file

@ -3,14 +3,14 @@ source: crates/ruff_linter/src/message/grouped.rs
expression: content
---
fib.py:
1:8 F401 [*] `os` imported but unused
1:8 F401 `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
6:5 F841 [*] Local variable `x` is assigned to but never used
6:5 F841 Local variable `x` is assigned to but never used
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""

View file

@ -0,0 +1,31 @@
---
source: crates/ruff_linter/src/message/grouped.rs
expression: content
---
fib.py:
1:8 F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
6:5 F841 [*] Local variable `x` is assigned to but never used
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""
6 | x = 1
| ^ F841
7 | if n == 0:
8 | return 0
|
= help: Remove assignment to unused variable `x`
undef.py:
1:4 F821 Undefined name `a`
|
1 | if a == 1: pass
| ^ F821
|

View file

@ -2,14 +2,14 @@
source: crates/ruff_linter/src/message/text.rs
expression: content
---
fib.py:1:8: F401 [*] `os` imported but unused
fib.py:1:8: F401 `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
fib.py:6:5: F841 [*] Local variable `x` is assigned to but never used
fib.py:6:5: F841 Local variable `x` is assigned to but never used
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""

View file

@ -0,0 +1,29 @@
---
source: crates/ruff_linter/src/message/text.rs
expression: content
---
fib.py:1:8: F401 [*] `os` imported but unused
|
1 | import os
| ^^ F401
|
= help: Remove unused import: `os`
fib.py:6:5: F841 [*] Local variable `x` is assigned to but never used
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""
6 | x = 1
| ^ F841
7 | if n == 0:
8 | return 0
|
= help: Remove assignment to unused variable `x`
undef.py:1:4: F821 Undefined name `a`
|
1 | if a == 1: pass
| ^ F821
|

View file

@ -16,6 +16,7 @@ use crate::line_width::{LineWidthBuilder, TabSize};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use crate::settings::types::UnsafeFixes;
bitflags! {
#[derive(Default)]
@ -32,6 +33,7 @@ bitflags! {
#[derive(Default)]
pub struct TextEmitter {
flags: EmitterFlags,
unsafe_fixes: UnsafeFixes,
}
impl TextEmitter {
@ -53,6 +55,12 @@ impl TextEmitter {
self.flags.set(EmitterFlags::SHOW_SOURCE, show_source);
self
}
#[must_use]
pub fn with_unsafe_fixes(mut self, unsafe_fixes: UnsafeFixes) -> Self {
self.unsafe_fixes = unsafe_fixes;
self
}
}
impl Emitter for TextEmitter {
@ -105,7 +113,8 @@ impl Emitter for TextEmitter {
sep = ":".cyan(),
code_and_body = RuleCodeAndBody {
message,
show_fix_status: self.flags.intersects(EmitterFlags::SHOW_FIX_STATUS)
show_fix_status: self.flags.intersects(EmitterFlags::SHOW_FIX_STATUS),
unsafe_fixes: self.unsafe_fixes,
}
)?;
@ -134,21 +143,27 @@ impl Emitter for TextEmitter {
pub(super) struct RuleCodeAndBody<'a> {
pub(crate) message: &'a Message,
pub(crate) show_fix_status: bool,
pub(crate) unsafe_fixes: UnsafeFixes,
}
impl Display for RuleCodeAndBody<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let kind = &self.message.kind;
if self.show_fix_status && self.message.fix.is_some() {
write!(
if self.show_fix_status {
if let Some(fix) = self.message.fix.as_ref() {
// Do not display an indicator for unapplicable fixes
if fix.applies(self.unsafe_fixes.required_applicability()) {
return write!(
f,
"{code} {fix}{body}",
code = kind.rule().noqa_code().to_string().red().bold(),
fix = format_args!("[{}] ", "*".cyan()),
body = kind.body,
)
} else {
);
}
}
};
write!(
f,
"{code} {body}",
@ -157,7 +172,6 @@ impl Display for RuleCodeAndBody<'_> {
)
}
}
}
pub(super) struct MessageCodeFrame<'a> {
pub(crate) message: &'a Message,
@ -341,6 +355,7 @@ mod tests {
use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::TextEmitter;
use crate::settings::types::UnsafeFixes;
#[test]
fn default() {
@ -359,4 +374,15 @@ mod tests {
assert_snapshot!(content);
}
#[test]
fn fix_status_unsafe() {
let mut emitter = TextEmitter::default()
.with_show_fix_status(true)
.with_show_source(true)
.with_unsafe_fixes(UnsafeFixes::Enabled);
let content = capture_emitter_output(&mut emitter, &create_messages());
assert_snapshot!(content);
}
}

View file

@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/eradicate/mod.rs
---
ERA001.py:1:1: ERA001 [*] Found commented-out code
ERA001.py:1:1: ERA001 Found commented-out code
|
1 | #import os
| ^^^^^^^^^^ ERA001
@ -16,7 +16,7 @@ ERA001.py:1:1: ERA001 [*] Found commented-out code
3 2 | #a = 3
4 3 | a = 4
ERA001.py:2:1: ERA001 [*] Found commented-out code
ERA001.py:2:1: ERA001 Found commented-out code
|
1 | #import os
2 | # from foo import junk
@ -33,7 +33,7 @@ ERA001.py:2:1: ERA001 [*] Found commented-out code
4 3 | a = 4
5 4 | #foo(1, 2, 3)
ERA001.py:3:1: ERA001 [*] Found commented-out code
ERA001.py:3:1: ERA001 Found commented-out code
|
1 | #import os
2 | # from foo import junk
@ -52,7 +52,7 @@ ERA001.py:3:1: ERA001 [*] Found commented-out code
5 4 | #foo(1, 2, 3)
6 5 |
ERA001.py:5:1: ERA001 [*] Found commented-out code
ERA001.py:5:1: ERA001 Found commented-out code
|
3 | #a = 3
4 | a = 4
@ -72,7 +72,7 @@ ERA001.py:5:1: ERA001 [*] Found commented-out code
7 6 | def foo(x, y, z):
8 7 | content = 1 # print('hello')
ERA001.py:13:5: ERA001 [*] Found commented-out code
ERA001.py:13:5: ERA001 Found commented-out code
|
11 | # This is a real comment.
12 | # # This is a (nested) comment.
@ -91,7 +91,7 @@ ERA001.py:13:5: ERA001 [*] Found commented-out code
15 14 |
16 15 | #import os # noqa: ERA001
ERA001.py:21:5: ERA001 [*] Found commented-out code
ERA001.py:21:5: ERA001 Found commented-out code
|
19 | class A():
20 | pass
@ -109,7 +109,7 @@ ERA001.py:21:5: ERA001 [*] Found commented-out code
23 22 |
24 23 | dictionary = {
ERA001.py:26:5: ERA001 [*] Found commented-out code
ERA001.py:26:5: ERA001 Found commented-out code
|
24 | dictionary = {
25 | # "key1": 123, # noqa: ERA001
@ -129,7 +129,7 @@ ERA001.py:26:5: ERA001 [*] Found commented-out code
28 27 | }
29 28 |
ERA001.py:27:5: ERA001 [*] Found commented-out code
ERA001.py:27:5: ERA001 Found commented-out code
|
25 | # "key1": 123, # noqa: ERA001
26 | # "key2": 456,

View file

@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B006_1.py:3:22: B006 [*] Do not use mutable data structures for argument defaults
B006_1.py:3:22: B006 Do not use mutable data structures for argument defaults
|
1 | # Docstring followed by a newline
2 |

View file

@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B006_2.py:4:22: B006 [*] Do not use mutable data structures for argument defaults
B006_2.py:4:22: B006 Do not use mutable data structures for argument defaults
|
2 | # Regression test for https://github.com/astral-sh/ruff/issues/7155
3 |

View file

@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B006_3.py:4:22: B006 [*] Do not use mutable data structures for argument defaults
B006_3.py:4:22: B006 Do not use mutable data structures for argument defaults
|
4 | def foobar(foor, bar={}):
| ^^ B006

View file

@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B006_4.py:7:26: B006 [*] Do not use mutable data structures for argument defaults
B006_4.py:7:26: B006 Do not use mutable data structures for argument defaults
|
6 | class FormFeedIndent:
7 | def __init__(self, a=[]):

View file

@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B006_5.py:5:49: B006 [*] Do not use mutable data structures for argument defaults
B006_5.py:5:49: B006 Do not use mutable data structures for argument defaults
|
5 | def import_module_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -22,7 +22,7 @@ B006_5.py:5:49: B006 [*] Do not use mutable data structures for argument default
8 10 |
9 11 | def import_module_with_values_wrong(value: dict[str, str] = {}):
B006_5.py:9:61: B006 [*] Do not use mutable data structures for argument defaults
B006_5.py:9:61: B006 Do not use mutable data structures for argument defaults
|
9 | def import_module_with_values_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -44,7 +44,7 @@ B006_5.py:9:61: B006 [*] Do not use mutable data structures for argument default
13 15 |
14 16 |
B006_5.py:15:50: B006 [*] Do not use mutable data structures for argument defaults
B006_5.py:15:50: B006 Do not use mutable data structures for argument defaults
|
15 | def import_modules_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -68,7 +68,7 @@ B006_5.py:15:50: B006 [*] Do not use mutable data structures for argument defaul
20 22 |
21 23 | def from_import_module_wrong(value: dict[str, str] = {}):
B006_5.py:21:54: B006 [*] Do not use mutable data structures for argument defaults
B006_5.py:21:54: B006 Do not use mutable data structures for argument defaults
|
21 | def from_import_module_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -89,7 +89,7 @@ B006_5.py:21:54: B006 [*] Do not use mutable data structures for argument defaul
24 26 |
25 27 | def from_imports_module_wrong(value: dict[str, str] = {}):
B006_5.py:25:55: B006 [*] Do not use mutable data structures for argument defaults
B006_5.py:25:55: B006 Do not use mutable data structures for argument defaults
|
25 | def from_imports_module_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -112,7 +112,7 @@ B006_5.py:25:55: B006 [*] Do not use mutable data structures for argument defaul
29 31 |
30 32 | def import_and_from_imports_module_wrong(value: dict[str, str] = {}):
B006_5.py:30:66: B006 [*] Do not use mutable data structures for argument defaults
B006_5.py:30:66: B006 Do not use mutable data structures for argument defaults
|
30 | def import_and_from_imports_module_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -135,7 +135,7 @@ B006_5.py:30:66: B006 [*] Do not use mutable data structures for argument defaul
34 36 |
35 37 | def import_docstring_module_wrong(value: dict[str, str] = {}):
B006_5.py:35:59: B006 [*] Do not use mutable data structures for argument defaults
B006_5.py:35:59: B006 Do not use mutable data structures for argument defaults
|
35 | def import_docstring_module_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -158,7 +158,7 @@ B006_5.py:35:59: B006 [*] Do not use mutable data structures for argument defaul
39 41 |
40 42 | def import_module_wrong(value: dict[str, str] = {}):
B006_5.py:40:49: B006 [*] Do not use mutable data structures for argument defaults
B006_5.py:40:49: B006 Do not use mutable data structures for argument defaults
|
40 | def import_module_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -181,7 +181,7 @@ B006_5.py:40:49: B006 [*] Do not use mutable data structures for argument defaul
44 46 |
45 47 | def import_module_wrong(value: dict[str, str] = {}):
B006_5.py:45:49: B006 [*] Do not use mutable data structures for argument defaults
B006_5.py:45:49: B006 Do not use mutable data structures for argument defaults
|
45 | def import_module_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -203,7 +203,7 @@ B006_5.py:45:49: B006 [*] Do not use mutable data structures for argument defaul
48 50 |
49 51 |
B006_5.py:50:49: B006 [*] Do not use mutable data structures for argument defaults
B006_5.py:50:49: B006 Do not use mutable data structures for argument defaults
|
50 | def import_module_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -226,7 +226,7 @@ B006_5.py:50:49: B006 [*] Do not use mutable data structures for argument defaul
54 56 |
55 57 | def import_module_wrong(value: dict[str, str] = {}):
B006_5.py:55:49: B006 [*] Do not use mutable data structures for argument defaults
B006_5.py:55:49: B006 Do not use mutable data structures for argument defaults
|
55 | def import_module_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -247,7 +247,7 @@ B006_5.py:55:49: B006 [*] Do not use mutable data structures for argument defaul
58 60 |
59 61 | def import_module_wrong(value: dict[str, str] = {}):
B006_5.py:59:49: B006 [*] Do not use mutable data structures for argument defaults
B006_5.py:59:49: B006 Do not use mutable data structures for argument defaults
|
59 | def import_module_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -267,7 +267,7 @@ B006_5.py:59:49: B006 [*] Do not use mutable data structures for argument defaul
61 63 |
62 64 |
B006_5.py:63:49: B006 [*] Do not use mutable data structures for argument defaults
B006_5.py:63:49: B006 Do not use mutable data structures for argument defaults
|
63 | def import_module_wrong(value: dict[str, str] = {}):
| ^^ B006

View file

@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B006_6.py:4:22: B006 [*] Do not use mutable data structures for argument defaults
B006_6.py:4:22: B006 Do not use mutable data structures for argument defaults
|
2 | # Same as B006_2.py, but import instead of docstring
3 |

View file

@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B006_7.py:4:22: B006 [*] Do not use mutable data structures for argument defaults
B006_7.py:4:22: B006 Do not use mutable data structures for argument defaults
|
2 | # Same as B006_3.py, but import instead of docstring
3 |

View file

@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B006_B008.py:63:25: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:63:25: B006 Do not use mutable data structures for argument defaults
|
63 | def this_is_wrong(value=[1, 2, 3]):
| ^^^^^^^^^ B006
@ -21,7 +21,7 @@ B006_B008.py:63:25: B006 [*] Do not use mutable data structures for argument def
65 67 |
66 68 |
B006_B008.py:67:30: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:67:30: B006 Do not use mutable data structures for argument defaults
|
67 | def this_is_also_wrong(value={}):
| ^^ B006
@ -41,7 +41,7 @@ B006_B008.py:67:30: B006 [*] Do not use mutable data structures for argument def
69 71 |
70 72 |
B006_B008.py:73:52: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:73:52: B006 Do not use mutable data structures for argument defaults
|
71 | class Foo:
72 | @staticmethod
@ -63,7 +63,7 @@ B006_B008.py:73:52: B006 [*] Do not use mutable data structures for argument def
75 77 |
76 78 |
B006_B008.py:77:31: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:77:31: B006 Do not use mutable data structures for argument defaults
|
77 | def multiline_arg_wrong(value={
| _______________________________^
@ -97,7 +97,7 @@ B006_B008.py:82:36: B006 Do not use mutable data structures for argument default
|
= help: Replace with `None`; initialize within function
B006_B008.py:85:20: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:85:20: B006 Do not use mutable data structures for argument defaults
|
85 | def and_this(value=set()):
| ^^^^^ B006
@ -117,7 +117,7 @@ B006_B008.py:85:20: B006 [*] Do not use mutable data structures for argument def
87 89 |
88 90 |
B006_B008.py:89:20: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:89:20: B006 Do not use mutable data structures for argument defaults
|
89 | def this_too(value=collections.OrderedDict()):
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
@ -137,7 +137,7 @@ B006_B008.py:89:20: B006 [*] Do not use mutable data structures for argument def
91 93 |
92 94 |
B006_B008.py:93:32: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:93:32: B006 Do not use mutable data structures for argument defaults
|
93 | async def async_this_too(value=collections.defaultdict()):
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
@ -157,7 +157,7 @@ B006_B008.py:93:32: B006 [*] Do not use mutable data structures for argument def
95 97 |
96 98 |
B006_B008.py:97:26: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:97:26: B006 Do not use mutable data structures for argument defaults
|
97 | def dont_forget_me(value=collections.deque()):
| ^^^^^^^^^^^^^^^^^^^ B006
@ -177,7 +177,7 @@ B006_B008.py:97:26: B006 [*] Do not use mutable data structures for argument def
99 101 |
100 102 |
B006_B008.py:102:46: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:102:46: B006 Do not use mutable data structures for argument defaults
|
101 | # N.B. we're also flagging the function call in the comprehension
102 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
@ -198,7 +198,7 @@ B006_B008.py:102:46: B006 [*] Do not use mutable data structures for argument de
104 106 |
105 107 |
B006_B008.py:106:46: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:106:46: B006 Do not use mutable data structures for argument defaults
|
106 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
@ -218,7 +218,7 @@ B006_B008.py:106:46: B006 [*] Do not use mutable data structures for argument de
108 110 |
109 111 |
B006_B008.py:110:45: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:110:45: B006 Do not use mutable data structures for argument defaults
|
110 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006
@ -238,7 +238,7 @@ B006_B008.py:110:45: B006 [*] Do not use mutable data structures for argument de
112 114 |
113 115 |
B006_B008.py:114:33: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:114:33: B006 Do not use mutable data structures for argument defaults
|
114 | def kwonlyargs_mutable(*, value=[]):
| ^^ B006
@ -258,7 +258,7 @@ B006_B008.py:114:33: B006 [*] Do not use mutable data structures for argument de
116 118 |
117 119 |
B006_B008.py:239:20: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:239:20: B006 Do not use mutable data structures for argument defaults
|
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
@ -280,7 +280,7 @@ B006_B008.py:239:20: B006 [*] Do not use mutable data structures for argument de
241 243 |
242 244 |
B006_B008.py:276:27: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:276:27: B006 Do not use mutable data structures for argument defaults
|
275 | def mutable_annotations(
276 | a: list[int] | None = [],
@ -306,7 +306,7 @@ B006_B008.py:276:27: B006 [*] Do not use mutable data structures for argument de
282 284 |
283 285 |
B006_B008.py:277:35: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:277:35: B006 Do not use mutable data structures for argument defaults
|
275 | def mutable_annotations(
276 | a: list[int] | None = [],
@ -332,7 +332,7 @@ B006_B008.py:277:35: B006 [*] Do not use mutable data structures for argument de
282 284 |
283 285 |
B006_B008.py:278:62: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:278:62: B006 Do not use mutable data structures for argument defaults
|
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
@ -357,7 +357,7 @@ B006_B008.py:278:62: B006 [*] Do not use mutable data structures for argument de
282 284 |
283 285 |
B006_B008.py:279:80: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:279:80: B006 Do not use mutable data structures for argument defaults
|
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
@ -381,7 +381,7 @@ B006_B008.py:279:80: B006 [*] Do not use mutable data structures for argument de
282 284 |
283 285 |
B006_B008.py:284:52: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:284:52: B006 Do not use mutable data structures for argument defaults
|
284 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -402,7 +402,7 @@ B006_B008.py:284:52: B006 [*] Do not use mutable data structures for argument de
287 289 |
288 290 | def single_line_func_wrong(value: dict[str, str] = {}):
B006_B008.py:288:52: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:288:52: B006 Do not use mutable data structures for argument defaults
|
288 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -424,7 +424,7 @@ B006_B008.py:288:52: B006 [*] Do not use mutable data structures for argument de
291 293 |
292 294 |
B006_B008.py:293:52: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:293:52: B006 Do not use mutable data structures for argument defaults
|
293 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -444,7 +444,7 @@ B006_B008.py:293:52: B006 [*] Do not use mutable data structures for argument de
295 297 |
296 298 |
B006_B008.py:297:52: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:297:52: B006 Do not use mutable data structures for argument defaults
|
297 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
@ -465,7 +465,7 @@ B006_B008.py:297:52: B006 [*] Do not use mutable data structures for argument de
299 301 | ...
300 302 |
B006_B008.py:302:52: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:302:52: B006 Do not use mutable data structures for argument defaults
|
302 | def single_line_func_wrong(value: dict[str, str] = {
| ____________________________________________________^
@ -500,7 +500,7 @@ B006_B008.py:308:52: B006 Do not use mutable data structures for argument defaul
|
= help: Replace with `None`; initialize within function
B006_B008.py:313:52: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:313:52: B006 Do not use mutable data structures for argument defaults
|
313 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006

View file

@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B006_extended.py:17:55: B006 [*] Do not use mutable data structures for argument defaults
B006_extended.py:17:55: B006 Do not use mutable data structures for argument defaults
|
17 | def error_due_to_missing_import(foo: ImmutableTypeA = []):
| ^^ B006

View file

@ -100,7 +100,7 @@ E731.py:24:5: E731 [*] Do not assign a `lambda` expression, use a `def`
26 27 |
27 28 | def scope():
E731.py:57:5: E731 [*] Do not assign a `lambda` expression, use a `def`
E731.py:57:5: E731 Do not assign a `lambda` expression, use a `def`
|
55 | class Scope:
56 | # E731
@ -120,7 +120,7 @@ E731.py:57:5: E731 [*] Do not assign a `lambda` expression, use a `def`
59 60 |
60 61 | class Scope:
E731.py:64:5: E731 [*] Do not assign a `lambda` expression, use a `def`
E731.py:64:5: E731 Do not assign a `lambda` expression, use a `def`
|
63 | # E731
64 | f: Callable[[int], int] = lambda x: 2 * x
@ -139,7 +139,7 @@ E731.py:64:5: E731 [*] Do not assign a `lambda` expression, use a `def`
66 67 |
67 68 | def scope():
E731.py:73:9: E731 [*] Do not assign a `lambda` expression, use a `def`
E731.py:73:9: E731 Do not assign a `lambda` expression, use a `def`
|
71 | x: Callable[[int], int]
72 | if True:
@ -161,7 +161,7 @@ E731.py:73:9: E731 [*] Do not assign a `lambda` expression, use a `def`
75 76 | x = lambda: 2
76 77 | return x
E731.py:75:9: E731 [*] Do not assign a `lambda` expression, use a `def`
E731.py:75:9: E731 Do not assign a `lambda` expression, use a `def`
|
73 | x = lambda: 1
74 | else:
@ -322,7 +322,7 @@ E731.py:135:5: E731 [*] Do not assign a `lambda` expression, use a `def`
137 138 |
138 139 | class TemperatureScales(Enum):
E731.py:139:5: E731 [*] Do not assign a `lambda` expression, use a `def`
E731.py:139:5: E731 Do not assign a `lambda` expression, use a `def`
|
138 | class TemperatureScales(Enum):
139 | CELSIUS = (lambda deg_c: deg_c)
@ -342,7 +342,7 @@ E731.py:139:5: E731 [*] Do not assign a `lambda` expression, use a `def`
141 142 |
142 143 |
E731.py:140:5: E731 [*] Do not assign a `lambda` expression, use a `def`
E731.py:140:5: E731 Do not assign a `lambda` expression, use a `def`
|
138 | class TemperatureScales(Enum):
139 | CELSIUS = (lambda deg_c: deg_c)

View file

@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF100_5.py:7:5: ERA001 [*] Found commented-out code
RUF100_5.py:7:5: ERA001 Found commented-out code
|
5 | # "key1": 123, # noqa: ERA001
6 | # "key2": 456, # noqa
@ -20,7 +20,7 @@ RUF100_5.py:7:5: ERA001 [*] Found commented-out code
9 8 |
10 9 |
RUF100_5.py:11:1: ERA001 [*] Found commented-out code
RUF100_5.py:11:1: ERA001 Found commented-out code
|
11 | #import os # noqa: E501
| ^^^^^^^^^^^^^^^^^^^^^^^^ ERA001

View file

@ -60,6 +60,7 @@ pub struct LinterSettings {
pub tab_size: TabSize,
pub task_tags: Vec<String>,
pub typing_modules: Vec<String>,
// Plugins
pub flake8_annotations: flake8_annotations::settings::Settings,
pub flake8_bandit: flake8_bandit::settings::Settings,

View file

@ -7,6 +7,7 @@ use std::string::ToString;
use anyhow::{bail, Result};
use globset::{Glob, GlobSet, GlobSetBuilder};
use pep440_rs::{Version as Pep440Version, VersionSpecifiers};
use ruff_diagnostics::Applicability;
use serde::{de, Deserialize, Deserializer, Serialize};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
@ -99,7 +100,7 @@ impl PythonVersion {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, CacheKey, is_macro::Is)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, CacheKey, is_macro::Is)]
pub enum PreviewMode {
#[default]
Disabled,
@ -116,6 +117,32 @@ impl From<bool> for PreviewMode {
}
}
#[derive(Debug, Copy, Clone, CacheKey, Default, PartialEq, Eq, is_macro::Is)]
pub enum UnsafeFixes {
#[default]
Disabled,
Enabled,
}
impl From<bool> for UnsafeFixes {
fn from(version: bool) -> Self {
if version {
UnsafeFixes::Enabled
} else {
UnsafeFixes::Disabled
}
}
}
impl UnsafeFixes {
pub fn required_applicability(&self) -> Applicability {
match self {
Self::Enabled => Applicability::Sometimes,
Self::Disabled => Applicability::Always,
}
}
}
#[derive(Debug, Clone, CacheKey, PartialEq, PartialOrd, Eq, Ord)]
pub enum FilePattern {
Builtin(&'static str),

View file

@ -26,6 +26,7 @@ use crate::message::{Emitter, EmitterContext, Message, TextEmitter};
use crate::packaging::detect_package_root;
use crate::registry::AsRule;
use crate::rules::pycodestyle::rules::syntax_error;
use crate::settings::types::UnsafeFixes;
use crate::settings::{flags, LinterSettings};
use crate::source_kind::SourceKind;
use ruff_notebook::Notebook;
@ -155,8 +156,11 @@ pub(crate) fn test_contents<'a>(
code: fixed_contents,
source_map,
..
}) = fix_file(&diagnostics, &Locator::new(transformed.source_code()))
{
}) = fix_file(
&diagnostics,
&Locator::new(transformed.source_code()),
UnsafeFixes::Enabled,
) {
if iterations < max_iterations() {
iterations += 1;
} else {
@ -294,6 +298,7 @@ pub(crate) fn print_jupyter_messages(
.with_show_fix_status(true)
.with_show_fix_diff(true)
.with_show_source(true)
.with_unsafe_fixes(UnsafeFixes::Enabled)
.emit(
&mut output,
messages,
@ -314,6 +319,7 @@ pub(crate) fn print_messages(messages: &[Message]) -> String {
.with_show_fix_status(true)
.with_show_fix_diff(true)
.with_show_source(true)
.with_unsafe_fixes(UnsafeFixes::Enabled)
.emit(
&mut output,
messages,

View file

@ -24,7 +24,7 @@ use ruff_linter::rule_selector::{PreviewOptions, Specificity};
use ruff_linter::settings::rule_table::RuleTable;
use ruff_linter::settings::types::{
FilePattern, FilePatternSet, PerFileIgnore, PreviewMode, PythonVersion, SerializationFormat,
Version,
UnsafeFixes, Version,
};
use ruff_linter::settings::{
resolve_per_file_ignores, LinterSettings, DUMMY_VARIABLE_RGX, PREFIXES, TASK_TAGS,
@ -64,6 +64,7 @@ pub struct Configuration {
pub extend: Option<PathBuf>,
pub fix: Option<bool>,
pub fix_only: Option<bool>,
pub unsafe_fixes: Option<UnsafeFixes>,
pub output_format: Option<SerializationFormat>,
pub preview: Option<PreviewMode>,
pub required_version: Option<Version>,
@ -137,6 +138,7 @@ impl Configuration {
.unwrap_or_else(|| cache_dir(project_root)),
fix: self.fix.unwrap_or(false),
fix_only: self.fix_only.unwrap_or(false),
unsafe_fixes: self.unsafe_fixes.unwrap_or_default(),
output_format: self.output_format.unwrap_or_default(),
show_fixes: self.show_fixes.unwrap_or(false),
show_source: self.show_source.unwrap_or(false),
@ -365,6 +367,7 @@ impl Configuration {
}),
fix: options.fix,
fix_only: options.fix_only,
unsafe_fixes: options.unsafe_fixes.map(UnsafeFixes::from),
output_format: options.output_format.or_else(|| {
options
.format
@ -418,6 +421,7 @@ impl Configuration {
include: self.include.or(config.include),
fix: self.fix.or(config.fix),
fix_only: self.fix_only.or(config.fix_only),
unsafe_fixes: self.unsafe_fixes.or(config.unsafe_fixes),
output_format: self.output_format.or(config.output_format),
force_exclude: self.force_exclude.or(config.force_exclude),
line_length: self.line_length.or(config.line_length),

View file

@ -89,9 +89,18 @@ pub struct Options {
/// Enable fix behavior by-default when running `ruff` (overridden
/// by the `--fix` and `--no-fix` command-line flags).
/// Only includes automatic fixes unless `--unsafe-fixes` is provided.
#[option(default = "false", value_type = "bool", example = "fix = true")]
pub fix: Option<bool>,
/// Enable application of unsafe fixes.
#[option(
default = "false",
value_type = "bool",
example = "unsafe-fixes = true"
)]
pub unsafe_fixes: Option<bool>,
/// Like `fix`, but disables reporting on leftover violation. Implies `fix`.
#[option(default = "false", value_type = "bool", example = "fix-only = true")]
pub fix_only: Option<bool>,

View file

@ -1,7 +1,7 @@
use path_absolutize::path_dedot;
use ruff_cache::cache_dir;
use ruff_formatter::{FormatOptions, IndentStyle, LineWidth};
use ruff_linter::settings::types::{FilePattern, FilePatternSet, SerializationFormat};
use ruff_linter::settings::types::{FilePattern, FilePatternSet, SerializationFormat, UnsafeFixes};
use ruff_linter::settings::LinterSettings;
use ruff_macros::CacheKey;
use ruff_python_ast::PySourceType;
@ -19,6 +19,8 @@ pub struct Settings {
#[cache_key(ignore)]
pub fix_only: bool,
#[cache_key(ignore)]
pub unsafe_fixes: UnsafeFixes,
#[cache_key(ignore)]
pub output_format: SerializationFormat,
#[cache_key(ignore)]
pub show_fixes: bool,
@ -40,6 +42,7 @@ impl Default for Settings {
output_format: SerializationFormat::default(),
show_fixes: false,
show_source: false,
unsafe_fixes: UnsafeFixes::default(),
linter: LinterSettings::new(project_root),
file_resolver: FileResolverSettings::new(project_root),
formatter: FormatterSettings::default(),

View file

@ -193,7 +193,9 @@ Arguments:
Options:
--fix
Attempt to automatically fix lint violations. Use `--no-fix` to disable
Apply fixes to resolve lint violations. Use `--no-fix` to disable or `--unsafe-fixes` to include unsafe fixes
--unsafe-fixes
Include fixes that may not retain the original intent of the code. Use `--no-unsafe-fixes` to disable
--show-source
Show violations with source code. Use `--no-show-source` to disable
--show-fixes
@ -203,7 +205,7 @@ Options:
-w, --watch
Run in watch mode by re-running whenever files change
--fix-only
Fix any fixable lint violations, but don't report on leftover violations. Implies `--fix`. Use `--no-fix-only` to disable
Apply fixes to resolve lint violations, but don't report on leftover violations. Implies `--fix`. Use `--no-fix-only` to disable or `--unsafe-fixes` to include unsafe fixes
--ignore-noqa
Ignore any `# noqa` comments
--output-format <OUTPUT_FORMAT>

9
ruff.schema.json generated
View file

@ -128,7 +128,7 @@
}
},
"fix": {
"description": "Enable fix behavior by-default when running `ruff` (overridden by the `--fix` and `--no-fix` command-line flags).",
"description": "Enable fix behavior by-default when running `ruff` (overridden by the `--fix` and `--no-fix` command-line flags). Only includes automatic fixes unless `--unsafe-fixes` is provided.",
"type": [
"boolean",
"null"
@ -637,6 +637,13 @@
"items": {
"$ref": "#/definitions/RuleSelector"
}
},
"unsafe-fixes": {
"description": "Enable application of unsafe fixes.",
"type": [
"boolean",
"null"
]
}
},
"additionalProperties": false,